From 1c55221b83b0679db7e51099343bd36b54682977 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 17 Jul 2023 11:36:32 -0600 Subject: [PATCH 001/220] add initial work on nested inner repn pw to gdp transformation. identify variables mode does not work, gives infeasible models --- .../tests/test_nested_inner_repn_gdp.py | 36 ++++ .../piecewise/transform/nested_inner_repn.py | 171 ++++++++++++++++++ 2 files changed, 207 insertions(+) create mode 100644 pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py create mode 100644 pyomo/contrib/piecewise/transform/nested_inner_repn.py diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py new file mode 100644 index 00000000000..48357c828df --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -0,0 +1,36 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# 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 +from pyomo.contrib.piecewise.tests import models +import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.core.base import TransformationFactory +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.environ import Constraint, SolverFactory, Var + +from pyomo.contrib.piecewise.transform.nested_inner_repn import NestedInnerRepresentationGDPTransformation + +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + + def test_solve_log_model(self): + m = models.make_log_x_model() + TransformationFactory( + 'contrib.piecewise.nested_inner_repn_gdp' + ).apply_to(m) + TransformationFactory( + 'gdp.bigm' + ).apply_to(m) + SolverFactory('gurobi').solve(m) + ct.check_log_x_model_soln(self, m) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py new file mode 100644 index 00000000000..b25ca3981a8 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -0,0 +1,171 @@ +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( + PiecewiseLinearToGDP, +) +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunct, Disjunction +from pyomo.common.errors import DeveloperError +from pyomo.core.expr.visitor import SimpleExpressionVisitor +from pyomo.core.expr.current import identify_components + +@TransformationFactory.register( + 'contrib.piecewise.nested_inner_repn_gdp', + doc="TODO document", +) +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a nested + GDP to determine which polytope a point is in, then representing it as a + convex combination of extreme points, with multipliers "local" to that + particular polytope, i.e., not shared with neighbors. This method of + logarithmically formulating the piecewise linear function imposes no + restrictions on the family of polytopes. We rely on the identification of + variables to make this logarithmic in the number of binaries. This method + is due to Vielma et al., 2010. + """ + CONFIG = PiecewiseLinearToGDP.CONFIG() + _transformation_name = 'pw_linear_nested_inner_repn' + + # Implement to use PiecewiseLinearToGDP. 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): + self.DEBUG = True + identify_vars = True + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + # these copy-pasted lines (from inner_representation_gdp) seem useful + # adding some of this stuff to self so I don't have to pass it around + self.pw_linear_func = pw_linear_func + # map number -> list of Disjuncts which contain Disjunctions at that level + self.disjunct_levels = {} + self.dimension = pw_expr.nargs() + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + self.substitute_var_lb = float('inf') + self.substitute_var_ub = -float('inf') + + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + + if self.DEBUG: + print(f"dimension is {self.dimension}") + + # Add the disjunction + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + + # Widen bounds as determined when setting up the disjunction + if self.substitute_var_lb < float('inf'): + transBlock.substitute_var.setlb(self.substitute_var_lb) + if self.substitute_var_ub > -float('inf'): + transBlock.substitute_var.setub(self.substitute_var_ub) + + if self.DEBUG: + print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") + + if identify_vars: + if self.DEBUG: + print("Now identifying variables") + for i in self.disjunct_levels.keys(): + print(f"level {i}: {len(self.disjunct_levels[i])} disjuncts") + transBlock.var_identifications_l = Constraint(NonNegativeIntegers, NonNegativeIntegers) + transBlock.var_identifications_r = Constraint(NonNegativeIntegers, NonNegativeIntegers) + for k in self.disjunct_levels.keys(): + disj_0 = self.disjunct_levels[k][0] + for i, disj in enumerate(self.disjunct_levels[k][1:]): + transBlock.var_identifications_l[k, i] = disj.d_l.binary_indicator_var == disj_0.d_l.binary_indicator_var + transBlock.var_identifications_r[k, i] = disj.d_r.binary_indicator_var == disj_0.d_r.binary_indicator_var + return substitute_var + + # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up + # the stack, since the whole point is that we'll only go logarithmically + # many calls deep. + def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): + size = len(choices) + if self.DEBUG: + print(f"calling _get_disjunction with size={size}") + # Our base cases will be 3 and 2, since it would be silly to construct + # a Disjunction containing only one Disjunct. We can ensure that size + # is never 1 unless it was only passsed a single choice from the start, + # which we can handle before calling. + if size > 3: + half = size // 2 # (integer divide) + # This tree will be slightly heavier on the right side + choices_l = choices[:half] + choices_r = choices[half:] + # Is this valid Pyomo? + @parent_block.Disjunct() + def d_l(b): + b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block, level + 1) + @parent_block.Disjunct() + def d_r(b): + b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block, level + 1) + if level not in self.disjunct_levels.keys(): + self.disjunct_levels[level] = [] + self.disjunct_levels[level].append(parent_block.d_l) + self.disjunct_levels[level].append(parent_block.d_r) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + elif size == 3: + # Let's stay heavier on the right side for consistency. So the left + # Disjunct will be the one to contain constraints, rather than a + # Disjunction + @parent_block.Disjunct() + def d_l(b): + simplex, linear_func = choices[0] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + @parent_block.Disjunct() + def d_r(b): + b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) + if level not in self.disjunct_levels.keys(): + self.disjunct_levels[level] = [] + self.disjunct_levels[level].append(parent_block.d_r) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + elif size == 2: + # In this case both sides are regular Disjuncts + @parent_block.Disjunct() + def d_l(b): + simplex, linear_func = choices[0] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + @parent_block.Disjunct() + def d_r(b): + simplex, linear_func = choices[1] + self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) + return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) + else: + raise DeveloperError("Unreachable: 1 or 0 choices were passed to " + "_get_disjunction in nested_inner_repn.py.") + + def _set_disjunct_block_constraints(self, b, simplex, linear_func, pw_expr, root_block): + # Define the lambdas sparsely like in the version I'm copying, + # only the first few will participate in constraints + b.lambdas = Var(NonNegativeIntegers, dense=False, bounds=(0, 1)) + # Get the extreme points to add up + extreme_pts = [] + for idx in simplex: + extreme_pts.append(self.pw_linear_func._points[idx]) + # Constrain sum(lambda_i) = 1 + b.convex_combo = Constraint( + expr=sum(b.lambdas[i] for i in range(len(extreme_pts))) == 1 + ) + linear_func_expr = linear_func(*pw_expr.args) + # Make the substitute Var equal the PWLE + b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + # Widen the variable bounds to those of this linear func expression + (lb, ub) = compute_bounds_on_expr(linear_func_expr) + if lb is not None and lb < self.substitute_var_lb: + self.substitute_var_lb = lb + if ub is not None and ub > self.substitute_var_ub: + self.substitute_var_ub = ub + # Constrain x = \sum \lambda_i v_i + @b.Constraint(range(self.dimension)) + def linear_combo(d, i): + return pw_expr.args[i] == sum( + d.lambdas[j] * pt[i] for j, pt in enumerate(extreme_pts) + ) + # Mark the lambdas as local in order to prevent disagreggating multiple + # times in the hull transformation + b.LocalVars = Suffix(direction=Suffix.LOCAL) + b.LocalVars[b] = [v for v in b.lambdas.values()] From 91ed57e8c7c49ebe2d547a8f6b065f5231cf2164 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 1 Aug 2023 12:48:46 -0600 Subject: [PATCH 002/220] wip: working on some other pw linear representations --- .../transform/disagreggated_logarithmic.py | 102 ++++++++++++++++++ .../piecewise/transform/nested_inner_repn.py | 7 +- 2 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py new file mode 100644 index 00000000000..fceb02d4d8c --- /dev/null +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -0,0 +1,102 @@ +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( + PiecewiseLinearToGDP, +) +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunct, Disjunction +from pyomo.common.errors import DeveloperError +from pyomo.core.expr.visitor import SimpleExpressionVisitor +from pyomo.core.expr.current import identify_components +from math import ceil, log2 + +@TransformationFactory.register( + 'contrib.piecewise.disaggregated_logarithmic', + doc="TODO document", +) +class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This method of logarithmically + formulating the piecewise linear function imposes no restrictions on the + family of polytopes. This method is due to Vielma et al., 2010. + """ + CONFIG = PiecewiseLinearToGDP.CONFIG() + _transformation_name = 'pw_linear_disaggregated_log' + + # Implement to use PiecewiseLinearToGDP. 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): + self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + dimension = pw_expr.nargs() + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + self.substitute_var_lb = float('inf') + self.substitute_var_ub = -float('inf') + + simplices = pw_linear_func._simplices + num_simplices = len(simplices) + simplex_indices = range(num_simplices) + # Assumption: the simplices are really simplices and all have the same number of points + simplex_point_indices = range(len(simplices[0])) + + choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + + log_dimension = ceil(log2(num_simplices)) + binaries = transBlock.binaries = Var(range(log_dimension), domain=Binary) + + # injective function \mathcal{P} -> ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors + B = {} + for i, p in enumerate(simplices): + B[id(p)] = self._get_binary_vector(i, log_dimension) + + # The lambdas \lambda_{P,v} + lambdas = transBlock.lambdas = Var(simplex_indices, simplex_point_indices, bounds=(0, 1)) + transBlock.convex_combo = Constraint(sum(lambdas[P, v] for P in simplex_indices for v in simplex_point_indices) == 1) + + # The branching rules, establishing using the binaries that only one simplex's lambdas + # may be nonzero + @transBlock.Constraint(range(log_dimension)) + def simplex_choice_1(b, l): + return ( + sum(lambdas[P, v] for P in self._P_plus(B, l) for v in simplex_point_indices) <= binaries[l] + ) + @transBlock.Constraint(range(log_dimension)) + def simplex_choice_2(b, l): + return ( + sum(lambdas[P, v] for P in self._P_0(B, l) for v in simplex_point_indices) <= 1 - binaries[l] + ) + + #for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i) + @transBlock.Constraint(range(dimension)) + def x_constraint(b, i): + return sum([stuff] for ) + + + #linear_func_expr = linear_func(*pw_expr.args) + ## Make the substitute Var equal the PWLE + #b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) + + # Not a gray code, just a regular binary representation + # TODO this is probably not optimal, test the gray codes too + def _get_binary_vector(self, num, length): + if ceil(log2(num)) > length: + raise DeveloperError("Invalid input in _get_binary_vector") + # Use python's string formatting instead of bothering with modular + # arithmetic. May be slow. + return (int(x) for x in format(num, f'0{length}b')) + + # Return {P \in \mathcal{P} | B(P)_l = 0} + def _P_0(B, l, simplices): + return [p for p in simplices if B[id(p)][l] == 0] + # Return {P \in \mathcal{P} | B(P)_l = 1} + def _P_plus(B, l, simplices): + return [p for p in simplices if B[id(p)][l] == 1] \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index b25ca3981a8..fc5761de434 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -30,8 +30,8 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. 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): - self.DEBUG = True - identify_vars = True + self.DEBUG = False + identify_vars = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -66,6 +66,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") + # NOTE - This functionality does not work. Even when we can choose the indicator + # variables, it seems that infeasibilities will always be generated. We may need + # to just directly transform to mip :( if identify_vars: if self.DEBUG: print("Now identifying variables") From d9be75ec758aa2b5173fe419008707a8267d471f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 16:34:57 -0400 Subject: [PATCH 003/220] properly handle one-simplex case instead of ignoring --- .../piecewise/transform/nested_inner_repn.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index fc5761de434..1e86a1406b4 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -54,8 +54,17 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"dimension is {self.dimension}") - # Add the disjunction - transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + # If there was only one choice, don't bother making a disjunction, just + # use the linear function directly (but still use the substitute_var for + # consistency). + if len(choices) == 1: + (_, linear_func) = choices[0] # simplex isn't important in this case + linear_func_expr = linear_func(*pw_expr.args) + transBlock.set_substitute = Constraint(expr=substitute_var == linear_func_expr) + (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr(linear_func_expr) + else: + # Add the disjunction + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) # Widen bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): From 949e043d685a53cd7eb05df63935b8c3045a7a80 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 17:03:43 -0400 Subject: [PATCH 004/220] nested inner repn: remove non-working variable identification code --- .../piecewise/transform/nested_inner_repn.py | 55 +++++-------------- 1 file changed, 14 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 1e86a1406b4..aaa0e03c79b 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -2,27 +2,25 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core import Constraint, NonNegativeIntegers, Suffix, Var from pyomo.core.base import TransformationFactory -from pyomo.gdp import Disjunct, Disjunction +from pyomo.gdp import Disjunction from pyomo.common.errors import DeveloperError -from pyomo.core.expr.visitor import SimpleExpressionVisitor -from pyomo.core.expr.current import identify_components @TransformationFactory.register( 'contrib.piecewise.nested_inner_repn_gdp', - doc="TODO document", + doc="TODO document", # TODO ) class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): """ - Represent a piecewise linear function "logarithmically" by using a nested - GDP to determine which polytope a point is in, then representing it as a - convex combination of extreme points, with multipliers "local" to that - particular polytope, i.e., not shared with neighbors. This method of - logarithmically formulating the piecewise linear function imposes no - restrictions on the family of polytopes. We rely on the identification of - variables to make this logarithmic in the number of binaries. This method - is due to Vielma et al., 2010. + Represent a piecewise linear function by using a nested GDP to determine + which polytope a point is in, then representing it as a convex combination + of extreme points, with multipliers "local" to that particular polytope, + i.e., not shared with neighbors. This method of formulating the piecewise + linear function imposes no restrictions on the family of polytopes. Note + that this is NOT a logarithmic formulation - it has linearly many binaries. + This method was, however, inspired by the disagreggated logarithmic + formulation of Vielma et al., 2010. """ CONFIG = PiecewiseLinearToGDP.CONFIG() _transformation_name = 'pw_linear_nested_inner_repn' @@ -31,7 +29,6 @@ class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): self.DEBUG = False - identify_vars = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -41,8 +38,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # these copy-pasted lines (from inner_representation_gdp) seem useful # adding some of this stuff to self so I don't have to pass it around self.pw_linear_func = pw_linear_func - # map number -> list of Disjuncts which contain Disjunctions at that level - self.disjunct_levels = {} self.dimension = pw_expr.nargs() substitute_var = transBlock.substitute_var = Var() pw_linear_func.map_transformation_var(pw_expr, substitute_var) @@ -64,7 +59,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc (self.substitute_var_lb, self.substitute_var_ub) = compute_bounds_on_expr(linear_func_expr) else: # Add the disjunction - transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock, 1) + transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock) # Widen bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): @@ -75,21 +70,6 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc if self.DEBUG: print(f"lb is {self.substitute_var_lb}, ub is {self.substitute_var_ub}") - # NOTE - This functionality does not work. Even when we can choose the indicator - # variables, it seems that infeasibilities will always be generated. We may need - # to just directly transform to mip :( - if identify_vars: - if self.DEBUG: - print("Now identifying variables") - for i in self.disjunct_levels.keys(): - print(f"level {i}: {len(self.disjunct_levels[i])} disjuncts") - transBlock.var_identifications_l = Constraint(NonNegativeIntegers, NonNegativeIntegers) - transBlock.var_identifications_r = Constraint(NonNegativeIntegers, NonNegativeIntegers) - for k in self.disjunct_levels.keys(): - disj_0 = self.disjunct_levels[k][0] - for i, disj in enumerate(self.disjunct_levels[k][1:]): - transBlock.var_identifications_l[k, i] = disj.d_l.binary_indicator_var == disj_0.d_l.binary_indicator_var - transBlock.var_identifications_r[k, i] = disj.d_r.binary_indicator_var == disj_0.d_r.binary_indicator_var return substitute_var # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up @@ -111,14 +91,10 @@ def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): # Is this valid Pyomo? @parent_block.Disjunct() def d_l(b): - b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block, level + 1) + b.inner_disjunction_l = self._get_disjunction(choices_l, b, pw_expr, root_block) @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block, level + 1) - if level not in self.disjunct_levels.keys(): - self.disjunct_levels[level] = [] - self.disjunct_levels[level].append(parent_block.d_l) - self.disjunct_levels[level].append(parent_block.d_r) + b.inner_disjunction_r = self._get_disjunction(choices_r, b, pw_expr, root_block) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 3: # Let's stay heavier on the right side for consistency. So the left @@ -131,9 +107,6 @@ def d_l(b): @parent_block.Disjunct() def d_r(b): b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) - if level not in self.disjunct_levels.keys(): - self.disjunct_levels[level] = [] - self.disjunct_levels[level].append(parent_block.d_r) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 2: # In this case both sides are regular Disjuncts From 0c54a1f270963e0f361b5dae93871b6bdce404e4 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 23 Aug 2023 17:13:15 -0400 Subject: [PATCH 005/220] fix errors --- pyomo/contrib/piecewise/transform/nested_inner_repn.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index aaa0e03c79b..6c551818c84 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -75,7 +75,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Recursively form the Disjunctions and Disjuncts. This shouldn't blow up # the stack, since the whole point is that we'll only go logarithmically # many calls deep. - def _get_disjunction(self, choices, parent_block, pw_expr, root_block, level): + def _get_disjunction(self, choices, parent_block, pw_expr, root_block): size = len(choices) if self.DEBUG: print(f"calling _get_disjunction with size={size}") @@ -106,7 +106,7 @@ def d_l(b): self._set_disjunct_block_constraints(b, simplex, linear_func, pw_expr, root_block) @parent_block.Disjunct() def d_r(b): - b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block, level + 1) + b.inner_disjunction_r = self._get_disjunction(choices[1:], b, pw_expr, root_block) return Disjunction(expr=[parent_block.d_l, parent_block.d_r]) elif size == 2: # In this case both sides are regular Disjuncts From cde25fc924eec83bbdbb2357feaeac1e3c4349f9 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 00:38:54 -0400 Subject: [PATCH 006/220] disaggregated logarithmic reworking --- .../transform/disagreggated_logarithmic.py | 186 +++++++++++++----- 1 file changed, 142 insertions(+), 44 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index fceb02d4d8c..e0b6d75d0e4 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -2,7 +2,7 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet from pyomo.core.base import TransformationFactory from pyomo.gdp import Disjunct, Disjunction from pyomo.common.errors import DeveloperError @@ -10,93 +10,191 @@ from pyomo.core.expr.current import identify_components from math import ceil, log2 + @TransformationFactory.register( - 'contrib.piecewise.disaggregated_logarithmic', - doc="TODO document", -) -class NestedInnerRepresentationGDPTransformation(PiecewiseLinearToGDP): - """ + "contrib.piecewise.disaggregated_logarithmic", + doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with log_2(|P|) binary decision variables. This method of logarithmically formulating the piecewise linear function imposes no restrictions on the family of polytopes. This method is due to Vielma et al., 2010. + """, +) +class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): + """ + Represent a piecewise linear function "logarithmically" by using a MIP with + log_2(|P|) binary decision variables. This method of logarithmically + formulating the piecewise linear function imposes no restrictions on the + family of polytopes. This method is due to Vielma et al., 2010. """ + CONFIG = PiecewiseLinearToGDP.CONFIG() - _transformation_name = 'pw_linear_disaggregated_log' - + _transformation_name = "pw_linear_disaggregated_log" + # Implement to use PiecewiseLinearToGDP. 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): self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which - # is a Block(Any) + # is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) ] + # Dimensionality of the PWLF dimension = pw_expr.nargs() + print(f"DIMENSIOn={dimension}") + 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) - self.substitute_var_lb = float('inf') - self.substitute_var_ub = -float('inf') + # Bounds for the substitute_var that we will tighten + self.substitute_var_lb = float("inf") + self.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) - simplex_indices = range(num_simplices) - # Assumption: the simplices are really simplices and all have the same number of points - simplex_point_indices = range(len(simplices[0])) + transBlock.simplex_indices = RangeSet(0, num_simplices - 1) + # 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) + + # Enumeration of simplices, map from simplex number to simplex object + self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} + # Inverse of previous enumeration + self.simplex_to_idx = {v: k for k, v in self.idx_to_simplex.items()} + + # List of tuples of simplices with their linear function + simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) - choices = list(zip(pw_linear_func._simplices, pw_linear_func._linear_functions)) + print("a") + print(f"Num_simplices: {num_simplices}") log_dimension = ceil(log2(num_simplices)) - binaries = transBlock.binaries = Var(range(log_dimension), domain=Binary) + transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) + binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - # injective function \mathcal{P} -> ceil(log_2(|P|)) used to identify simplices - # (really just polytopes are required) with binary vectors + # Injective function \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # (really just polytopes are required) with binary vectors. Any injective function + # is valid. B = {} - for i, p in enumerate(simplices): - B[id(p)] = self._get_binary_vector(i, log_dimension) - - # The lambdas \lambda_{P,v} - lambdas = transBlock.lambdas = Var(simplex_indices, simplex_point_indices, bounds=(0, 1)) - transBlock.convex_combo = Constraint(sum(lambdas[P, v] for P in simplex_indices for v in simplex_point_indices) == 1) + for i in transBlock.simplex_indices: + # map index(P) -> corresponding vector in {0, 1}^n + B[i] = self._get_binary_vector(i, log_dimension) + print(f"after construction, B = {B}") + + print("b") + # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it + transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) + print("b1") + + # Sum of all lambdas is one (6b) + transBlock.convex_combo = Constraint( + expr=sum( + transBlock.lambdas[P, v] + for P in transBlock.simplex_indices + for v in transBlock.simplex_point_indices + ) + == 1 + ) + + print("c") # The branching rules, establishing using the binaries that only one simplex's lambdas # may be nonzero - @transBlock.Constraint(range(log_dimension)) + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): + print("entering constraint generator") + print(f"thing={self._P_plus(B, l, simplices)}") + print("returning") return ( - sum(lambdas[P, v] for P in self._P_plus(B, l) for v in simplex_point_indices) <= binaries[l] + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + for P in self._P_plus(B, l, simplices) + for v in transBlock.simplex_point_indices + ) + <= binaries[l] ) - @transBlock.Constraint(range(log_dimension)) + + print("c1") + + @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( - sum(lambdas[P, v] for P in self._P_0(B, l) for v in simplex_point_indices) <= 1 - binaries[l] + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + for P in self._P_0(B, l, simplices) + for v in transBlock.simplex_point_indices + ) + <= 1 - binaries[l] ) - - #for i, (simplex, pwlf) in enumerate(choices): - # x_i = sum(lambda_P,v v_i) - @transBlock.Constraint(range(dimension)) + + print("d") + + # for i, (simplex, pwlf) in enumerate(choices): + # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) + @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): - return sum([stuff] for ) + print(f"simplices are {[P for P in simplices]}") + print(f"points are {pw_linear_func._points}") + print(f"simplex_point_indices is {list(transBlock.simplex_point_indices)}") + print(f"i={i}") + + return pw_expr.args[i] == sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + * pw_linear_func._points[P[v]][i] + for P in simplices + for v in transBlock.simplex_point_indices + ) + + # Make the substitute Var equal the PWLE (6a.2) + for P, linear_func in simplices_and_lin_funcs: + print(f"P, linear_func = {P}, {linear_func}") + for v in transBlock.simplex_point_indices: + print(f" v={v}") + print(f" pt={pw_linear_func._points[P[v]]}") + print( + f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" + ) + transBlock.set_substitute = Constraint( + expr=substitute_var + == sum( + sum( + transBlock.lambdas[self.simplex_to_idx[P], v] + * linear_func(*pw_linear_func._points[P[v]]) + for v in transBlock.simplex_point_indices + ) + for (P, linear_func) in simplices_and_lin_funcs + ) + ) + + print("f") + return substitute_var - #linear_func_expr = linear_func(*pw_expr.args) - ## Make the substitute Var equal the PWLE - #b.set_substitute = Constraint(expr=root_block.substitute_var == linear_func_expr) - # Not a gray code, just a regular binary representation # TODO this is probably not optimal, test the gray codes too def _get_binary_vector(self, num, length): - if ceil(log2(num)) > length: + if num != 0 and ceil(log2(num)) > length: raise DeveloperError("Invalid input in _get_binary_vector") - # Use python's string formatting instead of bothering with modular + # Hack: use python's string formatting instead of bothering with modular # arithmetic. May be slow. - return (int(x) for x in format(num, f'0{length}b')) + return tuple(int(x) for x in format(num, f"0{length}b")) # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(B, l, simplices): - return [p for p in simplices if B[id(p)][l] == 0] + def _P_0(self, B, l, simplices): + return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 0] + # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(B, l, simplices): - return [p for p in simplices if B[id(p)][l] == 1] \ No newline at end of file + def _P_plus(self, B, l, simplices): + print(f"p plus: B={B}, l={l}, simplices={simplices}") + for p in simplices: + print(f"for p={p}, simplex_to_idx[p]={self.simplex_to_idx[p]}") + print( + f"returning {[p for p in simplices if B[self.simplex_to_idx[p]][l] == 1]}" + ) + return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] From 3a072f7f080a3a327b057e6321ea6d8385e3e2f0 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 01:34:14 -0400 Subject: [PATCH 007/220] remove printf debugging --- .../transform/disagreggated_logarithmic.py | 67 +++++++------------ 1 file changed, 26 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index e0b6d75d0e4..00fb1546412 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -34,7 +34,6 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # Implement to use PiecewiseLinearToGDP. 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): - self.DEBUG = False # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any). This is where we will put our new components. @@ -44,14 +43,13 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Dimensionality of the PWLF dimension = pw_expr.nargs() - print(f"DIMENSIOn={dimension}") 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 tighten + # Bounds for the substitute_var that we will widen self.substitute_var_lb = float("inf") self.substitute_var_ub = -float("inf") @@ -71,26 +69,35 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # List of tuples of simplices with their linear function simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) - print("a") - print(f"Num_simplices: {num_simplices}") + # 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 simplices_and_lin_funcs: + for v in transBlock.simplex_point_indices: + val = linear_func(*pw_linear_func._points[P[v]]) + if val < self.substitute_var_lb: + self.substitute_var_lb = val + if val > self.substitute_var_ub: + self.substitute_var_ub = val + # Now set those bounds + if self.substitute_var_lb < float('inf'): + transBlock.substitute_var.setlb(self.substitute_var_lb) + if self.substitute_var_ub > -float('inf'): + transBlock.substitute_var.setub(self.substitute_var_ub) log_dimension = ceil(log2(num_simplices)) transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - # Injective function \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices + # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices # (really just polytopes are required) with binary vectors. Any injective function - # is valid. + # is enough here. B = {} for i in transBlock.simplex_indices: # map index(P) -> corresponding vector in {0, 1}^n B[i] = self._get_binary_vector(i, log_dimension) - print(f"after construction, B = {B}") - print("b") # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) - print("b1") # Sum of all lambdas is one (6b) transBlock.convex_combo = Constraint( @@ -102,15 +109,10 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc == 1 ) - print("c") - # The branching rules, establishing using the binaries that only one simplex's lambdas # may be nonzero @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) def simplex_choice_1(b, l): - print("entering constraint generator") - print(f"thing={self._P_plus(B, l, simplices)}") - print("returning") return ( sum( transBlock.lambdas[self.simplex_to_idx[P], v] @@ -120,8 +122,6 @@ def simplex_choice_1(b, l): <= binaries[l] ) - print("c1") - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( @@ -133,18 +133,10 @@ def simplex_choice_2(b, l): <= 1 - binaries[l] ) - print("d") - # for i, (simplex, pwlf) in enumerate(choices): # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): - - print(f"simplices are {[P for P in simplices]}") - print(f"points are {pw_linear_func._points}") - print(f"simplex_point_indices is {list(transBlock.simplex_point_indices)}") - print(f"i={i}") - return pw_expr.args[i] == sum( transBlock.lambdas[self.simplex_to_idx[P], v] * pw_linear_func._points[P[v]][i] @@ -153,14 +145,14 @@ def x_constraint(b, i): ) # Make the substitute Var equal the PWLE (6a.2) - for P, linear_func in simplices_and_lin_funcs: - print(f"P, linear_func = {P}, {linear_func}") - for v in transBlock.simplex_point_indices: - print(f" v={v}") - print(f" pt={pw_linear_func._points[P[v]]}") - print( - f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" - ) + #for P, linear_func in simplices_and_lin_funcs: + # print(f"P, linear_func = {P}, {linear_func}") + # for v in transBlock.simplex_point_indices: + # print(f" v={v}") + # print(f" pt={pw_linear_func._points[P[v]]}") + # print( + # f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" + # ) transBlock.set_substitute = Constraint( expr=substitute_var == sum( @@ -173,11 +165,10 @@ def x_constraint(b, i): ) ) - print("f") return substitute_var # Not a gray code, just a regular binary representation - # TODO this is probably not optimal, test the gray codes too + # TODO this may not be optimal, test the gray codes too def _get_binary_vector(self, num, length): if num != 0 and ceil(log2(num)) > length: raise DeveloperError("Invalid input in _get_binary_vector") @@ -191,10 +182,4 @@ def _P_0(self, B, l, simplices): # Return {P \in \mathcal{P} | B(P)_l = 1} def _P_plus(self, B, l, simplices): - print(f"p plus: B={B}, l={l}, simplices={simplices}") - for p in simplices: - print(f"for p={p}, simplex_to_idx[p]={self.simplex_to_idx[p]}") - print( - f"returning {[p for p in simplices if B[self.simplex_to_idx[p]][l] == 1]}" - ) return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] From 3f684add395284912902558ca7e34ab081aae083 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 02:23:00 -0400 Subject: [PATCH 008/220] fix strange reverse indexing --- .../transform/disagreggated_logarithmic.py | 70 +++++++++---------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py index 00fb1546412..e86d5539367 100644 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py @@ -15,17 +15,19 @@ "contrib.piecewise.disaggregated_logarithmic", doc=""" Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This method of logarithmically - formulating the piecewise linear function imposes no restrictions on the - family of polytopes. This method is due to Vielma et al., 2010. + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we + assume we have simplces in this code. This method is due to Vielma et al., 2010. """, ) class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): """ Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This method of logarithmically - formulating the piecewise linear function imposes no restrictions on the - family of polytopes. This method is due to Vielma et al., 2010. + log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; + GDP is not used. This method of logarithmically formulating the piecewise + linear function imposes no restrictions on the family of polytopes, but we + assume we have simplces in this code. This method is due to Vielma et al., 2010. """ CONFIG = PiecewiseLinearToGDP.CONFIG() @@ -35,8 +37,8 @@ class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): # that replaces the transformed piecewise linear expr def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_block): - # Get a new Block() in transformation_block.transformed_functions, which - # is a Block(Any). This is where we will put our new components. + # Get a new Block for our transformationin transformation_block.transformed_functions, + # which is a Block(Any). This is where we will put our new components. transBlock = transformation_block.transformed_functions[ len(transformation_block.transformed_functions) ] @@ -61,32 +63,28 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # which is dimension + 1 transBlock.simplex_point_indices = RangeSet(0, dimension) - # Enumeration of simplices, map from simplex number to simplex object + # Enumeration of simplices: map from simplex number to simplex object self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} - # Inverse of previous enumeration - self.simplex_to_idx = {v: k for k, v in self.idx_to_simplex.items()} - # List of tuples of simplices with their linear function - simplices_and_lin_funcs = list(zip(simplices, pw_linear_func._linear_functions)) + # List of tuples of simplex indices with their linear function + simplex_indices_and_lin_funcs = list(zip(transBlock.simplex_indices, pw_linear_func._linear_functions)) # 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 simplices_and_lin_funcs: + for P, linear_func in simplex_indices_and_lin_funcs: for v in transBlock.simplex_point_indices: - val = linear_func(*pw_linear_func._points[P[v]]) + val = linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) if val < self.substitute_var_lb: self.substitute_var_lb = val if val > self.substitute_var_ub: self.substitute_var_ub = val # Now set those bounds - if self.substitute_var_lb < float('inf'): - transBlock.substitute_var.setlb(self.substitute_var_lb) - if self.substitute_var_ub > -float('inf'): - transBlock.substitute_var.setub(self.substitute_var_ub) + transBlock.substitute_var.setlb(self.substitute_var_lb) + transBlock.substitute_var.setub(self.substitute_var_ub) log_dimension = ceil(log2(num_simplices)) transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) - binaries = transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) + transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices # (really just polytopes are required) with binary vectors. Any injective function @@ -115,22 +113,22 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc def simplex_choice_1(b, l): return ( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - for P in self._P_plus(B, l, simplices) + transBlock.lambdas[P, v] + for P in self._P_plus(B, l, transBlock.simplex_indices) for v in transBlock.simplex_point_indices ) - <= binaries[l] + <= transBlock.binaries[l] ) @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) def simplex_choice_2(b, l): return ( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - for P in self._P_0(B, l, simplices) + transBlock.lambdas[P, v] + for P in self._P_0(B, l, transBlock.simplex_indices) for v in transBlock.simplex_point_indices ) - <= 1 - binaries[l] + <= 1 - transBlock.binaries[l] ) # for i, (simplex, pwlf) in enumerate(choices): @@ -138,9 +136,9 @@ def simplex_choice_2(b, l): @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) def x_constraint(b, i): return pw_expr.args[i] == sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - * pw_linear_func._points[P[v]][i] - for P in simplices + transBlock.lambdas[P, v] + * pw_linear_func._points[self.idx_to_simplex[P][v]][i] + for P in transBlock.simplex_indices for v in transBlock.simplex_point_indices ) @@ -157,11 +155,11 @@ def x_constraint(b, i): expr=substitute_var == sum( sum( - transBlock.lambdas[self.simplex_to_idx[P], v] - * linear_func(*pw_linear_func._points[P[v]]) + transBlock.lambdas[P, v] + * linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) for v in transBlock.simplex_point_indices ) - for (P, linear_func) in simplices_and_lin_funcs + for (P, linear_func) in simplex_indices_and_lin_funcs ) ) @@ -177,9 +175,9 @@ def _get_binary_vector(self, num, length): return tuple(int(x) for x in format(num, f"0{length}b")) # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(self, B, l, simplices): - return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 0] + def _P_0(self, B, l, simplex_indices): + return [p for p in simplex_indices if B[p][l] == 0] # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(self, B, l, simplices): - return [p for p in simplices if B[self.simplex_to_idx[p]][l] == 1] + def _P_plus(self, B, l, simplex_indices): + return [p for p in simplex_indices if B[p][l] == 1] From 7dce14b335539d4523146a51beb926f44352e321 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 12 Oct 2023 02:24:42 -0400 Subject: [PATCH 009/220] incremental transform: initial --- .../piecewise/transform/incremental.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 pyomo/contrib/piecewise/transform/incremental.py diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py new file mode 100644 index 00000000000..0551f38d8f8 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -0,0 +1,41 @@ +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( + PiecewiseLinearToGDP, +) +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet +from pyomo.core.base import TransformationFactory +from pyomo.gdp import Disjunct, Disjunction +from pyomo.common.errors import DeveloperError +from pyomo.core.expr.visitor import SimpleExpressionVisitor +from pyomo.core.expr.current import identify_components +from math import ceil, log2 + +@TransformationFactory.register( + 'contrib.piecewise.incremental', + doc= + """ + TODO document + """, +) +class IncrementalInnerGDPTransformation(PiecewiseLinearToGDP): + """ + TODO document + """ + CONFIG = PiecewiseLinearToGDP.CONFIG() + _transformation_name = 'pw_linear_incremental' + + # Implement to use PiecewiseLinearToGDP. 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): + self.DEBUG = False + # Get a new Block() in transformation_block.transformed_functions, which + # is a Block(Any) + transBlock = transformation_block.transformed_functions[ + len(transformation_block.transformed_functions) + ] + + dimension = pw_expr.nargs() + substitute_var = transBlock.substitute_var = Var() + pw_linear_func.map_transformation_var(pw_expr, substitute_var) + self.substitute_var_lb = float('inf') + self.substitute_var_ub = -float('inf') From 82df834ba5010f97340b7c8c23bedc9cada4a377 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 26 Oct 2023 10:22:32 -0400 Subject: [PATCH 010/220] continue incremental transform implementation --- .../piecewise/transform/incremental.py | 126 ++++++++++++++++-- 1 file changed, 118 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 0551f38d8f8..92d1a9f8e04 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -2,7 +2,7 @@ from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, ) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet +from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet, Param from pyomo.core.base import TransformationFactory from pyomo.gdp import Disjunct, Disjunction from pyomo.common.errors import DeveloperError @@ -10,10 +10,10 @@ from pyomo.core.expr.current import identify_components from math import ceil, log2 + @TransformationFactory.register( - 'contrib.piecewise.incremental', - doc= - """ + "contrib.piecewise.incremental", + doc=""" TODO document """, ) @@ -21,9 +21,10 @@ class IncrementalInnerGDPTransformation(PiecewiseLinearToGDP): """ TODO document """ + CONFIG = PiecewiseLinearToGDP.CONFIG() - _transformation_name = 'pw_linear_incremental' - + _transformation_name = "pw_linear_incremental" + # Implement to use PiecewiseLinearToGDP. 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): @@ -34,8 +35,117 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc 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) - self.substitute_var_lb = float('inf') - self.substitute_var_ub = -float('inf') + + # Bounds for the substitute_var that we will widen + self.substitute_var_lb = float("inf") + self.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(dimension) + + + # Ordering of simplices to follow Vielma + # TODO: this enumeration must satisfy O1 (Vielma): each T_i \cap T_{i-1} is nonempty + self.simplex_ordering = { + n: n for n in transBlock.simplex_indices + } + + # Enumeration of simplices: map from simplex number to correct simplex object + self.idx_to_simplex = { + n: simplices[m] for n, m in self.simplex_ordering + } + # Associate simplex indices with correct linear functions + self.idx_to_lin_func = { + n: pw_linear_func._linear_functions[m] for n, m in self.simplex_ordering + } + + # For each individual simplex, the points need to be permuted in a way that + # satisfies O1 and O2 (Vielma). TODO TODO TODO + self.vertex_ordering = { + (T, n): n + for T in transBlock.simplex_indices + for n in transBlock.simplex_point_indices + } + + # Inital vertex (v_0^0 in Vielma) + self.initial_vertex = pw_linear_func._points[self.index_to_simplex[0][self.vertex_ordering[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( + # figure out if this needs to be 0 or 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, + 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] == + self.initial_vertex[n] + sum( + sum( + # delta_i^j * (v_i^j - v_i^0) + transBlock.delta[i, j] * (pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, j]]][n] + - pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[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 + == self.idx_to_lin_func[0](*self.initial_vertex) + sum( + sum( + # delta_i^j * (f(v_i^j) - f(v_i^0)) + transBlock.delta[i, j] * (self.idx_to_lin_func[i](*pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, j]]]) + - self.idx_to_lin_func[i](*pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, 0]]])) + for j in transBlock.nonzero_simplex_point_indices + ) + for i in transBlock.simplex_indices + ) + ) \ No newline at end of file From 16bc3bc61fddcfc9a568598a9fb0f5d5a4cf603f Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 26 Oct 2023 10:40:51 -0400 Subject: [PATCH 011/220] fix some incremental transform errors --- pyomo/contrib/piecewise/transform/incremental.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 92d1a9f8e04..58aadbc31a3 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -56,7 +56,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # 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(dimension) + transBlock.last_simplex_point_index = Param(initialize=dimension) # Ordering of simplices to follow Vielma @@ -67,11 +67,11 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Enumeration of simplices: map from simplex number to correct simplex object self.idx_to_simplex = { - n: simplices[m] for n, m in self.simplex_ordering + n: simplices[m] for n, m in self.simplex_ordering.items() } # Associate simplex indices with correct linear functions self.idx_to_lin_func = { - n: pw_linear_func._linear_functions[m] for n, m in self.simplex_ordering + n: pw_linear_func._linear_functions[m] for n, m in self.simplex_ordering.items() } # For each individual simplex, the points need to be permuted in a way that @@ -83,7 +83,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc } # Inital vertex (v_0^0 in Vielma) - self.initial_vertex = pw_linear_func._points[self.index_to_simplex[0][self.vertex_ordering[0, 0]]] + self.initial_vertex = pw_linear_func._points[self.idx_to_simplex[0][self.vertex_ordering[0, 0]]] # delta_i^j = delta[simplex][point] transBlock.delta = Var( @@ -128,8 +128,8 @@ def x_constraint(b, n): self.initial_vertex[n] + sum( sum( # delta_i^j * (v_i^j - v_i^0) - transBlock.delta[i, j] * (pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, j]]][n] - - pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, 0]]][n]) + transBlock.delta[i, j] * (pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, j]]][n] + - pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, 0]]][n]) for j in transBlock.nonzero_simplex_point_indices ) for i in transBlock.simplex_indices @@ -142,8 +142,8 @@ def x_constraint(b, n): == self.idx_to_lin_func[0](*self.initial_vertex) + sum( sum( # delta_i^j * (f(v_i^j) - f(v_i^0)) - transBlock.delta[i, j] * (self.idx_to_lin_func[i](*pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, j]]]) - - self.idx_to_lin_func[i](*pw_linear_func._points[self.index_to_simplex[i][self.vertex_ordering[i, 0]]])) + transBlock.delta[i, j] * (self.idx_to_lin_func[i](*pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, j]]]) + - self.idx_to_lin_func[i](*pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, 0]]])) for j in transBlock.nonzero_simplex_point_indices ) for i in transBlock.simplex_indices From ee3b3166a5372a1879e3bcb333405c9e6b9afb46 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 26 Oct 2023 12:00:09 -0400 Subject: [PATCH 012/220] minor pw linear changes --- .../piecewise/tests/test_incremental.py | 74 +++++++++++++++++++ .../tests/test_nested_inner_repn_gdp.py | 13 ++++ .../piecewise/transform/incremental.py | 2 +- .../piecewise/transform/nested_inner_repn.py | 2 +- 4 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 pyomo/contrib/piecewise/tests/test_incremental.py diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py new file mode 100644 index 00000000000..ea1b5158b1a --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -0,0 +1,74 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# 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 +from pyomo.contrib.piecewise.tests import models +import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.core.base import TransformationFactory +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) +from pyomo.gdp import Disjunct, Disjunction +from pyomo.environ import Constraint, SolverFactory, Var, ConcreteModel, Objective, log, value +from pyomo.contrib.piecewise import PiecewiseLinearFunction + +from pyomo.contrib.piecewise.transform.incremental import IncrementalInnerGDPTransformation + +class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + + #def test_solve_log_model(self): + # m = models.make_log_x_model() + # 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) + + def test_solve_univariate_log_model(self): + m = ConcreteModel() + m.x = Var(bounds=(1, 10)) + m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) + + # Here are the linear functions, for safe keeping. + def f1(x): + return (log(3) / 2) * x - log(3) / 2 + + m.f1 = f1 + + def f2(x): + return (log(2) / 3) * x + log(3 / 2) + + m.f2 = f2 + + def f3(x): + return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) + + m.f3 = f3 + + m.log_expr = m.pw_log(m.x) + m.obj = Objective(expr=m.log_expr) + + TransformationFactory( + 'contrib.piecewise.incremental' + ).apply_to(m) + m.pprint() + TransformationFactory( + 'gdp.hull' + ).apply_to(m) + print('####### PPRINTNG AGAIN AFTER BIGM #######') + m.pprint() + # log is increasing so the optimal value should be log(10) + SolverFactory('gurobi').solve(m) + self.assertTrue(abs(value(m.obj) - log(10)) < 0.001) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py index 48357c828df..2a87c86f6b4 100644 --- a/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py +++ b/pyomo/contrib/piecewise/tests/test_nested_inner_repn_gdp.py @@ -21,6 +21,8 @@ from pyomo.environ import Constraint, SolverFactory, Var from pyomo.contrib.piecewise.transform.nested_inner_repn import NestedInnerRepresentationGDPTransformation +from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import DisaggregatedLogarithmicInnerGDPTransformation +from pyomo.contrib.piecewise.transform.incremental import IncrementalInnerGDPTransformation class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): @@ -33,4 +35,15 @@ def test_solve_log_model(self): 'gdp.bigm' ).apply_to(m) SolverFactory('gurobi').solve(m) + ct.check_log_x_model_soln(self, m) + + def test_solve_log_model_2(self): + m = models.make_log_x_model() + TransformationFactory( + 'contrib.piecewise.disaggregated_logarithmic' + ).apply_to(m) + TransformationFactory( + 'gdp.bigm' + ).apply_to(m) + SolverFactory('gurobi').solve(m) ct.check_log_x_model_soln(self, m) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 58aadbc31a3..11de6c09011 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -101,7 +101,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # 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, + transBlock.simplex_indices_except_last, domain=Binary ) diff --git a/pyomo/contrib/piecewise/transform/nested_inner_repn.py b/pyomo/contrib/piecewise/transform/nested_inner_repn.py index 6c551818c84..d57a99600fe 100644 --- a/pyomo/contrib/piecewise/transform/nested_inner_repn.py +++ b/pyomo/contrib/piecewise/transform/nested_inner_repn.py @@ -61,7 +61,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Add the disjunction transBlock.disj = self._get_disjunction(choices, transBlock, pw_expr, transBlock) - # Widen bounds as determined when setting up the disjunction + # Set bounds as determined when setting up the disjunction if self.substitute_var_lb < float('inf'): transBlock.substitute_var.setlb(self.substitute_var_lb) if self.substitute_var_ub > -float('inf'): From 3d47204e9072015926515830ffbbaf0e016ced7d Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 9 Nov 2023 01:00:47 -0500 Subject: [PATCH 013/220] incremental: fix obvious bug --- .../piecewise/tests/test_incremental.py | 11 ++++++++--- .../contrib/piecewise/transform/incremental.py | 18 ++++++++++++++++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index ea1b5158b1a..36bade33381 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -18,10 +18,13 @@ assertExpressionsStructurallyEqual, ) from pyomo.gdp import Disjunct, Disjunction -from pyomo.environ import Constraint, SolverFactory, Var, ConcreteModel, Objective, log, value +from pyomo.environ import Constraint, SolverFactory, Var, ConcreteModel, Objective, log, value, maximize from pyomo.contrib.piecewise import PiecewiseLinearFunction from pyomo.contrib.piecewise.transform.incremental import IncrementalInnerGDPTransformation +from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( + DisaggregatedLogarithmicInnerGDPTransformation +) class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): @@ -58,17 +61,19 @@ def f3(x): m.f3 = f3 m.log_expr = m.pw_log(m.x) - m.obj = Objective(expr=m.log_expr) + m.obj = Objective(expr=m.log_expr, sense=maximize) TransformationFactory( 'contrib.piecewise.incremental' + #'contrib.piecewise.disaggregated_logarithmic' ).apply_to(m) m.pprint() TransformationFactory( - 'gdp.hull' + 'gdp.bigm' ).apply_to(m) print('####### PPRINTNG AGAIN AFTER BIGM #######') m.pprint() # log is increasing so the optimal value should be log(10) SolverFactory('gurobi').solve(m) + print(f"optimal value is {value(m.obj)}") self.assertTrue(abs(value(m.obj) - log(10)) < 0.001) \ No newline at end of file diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 11de6c09011..4287a2c5230 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -10,7 +10,6 @@ from pyomo.core.expr.current import identify_components from math import ceil, log2 - @TransformationFactory.register( "contrib.piecewise.incremental", doc=""" @@ -58,6 +57,19 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc 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 < self.substitute_var_lb: + self.substitute_var_lb = val + if val > self.substitute_var_ub: + self.substitute_var_ub = val + # Now set those bounds + transBlock.substitute_var.setlb(self.substitute_var_lb) + transBlock.substitute_var.setub(self.substitute_var_ub) + # Ordering of simplices to follow Vielma # TODO: this enumeration must satisfy O1 (Vielma): each T_i \cap T_{i-1} is nonempty @@ -148,4 +160,6 @@ def x_constraint(b, n): ) for i in transBlock.simplex_indices ) - ) \ No newline at end of file + ) + + return substitute_var \ No newline at end of file From 85f865d0128fc0019684c579ef81e7b4b353212a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 22 Feb 2024 14:58:23 -0500 Subject: [PATCH 014/220] add j1 triangulate script --- .../piecewise/transform/incremental.py | 17 +++- .../piecewise/union_jack_triangulate.py | 81 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 pyomo/contrib/piecewise/union_jack_triangulate.py diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 4287a2c5230..510b591ea82 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# 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.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( PiecewiseLinearToGDP, @@ -72,7 +83,11 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # Ordering of simplices to follow Vielma - # TODO: this enumeration must satisfy O1 (Vielma): each T_i \cap T_{i-1} is nonempty + # TODO: assumption: this enumeration must satisfy O1 (Vielma): each T_i \cap T_{i-1} + # is nonempty + # TODO: One way to make this true will be to use the union_jack__triangulate.py + # script to generate the triangulation, but it is also possible for other + # triangulations to be correct. This should be checkable using a MIP. self.simplex_ordering = { n: n for n in transBlock.simplex_indices } diff --git a/pyomo/contrib/piecewise/union_jack_triangulate.py b/pyomo/contrib/piecewise/union_jack_triangulate.py new file mode 100644 index 00000000000..2853add8161 --- /dev/null +++ b/pyomo/contrib/piecewise/union_jack_triangulate.py @@ -0,0 +1,81 @@ +# ___________________________________________________________________________ +# +# 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 math import factorial +import time + +# This implements the J1 "Union Jack" triangulation (Todd 77) as explained by +# Vielma 2010. + +# Triangulate {0, ..., K}^n for even K using the J1 triangulation. +def triangulate(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)) + J1 = [] + for v_0, pi, s in big_iterator: + simplex = [] + current = list(v_0) + simplex.append(current) + for i in range(0, n): + current = current.copy() + current[pi[i]] += s[pi[i]] + simplex.append(current) + J1.append(simplex) + return J1 + +if __name__ == '__main__': + # do some tests. TODO move to real test file + start0 = time.time() + small_2d = triangulate(2, 2) + elapsed0 = time.time() - start0 + print(f"triangulated small_2d in {elapsed0} sec.") + assert len(small_2d) == 8 + assert small_2d == [[[1, 1], [0, 1], [0, 0]], + [[1, 1], [0, 1], [0, 2]], + [[1, 1], [2, 1], [2, 0]], + [[1, 1], [2, 1], [2, 2]], + [[1, 1], [1, 0], [0, 0]], + [[1, 1], [1, 2], [0, 2]], + [[1, 1], [1, 0], [2, 0]], + [[1, 1], [1, 2], [2, 2]]] + start1 = time.time() + bigger_2d = triangulate(4, 2) + elapsed1 = time.time() - start1 + print(f"triangulated bigger_2d in {elapsed1} sec.") + assert len(bigger_2d) == 32 + + start2 = time.time() + medium_3d = triangulate(12, 3) + elapsed2 = time.time() - start2 + print(f"triangulated medium_3d in {elapsed2} sec.") + # A J1 triangulation of {0, ..., K}^n has K^n * n! simplices + assert len(medium_3d) == 12**3 * factorial(3) + + start3 = time.time() + big_4d = triangulate(20, 4) + elapsed3 = time.time() - start3 + print(f"triangulated big_4d in {elapsed3} sec.") + assert len(big_4d) == 20**4 * factorial(4) + + print("starting huge_5d") + start4 = time.time() + huge_5d = triangulate(10, 5) + elapsed4 = time.time() - start4 + print(f"triangulated huge_5d in {elapsed4} sec.") + + print("Success") From 4eb2fac76b152f836948392e3d595bbaebbd291d Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 14 Mar 2024 13:19:36 -0400 Subject: [PATCH 015/220] update comment --- pyomo/contrib/piecewise/transform/incremental.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 510b591ea82..dd26b8c5182 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -87,7 +87,9 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # is nonempty # TODO: One way to make this true will be to use the union_jack__triangulate.py # script to generate the triangulation, but it is also possible for other - # triangulations to be correct. This should be checkable using a MIP. + # triangulations to be correct. This should be checkable using a MIP. It + # is known that there is a correct ordering for any triangulation of a + # domain homeomorphic to a disc in R^2 (Wilson 1998). self.simplex_ordering = { n: n for n in transBlock.simplex_indices } From cc0a9ecfdc06e4e52df260a91e09a9b962756c86 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 18 Mar 2024 17:26:20 -0600 Subject: [PATCH 016/220] Adding infrastructure to both choose a triangulation and record the choice, but not J1 implementation yet. --- pyomo/contrib/piecewise/__init__.py | 1 + .../piecewise/piecewise_linear_function.py | 41 +++++++++++++++---- .../tests/test_piecewise_linear_function.py | 17 +++++++- pyomo/contrib/piecewise/triangulations.py | 35 ++++++++++++++++ 4 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 pyomo/contrib/piecewise/triangulations.py diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 37873c83b3b..832932c9b7d 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -33,3 +33,4 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) +from pyomo.contrib.piecewise.triangulations import Triangulation diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 66ca02ad125..0b82531f09c 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -19,6 +19,10 @@ from pyomo.contrib.piecewise.piecewise_linear_expression import ( PiecewiseLinearExpression, ) +from pyomo.contrib.piecewise.triangulations import ( + get_j1_triangulation, + Triangulation, +) from pyomo.core import Any, NonNegativeIntegers, value, Var from pyomo.core.base.block import _BlockData, Block from pyomo.core.base.component import ModelComponentFactory @@ -49,6 +53,11 @@ def __init__(self, component=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): """ @@ -251,6 +260,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', Triangulation.Delaunay) kwargs.setdefault('ctype', PiecewiseLinearFunction) Block.__init__(self, *args, **kwargs) @@ -269,6 +279,8 @@ 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 +296,23 @@ 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): + tri = self._triangulation_rule(parent, obj._index) + 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_j1_triangulation(points, dimension) + obj._triangulation = tri + else: + raise ValueError( + "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. @@ -350,7 +373,8 @@ def _construct_from_function_and_points(self, obj, parent, nonlinear_function): obj, nonlinear_function ) - 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 ) @@ -460,7 +484,8 @@ def _construct_from_tabular_data(self, obj, parent, nonlinear_function): obj, _tabular_data_functor(tabular_data, tupleize=True) ) - 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) ) diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index 571601fefbc..5dd331d7943 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,13 @@ 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) + self.assertEqual(m.pw.triangulation, Triangulation.J1) + def test_use_pw_function_in_constraint(self): m = self.make_ln_x_model() m.pw = PiecewiseLinearFunction( @@ -302,6 +309,14 @@ def test_pw_linear_approx_of_paraboloid_points(self): ) self.check_pw_linear_approximation(m) + 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)], function=m.g, + triangulation=Triangulation.J1 + ) + self.check_pw_linear_approximation(m) + @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/triangulations.py b/pyomo/contrib/piecewise/triangulations.py new file mode 100644 index 00000000000..d67cd302060 --- /dev/null +++ b/pyomo/contrib/piecewise/triangulations.py @@ -0,0 +1,35 @@ +# ___________________________________________________________________________ +# +# 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 pytest import set_trace + + +class Triangulation: + Delaunay = 1 + J1 = 2 + +def get_j1_triangulation(points, dimension): + if dimension == 2: + return _get_j1_triangulation_2d(points, dimension) + elif dimension == 3: + return _get_j1_triangulation_3d(points, dimension) + else: + return _get_j1_triangulation_for_more_than_4d(points, dimension) + +def _get_j1_triangulation_2d(points, dimension): + # I think this means coding up the proof by picture... + pass + +def _get_j1_triangulation_3d(points, dimension): + pass + +def _get_j1_triangulation_for_more_than_4d(points, dimension): + pass From 0f6fe16a4a46f2147dec052345de4d9291594313 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Mar 2024 11:45:57 -0600 Subject: [PATCH 017/220] Adding initial draft of nonlinear to pwl transformation --- pyomo/contrib/piecewise/__init__.py | 3 + .../piecewise/transform/nonlinear_to_pwl.py | 380 ++++++++++++++++++ 2 files changed, 383 insertions(+) create mode 100644 pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 37873c83b3b..b5452dd2bd5 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -33,3 +33,6 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) +from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( + NonlinearToPWL, +) 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..519736723f6 --- /dev/null +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -0,0 +1,380 @@ +# ___________________________________________________________________________ +# +# 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 numpy as np +import itertools + +from pyomo.environ import ( + TransformationFactory, + Transformation, + Var, + Constraint, + Objective, + Any, + value, +) +from pyomo.core.expr.numeric_expr import SumExpression +from pyomo.core.expr import identify_variables +from pyomo.repn.quadratic import QuadraticRepnVisitor +from pyomo.core.expr import SumExpression +from pyomo.contrib.piecewise import PiecewiseLinearExpression + + +# TODO remove +MAX_DIM = 5 + +# This should be safe to use many times; declare it globally +_quadratic_repn_visitor = QuadraticRepnVisitor( + subexpression_cache={}, var_map={}, var_order={}, sorter=None +) + + +def get_pwl_function_approximation(func, method, n, bounds, **kwargs): + """ + Get a piecewise-linear approximation to a function, given: + + func: function to approximate + method: method to use for the approximation, current options are: + - 'simple_random_point_grid' + - 'simple_uniform_point_grid' + - 'naive_lmt' + n: parameter controlling fineness of the approximation based on the specified method + bounds: list of tuples giving upper and lower bounds for each of func's arguments + kwargs: additional arguments to be specified to the method used + """ + + points = None + match (method): + case 'simple_random_point_grid': + points = get_simple_random_point_grid(bounds, n) + case 'simple_uniform_point_grid': + points = get_simple_uniform_point_grid(bounds, n) + case 'naive_lmt': + points = get_points_naive_lmt(bounds, n, func, randomize=True) + case 'naive_lmt_uniform': + points = get_points_naive_lmt(bounds, n, func, randomize=False) + case _: + raise NotImplementedError(f"Invalid method: {method}") + + # Default path: after getting the points, construct PWLF using the + # function-and-list-of-points constructor + + # DUCT TAPE WARNING: work around deficiency in PiecewiseLinearFunction constructor. TODO + dim = len(points[0]) + if dim == 1: + points = [pt[0] for pt in points] + + print( + f" Constructing PWLF with {len(points)} points, each of which are {dim}-dimensional" + ) + return PiecewiseLinearFunction(points=points, function=func) + + +def get_simple_random_point_grid(bounds, n, seed=42): + # Generate randomized grid of points + linspaces = [] + for b in bounds: + np.random.seed(seed) + linspaces.append(np.random.uniform(b[0], b[1], n)) + return list(itertools.product(*linspaces)) + + +def get_simple_uniform_point_grid(bounds, n): + # Generate non-randomized grid of points + linspaces = [] + for b in bounds: + # Issues happen when exactly using the boundary + nudge = (b[1] - b[0]) * 1e-4 + linspaces.append( + # np.linspace(b[0], b[1], n) + np.linspace(b[0] + nudge, b[1] - nudge, n) + ) + return list(itertools.product(*linspaces)) + + +# TODO this was copypasted from shumeng; make it better +def get_points_naive_lmt(bounds, n, func, seed=42, randomize=True): + from lineartree import LinearTreeRegressor + from sklearn.linear_model import LinearRegression + from sklearn.metrics import mean_squared_error + from sklearn.model_selection import train_test_split + import PWLTransformation.lmt as lmtutils + + points = None + if randomize: + points = get_simple_random_point_grid(bounds, n, seed=seed) + else: + points = get_simple_uniform_point_grid(bounds, n) + # perturb(points, 0.01) + x_list = np.array(points) + y_list = [] + for point in points: + y_list.append(func(*point)) + regr = LinearTreeRegressor( + LinearRegression(), + criterion='mse', + max_bins=120, + min_samples_leaf=4, + max_depth=5, + ) + + # Using train_test_split is silly. TODO: remove this and just sample my own + # extra points if I want to estimate the error. + X_train, X_test, y_train, y_test = train_test_split( + x_list, y_list, test_size=0.2, random_state=seed + ) + regr.fit(X_train, y_train) + y_pred = regr.predict(X_test) + error = mean_squared_error(y_test, y_pred) + + leaves, splits, ths = lmtutils.parse_linear_tree_regressor(regr, bounds) + + # This was originally part of the LMT_Model_component and used to calculate + # avg_leaves for the output data. TODO: get this back + # self.total_leaves += len(leaves) + + # bound_point_list = lmt.generate_bound(leaves) + bound_point_list = lmtutils.generate_bound_points(leaves, bounds) + # duct tape to fix possible issues from unknown bugs. TODO should this go + # here? + return bound_point_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. + """ + + def __init__(self): + super(Transformation).__init__() + + def _apply_to( + self, + model, + n=3, + method='simple_uniform_point_grid', + allow_quadratic_cons=True, + allow_quadratic_objs=True, + additively_decompose=True, + ): + """Apply the transformation""" + + # Check ahead of time whether there are any unbounded variables. If + # there are, we'll have to bail out + # But deactivated variables can be left alone -- or should they be? + # Let's not, for now. + for v in model.component_objects(Var): + if None in v.bounds: + print( + "Error: cannot apply transformation to model with unbounded variables" + ) + raise NotImplementedError( + "Cannot apply transformation to model with unbounded variables" + ) + + # Upcoming steps will trash the values of the vars, since I don't know + # a better way. But what if the user set them with initialize= ? We'd + # better restore them after we're done. + orig_var_map = {id(var): var.value for var in model.component_objects(Var)} + + # Now we are ready to start + original_cons = list(model.component_data_objects(Constraint)) + original_objs = list(model.component_data_objects(Objective)) + + model._pwl_quadratic_count = 0 + model._pwl_nonlinear_count = 0 + + # Let's put all our new constraints in one big index + model._pwl_cons = Constraint(Any) + + for con in original_cons: + repn = _quadratic_repn_visitor.walk_expression(con.body) + if repn.nonlinear is None: + if repn.quadratic is None: + # Linear constraint. Always skip. + continue + else: + model._pwl_quadratic_count += 1 + if allow_quadratic_cons: + continue + else: + model._pwl_nonlinear_count += 1 + _replace_con( + model, con, method, n, allow_quadratic_cons, additively_decompose + ) + + # And do the same for objectives + for obj in original_objs: + repn = _quadratic_repn_visitor.walk_expression(obj) + if repn.nonlinear is None: + if repn.quadratic is None: + # Linear objective. Skip. + continue + else: + model._pwl_quadratic_count += 1 + if allow_quadratic_objs: + continue + else: + model._pwl_nonlinear_count += 1 + _replace_obj( + model, obj, method, n, allow_quadratic_objs, additively_decompose + ) + + # Before we're done, replace the old variable values + for var in model.component_objects(Var): + var.value = orig_var_map[id(var)] + + +# Check whether a term should be skipped for approximation. Do not touch +# model's quadratic or nonlinear counts; those are only for top-level +# expressions which were already checked +def _check_skip_approx(expr, allow_quadratic, model): + repn = _quadratic_repn_visitor.walk_expression(expr) + if repn.nonlinear is None: + if repn.quadratic is None: + # Linear expression. Skip. + return True + else: + # model._pwl_quadratic_count += 1 + if allow_quadratic: + return True + else: + pass + # model._pwl_nonlinear_count += 1 + dim = len(list(identify_variables(expr))) + if dim > MAX_DIM: + print(f"Refusing to approximate function with {dim}-dimensional component.") + raise RuntimeError( + f"Refusing to approximate function with {dim}-dimensional component." + ) + return False + + +def _replace_con(model, con, method, n, allow_quadratic_cons, additively_decompose): + vars = list(identify_variables(con.body)) + bounds = [(v.bounds[0], v.bounds[1]) for v in vars] + + # Alright, let's do it like this. Additively decompose con.body and work on the pieces + func_pieces = [] + for k, expr in enumerate( + _additively_decompose_expr(con.body) if additively_decompose else [con.body] + ): + # First, check if we actually need to do anything + if _check_skip_approx(expr, allow_quadratic_cons, model): + # We're skipping this term. Just add expr directly to the pieces + func_pieces.append(expr) + continue + + vars_inner = list(identify_variables(expr)) + bounds = [(v.bounds[0], v.bounds[1]) for v in vars_inner] + + def eval_con_func(*args): + # sanity check + assert len(args) == len( + vars_inner + ), f"eval_con_func was called with {len(args)} arguments, but expected {len(vars_inner)}" + for i, v in enumerate(vars_inner): + v.value = args[i] + return value(con.body) + + pwlf = get_pwl_function_approximation(eval_con_func, method, n, bounds) + + con_name = con.getname(fully_qualified=False) + model.add_component(f"_pwle_{con_name}_{k}", pwlf) + # func_pieces.append(pwlf(*vars_inner).expr) + func_pieces.append(pwlf(*vars_inner)) + + pwl_func = sum(func_pieces) + + # Change the constraint. This is hard to do in-place, so I'll + # remake it and deactivate the old one as was done originally. + + # Now we need a ton of if statements to properly set up the constraint + if con.equality: + model._pwl_cons[str(con)] = pwl_func == con.ub + elif con.strict_lower: + model._pwl_cons[str(con)] = pwl_func > con.lb + elif con.strict_upper: + model._pwl_cons[str(con)] = pwl_func < con.ub + elif con.has_lb(): + if con.has_ub(): # constraint is of the form lb <= expr <= ub + model._pwl_cons[str(con)] = (con.lb, pwl_func, con.ub) + else: + model._pwl_cons[str(con)] = pwl_func >= con.lb + elif con.has_ub(): + model._pwl_cons[str(con)] = pwl_func <= con.ub + else: + assert ( + False + ), f"unreachable: original Constraint '{con_name}' did not have any upper or lower bound" + con.deactivate() + + +def _replace_obj(model, obj, method, n, allow_quadratic_obj, additively_decompose): + vars = list(identify_variables(obj)) + bounds = [(v.bounds[0], v.bounds[1]) for v in vars] + + func_pieces = [] + for k, expr in enumerate( + _additively_decompose_expr(obj.expr) if additively_decompose else [obj.expr] + ): + # First, check if we actually need to do anything + if _check_skip_approx(expr, allow_quadratic_obj, model): + # We're skipping this term. Just add expr directly to the pieces + func_pieces.append(expr) + continue + + vars_inner = list(identify_variables(expr)) + bounds = [(v.bounds[0], v.bounds[1]) for v in vars_inner] + + def eval_obj_func(*args): + # sanity check + assert len(args) == len( + vars_inner + ), f"eval_obj_func was called with {len(args)} arguments, but expected {len(vars_inner)}" + for i, v in enumerate(vars_inner): + v.value = args[i] + return value(obj) + + pwlf = get_pwl_function_approximation(eval_obj_func, method, n, bounds) + + obj_name = obj.getname(fully_qualified=False) + model.add_component(f"_pwle_{obj_name}_{k}", pwlf) + func_pieces.append(pwlf(*vars_inner)) + + pwl_func = sum(func_pieces[1:], func_pieces[0]) + + # Add the new objective + obj_name = obj.getname(fully_qualified=False) + # model.add_component(f"_pwle_{obj_name}", pwl_func) + model.add_component( + f"_pwl_obj_{obj_name}", Objective(expr=pwl_func, sense=obj.sense) + ) + obj.deactivate() + + +# Copypasted from gdp/plugins/partition_disjuncts.py for now. This is the +# stupid approach that will not properly catch all additive separability; to do +# it better we need a walker. +def _additively_decompose_expr(input_expr): + if input_expr.__class__ is not SumExpression: + # print(f"couldn't decompose: input_expr.__class__ was {input_expr.__class__}, not SumExpression") + # This isn't separable, so we just have the one expression + return [input_expr] + # else, it was a SumExpression, and we will break it into the summands + summands = list(input_expr.args) + # print(f"len(summands) is {len(summands)}") + # print(f"summands is {summands}") + return summands From b34820b8f78f1ecb7e83fd49224a4c3c79d3474f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 28 Mar 2024 14:19:43 -0600 Subject: [PATCH 018/220] Not screaming about fixed Var bounds, but this still doesn't work because the space isn't full-dimensional --- .../piecewise/transform/nonlinear_to_pwl.py | 216 ++++++++++++++---- 1 file changed, 174 insertions(+), 42 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 519736723f6..3db29af6784 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -9,9 +9,13 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -import numpy as np import itertools +from lineartree import LinearTreeRegressor +import lineartree + +import numpy as np + from pyomo.environ import ( TransformationFactory, Transformation, @@ -25,7 +29,15 @@ from pyomo.core.expr import identify_variables from pyomo.repn.quadratic import QuadraticRepnVisitor from pyomo.core.expr import SumExpression -from pyomo.contrib.piecewise import PiecewiseLinearExpression +from pyomo.contrib.piecewise import ( + PiecewiseLinearExpression, + PiecewiseLinearFunction +) + +from sklearn.linear_model import LinearRegression +import random +from sklearn.metrics import mean_squared_error +from sklearn.model_selection import train_test_split # TODO remove @@ -36,7 +48,6 @@ subexpression_cache={}, var_map={}, var_order={}, sorter=None ) - def get_pwl_function_approximation(func, method, n, bounds, **kwargs): """ Get a piecewise-linear approximation to a function, given: @@ -81,33 +92,28 @@ def get_pwl_function_approximation(func, method, n, bounds, **kwargs): def get_simple_random_point_grid(bounds, n, seed=42): # Generate randomized grid of points linspaces = [] - for b in bounds: + for (lb, ub) in bounds: np.random.seed(seed) - linspaces.append(np.random.uniform(b[0], b[1], n)) + linspaces.append(np.random.uniform(lb, ub, n)) return list(itertools.product(*linspaces)) def get_simple_uniform_point_grid(bounds, n): # Generate non-randomized grid of points linspaces = [] - for b in bounds: + for (lb, ub) in bounds: # Issues happen when exactly using the boundary - nudge = (b[1] - b[0]) * 1e-4 + nudge = (ub - lb) * 1e-4 linspaces.append( # np.linspace(b[0], b[1], n) - np.linspace(b[0] + nudge, b[1] - nudge, n) + np.linspace(lb + nudge, ub - nudge, n) ) return list(itertools.product(*linspaces)) # TODO this was copypasted from shumeng; make it better def get_points_naive_lmt(bounds, n, func, seed=42, randomize=True): - from lineartree import LinearTreeRegressor - from sklearn.linear_model import LinearRegression - from sklearn.metrics import mean_squared_error - from sklearn.model_selection import train_test_split - import PWLTransformation.lmt as lmtutils - + points = None if randomize: points = get_simple_random_point_grid(bounds, n, seed=seed) @@ -135,19 +141,149 @@ def get_points_naive_lmt(bounds, n, func, seed=42, randomize=True): y_pred = regr.predict(X_test) error = mean_squared_error(y_test, y_pred) - leaves, splits, ths = lmtutils.parse_linear_tree_regressor(regr, bounds) + leaves, splits, ths = parse_linear_tree_regressor(regr, bounds) # This was originally part of the LMT_Model_component and used to calculate # avg_leaves for the output data. TODO: get this back # self.total_leaves += len(leaves) # bound_point_list = lmt.generate_bound(leaves) - bound_point_list = lmtutils.generate_bound_points(leaves, bounds) + bound_point_list = generate_bound_points(leaves, bounds) # duct tape to fix possible issues from unknown bugs. TODO should this go # here? return bound_point_list +# TODO: this is still horrible. Maybe I should put these back together into +# a wrapper class again, but better this time? + + +# 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]) + + # Duct tape to fix issues from unknown bugs + 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]) + pt[i] = min(pt[i], bounds[i][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(*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 + + +# 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] + if leaves[l]['bounds'][f][1] == None: + leaves[l]['bounds'][f][1] = input_bounds[f][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.", @@ -159,7 +295,7 @@ class NonlinearToPWL(Transformation): def __init__(self): super(Transformation).__init__() - + # TODO: ConfigDict def _apply_to( self, model, @@ -169,25 +305,12 @@ def _apply_to( allow_quadratic_objs=True, additively_decompose=True, ): - """Apply the transformation""" - - # Check ahead of time whether there are any unbounded variables. If - # there are, we'll have to bail out - # But deactivated variables can be left alone -- or should they be? - # Let's not, for now. - for v in model.component_objects(Var): - if None in v.bounds: - print( - "Error: cannot apply transformation to model with unbounded variables" - ) - raise NotImplementedError( - "Cannot apply transformation to model with unbounded variables" - ) + """TODO: docstring""" # Upcoming steps will trash the values of the vars, since I don't know # a better way. But what if the user set them with initialize= ? We'd # better restore them after we're done. - orig_var_map = {id(var): var.value for var in model.component_objects(Var)} + orig_var_map = {id(var): var.value for var in model.component_data_objects(Var)} # Now we are ready to start original_cons = list(model.component_data_objects(Constraint)) @@ -233,7 +356,7 @@ def _apply_to( ) # Before we're done, replace the old variable values - for var in model.component_objects(Var): + for var in model.component_data_objects(Var): var.value = orig_var_map[id(var)] @@ -262,11 +385,23 @@ def _check_skip_approx(expr, allow_quadratic, model): return False -def _replace_con(model, con, method, n, allow_quadratic_cons, additively_decompose): - vars = list(identify_variables(con.body)) - bounds = [(v.bounds[0], v.bounds[1]) for v in vars] +def _generate_bounds_list(vars_inner, con): + bounds = [] + for v in vars_inner: + if v.fixed: + bounds.append((value(v), value(v))) + elif None in v.bounds: + raise ValueError( + "Cannot automatically approximate constraints with unbounded " + "variables. Var '%s' appearining in component '%s' is missing " + "at least one bound" % (con.name, v.name)) + else: + bounds.append(v.bounds) + return bounds + - # Alright, let's do it like this. Additively decompose con.body and work on the pieces +def _replace_con(model, con, method, n, allow_quadratic_cons, additively_decompose): + # Additively decompose con.body and work on the pieces func_pieces = [] for k, expr in enumerate( _additively_decompose_expr(con.body) if additively_decompose else [con.body] @@ -278,7 +413,7 @@ def _replace_con(model, con, method, n, allow_quadratic_cons, additively_decompo continue vars_inner = list(identify_variables(expr)) - bounds = [(v.bounds[0], v.bounds[1]) for v in vars_inner] + bounds = _generate_bounds_list(vars_inner, con) def eval_con_func(*args): # sanity check @@ -323,9 +458,6 @@ def eval_con_func(*args): def _replace_obj(model, obj, method, n, allow_quadratic_obj, additively_decompose): - vars = list(identify_variables(obj)) - bounds = [(v.bounds[0], v.bounds[1]) for v in vars] - func_pieces = [] for k, expr in enumerate( _additively_decompose_expr(obj.expr) if additively_decompose else [obj.expr] @@ -337,7 +469,7 @@ def _replace_obj(model, obj, method, n, allow_quadratic_obj, additively_decompos continue vars_inner = list(identify_variables(expr)) - bounds = [(v.bounds[0], v.bounds[1]) for v in vars_inner] + bounds = _generate_bounds_list(vars_inner, obj) def eval_obj_func(*args): # sanity check From 33fd778cce713598dbcd84296b3922c2bf9ffa59 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 29 Mar 2024 15:09:56 -0600 Subject: [PATCH 019/220] Complete rewrite of nonlinear to piecewise linear --- .../piecewise/transform/nonlinear_to_pwl.py | 645 ++++++++++-------- 1 file changed, 378 insertions(+), 267 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 3db29af6784..fde08103446 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -9,11 +9,12 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +import enum import itertools from lineartree import LinearTreeRegressor import lineartree - +import logging import numpy as np from pyomo.environ import ( @@ -24,72 +25,54 @@ Objective, Any, value, + BooleanVar, + Connector, + Expression, + Suffix, + Param, + Set, + SetOf, + RangeSet, + Block, + ExternalFunction, + SortComponents, + LogicalConstraint ) +from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.config import ConfigDict, ConfigValue, PositiveInt, InEnum +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.repn.quadratic import QuadraticRepnVisitor 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 sklearn.linear_model import LinearRegression import random from sklearn.metrics import mean_squared_error from sklearn.model_selection import train_test_split +logger = logging.getLogger(__name__) -# TODO remove -MAX_DIM = 5 +class DomainPartitioningMethod(enum.IntEnum): + RANDOM_GRID = 1 + UNIFORM_GRID = 2 + LINEAR_MODEL_TREE_UNIFORM = 3 + LINEAR_MODEL_TREE_RANDOM = 4 # This should be safe to use many times; declare it globally _quadratic_repn_visitor = QuadraticRepnVisitor( subexpression_cache={}, var_map={}, var_order={}, sorter=None ) -def get_pwl_function_approximation(func, method, n, bounds, **kwargs): - """ - Get a piecewise-linear approximation to a function, given: - - func: function to approximate - method: method to use for the approximation, current options are: - - 'simple_random_point_grid' - - 'simple_uniform_point_grid' - - 'naive_lmt' - n: parameter controlling fineness of the approximation based on the specified method - bounds: list of tuples giving upper and lower bounds for each of func's arguments - kwargs: additional arguments to be specified to the method used - """ - - points = None - match (method): - case 'simple_random_point_grid': - points = get_simple_random_point_grid(bounds, n) - case 'simple_uniform_point_grid': - points = get_simple_uniform_point_grid(bounds, n) - case 'naive_lmt': - points = get_points_naive_lmt(bounds, n, func, randomize=True) - case 'naive_lmt_uniform': - points = get_points_naive_lmt(bounds, n, func, randomize=False) - case _: - raise NotImplementedError(f"Invalid method: {method}") - - # Default path: after getting the points, construct PWLF using the - # function-and-list-of-points constructor - - # DUCT TAPE WARNING: work around deficiency in PiecewiseLinearFunction constructor. TODO - dim = len(points[0]) - if dim == 1: - points = [pt[0] for pt in points] - - print( - f" Constructing PWLF with {len(points)} points, each of which are {dim}-dimensional" - ) - return PiecewiseLinearFunction(points=points, function=func) - -def get_simple_random_point_grid(bounds, n, seed=42): +def get_random_point_grid(bounds, n, func, seed=42): # Generate randomized grid of points linspaces = [] for (lb, ub) in bounds: @@ -98,7 +81,7 @@ def get_simple_random_point_grid(bounds, n, seed=42): return list(itertools.product(*linspaces)) -def get_simple_uniform_point_grid(bounds, n): +def get_uniform_point_grid(bounds, n, func): # Generate non-randomized grid of points linspaces = [] for (lb, ub) in bounds: @@ -111,15 +94,17 @@ def get_simple_uniform_point_grid(bounds, n): return list(itertools.product(*linspaces)) -# TODO this was copypasted from shumeng; make it better -def get_points_naive_lmt(bounds, n, func, seed=42, randomize=True): - - points = None - if randomize: - points = get_simple_random_point_grid(bounds, n, seed=seed) - else: - points = get_simple_uniform_point_grid(bounds, n) - # perturb(points, 0.01) +def get_points_lmt_random_sample(bounds, n, func, seed=42): + points = get_random_point_grid(bounds, n, func, seed=seed) + return get_points_lmt(points, bounds, func, seed) + + +def get_points_lmt_uniform_sample(bounds, n, func, seed=42): + points = get_uniform_point_grid(bounds, n, func) + return get_points_lmt(points, bounds, func, seed) + + +def get_points_lmt(points, bounds, func, seed): x_list = np.array(points) y_list = [] for point in points: @@ -153,6 +138,36 @@ def get_points_naive_lmt(bounds, n, func, seed=42, randomize=True): # here? 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, method, n, bounds): + """ + Get a piecewise-linear approximation of a function, given: + + func: function to approximate + method: method to use for the approximation, member of DomainPartitioningMethod + n: parameter controlling fineness of the approximation based on the specified method + bounds: list of tuples giving upper and lower bounds for each of func's arguments + """ + points = _partition_method_dispatcher[method](bounds, n, func) + + # DUCT TAPE WARNING: work around deficiency in PiecewiseLinearFunction + # constructor. TODO + 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) + # TODO: this is still horrible. Maybe I should put these back together into # a wrapper class again, but better this time? @@ -213,11 +228,13 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): 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 + 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 + 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 @@ -250,6 +267,16 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): 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): + if input_expr.__class__ is not SumExpression: + # This isn't separable, 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): @@ -286,227 +313,311 @@ def find_leaves(splits, leaves, input_node): @TransformationFactory.register( 'contrib.piecewise.nonlinear_to_pwl', - doc="Convert nonlinear constraints and objectives to piecewise-linear approximations.", + 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 constraints 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 paritioning 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 the default + (5), it is likely that this transformation will stall triangulating the + points in order to partition the function domain.""", + ), + ) def __init__(self): super(Transformation).__init__() - # TODO: ConfigDict - def _apply_to( - self, - model, - n=3, - method='simple_uniform_point_grid', - allow_quadratic_cons=True, - allow_quadratic_objs=True, - additively_decompose=True, - ): - """TODO: docstring""" - - # Upcoming steps will trash the values of the vars, since I don't know - # a better way. But what if the user set them with initialize= ? We'd - # better restore them after we're done. - orig_var_map = {id(var): var.value for var in model.component_data_objects(Var)} - - # Now we are ready to start - original_cons = list(model.component_data_objects(Constraint)) - original_objs = list(model.component_data_objects(Objective)) - - model._pwl_quadratic_count = 0 - model._pwl_nonlinear_count = 0 - - # Let's put all our new constraints in one big index - model._pwl_cons = Constraint(Any) - - for con in original_cons: - repn = _quadratic_repn_visitor.walk_expression(con.body) - if repn.nonlinear is None: - if repn.quadratic is None: - # Linear constraint. Always skip. - continue - else: - model._pwl_quadratic_count += 1 - if allow_quadratic_cons: - continue + 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() + + 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: - model._pwl_nonlinear_count += 1 - _replace_con( - model, con, method, n, allow_quadratic_cons, additively_decompose - ) + raise ValueError( + "Target '%s' is not a Block, Constraint, or Objective. It " + "is of type '%s' and cannot be transformed." + % (target.name, type(t)) + ) + + 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) - # And do the same for objectives - for obj in original_objs: - repn = _quadratic_repn_visitor.walk_expression(obj) - if repn.nonlinear is None: - if repn.quadratic is None: - # Linear objective. Skip. + 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 - else: - model._pwl_quadratic_count += 1 - if allow_quadratic_objs: - continue + handler(obj, config) + + def _transform_constraint(self, cons, config): + trans_block = self._get_transformation_block(cons.parent_block()) + constraints = cons.values() if cons.is_indexed() else (cons,) + for c in constraints: + pw_approx = 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 + + trans_block._pwl_cons[c.name, len(trans_block._pwl_cons)] = (c.lower, + pw_approx, + c.upper) + # deactivate original + c.deactivate() + + def _transform_objective(self, objective, config): + trans_block = self._get_transformation_block(objective.parent_block()) + objectives = objective.values() if objective.is_indexed() else (objective,) + for obj in objectives: + pw_approx = 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 + + trans_block.add_component( + unique_component_name(trans_block, obj.name), + Objective(expr=pw_approx, sense=obj.sense) + ) + obj.deactivate() + + def _get_bounds_list(self, var_list, parent_component): + bounds = [] + for v in var_list: + if None in v.bounds: + raise ValueError( + "Cannot automatically approximate constraints with unbounded " + "variables. Var '%s' appearining in component '%s' is missing " + "at least one bound" % (con.name, v.name)) else: - model._pwl_nonlinear_count += 1 - _replace_obj( - model, obj, method, n, allow_quadratic_objs, additively_decompose + bounds.append(v.bounds) + return bounds + + def _needs_approximating(self, expr, approximate_quadratic): + repn = _quadratic_repn_visitor.walk_expression(expr) + if repn.nonlinear is None: + if repn.quadratic is None: + # Linear constraint. Always skip. + return False + else: + if not approximate_quadratic: + # Didn't need approximated, nothing to do + return False + return True + + def _approximate_expression(self, obj, parent_component, trans_block, + config, approximate_quadratic): + if not self._needs_approximating(obj, approximate_quadratic): + return + + # Additively decompose obj and work on the pieces + pwl_func = 0 + for k, expr in enumerate(_additively_decompose_expr(obj) if + config.additively_decompose else (obj,)): + # First check is this is a good idea + expr_vars = list(identify_variables(expr, include_fixed=False)) + orig_values = ComponentMap((v, v.value) for v in expr_vars) + + dim = len(expr_vars) + if dim > config.max_dimension: + logger.warning( + "Not approximating expression for component '%s' as " + "it exceeds the maximum dimension of %s. Try increasing " + "'max_dimension' or additively separating the expression." + % (parent_component.name, config.max_dimension)) + pwl_func += expr + continue + elif not self._needs_approximating(expr, approximate_quadratic): + pwl_func += expr + continue + + def eval_expr(*args): + for i, v in enumerate(expr_vars): + v.value = args[i] + return value(expr) + + pwlf = get_pwl_function_approximation( + eval_expr, config.domain_partitioning_method, + config.num_points, + self._get_bounds_list(expr_vars, parent_component) ) + name = unique_component_name( + trans_block, + parent_component.getname(fully_qualified=False) + ) + trans_block.add_component(f"_pwle_{name}_{k}", pwlf) + pwl_func += pwlf(*expr_vars) - # Before we're done, replace the old variable values - for var in model.component_data_objects(Var): - var.value = orig_var_map[id(var)] - - -# Check whether a term should be skipped for approximation. Do not touch -# model's quadratic or nonlinear counts; those are only for top-level -# expressions which were already checked -def _check_skip_approx(expr, allow_quadratic, model): - repn = _quadratic_repn_visitor.walk_expression(expr) - if repn.nonlinear is None: - if repn.quadratic is None: - # Linear expression. Skip. - return True - else: - # model._pwl_quadratic_count += 1 - if allow_quadratic: - return True - else: - pass - # model._pwl_nonlinear_count += 1 - dim = len(list(identify_variables(expr))) - if dim > MAX_DIM: - print(f"Refusing to approximate function with {dim}-dimensional component.") - raise RuntimeError( - f"Refusing to approximate function with {dim}-dimensional component." - ) - return False - - -def _generate_bounds_list(vars_inner, con): - bounds = [] - for v in vars_inner: - if v.fixed: - bounds.append((value(v), value(v))) - elif None in v.bounds: - raise ValueError( - "Cannot automatically approximate constraints with unbounded " - "variables. Var '%s' appearining in component '%s' is missing " - "at least one bound" % (con.name, v.name)) - else: - bounds.append(v.bounds) - return bounds - - -def _replace_con(model, con, method, n, allow_quadratic_cons, additively_decompose): - # Additively decompose con.body and work on the pieces - func_pieces = [] - for k, expr in enumerate( - _additively_decompose_expr(con.body) if additively_decompose else [con.body] - ): - # First, check if we actually need to do anything - if _check_skip_approx(expr, allow_quadratic_cons, model): - # We're skipping this term. Just add expr directly to the pieces - func_pieces.append(expr) - continue - - vars_inner = list(identify_variables(expr)) - bounds = _generate_bounds_list(vars_inner, con) - - def eval_con_func(*args): - # sanity check - assert len(args) == len( - vars_inner - ), f"eval_con_func was called with {len(args)} arguments, but expected {len(vars_inner)}" - for i, v in enumerate(vars_inner): - v.value = args[i] - return value(con.body) - - pwlf = get_pwl_function_approximation(eval_con_func, method, n, bounds) - - con_name = con.getname(fully_qualified=False) - model.add_component(f"_pwle_{con_name}_{k}", pwlf) - # func_pieces.append(pwlf(*vars_inner).expr) - func_pieces.append(pwlf(*vars_inner)) - - pwl_func = sum(func_pieces) - - # Change the constraint. This is hard to do in-place, so I'll - # remake it and deactivate the old one as was done originally. - - # Now we need a ton of if statements to properly set up the constraint - if con.equality: - model._pwl_cons[str(con)] = pwl_func == con.ub - elif con.strict_lower: - model._pwl_cons[str(con)] = pwl_func > con.lb - elif con.strict_upper: - model._pwl_cons[str(con)] = pwl_func < con.ub - elif con.has_lb(): - if con.has_ub(): # constraint is of the form lb <= expr <= ub - model._pwl_cons[str(con)] = (con.lb, pwl_func, con.ub) - else: - model._pwl_cons[str(con)] = pwl_func >= con.lb - elif con.has_ub(): - model._pwl_cons[str(con)] = pwl_func <= con.ub - else: - assert ( - False - ), f"unreachable: original Constraint '{con_name}' did not have any upper or lower bound" - con.deactivate() - - -def _replace_obj(model, obj, method, n, allow_quadratic_obj, additively_decompose): - func_pieces = [] - for k, expr in enumerate( - _additively_decompose_expr(obj.expr) if additively_decompose else [obj.expr] - ): - # First, check if we actually need to do anything - if _check_skip_approx(expr, allow_quadratic_obj, model): - # We're skipping this term. Just add expr directly to the pieces - func_pieces.append(expr) - continue - - vars_inner = list(identify_variables(expr)) - bounds = _generate_bounds_list(vars_inner, obj) - - def eval_obj_func(*args): - # sanity check - assert len(args) == len( - vars_inner - ), f"eval_obj_func was called with {len(args)} arguments, but expected {len(vars_inner)}" - for i, v in enumerate(vars_inner): - v.value = args[i] - return value(obj) - - pwlf = get_pwl_function_approximation(eval_obj_func, method, n, bounds) - - obj_name = obj.getname(fully_qualified=False) - model.add_component(f"_pwle_{obj_name}_{k}", pwlf) - func_pieces.append(pwlf(*vars_inner)) - - pwl_func = sum(func_pieces[1:], func_pieces[0]) - - # Add the new objective - obj_name = obj.getname(fully_qualified=False) - # model.add_component(f"_pwle_{obj_name}", pwl_func) - model.add_component( - f"_pwl_obj_{obj_name}", Objective(expr=pwl_func, sense=obj.sense) - ) - obj.deactivate() - + # restore var values + for v, val in orig_values.items(): + v.value = val -# Copypasted from gdp/plugins/partition_disjuncts.py for now. This is the -# stupid approach that will not properly catch all additive separability; to do -# it better we need a walker. -def _additively_decompose_expr(input_expr): - if input_expr.__class__ is not SumExpression: - # print(f"couldn't decompose: input_expr.__class__ was {input_expr.__class__}, not SumExpression") - # This isn't separable, so we just have the one expression - return [input_expr] - # else, it was a SumExpression, and we will break it into the summands - summands = list(input_expr.args) - # print(f"len(summands) is {len(summands)}") - # print(f"summands is {summands}") - return summands + return pwl_func From 3271a0ead174d17ad2135fb1bf44b65cf163b024 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 29 Mar 2024 15:10:46 -0600 Subject: [PATCH 020/220] Adding handler to safely ignore LogicalConstraints in piecewise linear to GDP transformations --- .../piecewise/transform/piecewise_to_gdp_transformation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py index 2e056c47a15..f36c222b4e0 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py +++ b/pyomo/contrib/piecewise/transform/piecewise_to_gdp_transformation.py @@ -31,6 +31,7 @@ Connector, SortComponents, Any, + LogicalConstraint, ) from pyomo.core.base import Transformation from pyomo.core.base.block import _BlockData, Block @@ -102,6 +103,7 @@ def __init__(self): ExternalFunction: False, Port: False, PiecewiseLinearFunction: self._transform_piecewise_linear_function, + LogicalConstraint: False, } self._transformation_blocks = {} From 159766a4e39e00190fa3da0c501a1e04549b2441 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 29 Mar 2024 17:01:01 -0600 Subject: [PATCH 021/220] Starting to add tests for nonlinear to pw linear transformation --- .../piecewise/tests/test_nonlinear_to_pwl.py | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py 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..4d7af9dc7a6 --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -0,0 +1,88 @@ +# ___________________________________________________________________________ +# +# 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 +from pyomo.contrib.piecewise import PiecewiseLinearFunction +from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( + NonlinearToPWL, + DomainPartitioningMethod +) +from pyomo.core.expr.compare import ( + assertExpressionsStructurallyEqual, +) +from pyomo.environ import ( + ConcreteModel, + Var, + Constraint, + TransformationFactory, + log, +) + +## debug +from pytest import set_trace + +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 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,)] + self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) + self.assertEqual(pwlf._points, points) + self.assertEqual(len(pwlf._linear_functions), 2) + + x1 = 1.0009 + x2 = 5.5 + 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 + ) + x1 = 5.5 + x2 = 9.9991 + assertExpressionsStructurallyEqual( + self, + pwlf._linear_functions[1](m.x), + ((log(x2) - log(x1))/(x2 - x1))*m.x + + (log(x2) - ((log(x2) - log(x1))/(x2 - x1))*x2), + 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[id(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) From f295490fa203e5963f70a47204e15c2bcc98f8bf Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 2 Apr 2024 21:23:15 -0600 Subject: [PATCH 022/220] Adding mapping between original and transformed components and starting on more tests --- .../piecewise/tests/test_nonlinear_to_pwl.py | 30 +++++++++++++ .../piecewise/transform/nonlinear_to_pwl.py | 45 +++++++++++++++++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 4d7af9dc7a6..020edee3924 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -36,6 +36,10 @@ def make_model(self): m.cons = Constraint(expr=log(m.x) >= 0.35) return m + + def check_pw_linear_log_x(self, m, points): + x1 = points[0][0] + x2 = points def test_log_constraint_uniform_grid(self): m = self.make_model() @@ -86,3 +90,29 @@ def test_log_constraint_uniform_grid(self): self.assertIsNone(new_cons.ub) self.assertEqual(new_cons.lb, 0.35) self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons) + + 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] + + set_trace() + points = [(4.370861069626263,), (7.587945476302646,), (9.556428757689245,)] + self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) + self.assertEqual(pwlf._points, points) + self.assertEqual(len(pwlf._linear_functions), 2) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index fde08103446..d819e893d5f 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -38,6 +38,7 @@ 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.modeling import unique_component_name @@ -71,6 +72,13 @@ class DomainPartitioningMethod(enum.IntEnum): subexpression_cache={}, var_map={}, var_order={}, sorter=None ) +class _NonlinearToPWLTransformationData(AutoSlots.Mixin): + __slots__ = ('transformed_component', 'src_component') + + def __init__(self): + self.transformed_component = ComponentMap() + self.src_component = ComponentMap() +Block.register_private_data_initializer(_NonlinearToPWLTransformationData) def get_random_point_grid(bounds, n, func, seed=42): # Generate randomized grid of points @@ -515,6 +523,8 @@ def _transform_block_components(self, block, 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 = self._approximate_expression( @@ -525,15 +535,20 @@ def _transform_constraint(self, cons, config): # Didn't need approximated, nothing to do continue - trans_block._pwl_cons[c.name, len(trans_block._pwl_cons)] = (c.lower, - pw_approx, - c.upper) + 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 = self._approximate_expression( obj.expr, obj, trans_block, config, @@ -543,10 +558,14 @@ def _transform_objective(self, objective, config): # Didn't need approximated, nothing to do continue + new_obj = Objective(expr=pw_approx, sense=obj.sense) trans_block.add_component( unique_component_name(trans_block, obj.name), - Objective(expr=pw_approx, sense=obj.sense) + 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, parent_component): @@ -621,3 +640,21 @@ def eval_expr(*args): v.value = val return pwl_func + + 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) From 12920b41f52e644c3762000f0b1535d6009c24e6 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 15 May 2024 16:40:36 -0400 Subject: [PATCH 023/220] j1 triangulate in 2d, but no ordering --- pyomo/contrib/piecewise/triangulations.py | 66 +++++++++++++++++++++-- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index d67cd302060..d0a8cb1d34d 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -10,6 +10,9 @@ # ___________________________________________________________________________ from pytest import set_trace +import math +import itertools +from functools import cmp_to_key class Triangulation: @@ -17,16 +20,71 @@ class Triangulation: J1 = 2 def get_j1_triangulation(points, dimension): + points_map, K = _process_points_j1(points, dimension) if dimension == 2: - return _get_j1_triangulation_2d(points, dimension) + return _get_j1_triangulation_2d(points_map, K) elif dimension == 3: return _get_j1_triangulation_3d(points, dimension) else: return _get_j1_triangulation_for_more_than_4d(points, dimension) -def _get_j1_triangulation_2d(points, dimension): - # I think this means coding up the proof by picture... - pass + +# 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") + K = math.floor(len(points) ** (1 / dimension)) + if not len(points) == K**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 K % 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 with n-dimensional keys + points.sort(key=cmp_to_key(_compare_lexicographic(dimension))) + points_map = {} + for point_index in itertools.product(range(K), repeat=dimension): + point_flat_index = 0 + for n in range(dimension): + point_flat_index += point_index[dimension - 1 - n] * K**n + points_map[point_index] = points[point_flat_index] + return points_map, K + +def _compare_lexicographic(dimension): + def compare_lexicographic_real(x, y): + for n in range(dimension): + if x[n] < y[n]: + return -1 + elif y[n] < x[n]: + return 1 + return 0 + return compare_lexicographic_real + +def _get_j1_triangulation_2d(points_map, K): + # Each square needs two triangles in it, orientation determined by the parity of + # the bottom-left corner's coordinate indices (x and y). Same parity = top-left + # and bottom-right triangles; different parity = top-right and bottom-left triangles. + simplices = [] + for i in range(K): + for j in range(K): + if i % 2 == j % 2: + simplices.append( + (points_map[i, j], + points_map[i + 1, j + 1], + points_map[i, j + 1])) + simplices.append( + (points_map[i, j], + points_map[i + 1, j + 1], + points_map[i + 1, j])) + else: + simplices.append( + (points_map[i + 1, j], + points_map[i, j + 1], + points_map[i, j])) + simplices.append( + (points_map[i + 1, j], + points_map[i, j + 1], + points_map[i + 1, j + 1])) + return simplices def _get_j1_triangulation_3d(points, dimension): pass From b185716ac7613a93cef9be2edf2fcb552fed0f5a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 16 May 2024 17:13:15 -0400 Subject: [PATCH 024/220] add a MIP to order the j1 triangulation --- .../transform/disagreggated_logarithmic.py | 183 ------------------ pyomo/contrib/piecewise/triangulations.py | 124 +++++++++++- 2 files changed, 122 insertions(+), 185 deletions(-) delete mode 100644 pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py diff --git a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py b/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py deleted file mode 100644 index e86d5539367..00000000000 --- a/pyomo/contrib/piecewise/transform/disagreggated_logarithmic.py +++ /dev/null @@ -1,183 +0,0 @@ -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, -) -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet -from pyomo.core.base import TransformationFactory -from pyomo.gdp import Disjunct, Disjunction -from pyomo.common.errors import DeveloperError -from pyomo.core.expr.visitor import SimpleExpressionVisitor -from pyomo.core.expr.current import identify_components -from math import ceil, log2 - - -@TransformationFactory.register( - "contrib.piecewise.disaggregated_logarithmic", - doc=""" - Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we - assume we have simplces in this code. This method is due to Vielma et al., 2010. - """, -) -class DisaggregatedLogarithmicInnerGDPTransformation(PiecewiseLinearToGDP): - """ - Represent a piecewise linear function "logarithmically" by using a MIP with - log_2(|P|) binary decision variables. This is a direct-to-MIP transformation; - GDP is not used. This method of logarithmically formulating the piecewise - linear function imposes no restrictions on the family of polytopes, but we - assume we have simplces in this code. This method is due to Vielma et al., 2010. - """ - - CONFIG = PiecewiseLinearToGDP.CONFIG() - _transformation_name = "pw_linear_disaggregated_log" - - # Implement to use PiecewiseLinearToGDP. 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): - - # Get a new Block for our transformationin transformation_block.transformed_functions, - # which is a Block(Any). This is where we will put our new components. - 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 - self.substitute_var_lb = float("inf") - self.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) - # 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) - - # Enumeration of simplices: map from simplex number to simplex object - self.idx_to_simplex = {k: v for k, v in zip(transBlock.simplex_indices, simplices)} - - # List of tuples of simplex indices with their linear function - simplex_indices_and_lin_funcs = list(zip(transBlock.simplex_indices, pw_linear_func._linear_functions)) - - # 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 simplex_indices_and_lin_funcs: - for v in transBlock.simplex_point_indices: - val = linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) - if val < self.substitute_var_lb: - self.substitute_var_lb = val - if val > self.substitute_var_ub: - self.substitute_var_ub = val - # Now set those bounds - transBlock.substitute_var.setlb(self.substitute_var_lb) - transBlock.substitute_var.setub(self.substitute_var_ub) - - log_dimension = ceil(log2(num_simplices)) - transBlock.log_simplex_indices = RangeSet(0, log_dimension - 1) - transBlock.binaries = Var(transBlock.log_simplex_indices, domain=Binary) - - # Injective function B: \mathcal{P} -> {0,1}^ceil(log_2(|P|)) used to identify simplices - # (really just polytopes are required) with binary vectors. Any injective function - # is enough here. - B = {} - for i in transBlock.simplex_indices: - # map index(P) -> corresponding vector in {0, 1}^n - B[i] = self._get_binary_vector(i, log_dimension) - - # The lambda variables \lambda_{P,v} are indexed by the simplex and the point in it - transBlock.lambdas = Var(transBlock.simplex_indices, transBlock.simplex_point_indices, bounds=(0, 1)) - - # Sum of all lambdas is one (6b) - transBlock.convex_combo = Constraint( - expr=sum( - transBlock.lambdas[P, v] - for P in transBlock.simplex_indices - for v in transBlock.simplex_point_indices - ) - == 1 - ) - - # The branching rules, establishing using the binaries that only one simplex's lambdas - # may be nonzero - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.1) - def simplex_choice_1(b, l): - return ( - sum( - transBlock.lambdas[P, v] - for P in self._P_plus(B, l, transBlock.simplex_indices) - for v in transBlock.simplex_point_indices - ) - <= transBlock.binaries[l] - ) - - @transBlock.Constraint(transBlock.log_simplex_indices) # (6c.2) - def simplex_choice_2(b, l): - return ( - sum( - transBlock.lambdas[P, v] - for P in self._P_0(B, l, transBlock.simplex_indices) - for v in transBlock.simplex_point_indices - ) - <= 1 - transBlock.binaries[l] - ) - - # for i, (simplex, pwlf) in enumerate(choices): - # x_i = sum(lambda_P,v v_i, P in polytopes, v in V(P)) - @transBlock.Constraint(transBlock.dimension_indices) # (6a.1) - def x_constraint(b, i): - return pw_expr.args[i] == sum( - transBlock.lambdas[P, v] - * pw_linear_func._points[self.idx_to_simplex[P][v]][i] - for P in transBlock.simplex_indices - for v in transBlock.simplex_point_indices - ) - - # Make the substitute Var equal the PWLE (6a.2) - #for P, linear_func in simplices_and_lin_funcs: - # print(f"P, linear_func = {P}, {linear_func}") - # for v in transBlock.simplex_point_indices: - # print(f" v={v}") - # print(f" pt={pw_linear_func._points[P[v]]}") - # print( - # f" lin_func_val = {linear_func(*pw_linear_func._points[P[v]])}" - # ) - transBlock.set_substitute = Constraint( - expr=substitute_var - == sum( - sum( - transBlock.lambdas[P, v] - * linear_func(*pw_linear_func._points[self.idx_to_simplex[P][v]]) - for v in transBlock.simplex_point_indices - ) - for (P, linear_func) in simplex_indices_and_lin_funcs - ) - ) - - return substitute_var - - # Not a gray code, just a regular binary representation - # TODO this may not be optimal, test the gray codes too - def _get_binary_vector(self, num, length): - if num != 0 and ceil(log2(num)) > length: - raise DeveloperError("Invalid input in _get_binary_vector") - # Hack: use python's string formatting instead of bothering with modular - # arithmetic. May be slow. - return tuple(int(x) for x in format(num, f"0{length}b")) - - # Return {P \in \mathcal{P} | B(P)_l = 0} - def _P_0(self, B, l, simplex_indices): - return [p for p in simplex_indices if B[p][l] == 0] - - # Return {P \in \mathcal{P} | B(P)_l = 1} - def _P_plus(self, B, l, simplex_indices): - return [p for p in simplex_indices if B[p][l] == 1] diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index d0a8cb1d34d..23ff61e6478 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -13,7 +13,18 @@ import math import itertools from functools import cmp_to_key - +from pyomo.environ import ( + ConcreteModel, + RangeSet, + Var, + Binary, + Constraint, + Param, + SolverFactory, + value, + Objective, + TerminationCondition, +) class Triangulation: Delaunay = 1 @@ -40,7 +51,9 @@ def _process_points_j1(points, 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") # munge the points into an organized map with n-dimensional keys - points.sort(key=cmp_to_key(_compare_lexicographic(dimension))) + #points.sort(key=cmp_to_key(_compare_lexicographic(dimension))) + # verify: does this do correct sorting by default? + points.sort() points_map = {} for point_index in itertools.product(range(K), repeat=dimension): point_flat_index = 0 @@ -91,3 +104,110 @@ def _get_j1_triangulation_3d(points, dimension): def _get_j1_triangulation_for_more_than_4d(points, dimension): pass + +def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): + # Set up a MIP (err, MIQCP) that orders our simplices and their vertices for us + # in the following way: + # + # (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 + m = ConcreteModel() + + # Sets and Params + m.SimplicesCount = Param(value=len(simplices)) + m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) + # For each of the simplices we need to choose an initial and a final vertex. + # The rest we can order arbitrarily after finishing the MIP solve. + m.SimplexVerticesCount = Param(value=len(simplices[0])) + m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) + @m.Param(m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + def TestVerticesEqual(m, i, n, j, k): + return 1 if simplices[i][n] == simplices[j][k] else 0 + + # Vars + # x_ij means simplex i is placed in slot j + m.x = Var(m.SIMPLICES, m.SIMPLICES, domain=Binary) + m.vertex_is_first = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + m.vertex_is_last = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + + + # Constraints + # Each simplex should have a slot and each slot should have a simplex + @m.Constraint(m.SIMPLICES) + def schedule_each_simplex(m, i): + return sum(m.x[i, j] for j in m.SIMPLICES) == 1 + @m.Constraint(m.SIMPLICES) + def schedule_each_slot(m, j): + return sum(m.x[i, j] for i in m.SIMPLICES) == 1 + + # Enforce property (1) + @m.Constraint(m.SIMPLICES) + def simplex_order(m, i): + if i == m.SimplicesCount - 1: + return Constraint.Skip # no ordering for the last one + # anything with at least a vertex in common is a neighbor + neighbors = [s for s in m.SIMPLICES if sum(TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= 1] + return sum(m.x[i, j] * m.x[k, j+1] for j in m.SIMPLICES for k in neighbors) == 1 + + # Each simplex needs exactly one first and exactly one last vertex + @m.Constraint(m.SIMPLICES) + def one_first_vertex(m, i): + return sum(m.vertex_is_first[i, n] for n in m.VERTEX_INDICES) == 1 + @m.Constraint(m.SIMPLICES) + def one_last_vertex(m, i): + return sum(m.vertex_is_last[i, n] for n in m.VERTEX_INDICES) == 1 + + # Enforce property (2) + @m.Constraint(m.SIMPLICES, m.SIMPLICES) + def vertex_order(m, i, j): + if i == m.SimplicesCount - 1: + return Constraint.Skip # no ordering for the last one + # Enforce only when j is the simplex following i. If not, RHS is zero + return ( + sum(m.vertex_is_last[i, n] * m.vertex_is_first[j, k] * m.TestVerticesEqual[i, n, j, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) + >= sum(m.x[i, p] * m.x[j, p + 1] for p in m.SIMPLICES if p != m.SimplicesCount - 1) + ) + + # Trivial objective (do I need this?) + m.obj = Objective(expr=0) + + # Solve model + results = SolverFactory(subsolver).solve(m) + match(results.solver.termination_condition): + case TerminationCondition.infeasible: + raise ValueError("The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1.") + case TerminationCondition.feasible: + pass + case _: + raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected termination condition {results.solver.termination_condition}") + + # Retrieve data + simplex_ordering = {} + for i in m.SIMPLICES: + for j in m.SIMPLICES: + if abs(value(m.x[i, j]) - 1) < 1e-5: + simplex_ordering[i] = j + break + vertex_ordering = {} + for i in m.SIMPLICES: + first = None + last = None + for n in m.VERTEX_INDICES: + if abs(value(m.vertex_is_first[i, n]) - 1) < 1e-5: + first = n + vertex_ordering[i, 0] = first + if abs(value(m.vertex_is_last[i, n]) - 1) < 1e-5: + last = n + vertex_ordering[i, m.SimplexVerticesCount - 1] = last + if first is not None and last is not None: + break + # Fill in the middle ones arbitrarily + idx = 1 + for j in range(m.SimplexVerticesCount): + if j != first and j != last: + vertex_ordering[idx] = j + idx += 1 + + return simplex_ordering, vertex_ordering From e8bfc8cb06f7e00334d969f874c5a56cf052ee3b Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 17 May 2024 17:12:43 -0400 Subject: [PATCH 025/220] Finish implementing a MIP to order triangulations for the incremental transformation. However, gurobi is not magical enough to see the structure here so this basically amounts to asking for a Hamiltonian path in a big graph, and then some. I think I will have to do the 70s stuff after all. Some optimizations would reduce this to "only" asking for a Hamiltonian path on a sparser graph but that is still terrible. --- .../piecewise/tests/test_triangulations.py | 84 ++++++++ pyomo/contrib/piecewise/triangulations.py | 185 +++++++++++------- 2 files changed, 202 insertions(+), 67 deletions(-) create mode 100644 pyomo/contrib/piecewise/tests/test_triangulations.py diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py new file mode 100644 index 00000000000..5eb36da4250 --- /dev/null +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -0,0 +1,84 @@ +# ___________________________________________________________________________ +# +# 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.triangulations import ( + get_j1_triangulation, + get_incremental_simplex_ordering +) + +class TestTriangulations(unittest.TestCase): + + def test_J1_small(self): + points = [ + [0, 0], [0, 1], [0, 2], + [1, 0], [1, 1], [1, 2], + [2, 0], [2, 1], [2, 2], + ] + triangulation = get_j1_triangulation(points, 2) + self.assertEqual(triangulation.simplices, + { + 0: [[0, 0], [0, 1], [1, 1]], + 1: [[0, 1], [0, 2], [1, 1]], + 2: [[1, 1], [2, 0], [2, 1]], + 3: [[1, 1], [2, 1], [2, 2]], + 4: [[0, 0], [1, 0], [1, 1]], + 5: [[0, 2], [1, 1], [1, 2]], + 6: [[1, 0], [1, 1], [2, 0]], + 7: [[1, 1], [1, 2], [2, 2]], + }) + + # check that the points_map functionality does what it should + def test_J1_small_offset(self): + points = [ + [0.5, 0.5], [0.5, 1.5], [0.5, 2.5], + [1.5, 0.5], [1.5, 1.5], [1.5, 2.5], + [2.5, 0.5], [2.5, 1.5], [2.5, 2.5], + ] + triangulation = get_j1_triangulation(points, 2) + self.assertEqual(triangulation.simplices, + { + 0: [[0.5, 0.5], [0.5, 1.5], [1.5, 1.5]], + 1: [[0.5, 1.5], [0.5, 2.5], [1.5, 1.5]], + 2: [[1.5, 1.5], [2.5, 0.5], [2.5, 1.5]], + 3: [[1.5, 1.5], [2.5, 1.5], [2.5, 2.5]], + 4: [[0.5, 0.5], [1.5, 0.5], [1.5, 1.5]], + 5: [[0.5, 2.5], [1.5, 1.5], [1.5, 2.5]], + 6: [[1.5, 0.5], [1.5, 1.5], [2.5, 0.5]], + 7: [[1.5, 1.5], [1.5, 2.5], [2.5, 2.5]], + }) + + def test_J1_small_ordering(self): + points = [ + [0.5, 0.5], [0.5, 1.5], [0.5, 2.5], + [1.5, 0.5], [1.5, 1.5], [1.5, 2.5], + [2.5, 0.5], [2.5, 1.5], [2.5, 2.5], + ] + triangulation = get_j1_triangulation(points, 2) + reordered_simplices = get_incremental_simplex_ordering(triangulation.simplices) + for idx, first_simplex in reordered_simplices.items(): + if idx != len(triangulation.points) - 1: + second_simplex = reordered_simplices[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_medium_ordering(self): + points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) + triangulation = get_j1_triangulation(points, 2) + reordered_simplices = get_incremental_simplex_ordering(triangulation.simplices) + for idx, first_simplex in reordered_simplices.items(): + if idx != len(triangulation.points) - 1: + second_simplex = reordered_simplices[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 23ff61e6478..d522c247a0f 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -12,6 +12,7 @@ from pytest import set_trace import math import itertools +from types import SimpleNamespace from functools import cmp_to_key from pyomo.environ import ( ConcreteModel, @@ -31,23 +32,33 @@ class Triangulation: J1 = 2 def get_j1_triangulation(points, dimension): - points_map, K = _process_points_j1(points, dimension) - if dimension == 2: - return _get_j1_triangulation_2d(points_map, K) - elif dimension == 3: - return _get_j1_triangulation_3d(points, dimension) - else: - return _get_j1_triangulation_for_more_than_4d(points, dimension) + points_map, num_pts = _process_points_j1(points, dimension) + simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) + # make a duck-typed thing that superficially looks like an instance of + # scipy.spatial.Delaunay (these are NDarrays in the original) + triangulation = SimpleNamespace() + triangulation.points = list(range(len(simplices_list))) + triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} + triangulation.coplanar = [] + + return triangulation + + #if dimension == 2: + # return _get_j1_triangulation_2d(points_map, num_pts) + #elif dimension == 3: + # return _get_j1_triangulation_3d(points, dimension) + #else: + # return _get_j1_triangulation_for_more_than_4d(points, dimension) # 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") - K = math.floor(len(points) ** (1 / dimension)) - if not len(points) == K**dimension: + num_pts = math.floor(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 K % 2 == 1: + 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 with n-dimensional keys @@ -55,30 +66,57 @@ def _process_points_j1(points, dimension): # verify: does this do correct sorting by default? points.sort() points_map = {} - for point_index in itertools.product(range(K), repeat=dimension): + 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] * K**n + point_flat_index += point_index[dimension - 1 - n] * num_pts**n points_map[point_index] = points[point_flat_index] - return points_map, K + return points_map, num_pts -def _compare_lexicographic(dimension): - def compare_lexicographic_real(x, y): - for n in range(dimension): - if x[n] < y[n]: - return -1 - elif y[n] < x[n]: - return 1 - return 0 - return compare_lexicographic_real - -def _get_j1_triangulation_2d(points_map, K): +#def _compare_lexicographic(dimension): +# def compare_lexicographic_real(x, y): +# for n in range(dimension): +# if x[n] < y[n]: +# return -1 +# elif y[n] < x[n]: +# return 1 +# return 0 +# return compare_lexicographic_real + +# This implements the J1 "Union Jack" triangulation (Todd 77) as explained by +# Vielma 2010. +# Triangulate {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[*current]) + for i in range(0, n): + current = current.copy() + current[pi[i]] += s[pi[i]] + simplex.append(points_map[*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 + +def _get_j1_triangulation_2d(points_map, num_pts): # Each square needs two triangles in it, orientation determined by the parity of # the bottom-left corner's coordinate indices (x and y). Same parity = top-left # and bottom-right triangles; different parity = top-right and bottom-left triangles. simplices = [] - for i in range(K): - for j in range(K): + for i in range(num_pts): + for j in range(num_pts): if i % 2 == j % 2: simplices.append( (points_map[i, j], @@ -113,14 +151,28 @@ def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): # 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 + # + # Note that (2) implies (1), so we only need to enforce that. + # + # TODO: issue: I don't think gurobi is magical enough to notice the special structure + # of this so it's basically looking for a hamiltonian path in a big graph... + # If we want to resolve this, we need to at least partially go back to the 70s thing + # + # An alternative approach is to order the simplices instead of the vertices. To + # do this, the condition (1) should be that they share a 1-face, not just a + # vertex. Then there is always a consistent way to choose distinct first and + # last vertices, which would otherwise be the issue - the rest of the vertex + # ordering can be arbitrary. Then we are really looking for a hamiltonian + # path which is what Todd did. However, we then fail to find orderings for + # strange triangulations such as two triangles intersecting at a point. m = ConcreteModel() # Sets and Params - m.SimplicesCount = Param(value=len(simplices)) + m.SimplicesCount = Param(initialize=len(simplices)) m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) # For each of the simplices we need to choose an initial and a final vertex. # The rest we can order arbitrarily after finishing the MIP solve. - m.SimplexVerticesCount = Param(value=len(simplices[0])) + m.SimplexVerticesCount = Param(initialize=len(simplices[0])) m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) @m.Param(m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) def TestVerticesEqual(m, i, n, j, k): @@ -142,14 +194,12 @@ def schedule_each_simplex(m, i): def schedule_each_slot(m, j): return sum(m.x[i, j] for i in m.SIMPLICES) == 1 - # Enforce property (1) - @m.Constraint(m.SIMPLICES) - def simplex_order(m, i): - if i == m.SimplicesCount - 1: - return Constraint.Skip # no ordering for the last one - # anything with at least a vertex in common is a neighbor - neighbors = [s for s in m.SIMPLICES if sum(TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= 1] - return sum(m.x[i, j] * m.x[k, j+1] for j in m.SIMPLICES for k in neighbors) == 1 + # Enforce property (1), but this is guaranteed by (2) so unnecessary + #@m.Constraint(m.SIMPLICES) + #def simplex_order(m, i): + # # anything with at least a vertex in common is a neighbor + # neighbors = [s for s in m.SIMPLICES if sum(m.TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= 1] + # return sum(m.x[i, j] * m.x[k, j+1] for j in m.SIMPLICES if j != m.SimplicesCount - 1 for k in neighbors) == 1 # Each simplex needs exactly one first and exactly one last vertex @m.Constraint(m.SIMPLICES) @@ -159,11 +209,14 @@ def one_first_vertex(m, i): def one_last_vertex(m, i): return sum(m.vertex_is_last[i, n] for n in m.VERTEX_INDICES) == 1 - # Enforce property (2) + # The last vertex cannot be the same as the first vertex + @m.Constraint(m.SIMPLICES, m.VERTEX_INDICES) + def first_last_distinct(m, i, n): + return m.vertex_is_first[i, n] * m.vertex_is_last[i, n] == 0 + + # Enforce property (2). This also guarantees property (1) @m.Constraint(m.SIMPLICES, m.SIMPLICES) def vertex_order(m, i, j): - if i == m.SimplicesCount - 1: - return Constraint.Skip # no ordering for the last one # Enforce only when j is the simplex following i. If not, RHS is zero return ( sum(m.vertex_is_last[i, n] * m.vertex_is_first[j, k] * m.TestVerticesEqual[i, n, j, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) @@ -174,40 +227,38 @@ def vertex_order(m, i, j): m.obj = Objective(expr=0) # Solve model - results = SolverFactory(subsolver).solve(m) + results = SolverFactory(subsolver).solve(m, tee=True) match(results.solver.termination_condition): case TerminationCondition.infeasible: raise ValueError("The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1.") - case TerminationCondition.feasible: + case TerminationCondition.optimal: pass case _: - raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected termination condition {results.solver.termination_condition}") + raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}") # Retrieve data - simplex_ordering = {} - for i in m.SIMPLICES: - for j in m.SIMPLICES: + #m.pprint() + new_simplices = {} + for j in m.SIMPLICES: + for i in m.SIMPLICES: if abs(value(m.x[i, j]) - 1) < 1e-5: - simplex_ordering[i] = j - break - vertex_ordering = {} - for i in m.SIMPLICES: - first = None - last = None - for n in m.VERTEX_INDICES: - if abs(value(m.vertex_is_first[i, n]) - 1) < 1e-5: - first = n - vertex_ordering[i, 0] = first - if abs(value(m.vertex_is_last[i, n]) - 1) < 1e-5: - last = n - vertex_ordering[i, m.SimplexVerticesCount - 1] = last - if first is not None and last is not None: + # The jth slot is occupied by the ith simplex + old_simplex = simplices[i] + # Reorder its vertices, too + first = None + last = None + for n in m.VERTEX_INDICES: + if abs(value(m.vertex_is_first[i, n]) - 1) < 1e-5: + first = n + if abs(value(m.vertex_is_last[i, n]) - 1) < 1e-5: + last = n + if first is not None and last is not None: + break + new_simplex = [old_simplex[first]] + for n in m.VERTEX_INDICES: + if n != first and n != last: + new_simplex.append(old_simplex[n]) + new_simplex.append(old_simplex[last]) + new_simplices[j] = new_simplex break - # Fill in the middle ones arbitrarily - idx = 1 - for j in range(m.SimplexVerticesCount): - if j != first and j != last: - vertex_ordering[idx] = j - idx += 1 - - return simplex_ordering, vertex_ordering + return new_simplices \ No newline at end of file From 9d1e6533e114b4a4026993d278f7dcda71c1646e Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 24 May 2024 10:28:28 -0400 Subject: [PATCH 026/220] add a slightly smaller but still unsolvable mip for when you can make mild assumptions --- .../piecewise/tests/test_triangulations.py | 23 ++- pyomo/contrib/piecewise/triangulations.py | 142 +++++++++++++++--- 2 files changed, 143 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 5eb36da4250..86bda3105eb 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -15,7 +15,8 @@ import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.triangulations import ( get_j1_triangulation, - get_incremental_simplex_ordering + get_incremental_simplex_ordering, + get_incremental_simplex_ordering_assume_connected_by_n_face, ) class TestTriangulations(unittest.TestCase): @@ -82,3 +83,23 @@ def test_J1_medium_ordering(self): second_simplex = reordered_simplices[idx + 1] # test property (2) which also guarantees property (1) self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_medium_ordering_alt(self): + points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) + triangulation = get_j1_triangulation(points, 2) + reordered_simplices = get_incremental_simplex_ordering_assume_connected_by_n_face(triangulation.simplices, 1) + for idx, first_simplex in reordered_simplices.items(): + if idx != len(triangulation.points) - 1: + second_simplex = reordered_simplices[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_medium_ordering_3d(self): + points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6], [-5, -1, 0.2, 3, 10])) + triangulation = get_j1_triangulation(points, 3) + reordered_simplices = get_incremental_simplex_ordering_assume_connected_by_n_face(triangulation.simplices, 2) + for idx, first_simplex in reordered_simplices.items(): + if idx != len(triangulation.points) - 1: + second_simplex = reordered_simplices[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index d522c247a0f..f33fd3f4304 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -14,6 +14,7 @@ import itertools from types import SimpleNamespace from functools import cmp_to_key +from pyomo.common.errors import DeveloperError from pyomo.environ import ( ConcreteModel, RangeSet, @@ -26,6 +27,10 @@ Objective, TerminationCondition, ) +from pyomo.common.dependencies import attempt_import +nx, nx_available = attempt_import( + 'networkx', 'Networkx is required to calculate incremental ordering.' +) class Triangulation: Delaunay = 1 @@ -55,15 +60,13 @@ def get_j1_triangulation(points, dimension): def _process_points_j1(points, dimension): if not len(points[0]) == dimension: raise ValueError("Points not consistent with specified dimension") - num_pts = math.floor(len(points) ** (1 / 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 with n-dimensional keys - #points.sort(key=cmp_to_key(_compare_lexicographic(dimension))) - # verify: does this do correct sorting by default? points.sort() points_map = {} for point_index in itertools.product(range(num_pts), repeat=dimension): @@ -73,16 +76,6 @@ def _process_points_j1(points, dimension): points_map[point_index] = points[point_flat_index] return points_map, num_pts -#def _compare_lexicographic(dimension): -# def compare_lexicographic_real(x, y): -# for n in range(dimension): -# if x[n] < y[n]: -# return -1 -# elif y[n] < x[n]: -# return 1 -# return 0 -# return compare_lexicographic_real - # This implements the J1 "Union Jack" triangulation (Todd 77) as explained by # Vielma 2010. # Triangulate {0, ..., K}^n for even K using the J1 triangulation, mapping the @@ -193,13 +186,6 @@ def schedule_each_simplex(m, i): @m.Constraint(m.SIMPLICES) def schedule_each_slot(m, j): return sum(m.x[i, j] for i in m.SIMPLICES) == 1 - - # Enforce property (1), but this is guaranteed by (2) so unnecessary - #@m.Constraint(m.SIMPLICES) - #def simplex_order(m, i): - # # anything with at least a vertex in common is a neighbor - # neighbors = [s for s in m.SIMPLICES if sum(m.TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= 1] - # return sum(m.x[i, j] * m.x[k, j+1] for j in m.SIMPLICES if j != m.SimplicesCount - 1 for k in neighbors) == 1 # Each simplex needs exactly one first and exactly one last vertex @m.Constraint(m.SIMPLICES) @@ -261,4 +247,118 @@ def vertex_order(m, i, j): new_simplex.append(old_simplex[last]) new_simplices[j] = new_simplex break - return new_simplices \ No newline at end of file + return new_simplices + +# If we have the assumption that our ordering is possible such that consecutively +# ordered simplices share at least a one-face, then getting an order for the +# simplices is enough to get one for the edges and we "just" need to find a +# Hamiltonian path +def get_incremental_simplex_ordering_assume_connected_by_n_face(simplices, connected_face_dim, subsolver='gurobi'): + if connected_face_dim == 0: + return get_incremental_simplex_ordering(simplices) + #if not nx_available: + # raise ImportError('Missing Networkx') + #G = nx.Graph() + #G.add_nodes_from(range(len(simplices))) + #for i in range(len(simplices)): + # for j in range(i + 1, len(simplices)): + # if len(set(simplices[i]) & set(simplices[j])) >= n + 1: + # G.add_edge(i, j) + + # ask Gurobi again because networkx doesn't seem to have a general hamiltonian + # path and I don't want to implement it myself + + m = ConcreteModel() + + # Sets and Params + m.SimplicesCount = Param(initialize=len(simplices)) + m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) + # For each of the simplices we need to choose an initial and a final vertex. + # The rest we can order arbitrarily after finishing the MIP solve. + m.SimplexVerticesCount = Param(initialize=len(simplices[0])) + m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) + @m.Param(m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + def TestVerticesEqual(m, i, n, j, k): + return 1 if simplices[i][n] == simplices[j][k] else 0 + + # Vars + # x_ij means simplex i is placed in slot j + m.x = Var(m.SIMPLICES, m.SIMPLICES, domain=Binary) + + # Constraints + # Each simplex should have a slot and each slot should have a simplex + @m.Constraint(m.SIMPLICES) + def schedule_each_simplex(m, i): + return sum(m.x[i, j] for j in m.SIMPLICES) == 1 + @m.Constraint(m.SIMPLICES) + def schedule_each_slot(m, j): + return sum(m.x[i, j] for i in m.SIMPLICES) == 1 + + # Enforce property (1) + @m.Constraint(m.SIMPLICES) + def simplex_order(m, i): + # anything with at least a vertex in common is a neighbor + neighbors = [s for s in m.SIMPLICES if sum(m.TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= connected_face_dim + 1 and s != i] + #print(f'neighbors of {i} are {neighbors}') + return sum(m.x[i, j] * m.x[k, j + 1] for j in m.SIMPLICES if j != m.SimplicesCount - 1 for k in neighbors) + m.x[i, m.SimplicesCount - 1] == 1 + + # Trivial objective (do I need this?) + m.obj = Objective(expr=0) + + #m.pprint() + # Solve model + results = SolverFactory(subsolver).solve(m, tee=True) + match(results.solver.termination_condition): + case TerminationCondition.infeasible: + raise ValueError(f"The triangulation was impossible to suitably order for the incremental transformation under the assumption that consecutive simplices share {connected_face_dim}-faces. Try relaxing that assumption, or try a different triangulation, such as J1.") + case TerminationCondition.optimal: + pass + case _: + raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}") + + # Retrieve data + new_simplices = {} + for j in m.SIMPLICES: + for i in m.SIMPLICES: + if abs(value(m.x[i, j]) - 1) < 1e-5: + # The jth slot is occupied by the ith simplex + new_simplices[j] = simplices[i] + # Note vertices need to be fixed after the fact now + break + fix_vertices_incremental_order(new_simplices) + return new_simplices + +# 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 simplices.items(): + # 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: + for n in range(last_vertex_index + 1): + if simplex[n] == simplices[i - 1][last_vertex_index]: + first = n + break + + 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 + if first == None or last == None: + raise DeveloperError("Couldn't fix vertex ordering for incremental.") + + # reorder the simplex with the desired first and last + new_simplex = [simplex[first]] + for n in range(last_vertex_index + 1): + if n != first and n != last: + new_simplex.append(simplex[n]) + new_simplex.append(simplex[last]) + simplices[i] = new_simplex \ No newline at end of file From 095c4e1f31b783160264fa17f2281e9e58fbf53b Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 24 May 2024 14:27:31 -0400 Subject: [PATCH 027/220] implement proof by picture, now 2d is fast --- .../piecewise/tests/test_triangulations.py | 40 ++++ pyomo/contrib/piecewise/triangulations.py | 198 +++++++++++++++--- 2 files changed, 210 insertions(+), 28 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 86bda3105eb..28ee61909da 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -103,3 +103,43 @@ def test_J1_medium_ordering_3d(self): second_simplex = reordered_simplices[idx + 1] # test property (2) which also guarantees property (1) self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_2d_ordering_0(self): + points = list(itertools.product([0, 1, 2], [1, 2.4, 3])) + ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices + self.assertEqual(len(ordered_triangulation), 8) + for idx, first_simplex in ordered_triangulation.items(): + if idx != len(ordered_triangulation) - 1: + second_simplex = ordered_triangulation[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_2d_ordering_1(self): + points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) + ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices + self.assertEqual(len(ordered_triangulation), 32) + for idx, first_simplex in ordered_triangulation.items(): + if idx != len(ordered_triangulation) - 1: + second_simplex = ordered_triangulation[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_2d_ordering_2(self): + points = list(itertools.product([0, 1, 2, 4, 5, 6.3, 7.1], [1, 2.4, 3, 5, 6, 9.1, 10])) + ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices + self.assertEqual(len(ordered_triangulation), 72) + for idx, first_simplex in ordered_triangulation.items(): + if idx != len(ordered_triangulation) - 1: + second_simplex = ordered_triangulation[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + def test_J1_2d_ordering_3(self): + points = 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])) + ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices + self.assertEqual(len(ordered_triangulation), 128) + for idx, first_simplex in ordered_triangulation.items(): + if idx != len(ordered_triangulation) - 1: + second_simplex = ordered_triangulation[idx + 1] + # test property (2) which also guarantees property (1) + self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index f33fd3f4304..51090dadcb0 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -13,6 +13,7 @@ import math import itertools from types import SimpleNamespace +from enum import Enum from functools import cmp_to_key from pyomo.common.errors import DeveloperError from pyomo.environ import ( @@ -36,9 +37,13 @@ class Triangulation: Delaunay = 1 J1 = 2 -def get_j1_triangulation(points, dimension): +def get_j1_triangulation(points, dimension, ordered=False): points_map, num_pts = _process_points_j1(points, dimension) - simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) + + if ordered and dimension == 2: + simplices_list = _get_j1_triangulation_2d_ordered(points_map, num_pts - 1) + else: + simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) # make a duck-typed thing that superficially looks like an instance of # scipy.spatial.Delaunay (these are NDarrays in the original) triangulation = SimpleNamespace() @@ -103,32 +108,169 @@ def _get_j1_triangulation(points_map, K, n): ret.append(sorted(simplex)) return ret -def _get_j1_triangulation_2d(points_map, num_pts): - # Each square needs two triangles in it, orientation determined by the parity of - # the bottom-left corner's coordinate indices (x and y). Same parity = top-left - # and bottom-right triangles; different parity = top-right and bottom-left triangles. - simplices = [] - for i in range(num_pts): - for j in range(num_pts): - if i % 2 == j % 2: - simplices.append( - (points_map[i, j], - points_map[i + 1, j + 1], - points_map[i, j + 1])) - simplices.append( - (points_map[i, j], - points_map[i + 1, j + 1], - points_map[i + 1, j])) - else: - simplices.append( - (points_map[i + 1, j], - points_map[i, j + 1], - points_map[i, j])) - simplices.append( - (points_map[i + 1, j], - points_map[i, j + 1], - points_map[i + 1, j + 1])) - return simplices +# Implement proof-by-picture from Todd 1977. I do the reverse order he does +# and also keep the pictures slightly more regular to make things easier to +# implement. +def _get_j1_triangulation_2d_ordered(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 + class Direction(Enum): + left = 0 + down = 1 + up = 2 + right = 3 + 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[len(simplices)] = (points_map[x, y], points_map[x + 1, y], points_map[x + 1, y + 1]) + def add_top_right(): + simplices[len(simplices)] = (points_map[x, y + 1], points_map[x + 1, y], points_map[x + 1, y + 1]) + def add_bottom_left(): + simplices[len(simplices)] = (points_map[x, y], points_map[x, y + 1], points_map[x + 1, y]) + def add_top_left(): + simplices[len(simplices)] = (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): + match(facing): + case 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 + case 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 + case 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 + assert square_parity_tlbr(x, y), "uh oh" + 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 + case 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 + assert not square_parity_tlbr(x, y), "uh oh" + 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_j1_triangulation_3d(points, dimension): pass From db5744b91f310c1ff4f4992bdff792887d235c1a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 24 May 2024 14:32:10 -0400 Subject: [PATCH 028/220] edit comment --- pyomo/contrib/piecewise/triangulations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 51090dadcb0..2e2f4e07cad 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -110,7 +110,8 @@ def _get_j1_triangulation(points_map, K, n): # Implement proof-by-picture from Todd 1977. I do the reverse order he does # and also keep the pictures slightly more regular to make things easier to -# implement. +# implement. Also remember that Todd's drawing is misleading to the point of +# almost being wrong so make sure you draw it properly first. def _get_j1_triangulation_2d_ordered(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 From dc043523d8465f3271f09971e1016a699fc62238 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 28 May 2024 13:20:03 -0600 Subject: [PATCH 029/220] handle uninitialized variable in propagate_solution of scaling transformation --- pyomo/core/plugins/transform/scaling.py | 11 +++++++---- pyomo/core/tests/transform/test_scaling.py | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 11d4ac8c493..654903773bd 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -313,10 +313,13 @@ 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 not None: + # NOTE: if the variable is set to None in the scaled model, + # we don't attempt to change its value in the original model + 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 d0fbfab61bd..e354916c309 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -690,6 +690,23 @@ def test_get_float_scaling_factor_intermediate_level(self): sf = ScaleModel()._get_float_scaling_factor(m.b1.b2.b3.v3) assert sf == float(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 + pyo.TransformationFactory("core.scale_model").propagate_solution( + scaled_model, m + ) + self.assertAlmostEqual(m.x[1].value, 2.0, delta=1e-8) + # Note that because x[2] was None in the scaled model, its value is unchanged + # (and has not been overridden and set to None). + self.assertEqual(m.x[2].value, 1.0) + if __name__ == "__main__": unittest.main() From 3d4eb861bfffee9d82049f10e5805474996cbf78 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 4 Jun 2024 12:42:15 -0400 Subject: [PATCH 030/220] further work on ordered J1 triangulation; add Gn_hamiltonian function --- .../piecewise/piecewise_linear_function.py | 4 + .../piecewise/tests/test_incremental.py | 139 ++++-- .../piecewise/tests/test_triangulations.py | 41 ++ .../piecewise/transform/incremental.py | 12 +- pyomo/contrib/piecewise/triangulations.py | 409 +++++++++++++++--- .../piecewise/union_jack_triangulate.py | 81 ---- 6 files changed, 499 insertions(+), 187 deletions(-) delete mode 100644 pyomo/contrib/piecewise/union_jack_triangulate.py diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 650145a143e..6285e1cd46f 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -21,6 +21,7 @@ ) from pyomo.contrib.piecewise.triangulations import ( get_j1_triangulation, + get_ordered_j1_triangulation, Triangulation, ) from pyomo.core import Any, NonNegativeIntegers, value, Var @@ -309,6 +310,9 @@ def _construct_simplices_from_multivariate_points(self, obj, parent, points, elif tri == Triangulation.J1: triangulation = get_j1_triangulation(points, dimension) obj._triangulation = tri + elif tri == Triangulation.OrderedJ1: + triangulation = get_ordered_j1_triangulation(points, dimension) + obj._triangulation = tri else: raise ValueError( "Unrecognized triangulation specified for '%s': %s" diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index 36bade33381..854629dccf5 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -12,6 +12,7 @@ import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct +from pyomo.contrib.piecewise.triangulations import Triangulation from pyomo.core.base import TransformationFactory from pyomo.core.expr.compare import ( assertExpressionsEqual, @@ -21,59 +22,119 @@ from pyomo.environ import Constraint, SolverFactory, Var, ConcreteModel, Objective, log, value, maximize from pyomo.contrib.piecewise import PiecewiseLinearFunction -from pyomo.contrib.piecewise.transform.incremental import IncrementalInnerGDPTransformation -from pyomo.contrib.piecewise.transform.disagreggated_logarithmic import ( - DisaggregatedLogarithmicInnerGDPTransformation -) +from pyomo.contrib.piecewise.transform.incremental import IncrementalGDPTransformation + +class TestTransformPiecewiseModelToIncrementalMIP(unittest.TestCase): + + def test_solve_log_model(self): + m = make_log_x_model_ordered() + 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) + + #def test_solve_univariate_log_model(self): + # m = ConcreteModel() + # m.x = Var(bounds=(1, 10)) + # m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) + + # # Here are the linear functions, for safe keeping. + # def f1(x): + # return (log(3) / 2) * x - log(3) / 2 + + # m.f1 = f1 + + # def f2(x): + # return (log(2) / 3) * x + log(3 / 2) + + # m.f2 = f2 + + # def f3(x): + # return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) + + # m.f3 = f3 -class TestTransformPiecewiseModelToNestedInnerRepnGDP(unittest.TestCase): + # m.log_expr = m.pw_log(m.x) + # m.obj = Objective(expr=m.log_expr, sense=maximize) - #def test_solve_log_model(self): - # m = models.make_log_x_model() # TransformationFactory( # 'contrib.piecewise.incremental' # ).apply_to(m) + # m.pprint() # TransformationFactory( # 'gdp.bigm' # ).apply_to(m) + # print('####### PPRINTNG AGAIN AFTER BIGM #######') + # m.pprint() + # # log is increasing so the optimal value should be log(10) # SolverFactory('gurobi').solve(m) - # ct.check_log_x_model_soln(self, m) - - def test_solve_univariate_log_model(self): - m = ConcreteModel() - m.x = Var(bounds=(1, 10)) - m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) + # print(f"optimal value is {value(m.obj)}") + # self.assertTrue(abs(value(m.obj) - log(10)) < 0.001) - # Here are the linear functions, for safe keeping. - def f1(x): - return (log(3) / 2) * x - log(3) / 2 - m.f1 = f1 +# Make a version of the log_x model with the simplices properly ordered for the +# incremental transform +def make_log_x_model_ordered(): + m = ConcreteModel() + m.x = Var(bounds=(1, 10)) + m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) + m.pw_log._triangulation = Triangulation.AssumeValid - def f2(x): - return (log(2) / 3) * x + log(3 / 2) + # Here are the linear functions, for safe keeping. + def f1(x): + return (log(3) / 2) * x - log(3) / 2 - m.f2 = f2 + m.f1 = f1 - def f3(x): - return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) + def f2(x): + return (log(2) / 3) * x + log(3 / 2) - m.f3 = f3 + m.f2 = f2 - m.log_expr = m.pw_log(m.x) - m.obj = Objective(expr=m.log_expr, sense=maximize) + def f3(x): + return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) - TransformationFactory( - 'contrib.piecewise.incremental' - #'contrib.piecewise.disaggregated_logarithmic' - ).apply_to(m) - m.pprint() - TransformationFactory( - 'gdp.bigm' - ).apply_to(m) - print('####### PPRINTNG AGAIN AFTER BIGM #######') - m.pprint() - # log is increasing so the optimal value should be log(10) - SolverFactory('gurobi').solve(m) - print(f"optimal value is {value(m.obj)}") - self.assertTrue(abs(value(m.obj) - log(10)) < 0.001) \ No newline at end of file + m.f3 = f3 + + m.log_expr = m.pw_log(m.x) + m.obj = Objective(expr=m.log_expr) + + m.x1 = Var(bounds=(0, 3)) + m.x2 = Var(bounds=(1, 7)) + + ## apprximates paraboloid x1**2 + x2**2 + def g1(x1, x2): + return 3 * x1 + 5 * x2 - 4 + + m.g1 = g1 + + def g2(x1, x2): + return 3 * x1 + 11 * x2 - 28 + + m.g2 = g2 + # order for incremental transformation + 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)], + ] + m.pw_paraboloid = PiecewiseLinearFunction( + simplices=simplices, linear_functions=[g1, g1, g2, g2] + ) + m.pw_paraboloid._triangulation = Triangulation.AssumeValid + m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2) + + def c_rule(m, i): + if i == 0: + return m.x >= m.paraboloid_expr + else: + return (1, m.x1, 2) + + m.indexed_c = Constraint([0, 1], rule=c_rule) + + return m \ No newline at end of file diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 28ee61909da..19b17b24d36 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -17,7 +17,10 @@ get_j1_triangulation, get_incremental_simplex_ordering, get_incremental_simplex_ordering_assume_connected_by_n_face, + get_Gn_hamiltonian, ) +from math import factorial +import itertools class TestTriangulations(unittest.TestCase): @@ -143,3 +146,41 @@ def test_J1_2d_ordering_3(self): second_simplex = ordered_triangulation[idx + 1] # test property (2) which also guarantees property (1) self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + + 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) + diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index dd26b8c5182..266784297e0 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -10,9 +10,10 @@ # ___________________________________________________________________________ from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -from pyomo.contrib.piecewise.transform.piecewise_to_gdp_transformation import ( - PiecewiseLinearToGDP, +from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( + PiecewiseLinearTransformationBase, ) +from pyomo.contrib.piecewise.triangulations import Triangulation from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet, Param from pyomo.core.base import TransformationFactory from pyomo.gdp import Disjunct, Disjunction @@ -20,6 +21,7 @@ from pyomo.core.expr.visitor import SimpleExpressionVisitor from pyomo.core.expr.current import identify_components from math import ceil, log2 +import logging @TransformationFactory.register( "contrib.piecewise.incremental", @@ -27,18 +29,20 @@ TODO document """, ) -class IncrementalInnerGDPTransformation(PiecewiseLinearToGDP): +class IncrementalGDPTransformation(PiecewiseLinearTransformationBase): """ TODO document """ - CONFIG = PiecewiseLinearToGDP.CONFIG() + CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_incremental" # Implement to use PiecewiseLinearToGDP. 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): self.DEBUG = False + if not (pw_linear_func.triangulation == Triangulation.OrderedJ1 or pw_linear_func.triangulation == Triangulation.AssumeValid): + logging.getLogger('pyomo.contrib.piecewise.transform.incremental').warning("Incremental transformation specified, but the triangulation may not be appropriately ordered. This is likely to lead to incorrect results!") # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 2e2f4e07cad..c5eaaead327 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -29,23 +29,42 @@ TerminationCondition, ) from pyomo.common.dependencies import attempt_import + nx, nx_available = attempt_import( 'networkx', 'Networkx is required to calculate incremental ordering.' ) + class Triangulation: + AssumeValid = 0 Delaunay = 1 J1 = 2 + OrderedJ1 = 3 + def get_j1_triangulation(points, dimension, ordered=False): points_map, num_pts = _process_points_j1(points, dimension) + simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) + # make a duck-typed thing that superficially looks like an instance of + # scipy.spatial.Delaunay (these are NDarrays in the original) + triangulation = SimpleNamespace() + triangulation.points = list(range(len(simplices_list))) + triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} + triangulation.coplanar = [] - if ordered and dimension == 2: + return triangulation + + +def get_ordered_j1_triangulation(points, dimension): + points_map, num_pts = _process_points_j1(points, dimension) + if dimension == 2: simplices_list = _get_j1_triangulation_2d_ordered(points_map, num_pts - 1) else: - simplices_list = _get_j1_triangulation(points_map, num_pts - 1, dimension) - # make a duck-typed thing that superficially looks like an instance of - # scipy.spatial.Delaunay (these are NDarrays in the original) + raise DeveloperError("Unimplemented!") + # elif dimension == 3: + # return _get_j1_triangulation_3d(points_map, num_pts - 1) + # else: + # return _get_j1_triangulation_for_more_than_4d(points_map, num_pts - 1) triangulation = SimpleNamespace() triangulation.points = list(range(len(simplices_list))) triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} @@ -53,13 +72,6 @@ def get_j1_triangulation(points, dimension, ordered=False): return triangulation - #if dimension == 2: - # return _get_j1_triangulation_2d(points_map, num_pts) - #elif dimension == 3: - # return _get_j1_triangulation_3d(points, dimension) - #else: - # return _get_j1_triangulation_for_more_than_4d(points, dimension) - # Does some validation but mostly assumes the user did the right thing def _process_points_j1(points, dimension): @@ -67,10 +79,14 @@ def _process_points_j1(points, 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") + 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") - + 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 with n-dimensional keys points.sort() points_map = {} @@ -81,6 +97,7 @@ def _process_points_j1(points, dimension): points_map[point_index] = points[point_flat_index] return points_map, num_pts + # This implements the J1 "Union Jack" triangulation (Todd 77) as explained by # Vielma 2010. # Triangulate {0, ..., K}^n for even K using the J1 triangulation, mapping the @@ -91,9 +108,11 @@ def _get_j1_triangulation(points_map, K, n): # 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)) + 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 = [] @@ -105,9 +124,10 @@ def _get_j1_triangulation(points_map, K, n): simplex.append(points_map[*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)) + ret.append(sorted(simplex)) return ret + # Implement proof-by-picture from Todd 1977. I do the reverse order he does # and also keep the pictures slightly more regular to make things easier to # implement. Also remember that Todd's drawing is misleading to the point of @@ -117,11 +137,13 @@ def _get_j1_triangulation_2d_ordered(points_map, num_pts): 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 + class Direction(Enum): left = 0 down = 1 up = 2 right = 3 + facing = None simplices = {} @@ -129,18 +151,36 @@ class Direction(Enum): # make it easier to read what I'm doing def add_bottom_right(): - simplices[len(simplices)] = (points_map[x, y], points_map[x + 1, y], points_map[x + 1, y + 1]) + simplices[len(simplices)] = ( + points_map[x, y], + points_map[x + 1, y], + points_map[x + 1, y + 1], + ) + def add_top_right(): - simplices[len(simplices)] = (points_map[x, y + 1], points_map[x + 1, y], points_map[x + 1, y + 1]) + simplices[len(simplices)] = ( + points_map[x, y + 1], + points_map[x + 1, y], + points_map[x + 1, y + 1], + ) + def add_bottom_left(): - simplices[len(simplices)] = (points_map[x, y], points_map[x, y + 1], points_map[x + 1, y]) - def add_top_left(): - simplices[len(simplices)] = (points_map[x, y], points_map[x, y + 1], points_map[x + 1, y + 1]) + simplices[len(simplices)] = ( + points_map[x, y], + points_map[x, y + 1], + points_map[x + 1, y], + ) + def add_top_left(): + simplices[len(simplices)] = ( + 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 + 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): @@ -151,10 +191,10 @@ def add_top_left(): add_top_right() facing = Direction.up y += 1 - + # state machine - while (True): - match(facing): + while True: + match (facing): case Direction.left: if square_parity_tlbr(x, y): add_bottom_right() @@ -276,9 +316,11 @@ def add_top_left(): def _get_j1_triangulation_3d(points, dimension): pass + def _get_j1_triangulation_for_more_than_4d(points, dimension): pass + def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): # Set up a MIP (err, MIQCP) that orders our simplices and their vertices for us # in the following way: @@ -287,7 +329,7 @@ def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): # 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 - # + # # Note that (2) implies (1), so we only need to enforce that. # # TODO: issue: I don't think gurobi is magical enough to notice the special structure @@ -310,7 +352,10 @@ def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): # The rest we can order arbitrarily after finishing the MIP solve. m.SimplexVerticesCount = Param(initialize=len(simplices[0])) m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) - @m.Param(m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + + @m.Param( + m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary + ) def TestVerticesEqual(m, i, n, j, k): return 1 if simplices[i][n] == simplices[j][k] else 0 @@ -320,12 +365,12 @@ def TestVerticesEqual(m, i, n, j, k): m.vertex_is_first = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) m.vertex_is_last = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) - # Constraints # Each simplex should have a slot and each slot should have a simplex @m.Constraint(m.SIMPLICES) def schedule_each_simplex(m, i): return sum(m.x[i, j] for j in m.SIMPLICES) == 1 + @m.Constraint(m.SIMPLICES) def schedule_each_slot(m, j): return sum(m.x[i, j] for i in m.SIMPLICES) == 1 @@ -334,39 +379,49 @@ def schedule_each_slot(m, j): @m.Constraint(m.SIMPLICES) def one_first_vertex(m, i): return sum(m.vertex_is_first[i, n] for n in m.VERTEX_INDICES) == 1 + @m.Constraint(m.SIMPLICES) def one_last_vertex(m, i): return sum(m.vertex_is_last[i, n] for n in m.VERTEX_INDICES) == 1 - + # The last vertex cannot be the same as the first vertex @m.Constraint(m.SIMPLICES, m.VERTEX_INDICES) def first_last_distinct(m, i, n): return m.vertex_is_first[i, n] * m.vertex_is_last[i, n] == 0 - + # Enforce property (2). This also guarantees property (1) @m.Constraint(m.SIMPLICES, m.SIMPLICES) def vertex_order(m, i, j): # Enforce only when j is the simplex following i. If not, RHS is zero - return ( - sum(m.vertex_is_last[i, n] * m.vertex_is_first[j, k] * m.TestVerticesEqual[i, n, j, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) - >= sum(m.x[i, p] * m.x[j, p + 1] for p in m.SIMPLICES if p != m.SimplicesCount - 1) + return sum( + m.vertex_is_last[i, n] + * m.vertex_is_first[j, k] + * m.TestVerticesEqual[i, n, j, k] + for n in m.VERTEX_INDICES + for k in m.VERTEX_INDICES + ) >= sum( + m.x[i, p] * m.x[j, p + 1] for p in m.SIMPLICES if p != m.SimplicesCount - 1 ) - + # Trivial objective (do I need this?) m.obj = Objective(expr=0) - + # Solve model results = SolverFactory(subsolver).solve(m, tee=True) - match(results.solver.termination_condition): + match (results.solver.termination_condition): case TerminationCondition.infeasible: - raise ValueError("The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1.") + raise ValueError( + "The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1." + ) case TerminationCondition.optimal: pass case _: - raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}") - + raise ValueError( + f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}" + ) + # Retrieve data - #m.pprint() + # m.pprint() new_simplices = {} for j in m.SIMPLICES: for i in m.SIMPLICES: @@ -392,22 +447,25 @@ def vertex_order(m, i, j): break return new_simplices + # If we have the assumption that our ordering is possible such that consecutively # ordered simplices share at least a one-face, then getting an order for the -# simplices is enough to get one for the edges and we "just" need to find a +# simplices is enough to get one for the edges and we "just" need to find a # Hamiltonian path -def get_incremental_simplex_ordering_assume_connected_by_n_face(simplices, connected_face_dim, subsolver='gurobi'): +def get_incremental_simplex_ordering_assume_connected_by_n_face( + simplices, connected_face_dim, subsolver='gurobi' +): if connected_face_dim == 0: return get_incremental_simplex_ordering(simplices) - #if not nx_available: + # if not nx_available: # raise ImportError('Missing Networkx') - #G = nx.Graph() - #G.add_nodes_from(range(len(simplices))) - #for i in range(len(simplices)): + # G = nx.Graph() + # G.add_nodes_from(range(len(simplices))) + # for i in range(len(simplices)): # for j in range(i + 1, len(simplices)): # if len(set(simplices[i]) & set(simplices[j])) >= n + 1: # G.add_edge(i, j) - + # ask Gurobi again because networkx doesn't seem to have a general hamiltonian # path and I don't want to implement it myself @@ -420,7 +478,10 @@ def get_incremental_simplex_ordering_assume_connected_by_n_face(simplices, conne # The rest we can order arbitrarily after finishing the MIP solve. m.SimplexVerticesCount = Param(initialize=len(simplices[0])) m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) - @m.Param(m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + + @m.Param( + m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary + ) def TestVerticesEqual(m, i, n, j, k): return 1 if simplices[i][n] == simplices[j][k] else 0 @@ -433,31 +494,55 @@ def TestVerticesEqual(m, i, n, j, k): @m.Constraint(m.SIMPLICES) def schedule_each_simplex(m, i): return sum(m.x[i, j] for j in m.SIMPLICES) == 1 + @m.Constraint(m.SIMPLICES) def schedule_each_slot(m, j): return sum(m.x[i, j] for i in m.SIMPLICES) == 1 - + # Enforce property (1) @m.Constraint(m.SIMPLICES) def simplex_order(m, i): # anything with at least a vertex in common is a neighbor - neighbors = [s for s in m.SIMPLICES if sum(m.TestVerticesEqual[i, n, s, k] for n in m.VERTEX_INDICES for k in m.VERTEX_INDICES) >= connected_face_dim + 1 and s != i] - #print(f'neighbors of {i} are {neighbors}') - return sum(m.x[i, j] * m.x[k, j + 1] for j in m.SIMPLICES if j != m.SimplicesCount - 1 for k in neighbors) + m.x[i, m.SimplicesCount - 1] == 1 - + neighbors = [ + s + for s in m.SIMPLICES + if sum( + m.TestVerticesEqual[i, n, s, k] + for n in m.VERTEX_INDICES + for k in m.VERTEX_INDICES + ) + >= connected_face_dim + 1 + and s != i + ] + # print(f'neighbors of {i} are {neighbors}') + return ( + sum( + m.x[i, j] * m.x[k, j + 1] + for j in m.SIMPLICES + if j != m.SimplicesCount - 1 + for k in neighbors + ) + + m.x[i, m.SimplicesCount - 1] + == 1 + ) + # Trivial objective (do I need this?) m.obj = Objective(expr=0) - - #m.pprint() + + # m.pprint() # Solve model results = SolverFactory(subsolver).solve(m, tee=True) - match(results.solver.termination_condition): + match (results.solver.termination_condition): case TerminationCondition.infeasible: - raise ValueError(f"The triangulation was impossible to suitably order for the incremental transformation under the assumption that consecutive simplices share {connected_face_dim}-faces. Try relaxing that assumption, or try a different triangulation, such as J1.") + raise ValueError( + f"The triangulation was impossible to suitably order for the incremental transformation under the assumption that consecutive simplices share {connected_face_dim}-faces. Try relaxing that assumption, or try a different triangulation, such as J1." + ) case TerminationCondition.optimal: pass case _: - raise ValueError(f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}") + raise ValueError( + f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}" + ) # Retrieve data new_simplices = {} @@ -471,6 +556,7 @@ def simplex_order(m, i): fix_vertices_incremental_order(new_simplices) return new_simplices + # 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 @@ -487,7 +573,7 @@ def fix_vertices_incremental_order(simplices): if simplex[n] == simplices[i - 1][last_vertex_index]: first = n break - + if i == len(simplices) - 1: last = last_vertex_index else: @@ -497,11 +583,208 @@ def fix_vertices_incremental_order(simplices): break if first == None or last == None: raise DeveloperError("Couldn't fix vertex ordering for incremental.") - + # reorder the simplex with the desired first and last new_simplex = [simplex[first]] for n in range(last_vertex_index + 1): if n != first and n != last: new_simplex.append(simplex[n]) new_simplex.append(simplex[last]) - simplices[i] = new_simplex \ No newline at end of file + simplices[i] = new_simplex + + +# G_n is 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): + if n < 4: + raise ValueError("n must be at least 4 for this operation to be possible") + # first is enough because we can just reverse every permutation + if last: + return [ + tuple(reversed(pi)) + for pi in get_Gn_hamiltonian( + n, tuple(reversed(start_permutation)), target_symbol, False + ) + ] + # 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) + return [ + tuple(start_permutation[pi[i] - 1] for i in range(n)) + for pi in _get_Gn_hamiltonian(n, new_target_symbol) + ] + else: + return _get_Gn_hamiltonian(n, target_symbol) + + +# 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(n, target_symbol): + # base case: proof by picture from Todd, Figure 2 + # note: Figure 2 contains an error, like half the figures and paragraphs do + 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: # non-awful case + # Well, it's still pretty awful. + idx = n - 1 + facing = -1 + ret = [] + for pi in _get_Gn_hamiltonian(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(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) + 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/piecewise/union_jack_triangulate.py b/pyomo/contrib/piecewise/union_jack_triangulate.py deleted file mode 100644 index 2853add8161..00000000000 --- a/pyomo/contrib/piecewise/union_jack_triangulate.py +++ /dev/null @@ -1,81 +0,0 @@ -# ___________________________________________________________________________ -# -# 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 math import factorial -import time - -# This implements the J1 "Union Jack" triangulation (Todd 77) as explained by -# Vielma 2010. - -# Triangulate {0, ..., K}^n for even K using the J1 triangulation. -def triangulate(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)) - J1 = [] - for v_0, pi, s in big_iterator: - simplex = [] - current = list(v_0) - simplex.append(current) - for i in range(0, n): - current = current.copy() - current[pi[i]] += s[pi[i]] - simplex.append(current) - J1.append(simplex) - return J1 - -if __name__ == '__main__': - # do some tests. TODO move to real test file - start0 = time.time() - small_2d = triangulate(2, 2) - elapsed0 = time.time() - start0 - print(f"triangulated small_2d in {elapsed0} sec.") - assert len(small_2d) == 8 - assert small_2d == [[[1, 1], [0, 1], [0, 0]], - [[1, 1], [0, 1], [0, 2]], - [[1, 1], [2, 1], [2, 0]], - [[1, 1], [2, 1], [2, 2]], - [[1, 1], [1, 0], [0, 0]], - [[1, 1], [1, 2], [0, 2]], - [[1, 1], [1, 0], [2, 0]], - [[1, 1], [1, 2], [2, 2]]] - start1 = time.time() - bigger_2d = triangulate(4, 2) - elapsed1 = time.time() - start1 - print(f"triangulated bigger_2d in {elapsed1} sec.") - assert len(bigger_2d) == 32 - - start2 = time.time() - medium_3d = triangulate(12, 3) - elapsed2 = time.time() - start2 - print(f"triangulated medium_3d in {elapsed2} sec.") - # A J1 triangulation of {0, ..., K}^n has K^n * n! simplices - assert len(medium_3d) == 12**3 * factorial(3) - - start3 = time.time() - big_4d = triangulate(20, 4) - elapsed3 = time.time() - start3 - print(f"triangulated big_4d in {elapsed3} sec.") - assert len(big_4d) == 20**4 * factorial(4) - - print("starting huge_5d") - start4 = time.time() - huge_5d = triangulate(10, 5) - elapsed4 = time.time() - start4 - print(f"triangulated huge_5d in {elapsed4} sec.") - - print("Success") From 5c1924ff011b7e30614eb2d8d7d36e2e83ba6e28 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 4 Jun 2024 17:49:27 -0400 Subject: [PATCH 031/220] implement and test J1 for 4d and above --- .../piecewise/piecewise_linear_function.py | 4 +- .../piecewise/tests/test_triangulations.py | 164 +++++++++--------- pyomo/contrib/piecewise/triangulations.py | 117 +++++++++++-- 3 files changed, 184 insertions(+), 101 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 6285e1cd46f..1050836626b 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -20,7 +20,7 @@ PiecewiseLinearExpression, ) from pyomo.contrib.piecewise.triangulations import ( - get_j1_triangulation, + get_unordered_j1_triangulation, get_ordered_j1_triangulation, Triangulation, ) @@ -308,7 +308,7 @@ def _construct_simplices_from_multivariate_points(self, obj, parent, points, raise obj._triangulation = tri elif tri == Triangulation.J1: - triangulation = get_j1_triangulation(points, dimension) + triangulation = get_unordered_j1_triangulation(points, dimension) obj._triangulation = tri elif tri == Triangulation.OrderedJ1: triangulation = get_ordered_j1_triangulation(points, dimension) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 19b17b24d36..ffad3b18f13 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -14,10 +14,13 @@ from unittest import skipUnless import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.triangulations import ( - get_j1_triangulation, + get_unordered_j1_triangulation, + get_ordered_j1_triangulation, get_incremental_simplex_ordering, get_incremental_simplex_ordering_assume_connected_by_n_face, get_Gn_hamiltonian, + get_grid_hamiltonian, + ) from math import factorial import itertools @@ -30,7 +33,7 @@ def test_J1_small(self): [1, 0], [1, 1], [1, 2], [2, 0], [2, 1], [2, 2], ] - triangulation = get_j1_triangulation(points, 2) + triangulation = get_unordered_j1_triangulation(points, 2) self.assertEqual(triangulation.simplices, { 0: [[0, 0], [0, 1], [1, 1]], @@ -50,7 +53,7 @@ def test_J1_small_offset(self): [1.5, 0.5], [1.5, 1.5], [1.5, 2.5], [2.5, 0.5], [2.5, 1.5], [2.5, 2.5], ] - triangulation = get_j1_triangulation(points, 2) + triangulation = get_unordered_j1_triangulation(points, 2) self.assertEqual(triangulation.simplices, { 0: [[0.5, 0.5], [0.5, 1.5], [1.5, 1.5]], @@ -62,90 +65,73 @@ def test_J1_small_offset(self): 6: [[1.5, 0.5], [1.5, 1.5], [2.5, 0.5]], 7: [[1.5, 1.5], [1.5, 2.5], [2.5, 2.5]], }) - - def test_J1_small_ordering(self): - points = [ - [0.5, 0.5], [0.5, 1.5], [0.5, 2.5], - [1.5, 0.5], [1.5, 1.5], [1.5, 2.5], - [2.5, 0.5], [2.5, 1.5], [2.5, 2.5], - ] - triangulation = get_j1_triangulation(points, 2) - reordered_simplices = get_incremental_simplex_ordering(triangulation.simplices) - for idx, first_simplex in reordered_simplices.items(): - if idx != len(triangulation.points) - 1: - second_simplex = reordered_simplices[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - - def test_J1_medium_ordering(self): - points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) - triangulation = get_j1_triangulation(points, 2) - reordered_simplices = get_incremental_simplex_ordering(triangulation.simplices) - for idx, first_simplex in reordered_simplices.items(): - if idx != len(triangulation.points) - 1: - second_simplex = reordered_simplices[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - - def test_J1_medium_ordering_alt(self): - points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) - triangulation = get_j1_triangulation(points, 2) - reordered_simplices = get_incremental_simplex_ordering_assume_connected_by_n_face(triangulation.simplices, 1) - for idx, first_simplex in reordered_simplices.items(): - if idx != len(triangulation.points) - 1: - second_simplex = reordered_simplices[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - - def test_J1_medium_ordering_3d(self): - points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6], [-5, -1, 0.2, 3, 10])) - triangulation = get_j1_triangulation(points, 3) - reordered_simplices = get_incremental_simplex_ordering_assume_connected_by_n_face(triangulation.simplices, 2) - for idx, first_simplex in reordered_simplices.items(): - if idx != len(triangulation.points) - 1: - second_simplex = reordered_simplices[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - - def test_J1_2d_ordering_0(self): - points = list(itertools.product([0, 1, 2], [1, 2.4, 3])) - ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices - self.assertEqual(len(ordered_triangulation), 8) - for idx, first_simplex in ordered_triangulation.items(): - if idx != len(ordered_triangulation) - 1: - second_simplex = ordered_triangulation[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - def test_J1_2d_ordering_1(self): - points = list(itertools.product([0, 1, 2, 4, 5], [1, 2.4, 3, 5, 6])) - ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices - self.assertEqual(len(ordered_triangulation), 32) - for idx, first_simplex in ordered_triangulation.items(): - if idx != len(ordered_triangulation) - 1: - second_simplex = ordered_triangulation[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") - - def test_J1_2d_ordering_2(self): - points = list(itertools.product([0, 1, 2, 4, 5, 6.3, 7.1], [1, 2.4, 3, 5, 6, 9.1, 10])) - ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices - self.assertEqual(len(ordered_triangulation), 72) + def check_J1_ordered(self, points, num_points, dim): + ordered_triangulation = get_ordered_j1_triangulation(points, dim).simplices + #print(ordered_triangulation) + self.assertEqual(len(ordered_triangulation), factorial(dim) * (num_points - 1) ** dim) for idx, first_simplex in ordered_triangulation.items(): if idx != len(ordered_triangulation) - 1: second_simplex = ordered_triangulation[idx + 1] # test property (2) which also guarantees property (1) 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 = 0 + for pt in first_simplex: + if pt in second_simplex: + count += 1 + #print(f"first_simplex={first_simplex}; second_simplex={second_simplex}") + self.assertEqual(count, dim) # (n-1)-face has n points + #if count != dim: + # print(f"error: count {count} was not the correct {dim}") + + 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, + ) + + 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, + ) + + 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 test_J1_2d_ordering_3(self): - points = 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])) - ordered_triangulation = get_j1_triangulation(points, 2, ordered=True).simplices - self.assertEqual(len(ordered_triangulation), 128) - for idx, first_simplex in ordered_triangulation.items(): - if idx != len(ordered_triangulation) - 1: - second_simplex = ordered_triangulation[idx + 1] - # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") def check_Gn_hamiltonian_path(self, n, start_permutation, target_symbol, last): path = get_Gn_hamiltonian(n, start_permutation, target_symbol, last) @@ -183,4 +169,22 @@ def test_Gn_hamiltonian_paths(self): 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) + diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index c5eaaead327..699d55931a5 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -28,21 +28,15 @@ Objective, TerminationCondition, ) -from pyomo.common.dependencies import attempt_import -nx, nx_available = attempt_import( - 'networkx', 'Networkx is required to calculate incremental ordering.' -) - - -class Triangulation: +class Triangulation(Enum): AssumeValid = 0 Delaunay = 1 J1 = 2 OrderedJ1 = 3 -def get_j1_triangulation(points, dimension, ordered=False): +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) # make a duck-typed thing that superficially looks like an instance of @@ -58,13 +52,12 @@ def get_j1_triangulation(points, dimension, ordered=False): def get_ordered_j1_triangulation(points, dimension): points_map, num_pts = _process_points_j1(points, dimension) if dimension == 2: - simplices_list = _get_j1_triangulation_2d_ordered(points_map, num_pts - 1) - else: + simplices_list = _get_ordered_j1_triangulation_2d(points_map, num_pts - 1) + elif dimension == 3: raise DeveloperError("Unimplemented!") - # elif dimension == 3: - # return _get_j1_triangulation_3d(points_map, num_pts - 1) - # else: - # return _get_j1_triangulation_for_more_than_4d(points_map, num_pts - 1) + #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) triangulation = SimpleNamespace() triangulation.points = list(range(len(simplices_list))) triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} @@ -132,7 +125,7 @@ def _get_j1_triangulation(points_map, K, n): # and also keep the pictures slightly more regular to make things easier to # implement. Also remember that Todd's drawing is misleading to the point of # almost being wrong so make sure you draw it properly first. -def _get_j1_triangulation_2d_ordered(points_map, num_pts): +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 @@ -313,12 +306,98 @@ def add_top_left(): continue -def _get_j1_triangulation_3d(points, dimension): +def _get_ordered_j1_triangulation_3d(points_map, num_pts): pass -def _get_j1_triangulation_for_more_than_4d(points, dimension): - pass +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[len(simplices)] = 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[len(simplices)] = get_one_j1_simplex(v_0, pi, sign, dim, points_map) + # should be true regardless of odd or even? I hope + 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[len(simplices)] = 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[*current]) + for i in range(0, dim): + current = current.copy() + current[pi[i] - 1] += sign[pi[i] - 1] + simplex.append(points_map[*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 def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): @@ -776,7 +855,7 @@ def _get_Gn_hamiltonian(n, target_symbol): second_last = ret.pop() # of form (n, 1, i, j, ...) i = last[2] j = last[3] - test = list(last) + test = list(last) # want permutation of form (n, 1, j, i, ...) with same tail test[0] = n test[1] = 1 test[2] = j From 56fd5e54e1520155733073b8c548d6c62833ea16 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 5 Jun 2024 16:05:12 -0400 Subject: [PATCH 032/220] Implement 3d version of ordered j1, finishing the necessary j1 functionality --- ...nerate_ordered_3d_j1_triangulation_data.py | 80 +++++++++++++++++++ .../ordered_3d_j1_triangulation_data.py | 2 + .../piecewise/tests/test_triangulations.py | 14 +++- pyomo/contrib/piecewise/triangulations.py | 63 +++++++++++++-- 4 files changed, 148 insertions(+), 11 deletions(-) create mode 100644 pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py create mode 100644 pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py diff --git a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py new file mode 100644 index 00000000000..738c09790de --- /dev/null +++ b/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py @@ -0,0 +1,80 @@ +import networkx as nx +import itertools + +# 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__': + # 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) + + # 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 path from {i} to {(j[0], 1) if (i, (j[0], 1)) in 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) 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..a1af2eb98dc --- /dev/null +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -0,0 +1,2 @@ +# Generated using generate_ordered_3d_j1_triangulation_data.py +hamiltonian_paths = {(((-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))]} \ No newline at end of file diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index ffad3b18f13..4eab2f9fd4c 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -16,17 +16,15 @@ from pyomo.contrib.piecewise.triangulations import ( get_unordered_j1_triangulation, get_ordered_j1_triangulation, - get_incremental_simplex_ordering, - get_incremental_simplex_ordering_assume_connected_by_n_face, get_Gn_hamiltonian, get_grid_hamiltonian, - ) from math import factorial import itertools class TestTriangulations(unittest.TestCase): + # check basic functionality for the unordered j1 triangulation. def test_J1_small(self): points = [ [0, 0], [0, 1], [0, 2], @@ -119,6 +117,16 @@ def test_J1_ordered_3d(self): 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, + ) def test_J1_ordered_4d_and_above(self): self.check_J1_ordered( diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 699d55931a5..754b83052a8 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -30,16 +30,17 @@ ) class Triangulation(Enum): - AssumeValid = 0 - Delaunay = 1 - J1 = 2 - OrderedJ1 = 3 + Unknown = 0 + AssumeValid = 1 + Delaunay = 2 + J1 = 3 + OrderedJ1 = 4 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) - # make a duck-typed thing that superficially looks like an instance of + # for now make a duck-typed thing that superficially looks like an instance of # scipy.spatial.Delaunay (these are NDarrays in the original) triangulation = SimpleNamespace() triangulation.points = list(range(len(simplices_list))) @@ -54,8 +55,7 @@ def get_ordered_j1_triangulation(points, dimension): if dimension == 2: simplices_list = _get_ordered_j1_triangulation_2d(points_map, num_pts - 1) elif dimension == 3: - raise DeveloperError("Unimplemented!") - #simplices_list = _get_ordered_j1_triangulation_3d(points_map, num_pts - 1) + 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) triangulation = SimpleNamespace() @@ -307,7 +307,54 @@ def add_top_left(): def _get_ordered_j1_triangulation_3d(points_map, num_pts): - pass + # Import these precomputed paths + from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import hamiltonian_paths as simplex_pair_to_path + + # 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 + 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 simplex_pair_to_path.keys(): + current_cube_path = 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 = 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[len(simplices)] = 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 simplex_pair_to_path.keys(): + current_cube_path = simplex_pair_to_path[(start_data, (direction_to_next, 1))] + else: + current_cube_path = simplex_pair_to_path[(start_data, (direction_to_next, 2))] + + for simplex_data in current_cube_path: + simplices[len(simplices)] = 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): From a8a1ab72d9470d7641d8ab56c284fac1d63318f9 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 5 Jun 2024 16:24:59 -0400 Subject: [PATCH 033/220] move probably-useless MIPs into their own util file --- pyomo/contrib/piecewise/triangulations.py | 252 ------------------ .../contrib/piecewise/triangulations_util.py | 240 +++++++++++++++++ 2 files changed, 240 insertions(+), 252 deletions(-) create mode 100644 pyomo/contrib/piecewise/triangulations_util.py diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 754b83052a8..f4b628f7a02 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -9,25 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pytest import set_trace -import math import itertools from types import SimpleNamespace from enum import Enum -from functools import cmp_to_key from pyomo.common.errors import DeveloperError -from pyomo.environ import ( - ConcreteModel, - RangeSet, - Var, - Binary, - Constraint, - Param, - SolverFactory, - value, - Objective, - TerminationCondition, -) class Triangulation(Enum): Unknown = 0 @@ -36,7 +21,6 @@ class Triangulation(Enum): J1 = 3 OrderedJ1 = 4 - 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) @@ -447,242 +431,6 @@ def get_grid_hamiltonian(dim, length): return ret -def get_incremental_simplex_ordering(simplices, subsolver='gurobi'): - # Set up a MIP (err, MIQCP) that orders our simplices and their vertices for us - # in the following way: - # - # (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 - # - # Note that (2) implies (1), so we only need to enforce that. - # - # TODO: issue: I don't think gurobi is magical enough to notice the special structure - # of this so it's basically looking for a hamiltonian path in a big graph... - # If we want to resolve this, we need to at least partially go back to the 70s thing - # - # An alternative approach is to order the simplices instead of the vertices. To - # do this, the condition (1) should be that they share a 1-face, not just a - # vertex. Then there is always a consistent way to choose distinct first and - # last vertices, which would otherwise be the issue - the rest of the vertex - # ordering can be arbitrary. Then we are really looking for a hamiltonian - # path which is what Todd did. However, we then fail to find orderings for - # strange triangulations such as two triangles intersecting at a point. - m = ConcreteModel() - - # Sets and Params - m.SimplicesCount = Param(initialize=len(simplices)) - m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) - # For each of the simplices we need to choose an initial and a final vertex. - # The rest we can order arbitrarily after finishing the MIP solve. - m.SimplexVerticesCount = Param(initialize=len(simplices[0])) - m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) - - @m.Param( - m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary - ) - def TestVerticesEqual(m, i, n, j, k): - return 1 if simplices[i][n] == simplices[j][k] else 0 - - # Vars - # x_ij means simplex i is placed in slot j - m.x = Var(m.SIMPLICES, m.SIMPLICES, domain=Binary) - m.vertex_is_first = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) - m.vertex_is_last = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) - - # Constraints - # Each simplex should have a slot and each slot should have a simplex - @m.Constraint(m.SIMPLICES) - def schedule_each_simplex(m, i): - return sum(m.x[i, j] for j in m.SIMPLICES) == 1 - - @m.Constraint(m.SIMPLICES) - def schedule_each_slot(m, j): - return sum(m.x[i, j] for i in m.SIMPLICES) == 1 - - # Each simplex needs exactly one first and exactly one last vertex - @m.Constraint(m.SIMPLICES) - def one_first_vertex(m, i): - return sum(m.vertex_is_first[i, n] for n in m.VERTEX_INDICES) == 1 - - @m.Constraint(m.SIMPLICES) - def one_last_vertex(m, i): - return sum(m.vertex_is_last[i, n] for n in m.VERTEX_INDICES) == 1 - - # The last vertex cannot be the same as the first vertex - @m.Constraint(m.SIMPLICES, m.VERTEX_INDICES) - def first_last_distinct(m, i, n): - return m.vertex_is_first[i, n] * m.vertex_is_last[i, n] == 0 - - # Enforce property (2). This also guarantees property (1) - @m.Constraint(m.SIMPLICES, m.SIMPLICES) - def vertex_order(m, i, j): - # Enforce only when j is the simplex following i. If not, RHS is zero - return sum( - m.vertex_is_last[i, n] - * m.vertex_is_first[j, k] - * m.TestVerticesEqual[i, n, j, k] - for n in m.VERTEX_INDICES - for k in m.VERTEX_INDICES - ) >= sum( - m.x[i, p] * m.x[j, p + 1] for p in m.SIMPLICES if p != m.SimplicesCount - 1 - ) - - # Trivial objective (do I need this?) - m.obj = Objective(expr=0) - - # Solve model - results = SolverFactory(subsolver).solve(m, tee=True) - match (results.solver.termination_condition): - case TerminationCondition.infeasible: - raise ValueError( - "The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1." - ) - case TerminationCondition.optimal: - pass - case _: - raise ValueError( - f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}" - ) - - # Retrieve data - # m.pprint() - new_simplices = {} - for j in m.SIMPLICES: - for i in m.SIMPLICES: - if abs(value(m.x[i, j]) - 1) < 1e-5: - # The jth slot is occupied by the ith simplex - old_simplex = simplices[i] - # Reorder its vertices, too - first = None - last = None - for n in m.VERTEX_INDICES: - if abs(value(m.vertex_is_first[i, n]) - 1) < 1e-5: - first = n - if abs(value(m.vertex_is_last[i, n]) - 1) < 1e-5: - last = n - if first is not None and last is not None: - break - new_simplex = [old_simplex[first]] - for n in m.VERTEX_INDICES: - if n != first and n != last: - new_simplex.append(old_simplex[n]) - new_simplex.append(old_simplex[last]) - new_simplices[j] = new_simplex - break - return new_simplices - - -# If we have the assumption that our ordering is possible such that consecutively -# ordered simplices share at least a one-face, then getting an order for the -# simplices is enough to get one for the edges and we "just" need to find a -# Hamiltonian path -def get_incremental_simplex_ordering_assume_connected_by_n_face( - simplices, connected_face_dim, subsolver='gurobi' -): - if connected_face_dim == 0: - return get_incremental_simplex_ordering(simplices) - # if not nx_available: - # raise ImportError('Missing Networkx') - # G = nx.Graph() - # G.add_nodes_from(range(len(simplices))) - # for i in range(len(simplices)): - # for j in range(i + 1, len(simplices)): - # if len(set(simplices[i]) & set(simplices[j])) >= n + 1: - # G.add_edge(i, j) - - # ask Gurobi again because networkx doesn't seem to have a general hamiltonian - # path and I don't want to implement it myself - - m = ConcreteModel() - - # Sets and Params - m.SimplicesCount = Param(initialize=len(simplices)) - m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) - # For each of the simplices we need to choose an initial and a final vertex. - # The rest we can order arbitrarily after finishing the MIP solve. - m.SimplexVerticesCount = Param(initialize=len(simplices[0])) - m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) - - @m.Param( - m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary - ) - def TestVerticesEqual(m, i, n, j, k): - return 1 if simplices[i][n] == simplices[j][k] else 0 - - # Vars - # x_ij means simplex i is placed in slot j - m.x = Var(m.SIMPLICES, m.SIMPLICES, domain=Binary) - - # Constraints - # Each simplex should have a slot and each slot should have a simplex - @m.Constraint(m.SIMPLICES) - def schedule_each_simplex(m, i): - return sum(m.x[i, j] for j in m.SIMPLICES) == 1 - - @m.Constraint(m.SIMPLICES) - def schedule_each_slot(m, j): - return sum(m.x[i, j] for i in m.SIMPLICES) == 1 - - # Enforce property (1) - @m.Constraint(m.SIMPLICES) - def simplex_order(m, i): - # anything with at least a vertex in common is a neighbor - neighbors = [ - s - for s in m.SIMPLICES - if sum( - m.TestVerticesEqual[i, n, s, k] - for n in m.VERTEX_INDICES - for k in m.VERTEX_INDICES - ) - >= connected_face_dim + 1 - and s != i - ] - # print(f'neighbors of {i} are {neighbors}') - return ( - sum( - m.x[i, j] * m.x[k, j + 1] - for j in m.SIMPLICES - if j != m.SimplicesCount - 1 - for k in neighbors - ) - + m.x[i, m.SimplicesCount - 1] - == 1 - ) - - # Trivial objective (do I need this?) - m.obj = Objective(expr=0) - - # m.pprint() - # Solve model - results = SolverFactory(subsolver).solve(m, tee=True) - match (results.solver.termination_condition): - case TerminationCondition.infeasible: - raise ValueError( - f"The triangulation was impossible to suitably order for the incremental transformation under the assumption that consecutive simplices share {connected_face_dim}-faces. Try relaxing that assumption, or try a different triangulation, such as J1." - ) - case TerminationCondition.optimal: - pass - case _: - raise ValueError( - f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}" - ) - - # Retrieve data - new_simplices = {} - for j in m.SIMPLICES: - for i in m.SIMPLICES: - if abs(value(m.x[i, j]) - 1) < 1e-5: - # The jth slot is occupied by the ith simplex - new_simplices[j] = simplices[i] - # Note vertices need to be fixed after the fact now - break - fix_vertices_incremental_order(new_simplices) - return new_simplices - - # 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 diff --git a/pyomo/contrib/piecewise/triangulations_util.py b/pyomo/contrib/piecewise/triangulations_util.py new file mode 100644 index 00000000000..51b52719d54 --- /dev/null +++ b/pyomo/contrib/piecewise/triangulations_util.py @@ -0,0 +1,240 @@ +# ___________________________________________________________________________ +# +# 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.environ import ( + ConcreteModel, + RangeSet, + Var, + Binary, + Constraint, + Param, + SolverFactory, + value, + Objective, + TerminationCondition, +) +from pyomo.contrib.piecewise.triangulations import fix_vertices_incremental_order + +# Set up a MIP (err, MIQCP) that orders our simplices and their vertices for us +# in the following way: +# +# (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 +# +# Note that (2) implies (1), so we only need to enforce that. +def reorder_simplices_for_incremental(simplices, subsolver='gurobi'): + m = ConcreteModel() + + # Sets and Params + m.SimplicesCount = Param(initialize=len(simplices)) + m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) + # For each of the simplices we need to choose an initial and a final vertex. + # The rest we can order arbitrarily after finishing the MIP solve. + m.SimplexVerticesCount = Param(initialize=len(simplices[0])) + m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) + + @m.Param( + m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary + ) + def TestVerticesEqual(m, i, n, j, k): + return 1 if simplices[i][n] == simplices[j][k] else 0 + + # Vars + # x_ij means simplex i is placed in slot j + m.x = Var(m.SIMPLICES, m.SIMPLICES, domain=Binary) + m.vertex_is_first = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + m.vertex_is_last = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) + + # Constraints + # Each simplex should have a slot and each slot should have a simplex + @m.Constraint(m.SIMPLICES) + def schedule_each_simplex(m, i): + return sum(m.x[i, j] for j in m.SIMPLICES) == 1 + + @m.Constraint(m.SIMPLICES) + def schedule_each_slot(m, j): + return sum(m.x[i, j] for i in m.SIMPLICES) == 1 + + # Each simplex needs exactly one first and exactly one last vertex + @m.Constraint(m.SIMPLICES) + def one_first_vertex(m, i): + return sum(m.vertex_is_first[i, n] for n in m.VERTEX_INDICES) == 1 + + @m.Constraint(m.SIMPLICES) + def one_last_vertex(m, i): + return sum(m.vertex_is_last[i, n] for n in m.VERTEX_INDICES) == 1 + + # The last vertex cannot be the same as the first vertex + @m.Constraint(m.SIMPLICES, m.VERTEX_INDICES) + def first_last_distinct(m, i, n): + return m.vertex_is_first[i, n] * m.vertex_is_last[i, n] == 0 + + # Enforce property (2). This also guarantees property (1) + @m.Constraint(m.SIMPLICES, m.SIMPLICES) + def vertex_order(m, i, j): + # Enforce only when j is the simplex following i. If not, RHS is zero + return sum( + m.vertex_is_last[i, n] + * m.vertex_is_first[j, k] + * m.TestVerticesEqual[i, n, j, k] + for n in m.VERTEX_INDICES + for k in m.VERTEX_INDICES + ) >= sum( + m.x[i, p] * m.x[j, p + 1] for p in m.SIMPLICES if p != m.SimplicesCount - 1 + ) + + # Trivial objective (do I need this?) + m.obj = Objective(expr=0) + + # Solve model + results = SolverFactory(subsolver).solve(m, tee=True) + match (results.solver.termination_condition): + case TerminationCondition.infeasible: + raise ValueError( + "The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1." + ) + case TerminationCondition.optimal: + pass + case _: + raise ValueError( + f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}" + ) + + # Retrieve data + # m.pprint() + new_simplices = {} + for j in m.SIMPLICES: + for i in m.SIMPLICES: + if abs(value(m.x[i, j]) - 1) < 1e-5: + # The jth slot is occupied by the ith simplex + old_simplex = simplices[i] + # Reorder its vertices, too + first = None + last = None + for n in m.VERTEX_INDICES: + if abs(value(m.vertex_is_first[i, n]) - 1) < 1e-5: + first = n + if abs(value(m.vertex_is_last[i, n]) - 1) < 1e-5: + last = n + if first is not None and last is not None: + break + new_simplex = [old_simplex[first]] + for n in m.VERTEX_INDICES: + if n != first and n != last: + new_simplex.append(old_simplex[n]) + new_simplex.append(old_simplex[last]) + new_simplices[j] = new_simplex + break + return new_simplices + + + +# An alternative approach is to order the simplices instead of the vertices. To +# do this, the condition (1) should be that they share a 1-face, not just a +# vertex. Then there is always a consistent way to choose distinct first and +# last vertices, which would otherwise be the issue - the rest of the vertex +# ordering can be arbitrary. By assuming we share 1- or n-faces, the problem +# is made somewhat smaller. +# Note that this case is literally just asking Gurobi to get a hamiltonian path +# in a large graph and hoping it can do it. +def reorder_simplices_for_incremental_assume_connected_by_n_face( + simplices, connected_face_dim, subsolver='gurobi' +): + if connected_face_dim == 0: + return reorder_simplices_for_incremental(simplices) + + m = ConcreteModel() + + # Sets and Params + m.SimplicesCount = Param(initialize=len(simplices)) + m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) + m.SimplexVerticesCount = Param(initialize=len(simplices[0])) + m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) + + @m.Param( + m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary + ) + def TestVerticesEqual(m, i, n, j, k): + return 1 if simplices[i][n] == simplices[j][k] else 0 + + # Vars + # x_ij means simplex i is placed in slot j + m.x = Var(m.SIMPLICES, m.SIMPLICES, domain=Binary) + + # Constraints + # Each simplex should have a slot and each slot should have a simplex + @m.Constraint(m.SIMPLICES) + def schedule_each_simplex(m, i): + return sum(m.x[i, j] for j in m.SIMPLICES) == 1 + + @m.Constraint(m.SIMPLICES) + def schedule_each_slot(m, j): + return sum(m.x[i, j] for i in m.SIMPLICES) == 1 + + # Enforce property (1) + @m.Constraint(m.SIMPLICES) + def simplex_order(m, i): + # anything with at least a vertex in common is a neighbor + neighbors = [ + s + for s in m.SIMPLICES + if sum( + m.TestVerticesEqual[i, n, s, k] + for n in m.VERTEX_INDICES + for k in m.VERTEX_INDICES + ) + >= connected_face_dim + 1 + and s != i + ] + # print(f'neighbors of {i} are {neighbors}') + return ( + sum( + m.x[i, j] * m.x[k, j + 1] + for j in m.SIMPLICES + if j != m.SimplicesCount - 1 + for k in neighbors + ) + + m.x[i, m.SimplicesCount - 1] + == 1 + ) + + # Trivial objective (do I need this?) + m.obj = Objective(expr=0) + + # m.pprint() + # Solve model + results = SolverFactory(subsolver).solve(m, tee=True) + match (results.solver.termination_condition): + case TerminationCondition.infeasible: + raise ValueError( + f"The triangulation was impossible to suitably order for the incremental transformation under the assumption that consecutive simplices share {connected_face_dim}-faces. Try relaxing that assumption, or try a different triangulation, such as J1." + ) + case TerminationCondition.optimal: + pass + case _: + raise ValueError( + f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}" + ) + + # Retrieve data + new_simplices = {} + for j in m.SIMPLICES: + for i in m.SIMPLICES: + if abs(value(m.x[i, j]) - 1) < 1e-5: + # The jth slot is occupied by the ith simplex + new_simplices[j] = simplices[i] + # Note vertices need to be fixed after the fact now + break + fix_vertices_incremental_order(new_simplices) + return new_simplices + From 146a312fa34688d61e59c530f3f4711e486ceb53 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 5 Jun 2024 16:31:57 -0400 Subject: [PATCH 034/220] apply black, except the generated file --- ...nerate_ordered_3d_j1_triangulation_data.py | 14 +- .../piecewise/piecewise_linear_function.py | 24 ++- .../piecewise/tests/test_incremental.py | 26 ++- .../tests/test_piecewise_linear_function.py | 10 +- .../piecewise/tests/test_triangulations.py | 176 +++++++++++------- pyomo/contrib/piecewise/triangulations.py | 79 +++++--- .../contrib/piecewise/triangulations_util.py | 3 +- 7 files changed, 208 insertions(+), 124 deletions(-) diff --git a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py index 738c09790de..93a087d883f 100644 --- a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py @@ -35,7 +35,6 @@ # 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)), @@ -45,9 +44,7 @@ # -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)), @@ -58,15 +55,20 @@ ((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 path from {i} to {(j[0], 1) if (i, (j[0], 1)) in all_needed_hamiltonians.keys() else (j[0], 2)}") + 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 path from {i} to {(j[0], 1) if (i, (j[0], 1)) in 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): diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 1050836626b..801ef0b2e19 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -280,8 +280,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) + self._triangulation_rule = Initializer( + _triangulation_rule_arg, treat_sequences_as_mappings=False + ) def _get_dimension_from_points(self, points): if len(points) < 1: @@ -297,8 +298,9 @@ def _get_dimension_from_points(self, points): return dimension - def _construct_simplices_from_multivariate_points(self, obj, parent, points, - dimension): + def _construct_simplices_from_multivariate_points( + self, obj, parent, points, dimension + ): tri = self._triangulation_rule(parent, obj._index) if tri == Triangulation.Delaunay: try: @@ -315,8 +317,8 @@ def _construct_simplices_from_multivariate_points(self, obj, parent, points, obj._triangulation = tri else: raise ValueError( - "Unrecognized triangulation specified for '%s': %s" - % (obj, tri)) + "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. @@ -377,8 +379,9 @@ def _construct_from_function_and_points(self, obj, parent, nonlinear_function): obj, nonlinear_function ) - self._construct_simplices_from_multivariate_points(obj, parent, 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 ) @@ -488,8 +491,9 @@ def _construct_from_tabular_data(self, obj, parent, nonlinear_function): obj, _tabular_data_functor(tabular_data, tupleize=True) ) - self._construct_simplices_from_multivariate_points(obj, parent, 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) ) diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index 854629dccf5..df8e3180906 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -19,25 +19,31 @@ assertExpressionsStructurallyEqual, ) from pyomo.gdp import Disjunct, Disjunction -from pyomo.environ import Constraint, SolverFactory, Var, ConcreteModel, Objective, log, value, maximize +from pyomo.environ import ( + Constraint, + SolverFactory, + Var, + ConcreteModel, + Objective, + log, + value, + maximize, +) from pyomo.contrib.piecewise import PiecewiseLinearFunction from pyomo.contrib.piecewise.transform.incremental import IncrementalGDPTransformation + class TestTransformPiecewiseModelToIncrementalMIP(unittest.TestCase): def test_solve_log_model(self): m = make_log_x_model_ordered() - TransformationFactory( - 'contrib.piecewise.incremental' - ).apply_to(m) - TransformationFactory( - 'gdp.bigm' - ).apply_to(m) + 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) - - #def test_solve_univariate_log_model(self): + + # def test_solve_univariate_log_model(self): # m = ConcreteModel() # m.x = Var(bounds=(1, 10)) # m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) @@ -137,4 +143,4 @@ def c_rule(m, i): m.indexed_c = Constraint([0, 1], rule=c_rule) - return m \ No newline at end of file + return m diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index 5dd331d7943..a2ac7f95d80 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -121,7 +121,8 @@ def test_pw_linear_approx_of_ln_x_tabular_data(self): 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) + points=[1, 3, 6, 10], triangulation=Triangulation.J1, function=m.f + ) self.check_ln_x_approx(m.pw, m.x) self.assertEqual(m.pw.triangulation, Triangulation.J1) @@ -312,10 +313,11 @@ def test_pw_linear_approx_of_paraboloid_points(self): 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)], function=m.g, - triangulation=Triangulation.J1 + points=[(0, 1), (0, 4), (0, 7), (3, 1), (3, 4), (3, 7)], + function=m.g, + triangulation=Triangulation.J1, ) - self.check_pw_linear_approximation(m) + self.check_pw_linear_approximation(m) @unittest.skipUnless(scipy_available, "scipy is not available") def test_pw_linear_approx_tabular_data(self): diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 4eab2f9fd4c..fab490a95ef 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -22,125 +22,175 @@ from math import factorial import itertools + class TestTriangulations(unittest.TestCase): # check basic functionality for the unordered j1 triangulation. def test_J1_small(self): points = [ - [0, 0], [0, 1], [0, 2], - [1, 0], [1, 1], [1, 2], - [2, 0], [2, 1], [2, 2], + [0, 0], + [0, 1], + [0, 2], + [1, 0], + [1, 1], + [1, 2], + [2, 0], + [2, 1], + [2, 2], ] triangulation = get_unordered_j1_triangulation(points, 2) - self.assertEqual(triangulation.simplices, - { - 0: [[0, 0], [0, 1], [1, 1]], - 1: [[0, 1], [0, 2], [1, 1]], - 2: [[1, 1], [2, 0], [2, 1]], - 3: [[1, 1], [2, 1], [2, 2]], - 4: [[0, 0], [1, 0], [1, 1]], - 5: [[0, 2], [1, 1], [1, 2]], - 6: [[1, 0], [1, 1], [2, 0]], - 7: [[1, 1], [1, 2], [2, 2]], - }) - + self.assertEqual( + triangulation.simplices, + { + 0: [[0, 0], [0, 1], [1, 1]], + 1: [[0, 1], [0, 2], [1, 1]], + 2: [[1, 1], [2, 0], [2, 1]], + 3: [[1, 1], [2, 1], [2, 2]], + 4: [[0, 0], [1, 0], [1, 1]], + 5: [[0, 2], [1, 1], [1, 2]], + 6: [[1, 0], [1, 1], [2, 0]], + 7: [[1, 1], [1, 2], [2, 2]], + }, + ) + # check that the points_map functionality does what it should def test_J1_small_offset(self): points = [ - [0.5, 0.5], [0.5, 1.5], [0.5, 2.5], - [1.5, 0.5], [1.5, 1.5], [1.5, 2.5], - [2.5, 0.5], [2.5, 1.5], [2.5, 2.5], + [0.5, 0.5], + [0.5, 1.5], + [0.5, 2.5], + [1.5, 0.5], + [1.5, 1.5], + [1.5, 2.5], + [2.5, 0.5], + [2.5, 1.5], + [2.5, 2.5], ] triangulation = get_unordered_j1_triangulation(points, 2) - self.assertEqual(triangulation.simplices, - { - 0: [[0.5, 0.5], [0.5, 1.5], [1.5, 1.5]], - 1: [[0.5, 1.5], [0.5, 2.5], [1.5, 1.5]], - 2: [[1.5, 1.5], [2.5, 0.5], [2.5, 1.5]], - 3: [[1.5, 1.5], [2.5, 1.5], [2.5, 2.5]], - 4: [[0.5, 0.5], [1.5, 0.5], [1.5, 1.5]], - 5: [[0.5, 2.5], [1.5, 1.5], [1.5, 2.5]], - 6: [[1.5, 0.5], [1.5, 1.5], [2.5, 0.5]], - 7: [[1.5, 1.5], [1.5, 2.5], [2.5, 2.5]], - }) + self.assertEqual( + triangulation.simplices, + { + 0: [[0.5, 0.5], [0.5, 1.5], [1.5, 1.5]], + 1: [[0.5, 1.5], [0.5, 2.5], [1.5, 1.5]], + 2: [[1.5, 1.5], [2.5, 0.5], [2.5, 1.5]], + 3: [[1.5, 1.5], [2.5, 1.5], [2.5, 2.5]], + 4: [[0.5, 0.5], [1.5, 0.5], [1.5, 1.5]], + 5: [[0.5, 2.5], [1.5, 1.5], [1.5, 2.5]], + 6: [[1.5, 0.5], [1.5, 1.5], [2.5, 0.5]], + 7: [[1.5, 1.5], [1.5, 2.5], [2.5, 2.5]], + }, + ) def check_J1_ordered(self, points, num_points, dim): ordered_triangulation = get_ordered_j1_triangulation(points, dim).simplices - #print(ordered_triangulation) - self.assertEqual(len(ordered_triangulation), factorial(dim) * (num_points - 1) ** dim) + # print(ordered_triangulation) + self.assertEqual( + len(ordered_triangulation), factorial(dim) * (num_points - 1) ** dim + ) for idx, first_simplex in ordered_triangulation.items(): if idx != len(ordered_triangulation) - 1: second_simplex = ordered_triangulation[idx + 1] # test property (2) which also guarantees property (1) - self.assertEqual(first_simplex[-1], second_simplex[0], msg="Last and first vertices of adjacent simplices did not match") + 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 = 0 for pt in first_simplex: if pt in second_simplex: count += 1 - #print(f"first_simplex={first_simplex}; second_simplex={second_simplex}") - self.assertEqual(count, dim) # (n-1)-face has n points - #if count != dim: + # print(f"first_simplex={first_simplex}; second_simplex={second_simplex}") + self.assertEqual(count, dim) # (n-1)-face has n points + # if count != dim: # print(f"error: count {count} was not the correct {dim}") - + 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], [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, + 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])), + 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])), + 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, ) - + 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, + 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])), + 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])), + 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])), + 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, ) - + 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])), + 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])), + 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)) @@ -177,22 +227,22 @@ def test_Gn_hamiltonian_paths(self): 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) + 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) - + 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) - - diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index f4b628f7a02..92da03b7fd9 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -14,6 +14,7 @@ from enum import Enum from pyomo.common.errors import DeveloperError + class Triangulation(Enum): Unknown = 0 AssumeValid = 1 @@ -21,6 +22,7 @@ class Triangulation(Enum): J1 = 3 OrderedJ1 = 4 + 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) @@ -41,7 +43,9 @@ def get_ordered_j1_triangulation(points, dimension): 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) + simplices_list = _get_ordered_j1_triangulation_4d_and_above( + points_map, num_pts - 1, dimension + ) triangulation = SimpleNamespace() triangulation.points = list(range(len(simplices_list))) triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} @@ -292,11 +296,13 @@ def add_top_left(): def _get_ordered_j1_triangulation_3d(points_map, num_pts): # Import these precomputed paths - from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import hamiltonian_paths as simplex_pair_to_path + from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import ( + hamiltonian_paths as simplex_pair_to_path, + ) # 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 + 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 @@ -306,22 +312,30 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): 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)) + 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 simplex_pair_to_path.keys(): - current_cube_path = simplex_pair_to_path[(start_data, (direction_to_next, 1))] + current_cube_path = 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 = simplex_pair_to_path[(start_data, (direction_to_next, 2))] + current_cube_path = 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[len(simplices)] = get_one_j1_simplex(current_v_0, simplex_data[1], simplex_data[0], 3, points_map) - + simplices[len(simplices)] = 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]) @@ -332,14 +346,13 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): current_cube_path = simplex_pair_to_path[(start_data, (direction_to_next, 2))] for simplex_data in current_cube_path: - simplices[len(simplices)] = get_one_j1_simplex(current_v_0, simplex_data[1], simplex_data[0], 3, points_map) + simplices[len(simplices)] = 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 @@ -371,11 +384,15 @@ def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): if c % 2 == 0: perm_sequence = get_Gn_hamiltonian(dim, start_perm, j, False) for pi in perm_sequence: - simplices[len(simplices)] = get_one_j1_simplex(v_0, pi, sign, dim, points_map) + simplices[len(simplices)] = 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[len(simplices)] = get_one_j1_simplex(v_0, pi, sign, dim, points_map) + simplices[len(simplices)] = get_one_j1_simplex( + v_0, pi, sign, dim, points_map + ) # should be true regardless of odd or even? I hope start_perm = perm_sequence[-1] @@ -384,11 +401,12 @@ def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): v_0, sign = get_nearest_odd_and_sign_vec(grid_hamiltonian[-1]) for pi in get_Gn_hamiltonian(dim, start_perm, 1, False): simplices[len(simplices)] = 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) @@ -399,6 +417,7 @@ def get_one_j1_simplex(v_0, pi, sign, dim, points_map): simplex.append(points_map[*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): @@ -413,6 +432,7 @@ def get_nearest_odd_and_sign_vec(corner): sign.append(1) return v_0, sign + def get_grid_hamiltonian(dim, length): if dim == 1: return [[n] for n in range(length)] @@ -490,7 +510,9 @@ def get_Gn_hamiltonian(n, start_permutation, target_symbol, last): 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) + ][ + 0 + ] # pi^-1(j) return [ tuple(start_permutation[pi[i] - 1] for i in range(n)) for pi in _get_Gn_hamiltonian(n, new_target_symbol) @@ -616,7 +638,7 @@ def _get_Gn_hamiltonian(n, target_symbol): # unreachable else: # recursive case - if target_symbol < n: # non-awful case + if target_symbol < n: # non-awful case # Well, it's still pretty awful. idx = n - 1 facing = -1 @@ -627,11 +649,11 @@ def _get_Gn_hamiltonian(n, target_symbol): l.insert(idx, n) ret.append(tuple(l)) idx += facing - if (idx == -1 or idx == n): # went too far + if idx == -1 or idx == n: # went too far facing *= -1 - idx += facing # stay once because we get a new pi + idx += facing # stay once because we get a new pi return ret - else: # awful case, target_symbol = n + else: # awful case, target_symbol = n idx = 0 facing = 1 ret = [] @@ -641,16 +663,18 @@ def _get_Gn_hamiltonian(n, target_symbol): l.insert(idx, 1) ret.append(tuple(l)) idx += facing - if (idx == -1 or idx == n): # went too far + if idx == -1 or idx == n: # went too far facing *= -1 - idx += facing # stay once because we get a new pi + 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, ...) + 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 = list( + last + ) # want permutation of form (n, 1, j, i, ...) with same tail test[0] = n test[1] = 1 test[2] = j @@ -659,6 +683,3 @@ def _get_Gn_hamiltonian(n, target_symbol): ret.insert(idx, second_last) ret.insert(idx, last) return ret - - - diff --git a/pyomo/contrib/piecewise/triangulations_util.py b/pyomo/contrib/piecewise/triangulations_util.py index 51b52719d54..37cb7c31cc8 100644 --- a/pyomo/contrib/piecewise/triangulations_util.py +++ b/pyomo/contrib/piecewise/triangulations_util.py @@ -23,6 +23,7 @@ ) from pyomo.contrib.piecewise.triangulations import fix_vertices_incremental_order + # Set up a MIP (err, MIQCP) that orders our simplices and their vertices for us # in the following way: # @@ -138,7 +139,6 @@ def vertex_order(m, i, j): return new_simplices - # An alternative approach is to order the simplices instead of the vertices. To # do this, the condition (1) should be that they share a 1-face, not just a # vertex. Then there is always a consistent way to choose distinct first and @@ -237,4 +237,3 @@ def simplex_order(m, i): break fix_vertices_incremental_order(new_simplices) return new_simplices - From 92758c7f6d7876589619d209efede2e24ba05c4d Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 5 Jun 2024 16:33:12 -0400 Subject: [PATCH 035/220] apply black to the generated data too --- .../ordered_3d_j1_triangulation_data.py | 3003 ++++++++++++++++- 1 file changed, 3002 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index a1af2eb98dc..a4e921f42d2 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -1,2 +1,3003 @@ # Generated using generate_ordered_3d_j1_triangulation_data.py -hamiltonian_paths = {(((-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))]} \ No newline at end of file +hamiltonian_paths = { + (((-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)), + ], +} From e9b6a5ed2587c1e5fcbb8027034026c3e68d303d Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 5 Jun 2024 17:10:54 -0400 Subject: [PATCH 036/220] remove code for dealing with orderings from incremental.py because the ordering is now handled entirely ahead of time in the triangulation. We'll just check instead --- pyomo/contrib/piecewise/__init__.py | 3 + .../piecewise/tests/test_incremental.py | 41 --------- .../piecewise/transform/incremental.py | 84 ++++++++----------- 3 files changed, 36 insertions(+), 92 deletions(-) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 076f66cd557..3690a8a66f2 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -39,4 +39,7 @@ 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/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index df8e3180906..fd85647a52e 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -31,8 +31,6 @@ ) from pyomo.contrib.piecewise import PiecewiseLinearFunction -from pyomo.contrib.piecewise.transform.incremental import IncrementalGDPTransformation - class TestTransformPiecewiseModelToIncrementalMIP(unittest.TestCase): @@ -43,45 +41,6 @@ def test_solve_log_model(self): SolverFactory('gurobi').solve(m) ct.check_log_x_model_soln(self, m) - # def test_solve_univariate_log_model(self): - # m = ConcreteModel() - # m.x = Var(bounds=(1, 10)) - # m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) - - # # Here are the linear functions, for safe keeping. - # def f1(x): - # return (log(3) / 2) * x - log(3) / 2 - - # m.f1 = f1 - - # def f2(x): - # return (log(2) / 3) * x + log(3 / 2) - - # m.f2 = f2 - - # def f3(x): - # return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) - - # m.f3 = f3 - - # m.log_expr = m.pw_log(m.x) - # m.obj = Objective(expr=m.log_expr, sense=maximize) - - # TransformationFactory( - # 'contrib.piecewise.incremental' - # ).apply_to(m) - # m.pprint() - # TransformationFactory( - # 'gdp.bigm' - # ).apply_to(m) - # print('####### PPRINTNG AGAIN AFTER BIGM #######') - # m.pprint() - # # log is increasing so the optimal value should be log(10) - # SolverFactory('gurobi').solve(m) - # print(f"optimal value is {value(m.obj)}") - # self.assertTrue(abs(value(m.obj) - log(10)) < 0.001) - - # Make a version of the log_x model with the simplices properly ordered for the # incremental transform def make_log_x_model_ordered(): diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 266784297e0..0d460026fac 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -26,12 +26,24 @@ @TransformationFactory.register( "contrib.piecewise.incremental", doc=""" - TODO document + The incremental MIP formulation of a piecewise-linear function, as described + by Vielma et al, 2010. 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 """, ) -class IncrementalGDPTransformation(PiecewiseLinearTransformationBase): +class IncrementalMIPTransformation(PiecewiseLinearTransformationBase): """ - TODO document + The incremental MIP formulation of a piecewise-linear function, as described + by Vielma et al, 2010. 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 """ CONFIG = PiecewiseLinearTransformationBase.CONFIG() @@ -40,9 +52,9 @@ class IncrementalGDPTransformation(PiecewiseLinearTransformationBase): # Implement to use PiecewiseLinearToGDP. 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): - self.DEBUG = False - if not (pw_linear_func.triangulation == Triangulation.OrderedJ1 or pw_linear_func.triangulation == Triangulation.AssumeValid): - logging.getLogger('pyomo.contrib.piecewise.transform.incremental').warning("Incremental transformation specified, but the triangulation may not be appropriately ordered. This is likely to lead to incorrect results!") + if pw_linear_func.triangulation not in (Triangulation.OrderedJ1, Triangulation.AssumeValid): + # almost certain not to work + logging.getLogger('pyomo.contrib.piecewise.transform.incremental').error("Incremental transformation specified, but the triangulation may not be appropriately ordered. This is likely to lead to incorrect results!") # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -58,8 +70,8 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc pw_linear_func.map_transformation_var(pw_expr, substitute_var) # Bounds for the substitute_var that we will widen - self.substitute_var_lb = float("inf") - self.substitute_var_ub = -float("inf") + 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 @@ -77,46 +89,16 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc 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 < self.substitute_var_lb: - self.substitute_var_lb = val - if val > self.substitute_var_ub: - self.substitute_var_ub = val + 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(self.substitute_var_lb) - transBlock.substitute_var.setub(self.substitute_var_ub) - - - # Ordering of simplices to follow Vielma - # TODO: assumption: this enumeration must satisfy O1 (Vielma): each T_i \cap T_{i-1} - # is nonempty - # TODO: One way to make this true will be to use the union_jack__triangulate.py - # script to generate the triangulation, but it is also possible for other - # triangulations to be correct. This should be checkable using a MIP. It - # is known that there is a correct ordering for any triangulation of a - # domain homeomorphic to a disc in R^2 (Wilson 1998). - self.simplex_ordering = { - n: n for n in transBlock.simplex_indices - } - - # Enumeration of simplices: map from simplex number to correct simplex object - self.idx_to_simplex = { - n: simplices[m] for n, m in self.simplex_ordering.items() - } - # Associate simplex indices with correct linear functions - self.idx_to_lin_func = { - n: pw_linear_func._linear_functions[m] for n, m in self.simplex_ordering.items() - } - - # For each individual simplex, the points need to be permuted in a way that - # satisfies O1 and O2 (Vielma). TODO TODO TODO - self.vertex_ordering = { - (T, n): n - for T in transBlock.simplex_indices - for n in transBlock.simplex_point_indices - } + transBlock.substitute_var.setlb(substitute_var_lb) + transBlock.substitute_var.setub(substitute_var_ub) # Inital vertex (v_0^0 in Vielma) - self.initial_vertex = pw_linear_func._points[self.idx_to_simplex[0][self.vertex_ordering[0, 0]]] + initial_vertex = pw_linear_func._points[simplices[0][0]] # delta_i^j = delta[simplex][point] transBlock.delta = Var( @@ -158,11 +140,11 @@ def deltas_below_y(m, i): @transBlock.Constraint(transBlock.dimension_indices) def x_constraint(b, n): return (pw_expr.args[n] == - self.initial_vertex[n] + sum( + initial_vertex[n] + sum( sum( # delta_i^j * (v_i^j - v_i^0) - transBlock.delta[i, j] * (pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, j]]][n] - - pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, 0]]][n]) + 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 @@ -172,11 +154,11 @@ def x_constraint(b, n): # Now we can set the substitute Var for the PWLE (12a.2) transBlock.set_substitute = Constraint( expr=substitute_var - == self.idx_to_lin_func[0](*self.initial_vertex) + sum( + == pw_linear_func._linear_functions[0](*initial_vertex) + sum( sum( # delta_i^j * (f(v_i^j) - f(v_i^0)) - transBlock.delta[i, j] * (self.idx_to_lin_func[i](*pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[i, j]]]) - - self.idx_to_lin_func[i](*pw_linear_func._points[self.idx_to_simplex[i][self.vertex_ordering[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 From 176c892d917068885d8b283f9d4abaa2490d5a46 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 5 Jun 2024 17:11:24 -0400 Subject: [PATCH 037/220] apply black again --- pyomo/contrib/piecewise/__init__.py | 4 +- .../piecewise/tests/test_incremental.py | 1 + .../piecewise/transform/incremental.py | 87 +++++++++++++------ 3 files changed, 62 insertions(+), 30 deletions(-) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 3690a8a66f2..f7cd12eb84b 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -39,7 +39,5 @@ from pyomo.contrib.piecewise.transform.disaggregated_logarithmic import ( DisaggregatedLogarithmicMIPTransformation, ) -from pyomo.contrib.piecewise.transform.incremental import ( - IncrementalMIPTransformation, -) +from pyomo.contrib.piecewise.transform.incremental import IncrementalMIPTransformation from pyomo.contrib.piecewise.triangulations import Triangulation diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index fd85647a52e..4a38e0621f5 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -41,6 +41,7 @@ def test_solve_log_model(self): SolverFactory('gurobi').solve(m) ct.check_log_x_model_soln(self, m) + # Make a version of the log_x model with the simplices properly ordered for the # incremental transform def make_log_x_model_ordered(): diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 0d460026fac..b7e8e44cb93 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -14,7 +14,15 @@ PiecewiseLinearTransformationBase, ) from pyomo.contrib.piecewise.triangulations import Triangulation -from pyomo.core import Constraint, Binary, NonNegativeIntegers, Suffix, Var, RangeSet, Param +from pyomo.core import ( + Constraint, + Binary, + NonNegativeIntegers, + Suffix, + Var, + RangeSet, + Param, +) from pyomo.core.base import TransformationFactory from pyomo.gdp import Disjunct, Disjunction from pyomo.common.errors import DeveloperError @@ -23,6 +31,7 @@ from math import ceil, log2 import logging + @TransformationFactory.register( "contrib.piecewise.incremental", doc=""" @@ -52,9 +61,14 @@ class IncrementalMIPTransformation(PiecewiseLinearTransformationBase): # Implement to use PiecewiseLinearToGDP. 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): + if pw_linear_func.triangulation not in ( + Triangulation.OrderedJ1, + Triangulation.AssumeValid, + ): # almost certain not to work - logging.getLogger('pyomo.contrib.piecewise.transform.incremental').error("Incremental transformation specified, but the triangulation may not be appropriately ordered. This is likely to lead to incorrect results!") + logging.getLogger('pyomo.contrib.piecewise.transform.incremental').error( + "Incremental transformation specified, but the triangulation may not be appropriately ordered. This is likely to lead to incorrect results!" + ) # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) transBlock = transformation_block.transformed_functions[ @@ -84,9 +98,11 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc 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 + # 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 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: @@ -116,16 +132,18 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc # 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 + 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]) - + 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, @@ -133,36 +151,51 @@ def y_below_delta(m, i): # 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]) + 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, + # 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( - 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 + return pw_expr.args[n] == initial_vertex[n] + sum( + 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 i in transBlock.simplex_indices + 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( - 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 + == pw_linear_func._linear_functions[0](*initial_vertex) + + sum( + 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 i in transBlock.simplex_indices + for j in transBlock.nonzero_simplex_point_indices ) + for i in transBlock.simplex_indices + ) ) - return substitute_var \ No newline at end of file + return substitute_var From 956a539e0ca3f04bdcc5e104e700f12a49653c56 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 10 Jun 2024 16:27:57 -0400 Subject: [PATCH 038/220] typo fix and minor comment edits --- .../piecewise/transform/incremental.py | 2 +- pyomo/contrib/piecewise/triangulations.py | 31 +++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index b7e8e44cb93..6e6bc055d53 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -113,7 +113,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc transBlock.substitute_var.setlb(substitute_var_lb) transBlock.substitute_var.setub(substitute_var_ub) - # Inital vertex (v_0^0 in Vielma) + # Initial vertex (v_0^0 in Vielma) initial_vertex = pw_linear_func._points[simplices[0][0]] # delta_i^j = delta[simplex][point] diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 92da03b7fd9..c37c8b231cb 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -79,9 +79,9 @@ def _process_points_j1(points, dimension): return points_map, num_pts -# This implements the J1 "Union Jack" triangulation (Todd 77) as explained by -# Vielma 2010. -# Triangulate {0, ..., K}^n for even K using the J1 triangulation, mapping the +# Implement the J1 "Union Jack" triangulation (Todd 77) 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: @@ -109,10 +109,10 @@ def _get_j1_triangulation(points_map, K, n): return ret -# Implement proof-by-picture from Todd 1977. I do the reverse order he does -# and also keep the pictures slightly more regular to make things easier to -# implement. Also remember that Todd's drawing is misleading to the point of -# almost being wrong so make sure you draw it properly first. +# Implement something similar to proof-by-picture from Todd 77 (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 @@ -230,7 +230,6 @@ def add_top_left(): 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 - assert square_parity_tlbr(x, y), "uh oh" add_top_left() x -= 1 add_top_right() @@ -263,7 +262,6 @@ def add_top_left(): 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 - assert not square_parity_tlbr(x, y), "uh oh" add_bottom_left() x -= 1 add_bottom_right() @@ -487,11 +485,11 @@ def fix_vertices_incremental_order(simplices): simplices[i] = new_simplex -# G_n is 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}. +# 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 +# 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. @@ -524,8 +522,8 @@ def get_Gn_hamiltonian(n, start_permutation, target_symbol, last): # 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(n, target_symbol): - # base case: proof by picture from Todd, Figure 2 - # note: Figure 2 contains an error, like half the figures and paragraphs do + # base case: proof by picture from Todd 77, Figure 2 + # note: Figure 2 contains an error, careful! if n == 4: if target_symbol == 1: return [ @@ -638,8 +636,7 @@ def _get_Gn_hamiltonian(n, target_symbol): # unreachable else: # recursive case - if target_symbol < n: # non-awful case - # Well, it's still pretty awful. + if target_symbol < n: # Less awful case idx = n - 1 facing = -1 ret = [] From bd24f20dba9678c059def215d85eeb2c18e42b1e Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 10 Jun 2024 16:49:46 -0400 Subject: [PATCH 039/220] skip test when gurobi unavailable --- pyomo/contrib/piecewise/tests/test_incremental.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index 4a38e0621f5..15b86bf8593 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -34,6 +34,8 @@ class TestTransformPiecewiseModelToIncrementalMIP(unittest.TestCase): + @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_ordered() TransformationFactory('contrib.piecewise.incremental').apply_to(m) From c109901000a0d94263c14a19166c38feec6ff30a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Thu, 13 Jun 2024 23:56:22 +0200 Subject: [PATCH 040/220] Using the validate argument with indexed set objects now allows access to the index. Original pyomo tests pass. --- examples/pyomo/tutorials/set.py | 4 ++-- pyomo/core/base/set.py | 14 +++++++++++++- pyomo/core/tests/unit/test_set.py | 8 +++++--- pyomo/core/tests/unit/test_sets.py | 8 ++++---- 4 files changed, 24 insertions(+), 10 deletions(-) diff --git a/examples/pyomo/tutorials/set.py b/examples/pyomo/tutorials/set.py index a14301484c9..fab2fe5bee9 100644 --- a/examples/pyomo/tutorials/set.py +++ b/examples/pyomo/tutorials/set.py @@ -176,11 +176,11 @@ def P_init(model, i, j): # Validation of set arrays can also be performed with the _validate_ option. # This is applied to all sets in the array: # -def T_validate(model, value): +def T_validate(model, value, index): return value in model.A -model.T = Set(model.B, validate=M_validate) +model.T = Set(model.B, validate=T_validate) ## diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 8b7c2a246d6..33737b130e8 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1409,7 +1409,19 @@ def add(self, *values): if self._validate is not None: try: - flag = self._validate(_block, _value) + # differentiate between indexed and non-indexed sets + if self._index is not None: + # indexed set: the value and the index are given + pass + if type(_value) == tuple: + # _value is a tuple: unpack it for the method arguments' tuple + flag = self._validate(_block, (*_value, self._index)) + else: + # _value is not a tuple: no need to unpack it for the method arguments' tuple + flag = self._validate(_block, (_value, self._index)) + else: + # non-indexed set: only the tentative member is given + flag = self._validate(_block, _value) except: logger.error( "Exception raised while validating element '%s' " diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index f62589a6873..70dfeb26f74 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4319,7 +4319,7 @@ def _lt_3(model, i): m = ConcreteModel() - def _validate(model, i, j): + def _validate_I(model, i, j): self.assertIs(model, m) if i + j < 2: return True @@ -4327,7 +4327,7 @@ def _validate(model, i, j): return False raise RuntimeError("Bogus value") - m.I = Set(validate=_validate) + m.I = Set(validate=_validate_I) output = StringIO() with LoggingIntercept(output, 'pyomo.core'): self.assertTrue(m.I.add((0, 1))) @@ -4347,7 +4347,9 @@ def _validate(model, i, j): # 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) + def _validate_J(model, i, j, index): + return _validate_I(model, i, j) + m.J = Set([(0, 0), (2, 2)], validate=_validate_J) output = StringIO() with LoggingIntercept(output, 'pyomo.core'): self.assertTrue(m.J[2, 2].add((0, 1))) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 48869397aae..872986e0d4c 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2790,7 +2790,7 @@ def test_validation1(self): # Create A with an error # self.model.Z = Set() - self.model.A = Set(self.model.Z, validate=lambda model, x: x < 6) + self.model.A = Set(self.model.Z, validate=lambda model, x, i: x < 6) try: self.instance = self.model.create_instance(currdir + "setA.dat") except ValueError: @@ -2809,7 +2809,7 @@ def test_validation2(self): # Create A with an error # self.model.Z = Set() - self.model.A = Set(self.model.Z, validate=lambda model, x: x < 6) + self.model.A = Set(self.model.Z, validate=lambda model, x, i: x < 6) try: self.instance = self.model.create_instance(currdir + "setA.dat") except ValueError: @@ -2822,7 +2822,7 @@ def test_other1(self): self.model.A = Set( self.model.Z, initialize={'A': [1, 2, 3, 'A']}, - validate=lambda model, x: x in Integers, + validate=lambda model, x, i: x in Integers, ) try: self.instance = self.model.create_instance() @@ -2853,7 +2853,7 @@ def tmp_init(model, i): self.model.n = Param(initialize=5) self.model.Z = Set(initialize=['A']) self.model.A = Set( - self.model.Z, initialize=tmp_init, validate=lambda model, x: x in Integers + self.model.Z, initialize=tmp_init, validate=lambda model, x, i: x in Integers ) try: self.instance = self.model.create_instance() From d11cfb127cb941ddf6b02aff4d39e622bbacebde Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 22:17:12 -0600 Subject: [PATCH 041/220] unit tests for intermediate callback --- .../pynumero/interfaces/cyipopt_interface.py | 95 ++++++++++++++++--- .../tests/test_cyipopt_interface.py | 66 ++++++++++++- 2 files changed, 145 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py index 7845a4c189e..77c5f5db2fa 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,38 @@ def __init__(self, nlp, intermediate_callback=None, halt_on_evaluation_error=Non # cyipopt.Problem.__init__ super(CyIpoptNLP, self).__init__() + 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 +469,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 vTBD + 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 vTBD 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() From fd039f5dacffe535dfd97b268ff250d4ab65e82f Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 22:43:18 -0600 Subject: [PATCH 042/220] add integration tests for cyipopt intermediate callback with 12 or 13 args --- .../solvers/tests/test_cyipopt_solver.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) 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..d2fc09ddb15 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py @@ -326,3 +326,86 @@ 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+ + @unittest.skipIf(not cyipopt_ge_1_3, "cyipopt version < 1.3.0") + def test_solve_13arg_callback(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)) From b1cc0a494bf8075f7cd86e4c6063305868d2f7f2 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Thu, 13 Jun 2024 22:45:52 -0600 Subject: [PATCH 043/220] apply black --- .../pynumero/algorithms/solvers/tests/test_cyipopt_solver.py | 2 ++ 1 file changed, 2 insertions(+) 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 d2fc09ddb15..b362e3ffca0 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py @@ -331,6 +331,7 @@ def test_solve_13arg_callback(self): m = create_model1() iterate_data = [] + def intermediate( nlp, alg_mod, @@ -374,6 +375,7 @@ def test_solve_13arg_callback(self): m = create_model1() iterate_data = [] + def intermediate( nlp, problem, From 629302ab09853ecd960644ff581c2524fa1ec043 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 14 Jun 2024 17:04:11 -0400 Subject: [PATCH 044/220] implement some improvements from review --- .../piecewise/transform/incremental.py | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 6e6bc055d53..646c1580ea6 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -45,20 +45,11 @@ """, ) class IncrementalMIPTransformation(PiecewiseLinearTransformationBase): - """ - The incremental MIP formulation of a piecewise-linear function, as described - by Vielma et al, 2010. 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 - """ CONFIG = PiecewiseLinearTransformationBase.CONFIG() _transformation_name = "pw_linear_incremental" - # Implement to use PiecewiseLinearToGDP. This function returns the Var + # 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 ( @@ -66,8 +57,11 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc Triangulation.AssumeValid, ): # almost certain not to work - logging.getLogger('pyomo.contrib.piecewise.transform.incremental').error( - "Incremental transformation specified, but the triangulation may not be appropriately ordered. This is likely to lead to incorrect results!" + raise ValueError( + f""" + Incremental transformation specified, but the triangulation {pw_linear_func.triangulation} may not be appropriately ordered. This would likely lead to incorrect results! + If you know what you are doing, you can suppress this error by overriding the triangulation tag to be Triangulation.AssumeValid during PiecewiseLinearFunction construction. + """ ) # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) @@ -164,15 +158,13 @@ def deltas_below_y(m, i): @transBlock.Constraint(transBlock.dimension_indices) def x_constraint(b, n): return pw_expr.args[n] == initial_vertex[n] + sum( - 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 + # 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 ) @@ -181,19 +173,17 @@ def x_constraint(b, n): expr=substitute_var == pw_linear_func._linear_functions[0](*initial_vertex) + sum( - 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]] - ) + # 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 j in transBlock.nonzero_simplex_point_indices for i in transBlock.simplex_indices ) ) From 46a1677c81c2cf432c1a587d8ad2b76850fb0cec Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Fri, 14 Jun 2024 17:17:37 -0400 Subject: [PATCH 045/220] put import statement at top of file --- pyomo/contrib/piecewise/triangulations.py | 30 ++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index c37c8b231cb..1a23a4516bd 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -13,6 +13,9 @@ from types import SimpleNamespace from enum import Enum from pyomo.common.errors import DeveloperError +from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import ( + hamiltonian_paths as incremental_3d_simplex_pair_to_path, +) class Triangulation(Enum): @@ -293,11 +296,6 @@ def add_top_left(): def _get_ordered_j1_triangulation_3d(points_map, num_pts): - # Import these precomputed paths - from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import ( - hamiltonian_paths as simplex_pair_to_path, - ) - # 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 @@ -317,14 +315,17 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): 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 simplex_pair_to_path.keys(): - current_cube_path = simplex_pair_to_path[ + 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 = simplex_pair_to_path[ + 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) @@ -338,10 +339,17 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): # 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 simplex_pair_to_path.keys(): - current_cube_path = simplex_pair_to_path[(start_data, (direction_to_next, 1))] + 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 = simplex_pair_to_path[(start_data, (direction_to_next, 2))] + current_cube_path = incremental_3d_simplex_pair_to_path[ + (start_data, (direction_to_next, 2)) + ] for simplex_data in current_cube_path: simplices[len(simplices)] = get_one_j1_simplex( From 170df3e9085478b7cba6bad52acebc6229bc8d9a Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 14 Jun 2024 16:51:26 -0600 Subject: [PATCH 046/220] guard against cyipopt not avilable --- .../pynumero/algorithms/solvers/tests/test_cyipopt_solver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 b362e3ffca0..60e3a72245e 100644 --- a/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py +++ b/pyomo/contrib/pynumero/algorithms/solvers/tests/test_cyipopt_solver.py @@ -370,7 +370,9 @@ def intermediate( # 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+ - @unittest.skipIf(not cyipopt_ge_1_3, "cyipopt version < 1.3.0") + @unittest.skipIf( + not cyipopt_available or not cyipopt_ge_1_3, "cyipopt version < 1.3.0" + ) def test_solve_13arg_callback(self): m = create_model1() From 8e4e85799084de64f33058e450e7afd2a2ca1d07 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 17 Jun 2024 15:08:32 -0400 Subject: [PATCH 047/220] improve error message and finish changes for PWLF constructor --- .../piecewise/piecewise_linear_function.py | 38 ++++++++++++++++--- .../piecewise/transform/incremental.py | 11 ++++-- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 801ef0b2e19..41d063f5b42 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -262,6 +262,7 @@ def __init__(self, *args, **kwargs): _tabular_data_arg = kwargs.pop('tabular_data', None) _tabular_data_rule_arg = kwargs.pop('tabular_data_rule', None) _triangulation_rule_arg = kwargs.pop('triangulation', Triangulation.Delaunay) + _triangulation_override_rule_arg = kwargs.pop('triangulation_override', None) kwargs.setdefault('ctype', PiecewiseLinearFunction) Block.__init__(self, *args, **kwargs) @@ -283,6 +284,9 @@ def __init__(self, *args, **kwargs): self._triangulation_rule = Initializer( _triangulation_rule_arg, treat_sequences_as_mappings=False ) + self._triangulation_override_rule = Initializer( + _triangulation_override_rule_arg, treat_sequences_as_mappings=False + ) def _get_dimension_from_points(self, points): if len(points) < 1: @@ -376,7 +380,7 @@ 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( @@ -386,7 +390,13 @@ def _construct_from_function_and_points(self, obj, parent, nonlinear_function): 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: + obj._triangulation = Triangulation.Unknown + else: + obj._triangulation = Triangulation.AssumeValid + for idx1, idx2 in obj._simplices: x1 = obj._points[idx1][0] x2 = obj._points[idx2][0] @@ -417,8 +427,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 it should be unknown. + if simplices_are_user_defined: + obj._triangulation = Triangulation.Unknown + # evaluate the function at each of the points and form the homogeneous # system of equations @@ -471,6 +487,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)] + obj._triangulation = Triangulation.Unknown return obj @_define_handler(_handlers, False, False, False, False, True) @@ -488,14 +505,14 @@ 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, 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): @@ -532,7 +549,16 @@ 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) + + # If the user wanted to override the triangulation tag, do it after we + # are finished setting it ourselves. + if self._triangulation_override_rule is not None: + triangulation_override = self._triangulation_override_rule(parent, obj._index) + if triangulation_override is not None: + obj._triangulation = triangulation_override + + return obj class ScalarPiecewiseLinearFunction( diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 646c1580ea6..80a6f9c3e8f 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -58,10 +58,13 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc ): # almost certain not to work raise ValueError( - f""" - Incremental transformation specified, but the triangulation {pw_linear_func.triangulation} may not be appropriately ordered. This would likely lead to incorrect results! - If you know what you are doing, you can suppress this error by overriding the triangulation tag to be Triangulation.AssumeValid during PiecewiseLinearFunction construction. - """ + "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 overriding the triangulation tag to be " + "Triangulation.AssumeValid during PiecewiseLinearFunction construction." ) # Get a new Block() in transformation_block.transformed_functions, which # is a Block(Any) From 0493902b2269421f000e671cd433b9e326b4b1ee Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 17 Jun 2024 18:16:09 -0400 Subject: [PATCH 048/220] change shape of triangulation output to properly match PWLF constructor. Fix currently failing tests --- .../piecewise/tests/test_incremental.py | 4 +- .../tests/test_piecewise_linear_function.py | 10 ++- .../piecewise/tests/test_triangulations.py | 75 +++++++------------ pyomo/contrib/piecewise/triangulations.py | 74 +++++++++--------- 4 files changed, 69 insertions(+), 94 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index 15b86bf8593..2e9da24c942 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -50,7 +50,6 @@ def make_log_x_model_ordered(): m = ConcreteModel() m.x = Var(bounds=(1, 10)) m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) - m.pw_log._triangulation = Triangulation.AssumeValid # Here are the linear functions, for safe keeping. def f1(x): @@ -92,9 +91,8 @@ def g2(x1, x2): [(3, 4), (3, 7), (0, 7)], ] m.pw_paraboloid = PiecewiseLinearFunction( - simplices=simplices, linear_functions=[g1, g1, g2, g2] + simplices=simplices, linear_functions=[g1, g1, g2, g2], triangulation_override=Triangulation.AssumeValid ) - m.pw_paraboloid._triangulation = Triangulation.AssumeValid m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2) def c_rule(m, i): diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index a2ac7f95d80..07932a91b23 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -124,7 +124,8 @@ def test_pw_linear_approx_of_ln_x_j1(self): points=[1, 3, 6, 10], triangulation=Triangulation.J1, function=m.f ) self.check_ln_x_approx(m.pw, m.x) - self.assertEqual(m.pw.triangulation, Triangulation.J1) + # TODO is this what we want? + self.assertEqual(m.pw.triangulation, Triangulation.AssumeValid) def test_use_pw_function_in_constraint(self): m = self.make_ln_x_model() @@ -313,11 +314,12 @@ def test_pw_linear_approx_of_paraboloid_points(self): 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)], + points=[(0, 1), (0, 4), (0, 7), (3, 1), (3, 4), (3, 7), (4, 1), (4, 4), (4, 7)], function=m.g, - triangulation=Triangulation.J1, + triangulation=Triangulation.OrderedJ1, ) - self.check_pw_linear_approximation(m) + 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): diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index fab490a95ef..8c55295c252 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -19,6 +19,7 @@ get_Gn_hamiltonian, get_grid_hamiltonian, ) +from pyomo.common.dependencies import numpy as np from math import factorial import itertools @@ -28,57 +29,31 @@ class TestTriangulations(unittest.TestCase): # check basic functionality for the unordered j1 triangulation. def test_J1_small(self): points = [ - [0, 0], - [0, 1], - [0, 2], - [1, 0], - [1, 1], - [1, 2], - [2, 0], - [2, 1], - [2, 2], + [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.assertEqual( - triangulation.simplices, - { - 0: [[0, 0], [0, 1], [1, 1]], - 1: [[0, 1], [0, 2], [1, 1]], - 2: [[1, 1], [2, 0], [2, 1]], - 3: [[1, 1], [2, 1], [2, 2]], - 4: [[0, 0], [1, 0], [1, 1]], - 5: [[0, 2], [1, 1], [1, 2]], - 6: [[1, 0], [1, 1], [2, 0]], - 7: [[1, 1], [1, 2], [2, 2]], - }, - ) - - # check that the points_map functionality does what it should - def test_J1_small_offset(self): - points = [ - [0.5, 0.5], - [0.5, 1.5], - [0.5, 2.5], - [1.5, 0.5], - [1.5, 1.5], - [1.5, 2.5], - [2.5, 0.5], - [2.5, 1.5], - [2.5, 2.5], - ] - triangulation = get_unordered_j1_triangulation(points, 2) - self.assertEqual( - triangulation.simplices, - { - 0: [[0.5, 0.5], [0.5, 1.5], [1.5, 1.5]], - 1: [[0.5, 1.5], [0.5, 2.5], [1.5, 1.5]], - 2: [[1.5, 1.5], [2.5, 0.5], [2.5, 1.5]], - 3: [[1.5, 1.5], [2.5, 1.5], [2.5, 2.5]], - 4: [[0.5, 0.5], [1.5, 0.5], [1.5, 1.5]], - 5: [[0.5, 2.5], [1.5, 1.5], [1.5, 2.5]], - 6: [[1.5, 0.5], [1.5, 1.5], [2.5, 0.5]], - 7: [[1.5, 1.5], [1.5, 2.5], [2.5, 2.5]], - }, + 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): @@ -87,7 +62,7 @@ def check_J1_ordered(self, points, num_points, dim): self.assertEqual( len(ordered_triangulation), factorial(dim) * (num_points - 1) ** dim ) - for idx, first_simplex in ordered_triangulation.items(): + 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) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 1a23a4516bd..cc0a6484fec 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -10,9 +10,9 @@ # ___________________________________________________________________________ import itertools -from types import SimpleNamespace 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 ( hamiltonian_paths as incremental_3d_simplex_pair_to_path, ) @@ -25,18 +25,23 @@ class Triangulation(Enum): 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 +# - 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 FakeScipyTriangulation(): + def __init__(self, points, simplices, coplanar): + self.points = points + self.simplices = simplices + self.coplanar = coplanar + 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) - # for now make a duck-typed thing that superficially looks like an instance of - # scipy.spatial.Delaunay (these are NDarrays in the original) - triangulation = SimpleNamespace() - triangulation.points = list(range(len(simplices_list))) - triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} - triangulation.coplanar = [] - - return triangulation + return FakeScipyTriangulation(points=np.array(points), simplices=np.array(simplices_list), coplanar=np.array([])) def get_ordered_j1_triangulation(points, dimension): @@ -49,12 +54,7 @@ def get_ordered_j1_triangulation(points, dimension): simplices_list = _get_ordered_j1_triangulation_4d_and_above( points_map, num_pts - 1, dimension ) - triangulation = SimpleNamespace() - triangulation.points = list(range(len(simplices_list))) - triangulation.simplices = {i: simplices_list[i] for i in triangulation.points} - triangulation.coplanar = [] - - return triangulation + return FakeScipyTriangulation(points=np.array(points), simplices=np.array(simplices_list), coplanar=np.array([])) # Does some validation but mostly assumes the user did the right thing @@ -78,7 +78,7 @@ def _process_points_j1(points, 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] = points[point_flat_index] + points_map[point_index] = point_flat_index return points_map, num_pts @@ -130,37 +130,37 @@ class Direction(Enum): facing = None - simplices = {} + simplices = [] start_square = (num_pts - 1, (num_pts / 2) - 1) # make it easier to read what I'm doing def add_bottom_right(): - simplices[len(simplices)] = ( + simplices.append(( points_map[x, y], points_map[x + 1, y], points_map[x + 1, y + 1], - ) + )) def add_top_right(): - simplices[len(simplices)] = ( + simplices.append(( points_map[x, y + 1], points_map[x + 1, y], points_map[x + 1, y + 1], - ) + )) def add_bottom_left(): - simplices[len(simplices)] = ( + simplices.append(( points_map[x, y], points_map[x, y + 1], points_map[x + 1, y], - ) + )) def add_top_left(): - simplices[len(simplices)] = ( + 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 @@ -304,7 +304,7 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): # start from the -x side start_data = ((-1, 0, 0), 1) - simplices = {} + simplices = [] for i in range(len(grid_hamiltonian) - 1): current_double_cube_idx = grid_hamiltonian[i] next_double_cube_idx = grid_hamiltonian[i + 1] @@ -331,9 +331,9 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): start_data = (tuple(-1 * i for i in direction_to_next), 2) for simplex_data in current_cube_path: - simplices[len(simplices)] = get_one_j1_simplex( + 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. @@ -352,9 +352,9 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): ] for simplex_data in current_cube_path: - simplices[len(simplices)] = get_one_j1_simplex( + 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 @@ -374,7 +374,7 @@ def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, 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 = {} + simplices = [] for i in range(len(grid_hamiltonian) - 1): current_corner = grid_hamiltonian[i] next_corner = grid_hamiltonian[i + 1] @@ -390,15 +390,15 @@ def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): if c % 2 == 0: perm_sequence = get_Gn_hamiltonian(dim, start_perm, j, False) for pi in perm_sequence: - simplices[len(simplices)] = get_one_j1_simplex( + 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[len(simplices)] = get_one_j1_simplex( + simplices.append(get_one_j1_simplex( v_0, pi, sign, dim, points_map - ) + )) # should be true regardless of odd or even? I hope start_perm = perm_sequence[-1] @@ -406,7 +406,7 @@ def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): # 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[len(simplices)] = get_one_j1_simplex(v_0, pi, sign, dim, points_map) + simplices.append(get_one_j1_simplex(v_0, pi, sign, dim, points_map)) # fix vertices and return fix_vertices_incremental_order(simplices) @@ -460,7 +460,7 @@ def get_grid_hamiltonian(dim, length): # 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 simplices.items(): + 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. From 46dd53feec09909eb983dc4b813e0717eba0711c Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 17 Jun 2024 18:18:38 -0400 Subject: [PATCH 049/220] clarify a comment --- pyomo/contrib/piecewise/triangulations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index cc0a6484fec..8384eaab169 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -28,7 +28,7 @@ class Triangulation(Enum): # 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 +# - 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 FakeScipyTriangulation(): From 1f6a8107896644648d5d3ccb154cbcda24368a2a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 18 Jun 2024 13:29:21 -0600 Subject: [PATCH 050/220] optimizations and rename argument --- .../piecewise/piecewise_linear_function.py | 11 ++++- .../piecewise/tests/test_incremental.py | 2 +- .../piecewise/tests/test_triangulations.py | 9 +--- pyomo/contrib/piecewise/triangulations.py | 45 +++++++++++-------- 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 41d063f5b42..7b802be60cc 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -262,7 +262,7 @@ def __init__(self, *args, **kwargs): _tabular_data_arg = kwargs.pop('tabular_data', None) _tabular_data_rule_arg = kwargs.pop('tabular_data_rule', None) _triangulation_rule_arg = kwargs.pop('triangulation', Triangulation.Delaunay) - _triangulation_override_rule_arg = kwargs.pop('triangulation_override', None) + _triangulation_override_rule_arg = kwargs.pop('override_triangulation', None) kwargs.setdefault('ctype', PiecewiseLinearFunction) Block.__init__(self, *args, **kwargs) @@ -394,6 +394,9 @@ def _construct_from_univariate_function_and_segments(self, obj, parent, func, se # We can trust they are nicely ordered if we made them, otherwise anything goes. if segments_are_user_defined: obj._triangulation = Triangulation.Unknown + tri = self._triangulation_rule(parent, obj._index) + if tri not in (None, Triangulation.Delaunay): + logger.warn(f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead.") else: obj._triangulation = Triangulation.AssumeValid @@ -434,6 +437,9 @@ def _construct_from_function_and_simplices( # then it should be unknown. if simplices_are_user_defined: obj._triangulation = Triangulation.Unknown + tri = self._triangulation_rule(parent, obj._index) + if tri not in (None, Triangulation.Delaunay): + logger.warn(f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead.") # evaluate the function at each of the points and form the homogeneous @@ -488,6 +494,9 @@ def _construct_from_linear_functions_and_simplices( 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)] obj._triangulation = Triangulation.Unknown + tri = self._triangulation_rule(parent, obj._index) + if tri not in (None, Triangulation.Delaunay): + logger.warn(f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead.") return obj @_define_handler(_handlers, False, False, False, False, True) diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index 2e9da24c942..8f6b0ebd68f 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -91,7 +91,7 @@ def g2(x1, x2): [(3, 4), (3, 7), (0, 7)], ] m.pw_paraboloid = PiecewiseLinearFunction( - simplices=simplices, linear_functions=[g1, g1, g2, g2], triangulation_override=Triangulation.AssumeValid + simplices=simplices, linear_functions=[g1, g1, g2, g2], override_triangulation=Triangulation.AssumeValid ) m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 8c55295c252..acb2fe217ab 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -58,7 +58,6 @@ def test_J1_small(self): def check_J1_ordered(self, points, num_points, dim): ordered_triangulation = get_ordered_j1_triangulation(points, dim).simplices - # print(ordered_triangulation) self.assertEqual( len(ordered_triangulation), factorial(dim) * (num_points - 1) ** dim ) @@ -73,14 +72,8 @@ def check_J1_ordered(self, points, num_points, dim): ) # The way I am constructing these, they should always share an (n-1)-face. # Check that too for good measure. - count = 0 - for pt in first_simplex: - if pt in second_simplex: - count += 1 - # print(f"first_simplex={first_simplex}; second_simplex={second_simplex}") + count = len(set(first_simplex).intersection(set(second_simplex))) self.assertEqual(count, dim) # (n-1)-face has n points - # if count != dim: - # print(f"error: count {count} was not the correct {dim}") def test_J1_ordered_2d(self): self.check_J1_ordered(list(itertools.product([0, 1, 2], [1, 2.4, 3])), 3, 2) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 8384eaab169..eaf5432f9c7 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -82,7 +82,7 @@ def _process_points_j1(points, dimension): return points_map, num_pts -# Implement the J1 "Union Jack" triangulation (Todd 77) as explained by +# 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. @@ -103,7 +103,6 @@ def _get_j1_triangulation(points_map, K, n): current = list(v_0) simplex.append(points_map[*current]) for i in range(0, n): - current = current.copy() current[pi[i]] += s[pi[i]] simplex.append(points_map[*current]) # sort this because it might happen again later and we'd like to stay @@ -112,7 +111,7 @@ def _get_j1_triangulation(points_map, K, n): return ret -# Implement something similar to proof-by-picture from Todd 77 (Figure 1). +# 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. @@ -418,7 +417,6 @@ def get_one_j1_simplex(v_0, pi, sign, dim, points_map): current = list(v_0) simplex.append(points_map[*current]) for i in range(0, dim): - current = current.copy() current[pi[i] - 1] += sign[pi[i] - 1] simplex.append(points_map[*current]) return sorted(simplex) @@ -469,10 +467,7 @@ def fix_vertices_incremental_order(simplices): if i == 0: first = 0 else: - for n in range(last_vertex_index + 1): - if simplex[n] == simplices[i - 1][last_vertex_index]: - first = n - break + first = simplex.index(simplices[i - 1][last_vertex_index]) if i == len(simplices) - 1: last = last_vertex_index @@ -485,12 +480,16 @@ def fix_vertices_incremental_order(simplices): raise DeveloperError("Couldn't fix vertex ordering for incremental.") # reorder the simplex with the desired first and last - new_simplex = [simplex[first]] - for n in range(last_vertex_index + 1): - if n != first and n != last: - new_simplex.append(simplex[n]) - new_simplex.append(simplex[last]) - simplices[i] = new_simplex + 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 @@ -501,17 +500,21 @@ def fix_vertices_incremental_order(simplices): # 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): +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: - return [ + 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 = [ @@ -519,18 +522,22 @@ def get_Gn_hamiltonian(n, start_permutation, target_symbol, last): ][ 0 ] # pi^-1(j) - return [ + ret = [ tuple(start_permutation[pi[i] - 1] for i in range(n)) for pi in _get_Gn_hamiltonian(n, new_target_symbol) ] + _cache[(n, start_permutation, target_symbol, last)] = ret + return ret else: - return _get_Gn_hamiltonian(n, target_symbol) + ret = _get_Gn_hamiltonian(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(n, target_symbol): - # base case: proof by picture from Todd 77, Figure 2 + # base case: proof by picture from Todd 79, Figure 2 # note: Figure 2 contains an error, careful! if n == 4: if target_symbol == 1: From c95c34c37eb544711201f2601c1eb9387b769688 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 20 Jun 2024 08:33:17 -0600 Subject: [PATCH 051/220] delete slow util functions --- .../contrib/piecewise/triangulations_util.py | 239 ------------------ 1 file changed, 239 deletions(-) delete mode 100644 pyomo/contrib/piecewise/triangulations_util.py diff --git a/pyomo/contrib/piecewise/triangulations_util.py b/pyomo/contrib/piecewise/triangulations_util.py deleted file mode 100644 index 37cb7c31cc8..00000000000 --- a/pyomo/contrib/piecewise/triangulations_util.py +++ /dev/null @@ -1,239 +0,0 @@ -# ___________________________________________________________________________ -# -# 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.environ import ( - ConcreteModel, - RangeSet, - Var, - Binary, - Constraint, - Param, - SolverFactory, - value, - Objective, - TerminationCondition, -) -from pyomo.contrib.piecewise.triangulations import fix_vertices_incremental_order - - -# Set up a MIP (err, MIQCP) that orders our simplices and their vertices for us -# in the following way: -# -# (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 -# -# Note that (2) implies (1), so we only need to enforce that. -def reorder_simplices_for_incremental(simplices, subsolver='gurobi'): - m = ConcreteModel() - - # Sets and Params - m.SimplicesCount = Param(initialize=len(simplices)) - m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) - # For each of the simplices we need to choose an initial and a final vertex. - # The rest we can order arbitrarily after finishing the MIP solve. - m.SimplexVerticesCount = Param(initialize=len(simplices[0])) - m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) - - @m.Param( - m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary - ) - def TestVerticesEqual(m, i, n, j, k): - return 1 if simplices[i][n] == simplices[j][k] else 0 - - # Vars - # x_ij means simplex i is placed in slot j - m.x = Var(m.SIMPLICES, m.SIMPLICES, domain=Binary) - m.vertex_is_first = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) - m.vertex_is_last = Var(m.SIMPLICES, m.VERTEX_INDICES, domain=Binary) - - # Constraints - # Each simplex should have a slot and each slot should have a simplex - @m.Constraint(m.SIMPLICES) - def schedule_each_simplex(m, i): - return sum(m.x[i, j] for j in m.SIMPLICES) == 1 - - @m.Constraint(m.SIMPLICES) - def schedule_each_slot(m, j): - return sum(m.x[i, j] for i in m.SIMPLICES) == 1 - - # Each simplex needs exactly one first and exactly one last vertex - @m.Constraint(m.SIMPLICES) - def one_first_vertex(m, i): - return sum(m.vertex_is_first[i, n] for n in m.VERTEX_INDICES) == 1 - - @m.Constraint(m.SIMPLICES) - def one_last_vertex(m, i): - return sum(m.vertex_is_last[i, n] for n in m.VERTEX_INDICES) == 1 - - # The last vertex cannot be the same as the first vertex - @m.Constraint(m.SIMPLICES, m.VERTEX_INDICES) - def first_last_distinct(m, i, n): - return m.vertex_is_first[i, n] * m.vertex_is_last[i, n] == 0 - - # Enforce property (2). This also guarantees property (1) - @m.Constraint(m.SIMPLICES, m.SIMPLICES) - def vertex_order(m, i, j): - # Enforce only when j is the simplex following i. If not, RHS is zero - return sum( - m.vertex_is_last[i, n] - * m.vertex_is_first[j, k] - * m.TestVerticesEqual[i, n, j, k] - for n in m.VERTEX_INDICES - for k in m.VERTEX_INDICES - ) >= sum( - m.x[i, p] * m.x[j, p + 1] for p in m.SIMPLICES if p != m.SimplicesCount - 1 - ) - - # Trivial objective (do I need this?) - m.obj = Objective(expr=0) - - # Solve model - results = SolverFactory(subsolver).solve(m, tee=True) - match (results.solver.termination_condition): - case TerminationCondition.infeasible: - raise ValueError( - "The triangulation was impossible to suitably order for the incremental transformation. Try a different triangulation, such as J1." - ) - case TerminationCondition.optimal: - pass - case _: - raise ValueError( - f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}" - ) - - # Retrieve data - # m.pprint() - new_simplices = {} - for j in m.SIMPLICES: - for i in m.SIMPLICES: - if abs(value(m.x[i, j]) - 1) < 1e-5: - # The jth slot is occupied by the ith simplex - old_simplex = simplices[i] - # Reorder its vertices, too - first = None - last = None - for n in m.VERTEX_INDICES: - if abs(value(m.vertex_is_first[i, n]) - 1) < 1e-5: - first = n - if abs(value(m.vertex_is_last[i, n]) - 1) < 1e-5: - last = n - if first is not None and last is not None: - break - new_simplex = [old_simplex[first]] - for n in m.VERTEX_INDICES: - if n != first and n != last: - new_simplex.append(old_simplex[n]) - new_simplex.append(old_simplex[last]) - new_simplices[j] = new_simplex - break - return new_simplices - - -# An alternative approach is to order the simplices instead of the vertices. To -# do this, the condition (1) should be that they share a 1-face, not just a -# vertex. Then there is always a consistent way to choose distinct first and -# last vertices, which would otherwise be the issue - the rest of the vertex -# ordering can be arbitrary. By assuming we share 1- or n-faces, the problem -# is made somewhat smaller. -# Note that this case is literally just asking Gurobi to get a hamiltonian path -# in a large graph and hoping it can do it. -def reorder_simplices_for_incremental_assume_connected_by_n_face( - simplices, connected_face_dim, subsolver='gurobi' -): - if connected_face_dim == 0: - return reorder_simplices_for_incremental(simplices) - - m = ConcreteModel() - - # Sets and Params - m.SimplicesCount = Param(initialize=len(simplices)) - m.SIMPLICES = RangeSet(0, m.SimplicesCount - 1) - m.SimplexVerticesCount = Param(initialize=len(simplices[0])) - m.VERTEX_INDICES = RangeSet(0, m.SimplexVerticesCount - 1) - - @m.Param( - m.SIMPLICES, m.VERTEX_INDICES, m.SIMPLICES, m.VERTEX_INDICES, domain=Binary - ) - def TestVerticesEqual(m, i, n, j, k): - return 1 if simplices[i][n] == simplices[j][k] else 0 - - # Vars - # x_ij means simplex i is placed in slot j - m.x = Var(m.SIMPLICES, m.SIMPLICES, domain=Binary) - - # Constraints - # Each simplex should have a slot and each slot should have a simplex - @m.Constraint(m.SIMPLICES) - def schedule_each_simplex(m, i): - return sum(m.x[i, j] for j in m.SIMPLICES) == 1 - - @m.Constraint(m.SIMPLICES) - def schedule_each_slot(m, j): - return sum(m.x[i, j] for i in m.SIMPLICES) == 1 - - # Enforce property (1) - @m.Constraint(m.SIMPLICES) - def simplex_order(m, i): - # anything with at least a vertex in common is a neighbor - neighbors = [ - s - for s in m.SIMPLICES - if sum( - m.TestVerticesEqual[i, n, s, k] - for n in m.VERTEX_INDICES - for k in m.VERTEX_INDICES - ) - >= connected_face_dim + 1 - and s != i - ] - # print(f'neighbors of {i} are {neighbors}') - return ( - sum( - m.x[i, j] * m.x[k, j + 1] - for j in m.SIMPLICES - if j != m.SimplicesCount - 1 - for k in neighbors - ) - + m.x[i, m.SimplicesCount - 1] - == 1 - ) - - # Trivial objective (do I need this?) - m.obj = Objective(expr=0) - - # m.pprint() - # Solve model - results = SolverFactory(subsolver).solve(m, tee=True) - match (results.solver.termination_condition): - case TerminationCondition.infeasible: - raise ValueError( - f"The triangulation was impossible to suitably order for the incremental transformation under the assumption that consecutive simplices share {connected_face_dim}-faces. Try relaxing that assumption, or try a different triangulation, such as J1." - ) - case TerminationCondition.optimal: - pass - case _: - raise ValueError( - f"Failed to generate suitable ordering for incremental transformation due to unexpected solver termination condition {results.solver.termination_condition}" - ) - - # Retrieve data - new_simplices = {} - for j in m.SIMPLICES: - for i in m.SIMPLICES: - if abs(value(m.x[i, j]) - 1) < 1e-5: - # The jth slot is occupied by the ith simplex - new_simplices[j] = simplices[i] - # Note vertices need to be fixed after the fact now - break - fix_vertices_incremental_order(new_simplices) - return new_simplices From a75048cde857559dc5b7a1a3276ac9c8fcc02438 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 20 Jun 2024 08:35:40 -0600 Subject: [PATCH 052/220] apply black --- .../piecewise/piecewise_linear_function.py | 37 +++++++--- .../piecewise/tests/test_incremental.py | 4 +- .../tests/test_piecewise_linear_function.py | 12 +++- .../piecewise/tests/test_triangulations.py | 22 +++--- pyomo/contrib/piecewise/triangulations.py | 69 +++++++++---------- 5 files changed, 86 insertions(+), 58 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 7b802be60cc..30ec635ca46 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -390,13 +390,17 @@ def _construct_from_function_and_points(self, obj, parent, nonlinear_function): obj, parent, nonlinear_function, simplices_are_user_defined=False ) - def _construct_from_univariate_function_and_segments(self, obj, parent, func, segments_are_user_defined=True): + 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: obj._triangulation = Triangulation.Unknown tri = self._triangulation_rule(parent, obj._index) if tri not in (None, Triangulation.Delaunay): - logger.warn(f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead.") + logger.warn( + f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead." + ) else: obj._triangulation = Triangulation.AssumeValid @@ -432,15 +436,16 @@ def _construct_from_function_and_simplices( return self._construct_from_univariate_function_and_segments( obj, parent, nonlinear_function, simplices_are_user_defined ) - - # If we triangulated, then this tag was already set. If they provided it, + + # If we triangulated, then this tag was already set. If they provided it, # then it should be unknown. if simplices_are_user_defined: obj._triangulation = Triangulation.Unknown tri = self._triangulation_rule(parent, obj._index) if tri not in (None, Triangulation.Delaunay): - logger.warn(f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead.") - + logger.warn( + f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead." + ) # evaluate the function at each of the points and form the homogeneous # system of equations @@ -496,7 +501,9 @@ def _construct_from_linear_functions_and_simplices( obj._triangulation = Triangulation.Unknown tri = self._triangulation_rule(parent, obj._index) if tri not in (None, Triangulation.Delaunay): - logger.warn(f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead.") + logger.warn( + f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead." + ) return obj @_define_handler(_handlers, False, False, False, False, True) @@ -514,14 +521,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, parent, _tabular_data_functor(tabular_data, tupleize=True), segments_are_user_defined=False + obj, + parent, + _tabular_data_functor(tabular_data, tupleize=True), + segments_are_user_defined=False, ) self._construct_simplices_from_multivariate_points( obj, parent, points, dimension ) return self._construct_from_function_and_simplices( - obj, parent, _tabular_data_functor(tabular_data), simplices_are_user_defined=False + obj, + parent, + _tabular_data_functor(tabular_data), + simplices_are_user_defined=False, ) def _getitem_when_not_present(self, index): @@ -563,10 +576,12 @@ def _getitem_when_not_present(self, index): # If the user wanted to override the triangulation tag, do it after we # are finished setting it ourselves. if self._triangulation_override_rule is not None: - triangulation_override = self._triangulation_override_rule(parent, obj._index) + triangulation_override = self._triangulation_override_rule( + parent, obj._index + ) if triangulation_override is not None: obj._triangulation = triangulation_override - + return obj diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index 8f6b0ebd68f..9230eefb3e4 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -91,7 +91,9 @@ def g2(x1, x2): [(3, 4), (3, 7), (0, 7)], ] m.pw_paraboloid = PiecewiseLinearFunction( - simplices=simplices, linear_functions=[g1, g1, g2, g2], override_triangulation=Triangulation.AssumeValid + simplices=simplices, + linear_functions=[g1, g1, g2, g2], + override_triangulation=Triangulation.AssumeValid, ) m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2) diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index 07932a91b23..8f0bb857a19 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -314,7 +314,17 @@ def test_pw_linear_approx_of_paraboloid_points(self): 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)], + 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, ) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index acb2fe217ab..c41951a95e9 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -43,16 +43,18 @@ def test_J1_small(self): 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] - ]) + 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], + ] + ), ) ) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index eaf5432f9c7..37c5a220b4c 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -25,13 +25,14 @@ class Triangulation(Enum): 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 FakeScipyTriangulation(): +class FakeScipyTriangulation: def __init__(self, points, simplices, coplanar): self.points = points self.simplices = simplices @@ -41,7 +42,11 @@ def __init__(self, points, simplices, coplanar): 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 FakeScipyTriangulation(points=np.array(points), simplices=np.array(simplices_list), coplanar=np.array([])) + return FakeScipyTriangulation( + points=np.array(points), + simplices=np.array(simplices_list), + coplanar=np.array([]), + ) def get_ordered_j1_triangulation(points, dimension): @@ -54,7 +59,11 @@ def get_ordered_j1_triangulation(points, dimension): simplices_list = _get_ordered_j1_triangulation_4d_and_above( points_map, num_pts - 1, dimension ) - return FakeScipyTriangulation(points=np.array(points), simplices=np.array(simplices_list), coplanar=np.array([])) + return FakeScipyTriangulation( + points=np.array(points), + simplices=np.array(simplices_list), + coplanar=np.array([]), + ) # Does some validation but mostly assumes the user did the right thing @@ -134,32 +143,22 @@ class Direction(Enum): # 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], - )) + 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], - )) + 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], - )) + 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], - )) + 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 @@ -330,9 +329,11 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): 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 - )) + 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. @@ -351,9 +352,11 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): ] 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 - )) + 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 @@ -389,15 +392,11 @@ def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): 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 - )) + 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 - )) + simplices.append(get_one_j1_simplex(v_0, pi, sign, dim, points_map)) # should be true regardless of odd or even? I hope start_perm = perm_sequence[-1] From 4cfcb7dd8e001ca4474fd7c73cb15339e8b78755 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 24 Jun 2024 09:13:20 -0600 Subject: [PATCH 053/220] add some additional renaming to a class we renamed a while ago --- .../transform/piecewise_linear_transformation_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py b/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py index 7e96891bbc4..57c659c80d2 100644 --- a/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py +++ b/pyomo/contrib/piecewise/transform/piecewise_linear_transformation_base.py @@ -42,10 +42,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( From 28cd412d41573f874c24989e9050c4294e79155b Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 24 Jun 2024 10:25:14 -0600 Subject: [PATCH 054/220] improve control flow --- .../piecewise/piecewise_linear_function.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 30ec635ca46..1ce85488340 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -396,11 +396,6 @@ def _construct_from_univariate_function_and_segments( # We can trust they are nicely ordered if we made them, otherwise anything goes. if segments_are_user_defined: obj._triangulation = Triangulation.Unknown - tri = self._triangulation_rule(parent, obj._index) - if tri not in (None, Triangulation.Delaunay): - logger.warn( - f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead." - ) else: obj._triangulation = Triangulation.AssumeValid @@ -441,11 +436,6 @@ def _construct_from_function_and_simplices( # then it should be unknown. if simplices_are_user_defined: obj._triangulation = Triangulation.Unknown - tri = self._triangulation_rule(parent, obj._index) - if tri not in (None, Triangulation.Delaunay): - logger.warn( - f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead." - ) # evaluate the function at each of the points and form the homogeneous # system of equations @@ -499,11 +489,6 @@ def _construct_from_linear_functions_and_simplices( 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)] obj._triangulation = Triangulation.Unknown - tri = self._triangulation_rule(parent, obj._index) - if tri not in (None, Triangulation.Delaunay): - logger.warn( - f"Non-default triangulation request {tri} was ignored because the simplices were provided. If you meant to override the tag, use `override_triangulation` instead." - ) return obj @_define_handler(_handlers, False, False, False, False, True) @@ -552,6 +537,17 @@ def _getitem_when_not_present(self, index): elif self._func is not None: nonlinear_function = self._func + # If the user asked for a specific triangulation but passed simplices, + # warn them that we're going to use the simplices and ignore the triangulation. + if self._simplices_rule is not None: + tri = self._triangulation_rule(parent, obj._index) + if tri not in (None, Triangulation.Delaunay): + logger.warn( + f"Non-default triangulation request {tri} was ignored because the " + "simplices were provided. If you meant to override the tag, use " + "`override_triangulation` instead." + ) + handler = self._handlers.get( ( nonlinear_function is not None, From 31ec5dabb3f55a67d17d75d5dd9d63a381f6d653 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 24 Jun 2024 10:27:08 -0600 Subject: [PATCH 055/220] add citations in comments --- .../piecewise/transform/incremental.py | 13 +++- pyomo/contrib/piecewise/triangulations.py | 59 ++++++++++++------- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 80a6f9c3e8f..d37b700bebb 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -36,12 +36,21 @@ "contrib.piecewise.incremental", doc=""" The incremental MIP formulation of a piecewise-linear function, as described - by Vielma et al, 2010. To work in the multivariate case, the underlying - triangulation must satisfy these properties: + 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 Triangulations.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): diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 37c5a220b4c..2c7cbbc50f5 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -26,29 +26,36 @@ class Triangulation(Enum): 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 FakeScipyTriangulation: - 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 FakeScipyTriangulation( + return _FakeScipyTriangulation( 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: @@ -59,12 +66,23 @@ def get_ordered_j1_triangulation(points, dimension): simplices_list = _get_ordered_j1_triangulation_4d_and_above( points_map, num_pts - 1, dimension ) - return FakeScipyTriangulation( + return _FakeScipyTriangulation( points=np.array(points), simplices=np.array(simplices_list), coplanar=np.array([]), ) +# 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 _FakeScipyTriangulation: + def __init__(self, points, simplices, coplanar): + self.points = points + self.simplices = simplices + self.coplanar = coplanar # Does some validation but mostly assumes the user did the right thing def _process_points_j1(points, dimension): @@ -80,7 +98,8 @@ def _process_points_j1(points, dimension): "'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 with n-dimensional keys + # 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): @@ -110,10 +129,10 @@ def _get_j1_triangulation(points_map, K, n): for v_0, pi, s in big_iterator: simplex = [] current = list(v_0) - simplex.append(points_map[*current]) + simplex.append(points_map[tuple(current)]) for i in range(0, n): current[pi[i]] += s[pi[i]] - simplex.append(points_map[*current]) + 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)) @@ -397,7 +416,7 @@ def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): 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? I hope + # should be true regardless of odd or even start_perm = perm_sequence[-1] # step three: finish out the last square From a65d70320f1e35b5ff264c06d7b0d3e6bee2822b Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 24 Jun 2024 16:25:42 -0600 Subject: [PATCH 056/220] fix bug that was de-ordering the OrderedJ1 vertices --- pyomo/contrib/piecewise/piecewise_linear_function.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 1ce85488340..7edba3d41d2 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -341,7 +341,13 @@ def _construct_simplices_from_multivariate_points( # 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 != Triangulation.Delaunay: + # 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) From 38ee6bc260c78ea3a22faa9b0d2aa128ece8f85c Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 24 Jun 2024 17:21:46 -0600 Subject: [PATCH 057/220] test incremental transform --- pyomo/contrib/piecewise/tests/common_tests.py | 25 +-- .../piecewise/tests/test_incremental.py | 149 +++++++++++++++++- .../tests/test_piecewise_linear_function.py | 31 +++- .../piecewise/transform/incremental.py | 4 +- 4 files changed, 191 insertions(+), 18 deletions(-) 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/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index 9230eefb3e4..1dfbee8960c 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -1,7 +1,7 @@ # ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects -# Copyright (c) 2008-2022 +# 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 @@ -10,13 +10,11 @@ # ___________________________________________________________________________ import pyomo.common.unittest as unittest -from pyomo.contrib.piecewise.tests import models import pyomo.contrib.piecewise.tests.common_tests as ct from pyomo.contrib.piecewise.triangulations import Triangulation from pyomo.core.base import TransformationFactory from pyomo.core.expr.compare import ( assertExpressionsEqual, - assertExpressionsStructurallyEqual, ) from pyomo.gdp import Disjunct, Disjunction from pyomo.environ import ( @@ -27,13 +25,140 @@ Objective, log, value, - maximize, + 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) + + # 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_ordered() + ) + + def test_transformation_PiecewiseLinearFunction_targets(self): + ct.check_transformation_PiecewiseLinearFunction_targets( + self, 'contrib.piecewise.incremental', make_log_x_model_ordered() + ) + + def test_descend_into_expressions(self): + ct.check_descend_into_expressions( + self, 'contrib.piecewise.incremental', make_log_x_model_ordered() + ) + + def test_descend_into_expressions_constraint_target(self): + ct.check_descend_into_expressions_constraint_target( + self, 'contrib.piecewise.incremental', make_log_x_model_ordered() + ) + + def test_descend_into_expressions_objective_target(self): + ct.check_descend_into_expressions_objective_target( + self, 'contrib.piecewise.incremental', make_log_x_model_ordered() + ) + @unittest.skipUnless(SolverFactory('gurobi').available(), 'Gurobi is not available') @unittest.skipUnless(SolverFactory('gurobi').license_is_valid(), 'No license') def test_solve_log_model(self): @@ -43,6 +168,20 @@ def test_solve_log_model(self): SolverFactory('gurobi').solve(m) ct.check_log_x_model_soln(self, m) + # Failed during development when 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)) + # Make a version of the log_x model with the simplices properly ordered for the # incremental transform @@ -73,7 +212,7 @@ def f3(x): m.x1 = Var(bounds=(0, 3)) m.x2 = Var(bounds=(1, 7)) - ## apprximates paraboloid x1**2 + x2**2 + ## approximates paraboloid x1**2 + x2**2 def g1(x1, x2): return 3 * x1 + 5 * x2 - 4 diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index 8f0bb857a19..a6686237ac3 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -124,9 +124,17 @@ def test_pw_linear_approx_of_ln_x_j1(self): points=[1, 3, 6, 10], triangulation=Triangulation.J1, function=m.f ) self.check_ln_x_approx(m.pw, m.x) - # TODO is this what we want? + # 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( @@ -331,6 +339,27 @@ def test_pw_linear_approx_of_paraboloid_j1(self): self.assertEqual(len(m.pw._simplices), 8) self.assertEqual(m.pw.triangulation, Triangulation.OrderedJ1) + def test_triangulation_override(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, + override_triangulation=Triangulation.AssumeValid + ) + self.assertEqual(len(m.pw._simplices), 8) + self.assertEqual(m.pw.triangulation, Triangulation.AssumeValid) + @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/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index d37b700bebb..59c9039fdc8 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -42,7 +42,7 @@ 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 Triangulations.OrderedJ1 triangulation is compatible with this + In Pyomo, the Triangulation.OrderedJ1 triangulation is compatible with this transformation. References @@ -129,7 +129,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc bounds=(0, 1), ) transBlock.delta_one_constraint = Constraint( - # figure out if this needs to be 0 or 1 + # 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 ) From 6409c58410be6843c7f20402a3a512991c7a4abe Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Mon, 24 Jun 2024 17:22:59 -0600 Subject: [PATCH 058/220] apply black --- .../piecewise/tests/test_incremental.py | 50 ++++++++++--------- .../tests/test_piecewise_linear_function.py | 2 +- pyomo/contrib/piecewise/triangulations.py | 11 ++-- 3 files changed, 35 insertions(+), 28 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index 1dfbee8960c..a3071982e6f 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -13,9 +13,7 @@ import pyomo.contrib.piecewise.tests.common_tests as ct from pyomo.contrib.piecewise.triangulations import Triangulation from pyomo.core.base import TransformationFactory -from pyomo.core.expr.compare import ( - assertExpressionsEqual, -) +from pyomo.core.expr.compare import assertExpressionsEqual from pyomo.gdp import Disjunct, Disjunction from pyomo.environ import ( Constraint, @@ -64,47 +62,49 @@ def check_pw_log(self, m): 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)) + == 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 + == 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 + 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] + 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] + 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] + 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] + 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) @@ -119,7 +119,7 @@ def check_pw_paraboloid(self, m): 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 + # 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) @@ -173,10 +173,14 @@ def test_solve_log_model(self): @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)) + 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.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) diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index a6686237ac3..4317e90f616 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -355,7 +355,7 @@ def test_triangulation_override(self): ], function=m.g, triangulation=Triangulation.OrderedJ1, - override_triangulation=Triangulation.AssumeValid + override_triangulation=Triangulation.AssumeValid, ) self.assertEqual(len(m.pw._simplices), 8) self.assertEqual(m.pw.triangulation, Triangulation.AssumeValid) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 2c7cbbc50f5..2d11bfea67a 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -26,7 +26,7 @@ class Triangulation(Enum): OrderedJ1 = 4 -# Get an unordered J1 triangulation, as described by [1], of a finite grid of +# 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 # ---------- @@ -43,14 +43,15 @@ def get_unordered_j1_triangulation(points, dimension): 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 +# [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 @@ -72,6 +73,7 @@ def get_ordered_j1_triangulation(points, dimension): coplanar=np.array([]), ) + # 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 @@ -84,6 +86,7 @@ def __init__(self, points, simplices, coplanar): self.simplices = simplices self.coplanar = coplanar + # Does some validation but mostly assumes the user did the right thing def _process_points_j1(points, dimension): if not len(points[0]) == dimension: From eee4646cfc9e0bd0142ac25e68a7152ec2378b13 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 25 Jun 2024 09:05:26 -0600 Subject: [PATCH 059/220] fix another syntax error on old python versions --- pyomo/contrib/piecewise/triangulations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 2d11bfea67a..2d3a42d880e 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -436,10 +436,10 @@ def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): def get_one_j1_simplex(v_0, pi, sign, dim, points_map): simplex = [] current = list(v_0) - simplex.append(points_map[*current]) + simplex.append(points_map[tuple(current)]) for i in range(0, dim): current[pi[i] - 1] += sign[pi[i] - 1] - simplex.append(points_map[*current]) + simplex.append(points_map[tuple(current)]) return sorted(simplex) From 8ddc2a2b77b85f8649070b168c28ae71011d8e48 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 25 Jun 2024 09:18:44 -0600 Subject: [PATCH 060/220] avoid use of match statement which does not exist before python 3.10 --- pyomo/contrib/piecewise/triangulations.py | 185 +++++++++++----------- 1 file changed, 92 insertions(+), 93 deletions(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 2d3a42d880e..8696aa31144 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -198,121 +198,120 @@ def add_top_left(): # state machine while True: - match (facing): - case Direction.left: + 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 - 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 (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): - y += 1 - facing = Direction.up + x += 1 + facing = Direction.right continue else: - y -= 1 - facing = Direction.down + x -= 1 + facing = Direction.left continue else: - x -= 1 + y -= 1 continue - case 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 + 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: - if square_parity_tlbr(x, y): - add_top_left() - add_bottom_right() - else: - add_bottom_left() - add_top_right() + 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 (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): - y -= 1 - facing = Direction.down + x -= 1 + facing = Direction.left continue else: - y += 1 - facing = Direction.up + x += 1 + facing = Direction.right continue else: - x += 1 - continue - case 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 - case 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): From 51beffb27a46aaa86ddc6b548bae2fabce2d0570 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Tue, 25 Jun 2024 09:34:24 -0600 Subject: [PATCH 061/220] skip tests when numpy unavailable --- .../piecewise/tests/test_piecewise_linear_function.py | 2 ++ pyomo/contrib/piecewise/tests/test_triangulations.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index 4317e90f616..022c82cb759 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -319,6 +319,7 @@ 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( @@ -339,6 +340,7 @@ def test_pw_linear_approx_of_paraboloid_j1(self): self.assertEqual(len(m.pw._simplices), 8) self.assertEqual(m.pw.triangulation, Triangulation.OrderedJ1) + @unittest.skipUnless(numpy_available, "numpy is not available") def test_triangulation_override(self): m = self.make_model() m.pw = PiecewiseLinearFunction( diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index c41951a95e9..0576191fc7d 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -19,7 +19,7 @@ get_Gn_hamiltonian, get_grid_hamiltonian, ) -from pyomo.common.dependencies import numpy as np +from pyomo.common.dependencies import numpy as np, numpy_available from math import factorial import itertools @@ -27,6 +27,7 @@ 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 @@ -77,6 +78,7 @@ def check_J1_ordered(self, points, num_points, dim): 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( @@ -100,6 +102,7 @@ def test_J1_ordered_2d(self): 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 @@ -134,6 +137,7 @@ def test_J1_ordered_3d(self): 3, ) + @unittest.skipUnless(numpy_available, "numpy is not available") def test_J1_ordered_4d_and_above(self): self.check_J1_ordered( list( From a06e99779fe1a2f016aeceb652c9d65ebe26adda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Thu, 27 Jun 2024 00:04:08 +0200 Subject: [PATCH 062/220] Added more tests to ensure the functionality is maintained. --- pyomo/core/tests/unit/test_sets.py | 105 +++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 872986e0d4c..c226cbea6a6 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2816,6 +2816,111 @@ def test_validation2(self): self.fail("fail test_within2") else: pass + + 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, 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]) + error_raised = False + try: + self.instance = self.model.create_instance(currdir + "setsAB.dat") + except ValueError: + error_raised = True + assert error_raised + + 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], + ) + error_raised = False + try: + self.instance = self.model.create_instance() + except ValueError: + error_raised = True + assert error_raised + + 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, + ) + error_raised = False + try: + self.instance = self.model.create_instance() + except ValueError: + error_raised = True + assert error_raised def test_other1(self): self.model.Z = Set(initialize=['A']) From e85a3399bbbfd19d53b3e8f6a821d7c2838e8c26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Thu, 27 Jun 2024 00:33:49 +0200 Subject: [PATCH 063/220] Applied black. --- pyomo/core/base/set.py | 328 +++++++++--------- pyomo/core/base/sets.py | 6 +- pyomo/core/tests/unit/test_set.py | 517 +++++++++++++++-------------- pyomo/core/tests/unit/test_sets.py | 362 ++++++++++---------- 4 files changed, 616 insertions(+), 597 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 33737b130e8..44d3aa2fcc2 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -70,9 +70,9 @@ from collections.abc import Sequence from operator import itemgetter -logger = logging.getLogger('pyomo.core') +logger = logging.getLogger("pyomo.core") -_inf = float('inf') +_inf = float("inf") FLATTEN_CROSS_PRODUCT = True @@ -127,13 +127,13 @@ def process_setarg(arg): if isinstance(arg, SetData): if ( - getattr(arg, '_parent', None) is not None - or getattr(arg, '_anonymous_sets', None) is GlobalSetBase + getattr(arg, "_parent", None) is not None + or getattr(arg, "_anonymous_sets", None) is GlobalSetBase or arg.parent_component()._parent is not None ): return arg, None _anonymous = ComponentSet((arg,)) - if getattr(arg, '_anonymous_sets', None) is not None: + if getattr(arg, "_anonymous_sets", None) is not None: _anonymous.update(arg._anonymous_sets) return arg, _anonymous @@ -156,20 +156,20 @@ def process_setarg(arg): # DEPRECATED: This functionality has never been documented, # and I don't know of a use of it in the wild. - if hasattr(arg, 'set_options'): + if hasattr(arg, "set_options"): deprecation_warning( "The set_options set attribute is deprecated. " "Please explicitly construct complex sets", - version='5.7.3', + version="5.7.3", ) # If the argument has a set_options attribute, then use # it to initialize a set args = arg.set_options - args.setdefault('initialize', arg) - args.setdefault('ordered', type(arg) not in Set._UnorderedInitializers) + args.setdefault("initialize", arg) + args.setdefault("ordered", type(arg) not in Set._UnorderedInitializers) ans = Set(**args) - _init = args['initialize'] + _init = args["initialize"] if not ( inspect.isgenerator(_init) or inspect.isfunction(_init) @@ -199,7 +199,7 @@ def process_setarg(arg): # create the Set: # _defer_construct = False - if not hasattr(arg, '__contains__'): + if not hasattr(arg, "__contains__"): if inspect.isgenerator(arg): _ordered = True _defer_construct = True @@ -234,16 +234,16 @@ def process_setarg(arg): # # ans = SetOf(arg) _anonymous = ComponentSet((ans,)) - if getattr(ans, '_anonymous_sets', None) is not None: + if getattr(ans, "_anonymous_sets", None) is not None: _anonymous.update(_anonymous_sets) return ans, _anonymous @deprecated( - 'The set_options decorator is deprecated; create Sets from ' - 'functions explicitly by passing the function to the Set ' + "The set_options decorator is deprecated; create Sets from " + "functions explicitly by passing the function to the Set " 'constructor using the "initialize=" keyword argument.', - version='5.7', + version="5.7", ) def set_options(**kwds): """ @@ -298,7 +298,7 @@ class SetInitializer(InitializerBase): """ - __slots__ = ('_set', 'verified') + __slots__ = ("_set", "verified") def __init__(self, init, allow_generators=True): self.verified = False @@ -329,7 +329,7 @@ def __call__(self, parent, idx, obj): _ans, _anonymous = process_setarg(self._set(parent, idx)) if _anonymous: pc = obj.parent_component() - if getattr(pc, '_anonymous_sets', None) is None: + if getattr(pc, "_anonymous_sets", None) is None: pc._anonymous_sets = _anonymous else: pc._anonymous_sets.update(_anonymous) @@ -366,7 +366,7 @@ class SetIntersectInitializer(InitializerBase): """ - __slots__ = ('_A', '_B') + __slots__ = ("_A", "_B") def __init__(self, setA, setB): self._A = setA @@ -410,7 +410,7 @@ class BoundsInitializer(InitializerBase): """ - __slots__ = ('_init', 'default_step') + __slots__ = ("_init", "default_step") def __init__(self, init, default_step=0): self._init = Initializer(init, treat_sequences_as_mappings=False) @@ -455,7 +455,7 @@ class TuplizeValuesInitializer(InitializerBase): """ - __slots__ = ('_init', '_dimen') + __slots__ = ("_init", "_dimen") def __new__(cls, *args): if args == (None,): @@ -533,7 +533,7 @@ def __contains__(self, value): deprecation_warning( "Testing for set subsets with 'a in b' is deprecated. " "Use 'a.issubset(b)'.", - version='5.7', + version="5.7", ) return value.issubset(self) else: @@ -580,7 +580,7 @@ def __eq__(self, other): # Special case: non-finite range sets that only contain finite # ranges (or no ranges). We will re-generate non-finite sets to # make sure we get an accurate "finiteness" flag. - if hasattr(other, 'isfinite'): + if hasattr(other, "isfinite"): if not other.parent_component().is_constructed(): return False other_isfinite = other.isfinite() @@ -590,7 +590,7 @@ def __eq__(self, other): other_isfinite = other.isfinite() except TypeError: pass - elif hasattr(other, '__contains__'): + elif hasattr(other, "__contains__"): # we assume that everything that does not implement # isfinite() is a discrete set. other_isfinite = True @@ -881,7 +881,7 @@ def _get_continuous_interval(self): return (start, end, interval.step) @property - @deprecated("The 'virtual' attribute is no longer supported", version='5.7') + @deprecated("The 'virtual' attribute is no longer supported", version="5.7") def virtual(self): return isinstance(self, (_AnySet, SetOperator, InfiniteRangeSetData)) @@ -897,7 +897,7 @@ def virtual(self, value): @deprecated( "The 'concrete' attribute is no longer supported. " "Use isdiscrete() or isfinite()", - version='5.7', + version="5.7", ) def concrete(self): return self.isfinite() @@ -913,18 +913,18 @@ def concrete(self, value): @property @deprecated( "The 'ordered' attribute is no longer supported. Use isordered()", - version='5.7', + version="5.7", ) def ordered(self): return self.isordered() @property - @deprecated("'filter' is no longer a public attribute.", version='5.7') + @deprecated("'filter' is no longer a public attribute.", version="5.7") def filter(self): return None @deprecated( - "check_values() is deprecated: Sets only contain valid members", version='5.7' + "check_values() is deprecated: Sets only contain valid members", version="5.7" ) def check_values(self): """ @@ -944,9 +944,9 @@ def isdisjoint(self, other): ------- bool : True if this set is disjoint from `other` """ - if hasattr(other, 'isfinite'): + if hasattr(other, "isfinite"): other_isfinite = other.isfinite() - elif hasattr(other, '__contains__'): + elif hasattr(other, "__contains__"): # we assume that everything that does not implement # isfinite() is a discrete set. other_isfinite = True @@ -987,7 +987,7 @@ def issubset(self, other): # Special case: non-finite range sets that only contain finite # ranges (or no ranges). We will re-generate non-finite sets to # make sure we get an accurate "finiteness" flag. - if hasattr(other, 'isfinite'): + if hasattr(other, "isfinite"): other_isfinite = other.isfinite() if not other_isfinite: try: @@ -995,7 +995,7 @@ def issubset(self, other): other_isfinite = other.isfinite() except TypeError: pass - elif hasattr(other, '__contains__'): + elif hasattr(other, "__contains__"): # we assume that everything that does not implement # isfinite() is a discrete set. other_isfinite = True @@ -1047,7 +1047,7 @@ def issuperset(self, other): # Special case: non-finite range sets that only contain finite # ranges (or no ranges). We will re-generate non-finite sets to # make sure we get an accurate "finiteness" flag. - if hasattr(other, 'isfinite'): + if hasattr(other, "isfinite"): other_isfinite = other.isfinite() if not other_isfinite: try: @@ -1055,7 +1055,7 @@ def issuperset(self, other): other_isfinite = other.isfinite() except TypeError: pass - elif hasattr(other, '__contains__'): + elif hasattr(other, "__contains__"): # we assume that everything that does not implement # isfinite() is a discrete set. other_isfinite = True @@ -1179,12 +1179,12 @@ def __gt__(self, other): class _SetData(metaclass=RenamedClass): __renamed__new_class__ = SetData - __renamed__version__ = '6.7.2' + __renamed__version__ = "6.7.2" class _SetDataBase(metaclass=RenamedClass): __renamed__new_class__ = SetData - __renamed__version__ = '6.7.2' + __renamed__version__ = "6.7.2" class _FiniteSetMixin(object): @@ -1237,7 +1237,7 @@ def data(self): @deprecated( "The 'value' attribute is deprecated. Use .data() to " "retrieve the values in a finite set.", - version='5.7', + version="5.7", ) def value(self): return set(self) @@ -1247,7 +1247,7 @@ def value(self): "The 'value_list' attribute is deprecated. Use " ".ordered_data() to retrieve the values from a finite set " "in a deterministic order.", - version='5.7', + version="5.7", ) def value_list(self): return list(self.ordered_data()) @@ -1296,13 +1296,13 @@ def ranges(self): class FiniteSetData(_FiniteSetMixin, SetData): """A general unordered iterable Set""" - __slots__ = ('_values', '_domain', '_validate', '_filter', '_dimen') + __slots__ = ("_values", "_domain", "_validate", "_filter", "_dimen") def __init__(self, component): SetData.__init__(self, component=component) # Derived classes (like OrderedSetData) may want to change the # storage - if not hasattr(self, '_values'): + if not hasattr(self, "_values"): self._values = set() self._domain = Any self._validate = None @@ -1342,7 +1342,7 @@ def __str__(self): return self.name if not self.parent_component()._constructed: return type(self).__name__ - return "{" + (', '.join(str(_) for _ in self)) + "}" + return "{" + (", ".join(str(_) for _ in self)) + "}" @property def dimen(self): @@ -1359,7 +1359,7 @@ def domain(self): return self._domain @property - @deprecated("'filter' is no longer a public attribute.", version='5.7') + @deprecated("'filter' is no longer a public attribute.", version="5.7") def filter(self): return self._filter @@ -1420,7 +1420,7 @@ def add(self, *values): # _value is not a tuple: no need to unpack it for the method arguments' tuple flag = self._validate(_block, (_value, self._index)) else: - # non-indexed set: only the tentative member is given + # non-indexed set: only the tentative member is given flag = self._validate(_block, _value) except: logger.error( @@ -1483,7 +1483,7 @@ def pop(self): class _FiniteSetData(metaclass=RenamedClass): __renamed__new_class__ = FiniteSetData - __renamed__version__ = '6.7.2' + __renamed__version__ = "6.7.2" class _ScalarOrderedSetMixin(object): @@ -1533,15 +1533,15 @@ def __getitem__(self, key): deprecation_warning( "Using __getitem__ to return a set value from its (ordered) " "position is deprecated. Please use at()", - version='6.1', - remove_in='7.0', + version="6.1", + remove_in="7.0", ) return self.at(key) @deprecated( "card() was incorrectly added to the Set API. Please use at()", - version='6.1.2', - remove_in='6.2', + version="6.1.2", + remove_in="6.2", ) def card(self, index): return self.at(index) @@ -1663,7 +1663,7 @@ class OrderedSetData(_OrderedSetMixin, FiniteSetData): Public Class Attributes: """ - __slots__ = ('_ordered_values',) + __slots__ = ("_ordered_values",) def __init__(self, component): self._values = {} @@ -1705,7 +1705,7 @@ def pop(self): except IndexError: # Map the index error to a KeyError for consistency with # set().pop() - raise KeyError('pop from an empty set') + raise KeyError("pop from an empty set") self.discard(ans) return ans @@ -1748,7 +1748,7 @@ def ord(self, item): class _OrderedSetData(metaclass=RenamedClass): __renamed__new_class__ = OrderedSetData - __renamed__version__ = '6.7.2' + __renamed__version__ = "6.7.2" class InsertionOrderSetData(OrderedSetData): @@ -1787,7 +1787,7 @@ def update(self, values): class _InsertionOrderSetData(metaclass=RenamedClass): __renamed__new_class__ = InsertionOrderSetData - __renamed__version__ = '6.7.2' + __renamed__version__ = "6.7.2" class _SortedSetMixin(object): @@ -1812,7 +1812,7 @@ class SortedSetData(_SortedSetMixin, OrderedSetData): Public Class Attributes: """ - __slots__ = ('_is_sorted',) + __slots__ = ("_is_sorted",) def __init__(self, component): # An empty set is sorted... @@ -1883,22 +1883,22 @@ def _sort(self): class _SortedSetData(metaclass=RenamedClass): __renamed__new_class__ = SortedSetData - __renamed__version__ = '6.7.2' + __renamed__version__ = "6.7.2" ############################################################################ -_SET_API = (('__contains__', 'test membership in'), 'get', 'ranges', 'bounds') +_SET_API = (("__contains__", "test membership in"), "get", "ranges", "bounds") _FINITESET_API = _SET_API + ( - ('__iter__', 'iterate over'), - '__reversed__', - '__len__', - 'data', - 'sorted_data', - 'ordered_data', + ("__iter__", "iterate over"), + "__reversed__", + "__len__", + "data", + "sorted_data", + "ordered_data", ) -_ORDEREDSET_API = _FINITESET_API + ('at', 'ord') -_SETDATA_API = ('set_value', 'add', 'remove', 'discard', 'clear', 'update', 'pop') +_ORDEREDSET_API = _FINITESET_API + ("at", "ord") +_SETDATA_API = ("set_value", "add", "remove", "discard", "clear", "update", "pop") @ModelComponentFactory.register("Set data that is used to define a model instance.") @@ -2024,7 +2024,7 @@ def __new__(cls, *args, **kwds): # # JDS [5/2019]: Until someone demands otherwise, I think we # should leave it constant across an IndexedSet - ordered = kwds.get('ordered', Set.InsertionOrder) + ordered = kwds.get("ordered", Set.InsertionOrder) if ordered is True: ordered = Set.InsertionOrder if ordered not in Set._ValidOrderedAuguments: @@ -2040,12 +2040,12 @@ def __new__(cls, *args, **kwds): raise TypeError( "Set 'ordered' argument is not valid (must be one of {%s})" % ( - ', '.join( + ", ".join( str(_) for _ in sorted_robust( - 'Set.' + x.__name__ if isinstance(x, type) else x + "Set." + x.__name__ if isinstance(x, type) else x for x in Set._ValidOrderedAuguments.union( - {''} + {""} ) ) ) @@ -2085,11 +2085,11 @@ def __init__( ): ... def __init__(self, *args, **kwds): - kwds.setdefault('ctype', Set) + kwds.setdefault("ctype", Set) # The ordered flag was processed by __new__, but if this is a # sorted set, then we need to set the sorting function - _ordered = kwds.pop('ordered', None) + _ordered = kwds.pop("ordered", None) if _ordered and _ordered is not Set.InsertionOrder and _ordered is not True: if inspect.isfunction(_ordered): self._sort_fcn = _ordered @@ -2101,36 +2101,36 @@ def __init__(self, *args, **kwds): # specified, we will restrict the Set values to the intersection # of the individual arguments self._init_domain = SetInitializer(None) - _domain = kwds.pop('domain', None) + _domain = kwds.pop("domain", None) if _domain is not None: self._init_domain.intersect(SetInitializer(_domain)) - _within = kwds.pop('within', None) + _within = kwds.pop("within", None) if _within is not None: self._init_domain.intersect(SetInitializer(_within)) - _bounds = kwds.pop('bounds', None) + _bounds = kwds.pop("bounds", None) if _bounds is not None: self._init_domain.intersect(BoundsInitializer(_bounds)) self._init_dimen = Initializer( - kwds.pop('dimen', UnknownSetDimen), arg_not_specified=NOTSET + kwds.pop("dimen", UnknownSetDimen), arg_not_specified=NOTSET ) self._init_values = TuplizeValuesInitializer( Initializer( - kwds.pop('initialize', None), + kwds.pop("initialize", None), treat_sequences_as_mappings=False, allow_generators=True, ) ) - self._init_validate = Initializer(kwds.pop('validate', None)) - self._init_filter = Initializer(kwds.pop('filter', None)) + self._init_validate = Initializer(kwds.pop("validate", None)) + self._init_filter = Initializer(kwds.pop("filter", None)) - if 'virtual' in kwds: + if "virtual" in kwds: deprecation_warning( "Pyomo Sets ignore the 'virtual' keyword argument", - logger='pyomo.core.base', - version='5.6.7', + logger="pyomo.core.base", + version="5.6.7", ) - kwds.pop('virtual') + kwds.pop("virtual") IndexedComponent.__init__(self, *args, **kwds) @@ -2153,7 +2153,7 @@ def __init__(self, *args, **kwds): self._dimen = self._init_dimen(self.parent_block(), None) @deprecated( - "check_values() is deprecated: Sets only contain valid members", version='5.7' + "check_values() is deprecated: Sets only contain valid members", version="5.7" ) def check_values(self): """ @@ -2324,10 +2324,10 @@ def _getitem_when_not_present(self, index): @staticmethod def _pprint_members(x): if x.isfinite(): - return '{' + str(x.ordered_data())[1:-1] + "}" + return "{" + str(x.ordered_data())[1:-1] + "}" else: - ans = ' | '.join(str(_) for _ in x.ranges()) - if ' | ' in ans: + ans = " | ".join(str(_) for _ in x.ranges()) + if " | " in ans: return "(" + ans + ")" if ans: return ans @@ -2402,7 +2402,7 @@ def _pprint(self): lambda k, v: [ Set._pprint_dimen(v), Set._pprint_domain(v), - len(v) if v.isfinite() else 'Inf', + len(v) if v.isfinite() else "Inf", Set._pprint_members(v), ], ) @@ -2428,14 +2428,14 @@ def __init__(self, **kwds): class FiniteSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = FiniteScalarSet - __renamed__version__ = '6.0' + __renamed__version__ = "6.0" class OrderedScalarSet(_ScalarOrderedSetMixin, InsertionOrderSetData, Set): def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag - kwds.setdefault('ordered', Set.InsertionOrder) + kwds.setdefault("ordered", Set.InsertionOrder) InsertionOrderSetData.__init__(self, component=self) Set.__init__(self, **kwds) @@ -2443,14 +2443,14 @@ def __init__(self, **kwds): class OrderedSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = OrderedScalarSet - __renamed__version__ = '6.0' + __renamed__version__ = "6.0" class SortedScalarSet(_ScalarOrderedSetMixin, SortedSetData, Set): def __init__(self, **kwds): # In case someone inherits from us, we will provide a rational # default for the "ordered" flag - kwds.setdefault('ordered', Set.SortedOrder) + kwds.setdefault("ordered", Set.SortedOrder) SortedSetData.__init__(self, component=self) Set.__init__(self, **kwds) @@ -2459,7 +2459,7 @@ def __init__(self, **kwds): class SortedSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = SortedScalarSet - __renamed__version__ = '6.0' + __renamed__version__ = "6.0" @disable_methods(_FINITESET_API + _SETDATA_API) @@ -2469,7 +2469,7 @@ class AbstractFiniteScalarSet(FiniteScalarSet): class AbstractFiniteSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = AbstractFiniteScalarSet - __renamed__version__ = '6.0' + __renamed__version__ = "6.0" @disable_methods(_ORDEREDSET_API + _SETDATA_API) @@ -2479,7 +2479,7 @@ class AbstractOrderedScalarSet(OrderedScalarSet): class AbstractOrderedSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = AbstractOrderedScalarSet - __renamed__version__ = '6.0' + __renamed__version__ = "6.0" @disable_methods(_ORDEREDSET_API + _SETDATA_API) @@ -2489,7 +2489,7 @@ class AbstractSortedScalarSet(SortedScalarSet): class AbstractSortedSimpleSet(metaclass=RenamedClass): __renamed__new_class__ = AbstractSortedScalarSet - __renamed__version__ = '6.0' + __renamed__version__ = "6.0" ############################################################################ @@ -2517,7 +2517,7 @@ def __new__(cls, *args, **kwds): def __init__(self, reference, **kwds): SetData.__init__(self, component=self) - kwds.setdefault('ctype', SetOf) + kwds.setdefault("ctype", SetOf) Component.__init__(self, **kwds) self._ref = reference self.construct() @@ -2606,7 +2606,7 @@ def __reversed__(self): class UnorderedSetOf(metaclass=RenamedClass): __renamed__new_class__ = FiniteSetOf - __renamed__version__ = '6.2' + __renamed__version__ = "6.2" class OrderedSetOf(_ScalarOrderedSetMixin, _OrderedSetMixin, FiniteSetOf): @@ -2643,7 +2643,7 @@ class InfiniteRangeSetData(SetData): """ - __slots__ = ('_ranges',) + __slots__ = ("_ranges",) def __init__(self, component): SetData.__init__(self, component=component) @@ -2681,7 +2681,7 @@ def ranges(self): class _InfiniteRangeSetData(metaclass=RenamedClass): __renamed__new_class__ = InfiniteRangeSetData - __renamed__version__ = '6.7.2' + __renamed__version__ = "6.7.2" class FiniteRangeSetData( @@ -2794,7 +2794,7 @@ def ord(self, item): class _FiniteRangeSetData(metaclass=RenamedClass): __renamed__new_class__ = FiniteRangeSetData - __renamed__version__ = '6.7.2' + __renamed__version__ = "6.7.2" @ModelComponentFactory.register( @@ -2891,10 +2891,10 @@ def __new__(cls, *args, **kwds): if cls is not RangeSet: return super(RangeSet, cls).__new__(cls) - finite = kwds.pop('finite', None) + finite = kwds.pop("finite", None) if finite is None: - if 'ranges' in kwds: - if any(not r.isfinite() for r in kwds['ranges']): + if "ranges" in kwds: + if any(not r.isfinite() for r in kwds["ranges"]): finite = False for i, _ in enumerate(args): if type(_) not in native_types: @@ -2975,17 +2975,17 @@ def __init__( def __init__(self, *args, **kwds): # Finite was processed by __new__ - kwds.setdefault('ctype', RangeSet) + kwds.setdefault("ctype", RangeSet) if len(args) > 3: raise ValueError( "RangeSet expects 3 or fewer positional " "arguments (received %s)" % (len(args),) ) - 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._init_bounds = kwds.pop('bounds', None) + 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._init_bounds = kwds.pop("bounds", None) if self._init_bounds is not None: self._init_bounds = BoundsInitializer(self._init_bounds) @@ -3020,8 +3020,8 @@ def __str__(self): if not self._constructed: return type(self).__name__ # Floating, unnamed constructed components return their ranges() - ans = ' | '.join(str(_) for _ in self.ranges()) - if ' | ' in ans: + ans = " | ".join(str(_) for _ in self.ranges()) + if " | " in ans: return "(" + ans + ")" if ans: return ans @@ -3247,14 +3247,14 @@ def _pprint(self): return ( [ ("Dimen", self.dimen), - ("Size", len(self) if self.isfinite() else 'Inf'), + ("Size", len(self) if self.isfinite() else "Inf"), ("Bounds", self.bounds()), ], {None: self}.items(), ("Finite", "Members"), lambda k, v: [ v.isfinite(), # isinstance(v, _FiniteSetMixin), - ', '.join(str(r) for r in self.ranges()) or '[]', + ", ".join(str(r) for r in self.ranges()) or "[]", ], ) @@ -3271,7 +3271,7 @@ def __init__(self, *args, **kwds): class InfiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__new_class__ = InfiniteScalarRangeSet - __renamed__version__ = '6.0' + __renamed__version__ = "6.0" class FiniteScalarRangeSet(_ScalarOrderedSetMixin, FiniteRangeSetData, RangeSet): @@ -3286,7 +3286,7 @@ def __init__(self, *args, **kwds): class FiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__new_class__ = FiniteScalarRangeSet - __renamed__version__ = '6.0' + __renamed__version__ = "6.0" @disable_methods(_SET_API) @@ -3296,7 +3296,7 @@ class AbstractInfiniteScalarRangeSet(InfiniteScalarRangeSet): class AbstractInfiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__new_class__ = AbstractInfiniteScalarRangeSet - __renamed__version__ = '6.0' + __renamed__version__ = "6.0" @disable_methods(_ORDEREDSET_API) @@ -3306,7 +3306,7 @@ class AbstractFiniteScalarRangeSet(FiniteScalarRangeSet): class AbstractFiniteSimpleRangeSet(metaclass=RenamedClass): __renamed__new_class__ = AbstractFiniteScalarRangeSet - __renamed__version__ = '6.0' + __renamed__version__ = "6.0" ############################################################################ @@ -3315,7 +3315,7 @@ class AbstractFiniteSimpleRangeSet(metaclass=RenamedClass): class SetOperator(SetData, Set): - __slots__ = ('_sets',) + __slots__ = ("_sets",) def __init__(self, *args, **kwds): SetData.__init__(self, component=self) @@ -3356,7 +3356,7 @@ def construct(self, data=None): "Providing construction data to SetOperator objects is " "deprecated. This data is ignored and in a future version " "will not be allowed", - version='5.7', + version="5.7", ) fail = len(data) > 1 or None not in data if not fail: @@ -3428,18 +3428,18 @@ def __deepcopy__(self, memo): # # Our solution is to cause SetOperators to be automatically # cloned if they haven't been assigned to a block. - if '__block_scope__' in memo: + if "__block_scope__" in memo: if self.parent_block() is None: # Hijack the block scope rules to cause this object to # be deepcopied. - memo['__block_scope__'][id(self)] = True + memo["__block_scope__"][id(self)] = True return super(SetOperator, self).__deepcopy__(memo) def _expression_str(self): _args = [] for arg in self._sets: arg_str = str(arg) - if ' ' in arg_str and isinstance(arg, SetOperator): + if " " in arg_str and isinstance(arg, SetOperator): arg_str = "(" + arg_str + ")" _args.append(arg_str) return self._operator.join(_args) @@ -3474,7 +3474,7 @@ def subsets(self, expand_all_set_operators=None): @deprecated( "SetProduct.set_tuple is deprecated. " "Use SetProduct.subsets() to get the operator arguments.", - version='5.7', + version="5.7", ) def set_tuple(self): # Despite its name, in the old SetProduct, set_tuple held a list @@ -4053,7 +4053,7 @@ def _find_val(self, val): # number of subsets, we will start by checking each value # against the corresponding subset. Failure is not sufficient # to determine the val is not in this set. - if hasattr(val, '__len__') and len(val) == len(self._sets): + if hasattr(val, "__len__") and len(val) == len(self._sets): if all(v in self._sets[i] for i, v in enumerate(val)): return val, None @@ -4267,7 +4267,7 @@ def __init__(self, **kwds): # constructing Any, so we need to bypass that logic. This # works, but requires us to declare a special domain setter to # accept (and ignore) this value. - kwds.setdefault('domain', self) + kwds.setdefault("domain", self) Set.__init__(self, **kwds) self.construct() @@ -4308,7 +4308,7 @@ class _AnyWithNoneSet(_AnySet): # backwards compatibility with the Book. @deprecated( "The AnyWithNone set is deprecated. Use Any, which includes None", - version='5.7', + version="5.7", ) def get(self, val, default=None): return super(_AnyWithNoneSet, self).get(val, default) @@ -4402,7 +4402,7 @@ class GlobalSet(GlobalSetBase, obj.__class__): # least in Python 2.7), so we will explicitly set the __doc__ # attribute. - __slots__ = ('_bounds', '_interval') + __slots__ = ("_bounds", "_interval") global_name = None @@ -4428,19 +4428,19 @@ def __new__(cls, *args, **kwds): "deprecated. Please either use one of the pre-declared " "global Sets (e.g., Reals, NonNegativeReals, Integers, " "PositiveIntegers, Binary), or create a custom RangeSet.", - version='5.7.1', + version="5.7.1", ) # Note: we will completely ignore any positional # arguments. In this situation, these could be the # parent_block and any indices; e.g., # Var(m.I, within=RealSet) base_set = GlobalSets[GlobalSet.global_name] - bounds = kwds.pop('bounds', None) + bounds = kwds.pop("bounds", None) range_init = SetInitializer(base_set) if bounds is not None: range_init.intersect(BoundsInitializer(bounds)) - name = name_kwd = kwds.pop('name', None) - cls_name = kwds.pop('class_name', None) + name = name_kwd = kwds.pop("name", None) + cls_name = kwds.pop("class_name", None) if name is None: if cls_name is None: name = base_set.name @@ -4487,53 +4487,53 @@ def get_interval(self): DeclareGlobalSet( - _AnySet(name='Any', doc="A global Pyomo Set that admits any value"), globals() + _AnySet(name="Any", doc="A global Pyomo Set that admits any value"), globals() ) DeclareGlobalSet( - _AnyWithNoneSet(name='AnyWithNone', doc="A global Pyomo Set that admits any value"), + _AnyWithNoneSet(name="AnyWithNone", doc="A global Pyomo Set that admits any value"), globals(), ) DeclareGlobalSet( - _EmptySet(name='EmptySet', doc="A global Pyomo Set that contains no members"), + _EmptySet(name="EmptySet", doc="A global Pyomo Set that contains no members"), globals(), ) DeclareGlobalSet( RangeSet( - name='Reals', - doc='A global Pyomo Set that admits any real (floating point) value', + name="Reals", + doc="A global Pyomo Set that admits any real (floating point) value", ranges=(NumericRange(None, None, 0),), ), globals(), ) DeclareGlobalSet( RangeSet( - name='NonNegativeReals', - doc='A global Pyomo Set admitting any real value in [0, +inf]', + name="NonNegativeReals", + doc="A global Pyomo Set admitting any real value in [0, +inf]", ranges=(NumericRange(0, None, 0),), ), globals(), ) DeclareGlobalSet( RangeSet( - name='NonPositiveReals', - doc='A global Pyomo Set admitting any real value in [-inf, 0]', + name="NonPositiveReals", + doc="A global Pyomo Set admitting any real value in [-inf, 0]", ranges=(NumericRange(None, 0, 0),), ), globals(), ) DeclareGlobalSet( RangeSet( - name='NegativeReals', - doc='A global Pyomo Set admitting any real value in [-inf, 0)', + name="NegativeReals", + doc="A global Pyomo Set admitting any real value in [-inf, 0)", ranges=(NumericRange(None, 0, 0, (True, False)),), ), globals(), ) DeclareGlobalSet( RangeSet( - name='PositiveReals', - doc='A global Pyomo Set admitting any real value in (0, +inf]', + name="PositiveReals", + doc="A global Pyomo Set admitting any real value in (0, +inf]", ranges=(NumericRange(0, None, 0, (False, True)),), ), globals(), @@ -4541,40 +4541,40 @@ def get_interval(self): DeclareGlobalSet( RangeSet( - name='Integers', - doc='A global Pyomo Set admitting any integer value', + name="Integers", + doc="A global Pyomo Set admitting any integer value", ranges=(NumericRange(0, None, 1), NumericRange(0, None, -1)), ), globals(), ) DeclareGlobalSet( RangeSet( - name='NonNegativeIntegers', - doc='A global Pyomo Set admitting any integer value in [0, +inf]', + name="NonNegativeIntegers", + doc="A global Pyomo Set admitting any integer value in [0, +inf]", ranges=(NumericRange(0, None, 1),), ), globals(), ) DeclareGlobalSet( RangeSet( - name='NonPositiveIntegers', - doc='A global Pyomo Set admitting any integer value in [-inf, 0]', + name="NonPositiveIntegers", + doc="A global Pyomo Set admitting any integer value in [-inf, 0]", ranges=(NumericRange(0, None, -1),), ), globals(), ) DeclareGlobalSet( RangeSet( - name='NegativeIntegers', - doc='A global Pyomo Set admitting any integer value in [-inf, -1]', + name="NegativeIntegers", + doc="A global Pyomo Set admitting any integer value in [-inf, -1]", ranges=(NumericRange(-1, None, -1),), ), globals(), ) DeclareGlobalSet( RangeSet( - name='PositiveIntegers', - doc='A global Pyomo Set admitting any integer value in [1, +inf]', + name="PositiveIntegers", + doc="A global Pyomo Set admitting any integer value in [1, +inf]", ranges=(NumericRange(1, None, 1),), ), globals(), @@ -4582,8 +4582,8 @@ def get_interval(self): DeclareGlobalSet( RangeSet( - name='Binary', - doc='A global Pyomo Set admitting the integers {0, 1}', + name="Binary", + doc="A global Pyomo Set admitting the integers {0, 1}", ranges=(NumericRange(0, 1, 1),), ), globals(), @@ -4593,8 +4593,8 @@ def get_interval(self): # admitting {True, False}) DeclareGlobalSet( RangeSet( - name='Boolean', - doc='A global Pyomo Set admitting the integers {0, 1}', + name="Boolean", + doc="A global Pyomo Set admitting the integers {0, 1}", ranges=(NumericRange(0, 1, 1),), ), globals(), @@ -4602,16 +4602,16 @@ def get_interval(self): DeclareGlobalSet( RangeSet( - name='PercentFraction', - doc='A global Pyomo Set admitting any real value in [0, 1]', + name="PercentFraction", + doc="A global Pyomo Set admitting any real value in [0, 1]", ranges=(NumericRange(0, 1, 0),), ), globals(), ) DeclareGlobalSet( RangeSet( - name='UnitInterval', - doc='A global Pyomo Set admitting any real value in [0, 1]', + name="UnitInterval", + doc="A global Pyomo Set admitting any real value in [0, 1]", ranges=(NumericRange(0, 1, 0),), ), globals(), @@ -4662,19 +4662,19 @@ def get_interval(self): @deprecated( "RealInterval has been deprecated. Please use RangeSet(lower, upper, 0)", - version='5.7', + version="5.7", ) class RealInterval(RealSet): def __new__(cls, **kwds): - kwds.setdefault('class_name', 'RealInterval') + kwds.setdefault("class_name", "RealInterval") return super(RealInterval, cls).__new__(RealSet, **kwds) @deprecated( "IntegerInterval has been deprecated. Please use RangeSet(lower, upper, 1)", - version='5.7', + version="5.7", ) class IntegerInterval(IntegerSet): def __new__(cls, **kwds): - kwds.setdefault('class_name', 'IntegerInterval') + kwds.setdefault("class_name", "IntegerInterval") return super(IntegerInterval, cls).__new__(IntegerSet, **kwds) diff --git a/pyomo/core/base/sets.py b/pyomo/core/base/sets.py index 72d49479dd3..19277c516a2 100644 --- a/pyomo/core/base/sets.py +++ b/pyomo/core/base/sets.py @@ -27,7 +27,7 @@ from pyomo.common.deprecation import deprecation_warning deprecation_warning( - 'The pyomo.core.base.sets module is deprecated. ' - 'Import Set objects from pyomo.core.base.set or pyomo.core.', - version='5.7', + "The pyomo.core.base.sets module is deprecated. " + "Import Set objects from pyomo.core.base.set or pyomo.core.", + version="5.7", ) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 70dfeb26f74..aca93869e6f 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -334,12 +334,12 @@ def test_setdefault(self): def test_indices(self): a = SetInitializer(None) self.assertFalse(a.contains_indices()) - with self.assertRaisesRegex(RuntimeError, 'does not contain embedded indices'): + with self.assertRaisesRegex(RuntimeError, "does not contain embedded indices"): a.indices() a = SetInitializer([1, 2, 3]) self.assertFalse(a.contains_indices()) - with self.assertRaisesRegex(RuntimeError, 'does not contain embedded indices'): + with self.assertRaisesRegex(RuntimeError, "does not contain embedded indices"): a.indices() # intersection initializers @@ -359,7 +359,7 @@ def test_indices(self): a.intersect(SetInitializer({1: [4], 3: [1, 2]})) self.assertTrue(a.contains_indices()) with self.assertRaisesRegex( - ValueError, 'contains two sub-initializers with inconsistent' + ValueError, "contains two sub-initializers with inconsistent" ): a.indices() @@ -368,7 +368,7 @@ def test_indices(self): self.assertFalse(a.contains_indices()) a.intersect(SetInitializer([1, 2])) self.assertFalse(a.contains_indices()) - with self.assertRaisesRegex(RuntimeError, 'does not contain embedded indices'): + with self.assertRaisesRegex(RuntimeError, "does not contain embedded indices"): a.indices() @@ -378,7 +378,7 @@ def test_Reals(self): self.assertIn(1.5, Reals) self.assertIn(100, Reals), self.assertIn(-100, Reals), - self.assertNotIn('A', Reals) + self.assertNotIn("A", Reals) self.assertNotIn(None, Reals) self.assertFalse(Reals.isdiscrete()) @@ -408,18 +408,18 @@ def test_Reals(self): self.assertEqual(tmp, EmptySet) self.assertEqual(tmp.domain, Reals) - self.assertEqual(str(Reals), 'Reals') - self.assertEqual(str(tmp), 'Reals') + self.assertEqual(str(Reals), "Reals") + self.assertEqual(str(tmp), "Reals") b = ConcreteModel() b.tmp = tmp - self.assertEqual(str(tmp), 'tmp') + self.assertEqual(str(tmp), "tmp") def test_Integers(self): self.assertIn(0, Integers) self.assertNotIn(1.5, Integers) self.assertIn(100, Integers), self.assertIn(-100, Integers), - self.assertNotIn('A', Integers) + self.assertNotIn("A", Integers) self.assertNotIn(None, Integers) self.assertTrue(Integers.isdiscrete()) @@ -449,18 +449,18 @@ def test_Integers(self): self.assertEqual(tmp, EmptySet) self.assertEqual(tmp.domain, Reals) - self.assertEqual(str(Integers), 'Integers') - self.assertEqual(str(tmp), 'Integers') + self.assertEqual(str(Integers), "Integers") + self.assertEqual(str(tmp), "Integers") b = ConcreteModel() b.tmp = tmp - self.assertEqual(str(tmp), 'tmp') + self.assertEqual(str(tmp), "tmp") def test_Any(self): self.assertIn(0, Any) self.assertIn(1.5, Any) self.assertIn(100, Any), self.assertIn(-100, Any), - self.assertIn('A', Any) + self.assertIn("A", Any) self.assertIn(None, Any) self.assertFalse(Any.isdiscrete()) @@ -488,15 +488,15 @@ def test_Any(self): self.assertEqual(Any, tmp) self.assertEqual(tmp.domain, Any) - self.assertEqual(str(Any), 'Any') - self.assertEqual(str(tmp), '_AnySet') + self.assertEqual(str(Any), "Any") + self.assertEqual(str(tmp), "_AnySet") b = ConcreteModel() b.tmp = tmp - self.assertEqual(str(tmp), 'tmp') + self.assertEqual(str(tmp), "tmp") def test_AnyWithNone(self): os = StringIO() - with LoggingIntercept(os, 'pyomo'): + with LoggingIntercept(os, "pyomo"): self.assertIn(None, AnyWithNone) self.assertIn(1, AnyWithNone) self.assertRegex( @@ -511,7 +511,7 @@ def test_EmptySet(self): self.assertNotIn(1.5, EmptySet) self.assertNotIn(100, EmptySet), self.assertNotIn(-100, EmptySet), - self.assertNotIn('A', EmptySet) + self.assertNotIn("A", EmptySet) self.assertNotIn(None, EmptySet) self.assertTrue(EmptySet.isdiscrete()) @@ -533,11 +533,11 @@ def test_EmptySet(self): self.assertEqual(EmptySet, tmp) self.assertEqual(tmp.domain, EmptySet) - self.assertEqual(str(EmptySet), 'EmptySet') - self.assertEqual(str(tmp), '_EmptySet') + self.assertEqual(str(EmptySet), "EmptySet") + self.assertEqual(str(tmp), "_EmptySet") b = ConcreteModel() b.tmp = tmp - self.assertEqual(str(tmp), 'tmp') + self.assertEqual(str(tmp), "tmp") @unittest.skipIf(not numpy_available, "NumPy required for these tests") def test_numpy_compatible(self): @@ -669,7 +669,7 @@ def test_bounds(self): class TestRangeOperations(unittest.TestCase): def test_mixed_ranges_isdisjoint(self): i = RangeSet(0, 10, 2) - j = SetOf([0, 1, 2, 'a']) + j = SetOf([0, 1, 2, "a"]) k = Any ir = list(i.ranges()) @@ -678,7 +678,7 @@ def test_mixed_ranges_isdisjoint(self): ir = ir[0] jr = list(j.ranges()) - self.assertEqual(jr, [NR(0, 0, 0), NR(1, 1, 0), NR(2, 2, 0), NNR('a')]) + self.assertEqual(jr, [NR(0, 0, 0), NR(1, 1, 0), NR(2, 2, 0), NNR("a")]) self.assertEqual(str(jr), "[[0], [1], [2], {a}]") jr0, jr1, jr2, jr3 = jr @@ -719,7 +719,7 @@ def test_mixed_ranges_isdisjoint(self): def test_mixed_ranges_issubset(self): i = RangeSet(0, 10, 2) - j = SetOf([0, 1, 2, 'a']) + j = SetOf([0, 1, 2, "a"]) k = Any # Note that these ranges are verified in the test above @@ -759,7 +759,7 @@ def test_mixed_ranges_issubset(self): def test_mixed_ranges_range_difference(self): i = RangeSet(0, 10, 2) - j = SetOf([0, 1, 2, 'a']) + j = SetOf([0, 1, 2, "a"]) k = Any # Note that these ranges are verified in the test above @@ -809,7 +809,7 @@ def test_mixed_ranges_range_difference(self): def test_mixed_ranges_range_intersection(self): i = RangeSet(0, 10, 2) - j = SetOf([0, 1, 2, 'a']) + j = SetOf([0, 1, 2, "a"]) k = Any # Note that these ranges are verified in the test above @@ -925,7 +925,7 @@ def test_constructor(self): with self.assertRaisesRegex( TypeError, "'ranges' argument must be an iterable of NumericRange objects" ): - RangeSet(ranges=(NR(1, 5, 1), NNR('a'))) + RangeSet(ranges=(NR(1, 5, 1), NNR("a"))) with self.assertRaisesRegex( ValueError, "Constructing a finite RangeSet over a non-finite range " @@ -984,10 +984,10 @@ def __ge__(self, other): self.assertFalse(i.is_constructed()) self.assertIs(type(i), AbstractFiniteScalarRangeSet) p.construct() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertEqual(output.getvalue(), "") i.construct() - ref = 'Constructing RangeSet, name=FiniteScalarRangeSet, from data=None\n' + ref = "Constructing RangeSet, name=FiniteScalarRangeSet, from data=None\n" self.assertEqual(output.getvalue(), ref) self.assertTrue(i.is_constructed()) self.assertIs(type(i), FiniteScalarRangeSet) @@ -997,9 +997,9 @@ def __ge__(self, other): self.assertEqual(output.getvalue(), ref) output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): i = SetOf([1, 2, 3]) - ref = 'Constructing SetOf, name=[1, 2, 3], from data=None\n' + ref = "Constructing SetOf, name=[1, 2, 3], from data=None\n" self.assertEqual(output.getvalue(), ref) # Calling construct() twice bypasses construction the second # time around @@ -1023,7 +1023,7 @@ def __ge__(self, other): self.assertIs(type(i), InfiniteScalarRangeSet) i = RangeSet(1, 1, 0) self.assertIs(type(i), FiniteScalarRangeSet) - j = RangeSet(1, float('inf')) + j = RangeSet(1, float("inf")) self.assertIs(type(j), InfiniteScalarRangeSet) i = RangeSet(1, None) self.assertIs(type(i), InfiniteScalarRangeSet) @@ -1053,7 +1053,7 @@ def __ge__(self, other): self.assertNotIn(-6, i) self.assertNotIn(0.5, i) - p = Param(initialize=float('inf')) + p = Param(initialize=float("inf")) i = RangeSet(1, p, 1) self.assertIs(type(i), AbstractFiniteScalarRangeSet) p.construct() @@ -1068,7 +1068,7 @@ def __ge__(self, other): m.i = RangeSet(m.p, m.q, m.s, finite=True) self.assertIs(type(m.i), AbstractFiniteScalarRangeSet) i = m.create_instance( - data={None: {'p': {None: 1}, 'q': {None: 5}, 's': {None: 2}}} + data={None: {"p": {None: 1}, "q": {None: 5}, "s": {None: 2}}} ) self.assertIs(type(i.i), FiniteScalarRangeSet) self.assertEqual(list(i.i), [1, 3, 5]) @@ -1077,7 +1077,7 @@ def __ge__(self, other): ValueError, r"finite RangeSet over a non-finite range \(\[1..5\]\)" ): i = m.create_instance( - data={None: {'p': {None: 1}, 'q': {None: 5}, 's': {None: 0}}} + data={None: {"p": {None: 1}, "q": {None: 5}, "s": {None: 0}}} ) with self.assertRaisesRegex( @@ -1086,10 +1086,10 @@ def __ge__(self, other): i = m.create_instance( data={ None: { - 'p': {None: 1}, - 'q': {None: 5}, - 's': {None: 1}, - 'i': {None: [1, 2, 3]}, + "p": {None: 1}, + "q": {None: 5}, + "s": {None: 1}, + "i": {None: [1, 2, 3]}, } } ) @@ -1161,7 +1161,7 @@ def badRule(m, i): raise RuntimeError("ERROR: %s" % i) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): with self.assertRaisesRegex(RuntimeError, "ERROR: 1"): r = RangeSet(10, validate=badRule) self.assertEqual( @@ -1505,9 +1505,9 @@ def __contains__(self, val): def __len__(self): return len(self.data) - with self.assertRaisesRegex(TypeError, 'not iterable'): + with self.assertRaisesRegex(TypeError, "not iterable"): SetOf({1, 5}).issuperset(_NonIterable()) - with self.assertRaisesRegex(TypeError, 'not iterable'): + with self.assertRaisesRegex(TypeError, "not iterable"): SetOf({1, 3, 4, 5}).issuperset(_NonIterable()) # test bad type @@ -1610,14 +1610,14 @@ def test_ordered_setof(self): with self.assertRaisesRegex(ValueError, "x not in tuple"): i.ord(5) - i = SetOf([1, None, 'a']) + i = SetOf([1, None, "a"]) self.assertTrue(i.isfinite()) self.assertTrue(i.isordered()) - self.assertEqual(i.ordered_data(), (1, None, 'a')) - self.assertEqual(i.sorted_data(), (None, 1, 'a')) - self.assertEqual(tuple(reversed(i)), ('a', None, 1)) + self.assertEqual(i.ordered_data(), (1, None, "a")) + self.assertEqual(i.sorted_data(), (None, 1, "a")) + self.assertEqual(tuple(reversed(i)), ("a", None, 1)) def test_ranges(self): i_data = [1, 3, 2, 0] @@ -1655,7 +1655,7 @@ def test_ranges(self): native_types.add(int) native_numeric_types.add(int) - i_data.append('abc') + i_data.append("abc") try: self.assertIn(str, native_types) self.assertNotIn(str, native_numeric_types) @@ -1682,8 +1682,8 @@ def test_ranges(self): def test_bounds(self): self.assertEqual(SetOf([1, 3, 2, 0]).bounds(), (0, 3)) self.assertEqual(SetOf([1, 3.0, 2, 0]).bounds(), (0, 3.0)) - self.assertEqual(SetOf([None, 1, 'a']).bounds(), (None, None)) - self.assertEqual(SetOf(['apple', 'cat', 'bear']).bounds(), ('apple', 'cat')) + self.assertEqual(SetOf([None, 1, "a"]).bounds(), (None, None)) + self.assertEqual(SetOf(["apple", "cat", "bear"]).bounds(), ("apple", "cat")) self.assertEqual( RangeSet(ranges=(NR(0, 10, 2), NR(3, 20, 2))).bounds(), (0, 19) @@ -1720,7 +1720,7 @@ def test_dimen(self): m.I = Set(initialize=[(1, 2), (3, 4)]) self.assertEqual(SetOf(m.I).dimen, 2) - a = [1, 2, 3, 'abc'] + a = [1, 2, 3, "abc"] SetOf_a = SetOf(a) self.assertEqual(SetOf_a.dimen, 1) a.append((1, 2)) @@ -1809,7 +1809,7 @@ def test_check_values(self): m.I = RangeSet(5) output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertTrue(m.I.check_values()) self.assertRegex( output.getvalue(), r"^DEPRECATED: check_values\(\) is deprecated:" @@ -1819,17 +1819,17 @@ def test_check_values(self): class Test_SetOperator(unittest.TestCase): def test_construct(self): p = Param(initialize=3) - a = RangeSet(p, name='a') + a = RangeSet(p, name="a") output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): i = a * a self.assertEqual(output.getvalue(), "") p.construct() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): i.construct() ref = ( - 'Constructing SetOperator, name=a*a, from data=None\n' - 'Constructing RangeSet, name=a, from data=None\n' + "Constructing SetOperator, name=a*a, from data=None\n" + "Constructing RangeSet, name=a, from data=None\n" ) self.assertEqual(output.getvalue(), ref) # Calling construct() twice bypasses construction the second @@ -1866,8 +1866,8 @@ def test_pandas_multiindex_set_init(self): # If it does, pandas will complain with the following error: # ValueError: The truth value of a MultiIndex is ambiguous. # Use a.empty, a.bool(), a.item(), a.any() or a.all(). - iterables = [['bar', 'baz', 'foo', 'qux'], ['one', 'two']] - pandas_index = pd.MultiIndex.from_product(iterables, names=['first', 'second']) + iterables = [["bar", "baz", "foo", "qux"], ["one", "two"]] + pandas_index = pd.MultiIndex.from_product(iterables, names=["first", "second"]) model = ConcreteModel() model.a = Set(initialize=pandas_index, dimen=pandas_index.nlevels) @@ -1896,7 +1896,7 @@ def test_len(self): self.assertEqual(len(a), 3) b = a | Reals with self.assertRaisesRegex( - OverflowError, 'The length of a non-finite Set is Inf' + OverflowError, "The length of a non-finite Set is Inf" ): len(b) @@ -3088,10 +3088,10 @@ def test_set_tuple(self): b = SetOf([1]) x = a * b os = StringIO() - with LoggingIntercept(os, 'pyomo'): + with LoggingIntercept(os, "pyomo"): self.assertEqual(x.set_tuple, [a, b]) self.assertRegex( - os.getvalue(), '^DEPRECATED: SetProduct.set_tuple is deprecated.' + os.getvalue(), "^DEPRECATED: SetProduct.set_tuple is deprecated." ) def test_no_normalize_index(self): @@ -3124,8 +3124,8 @@ def test_infinite_setproduct(self): self.assertIn((1, 2), x) self.assertNotIn((0, 2), x) self.assertNotIn((1, 1), x) - self.assertNotIn(('a', 2), x) - self.assertNotIn((2, 'a'), x) + self.assertNotIn(("a", 2), x) + self.assertNotIn((2, "a"), x) x = SetOf([2, 3, 5, 7]) * PositiveIntegers self.assertFalse(x.isfinite()) @@ -3133,8 +3133,8 @@ def test_infinite_setproduct(self): self.assertIn((3, 2), x) self.assertNotIn((1, 2), x) self.assertNotIn((2, 0), x) - self.assertNotIn(('a', 2), x) - self.assertNotIn((2, 'a'), x) + self.assertNotIn(("a", 2), x) + self.assertNotIn((2, "a"), x) x = PositiveIntegers * PositiveIntegers self.assertFalse(x.isfinite()) @@ -3142,8 +3142,8 @@ def test_infinite_setproduct(self): self.assertIn((3, 2), x) self.assertNotIn((0, 2), x) self.assertNotIn((2, 0), x) - self.assertNotIn(('a', 2), x) - self.assertNotIn((2, 'a'), x) + self.assertNotIn(("a", 2), x) + self.assertNotIn((2, "a"), x) def _verify_finite_product(self, a, b): if isinstance(a, (Set, SetOf, RangeSet)): @@ -3394,33 +3394,33 @@ def test_setproduct_construct_data(self): m.I = Set(initialize=[1, 2]) m.J = m.I * m.I output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.create_instance( - data={None: {'J': {None: [(1, 1), (1, 2), (2, 1), (2, 2)]}}} + data={None: {"J": {None: [(1, 1), (1, 2), (2, 1), (2, 2)]}}} ) self.assertRegex( - output.getvalue().replace('\n', ' '), + output.getvalue().replace("\n", " "), "^DEPRECATED: Providing construction data to SetOperator objects " "is deprecated", ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): with self.assertRaisesRegex( ValueError, "Constructing SetOperator J with " r"incompatible data \(data=\{None: \[\(1, 1\), \(1, 2\), " r"\(2, 1\)\]\}", ): - m.create_instance(data={None: {'J': {None: [(1, 1), (1, 2), (2, 1)]}}}) + m.create_instance(data={None: {"J": {None: [(1, 1), (1, 2), (2, 1)]}}}) self.assertRegex( - output.getvalue().replace('\n', ' '), + output.getvalue().replace("\n", " "), "^DEPRECATED: Providing construction data to SetOperator objects " "is deprecated", ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): with self.assertRaisesRegex( ValueError, "Constructing SetOperator J with " @@ -3428,10 +3428,10 @@ def test_setproduct_construct_data(self): r"\(2, 1\), \(2, 2\)\]\}", ): m.create_instance( - data={None: {'J': {None: [(1, 3), (1, 2), (2, 1), (2, 2)]}}} + data={None: {"J": {None: [(1, 3), (1, 2), (2, 1), (2, 2)]}}} ) self.assertRegex( - output.getvalue().replace('\n', ' '), + output.getvalue().replace("\n", " "), "^DEPRECATED: Providing construction data to SetOperator objects " "is deprecated", ) @@ -3467,7 +3467,7 @@ def test_setproduct_toolong_val(self): class TestGlobalSets(unittest.TestCase): def test_globals(self): - self.assertEqual(Reals.__class__.__name__, 'GlobalSet') + self.assertEqual(Reals.__class__.__name__, "GlobalSet") self.assertIsInstance(Reals, RangeSet) def test_pickle(self): @@ -3479,8 +3479,8 @@ def test_deepcopy(self): self.assertIs(a, Reals) def test_name(self): - self.assertEqual(str(Reals), 'Reals') - self.assertEqual(str(Integers), 'Integers') + self.assertEqual(str(Reals), "Reals") + self.assertEqual(str(Integers), "Integers") def test_block_independent(self): m = ConcreteModel() @@ -3488,14 +3488,14 @@ def test_block_independent(self): RuntimeError, "Cannot assign a GlobalSet 'Reals' to model 'unknown'" ): m.a_set = Reals - self.assertEqual(str(Reals), 'Reals') + self.assertEqual(str(Reals), "Reals") self.assertIsNone(Reals._parent) m.blk = Block() with self.assertRaisesRegex( RuntimeError, "Cannot assign a GlobalSet 'Reals' to block 'blk'" ): m.blk.a_set = Reals - self.assertEqual(str(Reals), 'Reals') + self.assertEqual(str(Reals), "Reals") self.assertIsNone(Reals._parent) def test_iteration(self): @@ -3517,22 +3517,22 @@ def test_iteration(self): def test_declare(self): NS = {} - DeclareGlobalSet(RangeSet(name='TrinarySet', ranges=(NR(0, 2, 1),)), NS) - self.assertEqual(list(NS['TrinarySet']), [0, 1, 2]) - a = pickle.loads(pickle.dumps(NS['TrinarySet'])) - self.assertIs(a, NS['TrinarySet']) + DeclareGlobalSet(RangeSet(name="TrinarySet", ranges=(NR(0, 2, 1),)), NS) + self.assertEqual(list(NS["TrinarySet"]), [0, 1, 2]) + a = pickle.loads(pickle.dumps(NS["TrinarySet"])) + self.assertIs(a, NS["TrinarySet"]) with self.assertRaisesRegex(NameError, "name 'TrinarySet' is not defined"): TrinarySet - del SetModule.GlobalSets['TrinarySet'] - del NS['TrinarySet'] + del SetModule.GlobalSets["TrinarySet"] + del NS["TrinarySet"] # Now test the automatic identification of the globals() scope - DeclareGlobalSet(RangeSet(name='TrinarySet', ranges=(NR(0, 2, 1),))) + DeclareGlobalSet(RangeSet(name="TrinarySet", ranges=(NR(0, 2, 1),))) self.assertEqual(list(TrinarySet), [0, 1, 2]) a = pickle.loads(pickle.dumps(TrinarySet)) self.assertIs(a, TrinarySet) - del SetModule.GlobalSets['TrinarySet'] - del globals()['TrinarySet'] + del SetModule.GlobalSets["TrinarySet"] + del globals()["TrinarySet"] with self.assertRaisesRegex(NameError, "name 'TrinarySet' is not defined"): TrinarySet @@ -3540,74 +3540,74 @@ def test_exceptions(self): with self.assertRaisesRegex( RuntimeError, "Duplicate Global Set declaration, Reals" ): - DeclareGlobalSet(RangeSet(name='Reals', ranges=(NR(0, 2, 1),))) + DeclareGlobalSet(RangeSet(name="Reals", ranges=(NR(0, 2, 1),))) # But repeat declarations are OK a = Reals DeclareGlobalSet(Reals) self.assertIs(a, Reals) - self.assertIs(a, globals()['Reals']) - self.assertIs(a, SetModule.GlobalSets['Reals']) + self.assertIs(a, globals()["Reals"]) + self.assertIs(a, SetModule.GlobalSets["Reals"]) NS = {} - ts = DeclareGlobalSet(RangeSet(name='TrinarySet', ranges=(NR(0, 2, 1),)), NS) - self.assertIs(NS['TrinarySet'], ts) + ts = DeclareGlobalSet(RangeSet(name="TrinarySet", ranges=(NR(0, 2, 1),)), NS) + self.assertIs(NS["TrinarySet"], ts) # Repeat declaration is OK DeclareGlobalSet(ts, NS) - self.assertIs(NS['TrinarySet'], ts) + self.assertIs(NS["TrinarySet"], ts) # but conflicting one raises exception - NS['foo'] = None + NS["foo"] = None with self.assertRaisesRegex( RuntimeError, "Refusing to overwrite global object, foo" ): - DeclareGlobalSet(RangeSet(name='foo', ranges=(NR(0, 2, 1),)), NS) + DeclareGlobalSet(RangeSet(name="foo", ranges=(NR(0, 2, 1),)), NS) def test_RealSet_IntegerSet(self): output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.RealSet() - self.assertIn('DEPRECATED: The use of RealSet,', output.getvalue()) + self.assertIn("DEPRECATED: The use of RealSet,", output.getvalue()) self.assertEqual(a, Reals) self.assertIsNot(a, Reals) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.RealSet(bounds=(1, 3)) - self.assertIn('DEPRECATED: The use of RealSet,', output.getvalue()) + self.assertIn("DEPRECATED: The use of RealSet,", output.getvalue()) self.assertEqual(a.bounds(), (1, 3)) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.IntegerSet() - self.assertIn('DEPRECATED: The use of RealSet,', output.getvalue()) + self.assertIn("DEPRECATED: The use of RealSet,", output.getvalue()) self.assertEqual(a, Integers) self.assertIsNot(a, Integers) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.IntegerSet(bounds=(1, 3)) - self.assertIn('DEPRECATED: The use of RealSet,', output.getvalue()) + self.assertIn("DEPRECATED: The use of RealSet,", output.getvalue()) self.assertEqual(a.bounds(), (1, 3)) self.assertEqual(list(a), [1, 2, 3]) m = ConcreteModel() output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.x = Var(within=SetModule.RealSet) - self.assertIn('DEPRECATED: The use of RealSet,', output.getvalue()) + self.assertIn("DEPRECATED: The use of RealSet,", output.getvalue()) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.y = Var(within=SetModule.RealSet()) - self.assertIn('DEPRECATED: The use of RealSet,', output.getvalue()) + self.assertIn("DEPRECATED: The use of RealSet,", output.getvalue()) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.z = Var(within=SetModule.RealSet(bounds=(0, None))) - self.assertIn('DEPRECATED: The use of RealSet,', output.getvalue()) + self.assertIn("DEPRECATED: The use of RealSet,", output.getvalue()) with self.assertRaisesRegex( RuntimeError, r"Unexpected keyword arguments: \{'foo': 5\}" @@ -3616,71 +3616,71 @@ def test_RealSet_IntegerSet(self): def test_intervals(self): output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.RealInterval() self.assertIn("RealInterval has been deprecated.", output.getvalue()) self.assertEqual(a, Reals) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.RealInterval(bounds=(0, None)) self.assertIn("RealInterval has been deprecated.", output.getvalue()) self.assertEqual(a, NonNegativeReals) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.RealInterval(bounds=5) self.assertIn("RealInterval has been deprecated.", output.getvalue()) self.assertEqual(a, RangeSet(1, 5, 0)) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.RealInterval(bounds=(5,)) self.assertIn("RealInterval has been deprecated.", output.getvalue()) self.assertEqual(a, RangeSet(1, 5, 0)) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.IntegerInterval() self.assertIn("IntegerInterval has been deprecated.", output.getvalue()) self.assertEqual(a, Integers) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.IntegerInterval(bounds=(0, None)) self.assertIn("IntegerInterval has been deprecated.", output.getvalue()) self.assertEqual(a, NonNegativeIntegers) self.assertFalse(a.isfinite()) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.IntegerInterval(bounds=(None, -1)) self.assertIn("IntegerInterval has been deprecated.", output.getvalue()) self.assertEqual(a, NegativeIntegers) self.assertFalse(a.isfinite()) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): - a = SetModule.IntegerInterval(bounds=(-float('inf'), -1)) + with LoggingIntercept(output, "pyomo.core"): + a = SetModule.IntegerInterval(bounds=(-float("inf"), -1)) self.assertIn("IntegerInterval has been deprecated.", output.getvalue()) self.assertEqual(a, NegativeIntegers) self.assertFalse(a.isfinite()) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.IntegerInterval(bounds=(0, 3)) self.assertIn("IntegerInterval has been deprecated.", output.getvalue()) self.assertEqual(list(a), [0, 1, 2, 3]) self.assertTrue(a.isfinite()) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.IntegerInterval(bounds=5) self.assertIn("IntegerInterval has been deprecated.", output.getvalue()) self.assertEqual(list(a), [1, 2, 3, 4, 5]) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): a = SetModule.IntegerInterval(bounds=(5,)) self.assertIn("IntegerInterval has been deprecated.", output.getvalue()) self.assertEqual(list(a), [1, 2, 3, 4, 5]) @@ -3697,7 +3697,7 @@ class TestSet(unittest.TestCase): def test_deprecated_args(self): m = ConcreteModel() output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.I = Set(virtual=True) self.assertEqual(len(m.I), 0) self.assertRegex( @@ -3729,7 +3729,7 @@ def test_scalar_set_initialize_and_iterate(self): m = ConcreteModel() with self.assertRaisesRegex( - ValueError, 'Set rule or initializer returned None' + ValueError, "Set rule or initializer returned None" ): m.I = Set(initialize=lambda m: None, dimen=2) self.assertTrue(m.I._init_values.constant()) @@ -3759,7 +3759,7 @@ def I_init(m): self.assertEqual(m.I.dimen, 1) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m = ConcreteModel() m.I = Set(initialize={1, 3, 2, 4}) ref = ( @@ -3776,7 +3776,7 @@ def I_init(m): self.assertEqual(m.I.dimen, 1) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m = ConcreteModel() m.I = Set(initialize={1, 3, 2, 4}, ordered=False) self.assertEqual(output.getvalue(), "") @@ -3812,7 +3812,7 @@ def I_init(m): self.assertEqual(m.I.dimen, 1) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): with self.assertRaisesRegex(TypeError, "'int' object is not iterable"): m = ConcreteModel() m.I = Set(initialize=5) @@ -3878,7 +3878,7 @@ def _verify(_s, _l): _verify(m.I, [1, 3, 2, 4]) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.I.add(3) self.assertEqual( output.getvalue(), "Element 3 already exists in Set I; no action taken\n" @@ -3913,11 +3913,11 @@ def _verify(_s, _l): tmp.add(m.I.pop()) _verify(m.I, []) self.assertEqual(tmp, {5, 6}) - with self.assertRaisesRegex(KeyError, 'pop from an empty set'): + with self.assertRaisesRegex(KeyError, "pop from an empty set"): m.I.pop() output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.I.update([6]) _verify(m.I, [6]) m.I.update([6, 5, 6]) @@ -3960,7 +3960,7 @@ def _verify(_s, _l): _verify(m.I, [1, 2, 3, 4]) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.I.add(3) self.assertEqual( output.getvalue(), "Element 3 already exists in Set I; no action taken\n" @@ -3995,11 +3995,11 @@ def _verify(_s, _l): tmp.add(m.I.pop()) _verify(m.I, []) self.assertEqual(tmp, {5, 6}) - with self.assertRaisesRegex(KeyError, 'pop from an empty set'): + with self.assertRaisesRegex(KeyError, "pop from an empty set"): m.I.pop() output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.I.update([6]) _verify(m.I, [6]) m.I.update([6, 5, 6]) @@ -4053,7 +4053,7 @@ def _verify(_s, _l): _verify(m.I, [1, 2, 3, 4]) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.I.add(3) self.assertEqual( output.getvalue(), "Element 3 already exists in Set I; no action taken\n" @@ -4088,7 +4088,7 @@ def _verify(_s, _l): tmp.add(m.I.pop()) _verify(m.I, []) self.assertEqual(tmp, {5, 6}) - with self.assertRaisesRegex(KeyError, 'pop from an empty set'): + with self.assertRaisesRegex(KeyError, "pop from an empty set"): m.I.pop() m.I.update([5]) @@ -4210,7 +4210,7 @@ def test_naming(self): k = Set([1, 2, 3]) self.assertEqual(str(k), "IndexedSet") with self.assertRaisesRegex( - ValueError, 'The component has not been constructed.' + ValueError, "The component has not been constructed." ): str(k[1]) m.K = k @@ -4229,7 +4229,7 @@ def test_indexing(self): with self.assertRaisesRegex( IndexError, "Set 'I' positional indices must be integers, not str" ): - m.I['a'] + m.I["a"] def test_add_filter_validate(self): m = ConcreteModel() @@ -4249,14 +4249,14 @@ def test_add_filter_validate(self): self.assertIn(1.0, m.I) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertFalse(m.I.add(1)) self.assertEqual( output.getvalue(), "Element 1 already exists in Set I; no action taken\n" ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertFalse(m.I.add((1,))) self.assertEqual( output.getvalue(), "Element (1,) already exists in Set I; no action taken\n" @@ -4273,7 +4273,7 @@ def test_add_filter_validate(self): self.assertTrue(m.J.add((1,))) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertFalse(m.J.add(1)) self.assertEqual( output.getvalue(), "Element 1 already exists in Set J; no action taken\n" @@ -4289,7 +4289,7 @@ def _l_tri(model, i, j): self.assertEqual(list(m.K), [(1, 1), (2, 1), (2, 2), (3, 1), (3, 2), (3, 3)]) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertTrue(m.K.add((0, 0))) self.assertFalse(m.K.add((0, 1))) self.assertEqual(output.getvalue(), "") @@ -4311,7 +4311,7 @@ def _lt_3(model, i): self.assertEqual(list(m.L[5]), [1, 2]) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertTrue(m.L[2].add(0)) self.assertFalse(m.L[2].add((100))) self.assertEqual(output.getvalue(), "") @@ -4329,7 +4329,7 @@ def _validate_I(model, i, j): m.I = Set(validate=_validate_I) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertTrue(m.I.add((0, 1))) self.assertEqual(output.getvalue(), "") with self.assertRaisesRegex( @@ -4349,9 +4349,10 @@ def _validate_I(model, i, j): # validot when it is called for the index. def _validate_J(model, i, j, index): return _validate_I(model, i, j) + m.J = Set([(0, 0), (2, 2)], validate=_validate_J) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertTrue(m.J[2, 2].add((0, 1))) self.assertEqual(output.getvalue(), "") with self.assertRaisesRegex( @@ -4379,7 +4380,7 @@ def test_domain(self): m.I.add(2.0) self.assertEqual(list(m.I), [1, 2.0]) with self.assertRaisesRegex( - ValueError, 'The value is not in the domain Integers' + ValueError, "The value is not in the domain Integers" ): m.I.add(1.5) @@ -4390,7 +4391,7 @@ def test_domain(self): m.I.add(2.0) self.assertEqual(list(m.I), [1, 2.0]) with self.assertRaisesRegex( - ValueError, 'The value is not in the domain Integers' + ValueError, "The value is not in the domain Integers" ): m.I.add(1.5) @@ -4401,7 +4402,7 @@ def test_domain(self): m.I.add(2.0) self.assertEqual(list(m.I), [1, 2.0]) with self.assertRaisesRegex( - ValueError, r'The value is not in the domain \[1..5\]' + ValueError, r"The value is not in the domain \[1..5\]" ): m.I.add(5.5) @@ -4412,17 +4413,17 @@ def test_domain(self): self.assertEqual(list(m.I), [0, 2.0, 4]) with self.assertRaisesRegex( ValueError, - r'The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]', + r"The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]", ): m.I.add(1.5) with self.assertRaisesRegex( ValueError, - r'The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]', + r"The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]", ): m.I.add(1) with self.assertRaisesRegex( ValueError, - r'The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]', + r"The value is not in the domain \(Integers & \[0:inf:2\]\) & \[0..9\]", ): m.I.add(10) @@ -4476,7 +4477,7 @@ def myFcn(x): def test_pickle(self): m = ConcreteModel() - m.I = Set(initialize={1, 2, 'a'}, ordered=False) + m.I = Set(initialize={1, 2, "a"}, ordered=False) m.J = Set(initialize=(2, 4, 1)) m.K = Set(initialize=(2, 4, 1), ordered=Set.SortedOrder) m.II = Set([1, 2, 3], m.J, initialize=_init_set) @@ -4536,7 +4537,7 @@ def test_dimen(self): self.assertEqual(a.I.dimen, UnknownSetDimen) a.J = Set(initialize=[1, 2, 3], dimen=1) self.assertEqual(a.J.dimen, 1) - m = a.create_instance(data={None: {'I': {None: [(1, 2), (3, 4)]}}}) + m = a.create_instance(data={None: {"I": {None: [(1, 2), (3, 4)]}}}) self.assertEqual(m.I.dimen, 2) self.assertEqual(m.J.dimen, 1) @@ -4568,9 +4569,9 @@ def test_construction(self): i = m.create_instance( data={ None: { - 'I': [-1, 0], - 'II': {1: [10, 11], 3: [30]}, - 'K': [-1, 4, -1, 6, 0, 5], + "I": [-1, 0], + "II": {1: [10, 11], 3: [30]}, + "K": [-1, 4, -1, 6, 0, 5], } } ) @@ -4590,25 +4591,25 @@ def test_construction(self): self.assertEqual(list(i.II[2]), [1, 2]) # Additional tests for tuplize: - i = m.create_instance(data={None: {'K': [(1, 4), (2, 6)], 'KK': [1, 4, 2, 6]}}) + i = m.create_instance(data={None: {"K": [(1, 4), (2, 6)], "KK": [1, 4, 2, 6]}}) self.assertEqual(list(i.K), [(1, 4), (2, 6)]) self.assertEqual(list(i.KK), [1, 2]) self.assertEqual(list(i.KK[1]), [1, 4, 2, 6]) self.assertEqual(list(i.KK[2]), [(1, 4), (2, 6)]) - i = m.create_instance(data={None: {'K': []}}) + i = m.create_instance(data={None: {"K": []}}) self.assertEqual(list(i.K), []) with self.assertRaisesRegex( ValueError, "Cannot tuplize list data for set K because " "its length 3 is not a multiple of dimen=2", ): - i = m.create_instance(data={None: {'K': [1, 2, 3]}}) + i = m.create_instance(data={None: {"K": [1, 2, 3]}}) with self.assertRaisesRegex( ValueError, r"Cannot tuplize list data for set KK\[2\] " "because its length 3 is not a multiple of dimen=2", ): - i = m.create_instance(data={None: {'KK': {2: [1, 2, 3]}}}) + i = m.create_instance(data={None: {"KK": {2: [1, 2, 3]}}}) ref = """ Constructing AbstractOrderedScalarSet 'I' on [Model] from data=None @@ -4620,7 +4621,7 @@ def test_construction(self): """.strip() m = ConcreteModel() output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): m.I = Set() self.assertEqual(output.getvalue().strip(), ref) # but re-constructing the set doesn't re-log the message @@ -4878,7 +4879,7 @@ def test_sorted_operations(self): i += 1 I.update((i, -i)) self.assertFalse(I._is_sorted) - self.assertEqual(str(I), "{%s}" % ', '.join(str(_) for _ in range(-i, i + 1))) + self.assertEqual(str(I), "{%s}" % ", ".join(str(_) for _ in range(-i, i + 1))) self.assertTrue(I._is_sorted) # ranges() @@ -4886,8 +4887,8 @@ def test_sorted_operations(self): I.update((i, -i)) self.assertFalse(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I.ranges()), - ','.join('[%s]' % _ for _ in range(-i, i + 1)), + ",".join(str(_) for _ in I.ranges()), + ",".join("[%s]" % _ for _ in range(-i, i + 1)), ) self.assertTrue(I._is_sorted) @@ -4896,7 +4897,7 @@ def test_sorted_operations(self): I.update((i, -i)) self.assertFalse(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I), ','.join(str(_) for _ in range(-i, i + 1)) + ",".join(str(_) for _ in I), ",".join(str(_) for _ in range(-i, i + 1)) ) self.assertTrue(I._is_sorted) @@ -4905,8 +4906,8 @@ def test_sorted_operations(self): I.update((i, -i)) self.assertFalse(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in reversed(I)), - ','.join(str(_) for _ in reversed(range(-i, i + 1))), + ",".join(str(_) for _ in reversed(I)), + ",".join(str(_) for _ in reversed(range(-i, i + 1))), ) self.assertTrue(I._is_sorted) @@ -4915,8 +4916,8 @@ def test_sorted_operations(self): I.update((i, -i)) self.assertFalse(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I.data()), - ','.join(str(_) for _ in range(-i, i + 1)), + ",".join(str(_) for _ in I.data()), + ",".join(str(_) for _ in range(-i, i + 1)), ) self.assertTrue(I._is_sorted) @@ -4925,8 +4926,8 @@ def test_sorted_operations(self): I.update((i, -i)) self.assertFalse(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I.ordered_data()), - ','.join(str(_) for _ in range(-i, i + 1)), + ",".join(str(_) for _ in I.ordered_data()), + ",".join(str(_) for _ in range(-i, i + 1)), ) self.assertTrue(I._is_sorted) @@ -4935,8 +4936,8 @@ def test_sorted_operations(self): I.update((i, -i)) self.assertFalse(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I.sorted_data()), - ','.join(str(_) for _ in range(-i, i + 1)), + ",".join(str(_) for _ in I.sorted_data()), + ",".join(str(_) for _ in range(-i, i + 1)), ) self.assertTrue(I._is_sorted) @@ -4945,8 +4946,8 @@ def test_sorted_operations(self): I.update((i, -i)) self.assertFalse(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I.ordered_iter()), - ','.join(str(_) for _ in range(-i, i + 1)), + ",".join(str(_) for _ in I.ordered_iter()), + ",".join(str(_) for _ in range(-i, i + 1)), ) self.assertTrue(I._is_sorted) @@ -4955,8 +4956,8 @@ def test_sorted_operations(self): I.update((i, -i)) self.assertFalse(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I.sorted_iter()), - ','.join(str(_) for _ in range(-i, i + 1)), + ",".join(str(_) for _ in I.sorted_iter()), + ",".join(str(_) for _ in range(-i, i + 1)), ) self.assertTrue(I._is_sorted) @@ -4971,8 +4972,8 @@ def test_sorted_operations(self): I.remove(0) self.assertTrue(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I), - ','.join(str(_) for _ in range(-i, i + 1) if _ != 0), + ",".join(str(_) for _ in I), + ",".join(str(_) for _ in range(-i, i + 1) if _ != 0), ) self.assertTrue(I._is_sorted) @@ -4980,7 +4981,7 @@ def test_sorted_operations(self): I.add(0) self.assertFalse(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I), ','.join(str(_) for _ in range(-i, i + 1)) + ",".join(str(_) for _ in I), ",".join(str(_) for _ in range(-i, i + 1)) ) self.assertTrue(I._is_sorted) @@ -4988,15 +4989,15 @@ def test_sorted_operations(self): I.discard(0) self.assertTrue(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I), - ','.join(str(_) for _ in range(-i, i + 1) if _ != 0), + ",".join(str(_) for _ in I), + ",".join(str(_) for _ in range(-i, i + 1) if _ != 0), ) self.assertTrue(I._is_sorted) # clear() I.clear() self.assertTrue(I._is_sorted) - self.assertEqual(','.join(str(_) for _ in I), '') + self.assertEqual(",".join(str(_) for _ in I), "") self.assertTrue(I._is_sorted) # set_value() @@ -5004,7 +5005,7 @@ def test_sorted_operations(self): I.set_value({-i, 0, i}) self.assertFalse(I._is_sorted) self.assertEqual( - ','.join(str(_) for _ in I), ','.join(str(_) for _ in range(-i, i + 1)) + ",".join(str(_) for _ in I), ",".join(str(_) for _ in range(-i, i + 1)) ) self.assertTrue(I._is_sorted) @@ -5096,7 +5097,7 @@ def test_sorted_operations(self): I.update() self.assertEqual(str(I), "UnindexedComponent_set") - self.assertEqual(','.join(str(_) for _ in I.ranges()), "{None}") + self.assertEqual(",".join(str(_) for _ in I.ranges()), "{None}") self.assertIsNone(I.construct()) @@ -5121,7 +5122,7 @@ def test_sorted_operations(self): self.assertEqual(I.last(), val[0]) self.assertEqual(I.at(1), val[0]) with self.assertRaisesRegex( - IndexError, 'UnindexedComponent_set index out of range' + IndexError, "UnindexedComponent_set index out of range" ): I.at(999) self.assertEqual(I.ord(val[0]), 1) @@ -5133,7 +5134,7 @@ def test_sorted_operations(self): I.ord(999) with self.assertRaisesRegex( - IndexError, 'Cannot advance past the end of the Set' + IndexError, "Cannot advance past the end of the Set" ): I.next(val[0]) with self.assertRaisesRegex( @@ -5151,7 +5152,7 @@ def test_sorted_operations(self): I.nextw(999) with self.assertRaisesRegex( - IndexError, 'Cannot advance before the beginning of the Set' + IndexError, "Cannot advance before the beginning of the Set" ): I.prev(val[0]) with self.assertRaisesRegex( @@ -5232,7 +5233,7 @@ def _i_idx(): def test_set_options(self): output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): @set_options(domain=Integers) def Bindex(m): @@ -5715,9 +5716,9 @@ def test_get_continuous_interval(self): self.assertEqual(a.get_interval(), (None, None, None)) a = Any | NegativeReals self.assertEqual(a.get_interval(), (None, None, None)) - a = SetOf('abc') | NegativeReals + a = SetOf("abc") | NegativeReals self.assertEqual(a.get_interval(), (None, None, None)) - a = NegativeReals | SetOf('abc') + a = NegativeReals | SetOf("abc") self.assertEqual(a.get_interval(), (None, None, None)) def test_get_discrete_interval(self): @@ -5735,7 +5736,7 @@ def test_get_discrete_interval(self): self.assertEqual(a.get_interval(), (1, 6, 1)) a = SetOf([1, 3, 5, 6, 2]) self.assertEqual(a.get_interval(), (1, 6, None)) - a = SetOf([1, 3, 5, 6, 4, 2, 'a']) + a = SetOf([1, 3, 5, 6, 4, 2, "a"]) self.assertEqual(a.get_interval(), (None, None, None)) a = SetOf([3]) self.assertEqual(a.get_interval(), (3, 3, 0)) @@ -5786,9 +5787,9 @@ def test_get_interval(self): self.assertEqual(Any.get_interval(), (None, None, None)) a = UnindexedComponent_set self.assertEqual(a.get_interval(), (None, None, None)) - a = Set(initialize=['a']) + a = Set(initialize=["a"]) a.construct() - self.assertEqual(a.get_interval(), ('a', 'a', None)) + self.assertEqual(a.get_interval(), ("a", "a", None)) a = Set(initialize=[1]) a.construct() self.assertEqual(a.get_interval(), (1, 1, 0)) @@ -5802,21 +5803,21 @@ def test_filter(self): m.K = Set(initialize=[1, 2, 3], filter=lambda m, i: i % 2) output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertIsNone(m.I.filter) self.assertRegex( output.getvalue(), "^DEPRECATED: 'filter' is no longer a public attribute" ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertIsNone(m.J.filter) self.assertRegex( output.getvalue(), "^DEPRECATED: 'filter' is no longer a public attribute" ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertIsInstance(m.K.filter, IndexedCallInitializer) self.assertRegex( output.getvalue(), "^DEPRECATED: 'filter' is no longer a public attribute" @@ -5828,7 +5829,7 @@ def test_virtual(self): m.J = m.I * m.I output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertFalse(m.I.virtual) self.assertRegex( output.getvalue(), @@ -5836,7 +5837,7 @@ def test_virtual(self): ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertTrue(m.J.virtual) self.assertRegex( output.getvalue(), @@ -5844,7 +5845,7 @@ def test_virtual(self): ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.J.virtual = True self.assertRegex( output.getvalue(), @@ -5863,7 +5864,7 @@ def test_concrete(self): m.J = m.I * m.I output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertTrue(m.I.concrete) self.assertRegex( output.getvalue(), @@ -5871,7 +5872,7 @@ def test_concrete(self): ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertTrue(m.J.concrete) self.assertRegex( output.getvalue(), @@ -5879,7 +5880,7 @@ def test_concrete(self): ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertFalse(Reals.concrete) self.assertRegex( output.getvalue(), @@ -5887,7 +5888,7 @@ def test_concrete(self): ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.J.concrete = True self.assertRegex( output.getvalue(), @@ -5905,14 +5906,14 @@ def test_ordered_attr(self): m.J = Set(ordered=True) m.K = Set(ordered=False) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertTrue(m.J.ordered) self.assertRegex( output.getvalue(), "^DEPRECATED: The 'ordered' attribute is no longer supported.", ) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertFalse(m.K.ordered) self.assertRegex( output.getvalue(), @@ -5923,7 +5924,7 @@ def test_value_attr(self): m = ConcreteModel() m.J = Set(ordered=True, initialize=[1, 3, 2]) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): tmp = m.J.value self.assertIs(type(tmp), set) self.assertEqual(tmp, set([1, 3, 2])) @@ -5936,12 +5937,12 @@ def test_value_list_attr(self): m = ConcreteModel() m.J = Set(ordered=True, initialize=[1, 3, 2]) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): tmp = m.J.value_list self.assertIs(type(tmp), list) self.assertEqual(tmp, list([1, 3, 2])) self.assertRegex( - output.getvalue().replace('\n', ' '), + output.getvalue().replace("\n", " "), r"^DEPRECATED: The 'value_list' attribute is deprecated. " r"Use .ordered_data\(\)", ) @@ -5950,7 +5951,7 @@ def test_check_values(self): m = ConcreteModel() m.I = Set(ordered=True, initialize=[1, 3, 2]) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertTrue(m.I.check_values()) self.assertRegex( output.getvalue(), @@ -5959,7 +5960,7 @@ def test_check_values(self): m.J = m.I * m.I output = StringIO() - with LoggingIntercept(output, 'pyomo.core', logging.DEBUG): + with LoggingIntercept(output, "pyomo.core", logging.DEBUG): self.assertTrue(m.J.check_values()) self.assertRegex( output.getvalue(), r"^DEPRECATED: check_values\(\) is deprecated:" @@ -5968,7 +5969,7 @@ def test_check_values(self): # We historically supported check_values on indexed sets m.K = Set([1, 2], ordered=True, initialize=[1, 3, 2]) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertTrue(m.K.check_values()) self.assertRegex( output.getvalue(), @@ -5977,23 +5978,23 @@ def test_check_values(self): def test_getitem(self): m = ConcreteModel() - m.I = Set(initialize=['a', 'b']) + m.I = Set(initialize=["a", "b"]) with LoggingIntercept() as OUT: self.assertIs(m.I[None], m.I) self.assertEqual(OUT.getvalue(), "") with LoggingIntercept() as OUT: - self.assertEqual(m.I[2], 'b') + self.assertEqual(m.I[2], "b") self.assertRegex( - OUT.getvalue().replace('\n', ' '), + OUT.getvalue().replace("\n", " "), r"^DEPRECATED: Using __getitem__ to return a set value from " r"its \(ordered\) position is deprecated. Please use at\(\)", ) with LoggingIntercept() as OUT: - self.assertEqual(m.I.card(2), 'b') + self.assertEqual(m.I.card(2), "b") self.assertRegex( - OUT.getvalue().replace('\n', ' '), + OUT.getvalue().replace("\n", " "), r"^DEPRECATED: card\(\) was incorrectly added to the Set API. " r"Please use at\(\)", ) @@ -6006,28 +6007,28 @@ def test_issue_43(self): model.Dummy = Set( model.Jobs, within=model.Jobs, initialize=lambda m, i: range(i) ) - model.Cars = Set(initialize=['a', 'b']) + model.Cars = Set(initialize=["a", "b"]) a = model.Cars * model.Dummy[1] self.assertEqual(len(a), 2) - self.assertIn(('a', 0), a) - self.assertIn(('b', 0), a) + self.assertIn(("a", 0), a) + self.assertIn(("b", 0), a) b = model.Dummy[2] * model.Cars self.assertEqual(len(b), 4) - self.assertIn((0, 'a'), b) - self.assertIn((0, 'b'), b) - self.assertIn((1, 'a'), b) - self.assertIn((1, 'b'), b) + self.assertIn((0, "a"), b) + self.assertIn((0, "b"), b) + self.assertIn((1, "a"), b) + self.assertIn((1, "b"), b) def test_issue_116(self): m = ConcreteModel() - m.s = Set(initialize=['one']) - m.t = Set([1], initialize=['one']) + m.s = Set(initialize=["one"]) + m.t = Set([1], initialize=["one"]) m.x = Var(m.s) output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): self.assertTrue(m.s in m.s) self.assertIn( "Testing for set subsets with 'a in b' is deprecated.", output.getvalue() @@ -6042,7 +6043,7 @@ def test_issue_116(self): with self.assertRaisesRegex(TypeError, err): m.x[m.s].display() - self.assertEqual(list(m.x), ['one']) + self.assertEqual(list(m.x), ["one"]) def test_issue_121(self): model = ConcreteModel() @@ -6073,10 +6074,10 @@ def test_issue_142(self): normalize_index.flatten = False m = ConcreteModel() output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.CHOICES = Set(initialize=CHOICES, dimen=3) self.assertIn( - 'Ignoring non-None dimen (3) for set CHOICES', output.getvalue() + "Ignoring non-None dimen (3) for set CHOICES", output.getvalue() ) self.assertEqual(m.CHOICES.dimen, None) @@ -6110,9 +6111,9 @@ def c_rule(m, a, b, c): normalize_index.flatten = True m = ConcreteModel() output = StringIO() - with LoggingIntercept(output, 'pyomo.core'): + with LoggingIntercept(output, "pyomo.core"): m.CHOICES = Set(initialize=CHOICES) - self.assertEqual('', output.getvalue()) + self.assertEqual("", output.getvalue()) self.assertEqual(m.CHOICES.dimen, 5) m.x = Var(m.CHOICES) @@ -6145,12 +6146,12 @@ def c_rule(m, a1, a2, a3, b, c): normalize_index.flatten = _oldFlatten def test_issue_148(self): - legal = set(['a', 'b', 'c']) + legal = set(["a", "b", "c"]) m = ConcreteModel() - m.s = Set(initialize=['a', 'b'], within=legal) - self.assertEqual(set(m.s), {'a', 'b'}) - with self.assertRaisesRegex(ValueError, 'Cannot add value d to Set s'): - m.s.add('d') + m.s = Set(initialize=["a", "b"], within=legal) + self.assertEqual(set(m.s), {"a", "b"}) + with self.assertRaisesRegex(ValueError, "Cannot add value d to Set s"): + m.s.add("d") def test_issue_165(self): m = ConcreteModel() @@ -6175,30 +6176,30 @@ def test_issue_165(self): def test_issue_191(self): m = ConcreteModel() - m.s = Set(['s1', 's2'], initialize=[1, 2, 3]) - m.s2 = Set(initialize=['a', 'b', 'c']) + m.s = Set(["s1", "s2"], initialize=[1, 2, 3]) + m.s2 = Set(initialize=["a", "b", "c"]) - m.p = Param(m.s['s1'], initialize=10) - temp = m.s['s1'] * m.s2 + m.p = Param(m.s["s1"], initialize=10) + temp = m.s["s1"] * m.s2 m.v = Var(temp, initialize=5) self.assertEqual(len(m.v), 9) - m.v_1 = Var(m.s['s1'], m.s2, initialize=10) + m.v_1 = Var(m.s["s1"], m.s2, initialize=10) self.assertEqual(len(m.v_1), 9) def test_issue_325(self): m = ConcreteModel() - m.I = Set(initialize=[1, 2, 'a', 3], ordered=False) - self.assertEqual(set(m.I.data()), set([1, 2, 'a', 3])) - self.assertEqual(list(m.I.ordered_data()), [1, 2, 3, 'a']) - self.assertEqual(list(m.I.sorted_data()), [1, 2, 3, 'a']) + m.I = Set(initialize=[1, 2, "a", 3], ordered=False) + self.assertEqual(set(m.I.data()), set([1, 2, "a", 3])) + self.assertEqual(list(m.I.ordered_data()), [1, 2, 3, "a"]) + self.assertEqual(list(m.I.sorted_data()), [1, 2, 3, "a"]) # Default sets are ordered by insertion order - m.I = Set(initialize=[1, 2, 'a', 3]) - self.assertEqual(set(m.I.data()), set([1, 2, 'a', 3])) - self.assertEqual(list(m.I.data()), [1, 2, 'a', 3]) - self.assertEqual(list(m.I.ordered_data()), [1, 2, 'a', 3]) - self.assertEqual(list(m.I.sorted_data()), [1, 2, 3, 'a']) + m.I = Set(initialize=[1, 2, "a", 3]) + self.assertEqual(set(m.I.data()), set([1, 2, "a", 3])) + self.assertEqual(list(m.I.data()), [1, 2, "a", 3]) + self.assertEqual(list(m.I.ordered_data()), [1, 2, "a", 3]) + self.assertEqual(list(m.I.sorted_data()), [1, 2, 3, "a"]) def test_issue_358(self): m = ConcreteModel() @@ -6216,8 +6217,8 @@ def _test(b, x, y, z): self.assertEqual(len(m.test2), 1) def test_issue_637(self): - constraints = {c for c in itertools.product(['constrA', 'constrB'], range(5))} - vars = {v for v in itertools.product(['var1', 'var2', 'var3'], range(5))} + constraints = {c for c in itertools.product(["constrA", "constrB"], range(5))} + vars = {v for v in itertools.product(["var1", "var2", "var3"], range(5))} matrix_coefficients = {m for m in itertools.product(constraints, vars)} m = ConcreteModel() m.IDX = Set(initialize=matrix_coefficients) @@ -6269,16 +6270,16 @@ def test_issue_835(self): @unittest.skipIf(NamedTuple is None, "typing module not available") def test_issue_938(self): - NodeKey = NamedTuple('NodeKey', [('id', int)]) - ArcKey = NamedTuple('ArcKey', [('node_from', NodeKey), ('node_to', NodeKey)]) + NodeKey = NamedTuple("NodeKey", [("id", int)]) + ArcKey = NamedTuple("ArcKey", [("node_from", NodeKey), ("node_to", NodeKey)]) def build_model(): model = ConcreteModel() model.node_keys = Set( - doc='Set of nodes', initialize=[NodeKey(0), NodeKey(1)] + doc="Set of nodes", initialize=[NodeKey(0), NodeKey(1)] ) model.arc_keys = Set( - doc='Set of arcs', + doc="Set of arcs", within=model.node_keys * model.node_keys, initialize=[ ArcKey(NodeKey(0), NodeKey(0)), diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 7595e1a2fe7..856af1130a4 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -210,8 +210,8 @@ def test_addInvalid(self): # is, the default within is None # self.assertEqual(self.instance.A.domain, Any) - self.instance.A.add('2', '3', '4') - self.assertFalse('2' not in self.instance.A, "Found invalid new element in A") + self.instance.A.add("2", "3", "4") + self.assertFalse("2" not in self.instance.A, "Found invalid new element in A") def test_removeValid(self): """Check that we can remove a valid set element""" @@ -298,7 +298,7 @@ def test_contains(self): """Various checks for contains() method""" self.assertEqual(self.e1 in self.instance.A, True) self.assertEqual(self.e2 in self.instance.A, False) - self.assertEqual('2' in self.instance.A, False) + self.assertEqual("2" in self.instance.A, False) def test_or(self): """Check that set union works""" @@ -550,9 +550,9 @@ def test_addInvalid(self): # is, the default within is None # with self.assertRaises(AttributeError): - self.instance.A.add('2', '3', '4') + self.instance.A.add("2", "3", "4") self.assertFalse( - '2' in self.instance.A, "Value we attempted to add is not in A" + "2" in self.instance.A, "Value we attempted to add is not in A" ) def test_removeValid(self): @@ -589,7 +589,7 @@ def test_contains(self): """Various checks for contains() method""" self.assertEqual(self.e1 in self.instance.A, True) self.assertEqual(self.e2 in self.instance.A, True) - self.assertEqual('2' in self.instance.A, False) + self.assertEqual("2" in self.instance.A, False) def test_len(self): """Check that a simple set of numeric elements has the right size""" @@ -844,49 +844,49 @@ def setUp(self): # # Misc datasets # - self.model.tmpset1 = Set(initialize=['A1', 'A3', 'A5', 'A7']) - self.model.tmpset2 = Set(initialize=['A1', 'A2', 'A3', 'A5', 'A7']) - self.model.tmpset3 = Set(initialize=['A2', 'A3', 'A5', 'A7', 'A9']) + self.model.tmpset1 = Set(initialize=["A1", "A3", "A5", "A7"]) + self.model.tmpset2 = Set(initialize=["A1", "A2", "A3", "A5", "A7"]) + self.model.tmpset3 = Set(initialize=["A2", "A3", "A5", "A7", "A9"]) - self.model.setunion = Set(initialize=['A1', 'A2', 'A3', 'A5', 'A7', 'A9']) - self.model.setintersection = Set(initialize=['A3', 'A5', 'A7']) - self.model.setxor = Set(initialize=['A1', 'A2', 'A9']) - self.model.setdiff = Set(initialize=['A1']) + self.model.setunion = Set(initialize=["A1", "A2", "A3", "A5", "A7", "A9"]) + self.model.setintersection = Set(initialize=["A3", "A5", "A7"]) + self.model.setxor = Set(initialize=["A1", "A2", "A9"]) + self.model.setdiff = Set(initialize=["A1"]) self.model.setmul = Set( initialize=[ - ('A1', 'A2'), - ('A1', 'A3'), - ('A1', 'A5'), - ('A1', 'A7'), - ('A1', 'A9'), - ('A3', 'A2'), - ('A3', 'A3'), - ('A3', 'A5'), - ('A3', 'A7'), - ('A3', 'A9'), - ('A5', 'A2'), - ('A5', 'A3'), - ('A5', 'A5'), - ('A5', 'A7'), - ('A5', 'A9'), - ('A7', 'A2'), - ('A7', 'A3'), - ('A7', 'A5'), - ('A7', 'A7'), - ('A7', 'A9'), + ("A1", "A2"), + ("A1", "A3"), + ("A1", "A5"), + ("A1", "A7"), + ("A1", "A9"), + ("A3", "A2"), + ("A3", "A3"), + ("A3", "A5"), + ("A3", "A7"), + ("A3", "A9"), + ("A5", "A2"), + ("A5", "A3"), + ("A5", "A5"), + ("A5", "A7"), + ("A5", "A9"), + ("A7", "A2"), + ("A7", "A3"), + ("A7", "A5"), + ("A7", "A7"), + ("A7", "A9"), ] ) self.instance = self.model.create_instance(currdir + "setA.dat") - self.e1 = 'A1' - self.e2 = 'A2' - self.e3 = 'A3' - self.e4 = 'A4' - self.e5 = 'A5' - self.e6 = 'A6' + self.e1 = "A1" + self.e2 = "A2" + self.e3 = "A3" + self.e4 = "A4" + self.e5 = "A5" + self.e6 = "A6" def test_bounds(self): - self.assertEqual(self.instance.A.bounds(), ('A1', 'A7')) + self.assertEqual(self.instance.A.bounds(), ("A1", "A7")) class SimpleSetC(SimpleSetA): @@ -911,60 +911,60 @@ def setUp(self): # Misc datasets # self.model.tmpset1 = Set( - initialize=[('A1', 1), ('A3', 1), ('A5', 1), ('A7', 1)] + initialize=[("A1", 1), ("A3", 1), ("A5", 1), ("A7", 1)] ) self.model.tmpset2 = Set( - initialize=[('A1', 1), ('A2', 1), ('A3', 1), ('A5', 1), ('A7', 1)] + initialize=[("A1", 1), ("A2", 1), ("A3", 1), ("A5", 1), ("A7", 1)] ) self.model.tmpset3 = Set( - initialize=[('A2', 1), ('A3', 1), ('A5', 1), ('A7', 1), ('A9', 1)] + initialize=[("A2", 1), ("A3", 1), ("A5", 1), ("A7", 1), ("A9", 1)] ) self.model.setunion = Set( initialize=[ - ('A1', 1), - ('A2', 1), - ('A3', 1), - ('A5', 1), - ('A7', 1), - ('A9', 1), + ("A1", 1), + ("A2", 1), + ("A3", 1), + ("A5", 1), + ("A7", 1), + ("A9", 1), ] ) - self.model.setintersection = Set(initialize=[('A3', 1), ('A5', 1), ('A7', 1)]) - self.model.setxor = Set(initialize=[('A1', 1), ('A2', 1), ('A9', 1)]) - self.model.setdiff = Set(initialize=[('A1', 1)]) + self.model.setintersection = Set(initialize=[("A3", 1), ("A5", 1), ("A7", 1)]) + self.model.setxor = Set(initialize=[("A1", 1), ("A2", 1), ("A9", 1)]) + self.model.setdiff = Set(initialize=[("A1", 1)]) self.model.setmul = Set( initialize=[ - (('A1', 1, 'A2', 1)), - (('A1', 1, 'A3', 1)), - (('A1', 1, 'A5', 1)), - (('A1', 1, 'A7', 1)), - (('A1', 1, 'A9', 1)), - (('A3', 1, 'A2', 1)), - (('A3', 1, 'A3', 1)), - (('A3', 1, 'A5', 1)), - (('A3', 1, 'A7', 1)), - (('A3', 1, 'A9', 1)), - (('A5', 1, 'A2', 1)), - (('A5', 1, 'A3', 1)), - (('A5', 1, 'A5', 1)), - (('A5', 1, 'A7', 1)), - (('A5', 1, 'A9', 1)), - (('A7', 1, 'A2', 1)), - (('A7', 1, 'A3', 1)), - (('A7', 1, 'A5', 1)), - (('A7', 1, 'A7', 1)), - (('A7', 1, 'A9', 1)), + (("A1", 1, "A2", 1)), + (("A1", 1, "A3", 1)), + (("A1", 1, "A5", 1)), + (("A1", 1, "A7", 1)), + (("A1", 1, "A9", 1)), + (("A3", 1, "A2", 1)), + (("A3", 1, "A3", 1)), + (("A3", 1, "A5", 1)), + (("A3", 1, "A7", 1)), + (("A3", 1, "A9", 1)), + (("A5", 1, "A2", 1)), + (("A5", 1, "A3", 1)), + (("A5", 1, "A5", 1)), + (("A5", 1, "A7", 1)), + (("A5", 1, "A9", 1)), + (("A7", 1, "A2", 1)), + (("A7", 1, "A3", 1)), + (("A7", 1, "A5", 1)), + (("A7", 1, "A7", 1)), + (("A7", 1, "A9", 1)), ] ) self.instance = self.model.create_instance(currdir + "setA.dat") - self.e1 = ('A1', 1) - self.e2 = ('A2', 1) - self.e3 = ('A3', 1) - self.e4 = ('A4', 1) - self.e5 = ('A5', 1) - self.e6 = ('A6', 1) + self.e1 = ("A1", 1) + self.e2 = ("A2", 1) + self.e3 = ("A3", 1) + self.e4 = ("A4", 1) + self.e5 = ("A5", 1) + self.e6 = ("A6", 1) def tearDown(self): # @@ -974,7 +974,7 @@ def tearDown(self): PyomoModel.tearDown(self) def test_bounds(self): - self.assertEqual(self.instance.A.bounds(), (('A1', 1), ('A7', 1))) + self.assertEqual(self.instance.A.bounds(), (("A1", 1), ("A7", 1))) def test_addInvalid(self): """Check that we get an error when adding invalid set elements""" @@ -984,13 +984,13 @@ def test_addInvalid(self): # self.assertEqual(self.instance.A.domain, Any) try: - self.instance.A.add('2', '3', '4') + self.instance.A.add("2", "3", "4") except ValueError: pass else: self.fail("fail test_addInvalid") - self.assertFalse('2' in self.instance.A, "Found invalid new element in A") - self.instance.A.add(('2', '3')) + self.assertFalse("2" in self.instance.A, "Found invalid new element in A") + self.instance.A.add(("2", "3")) @unittest.skipIf(not _has_numpy, "Numpy is not installed") @@ -1107,7 +1107,7 @@ def setUp(self): self.model.Q_c = Set(initialize=[3, 5, 7, 9]) self.instance = self.model.create_instance(currdir + "setA.dat") - self.e1 = ('A1', 1) + self.e1 = ("A1", 1) def Xtest_bounds(self): self.assertEqual(self.instance.A.bounds(), None) @@ -1116,14 +1116,14 @@ def test_getitem(self): """Check the access to items""" try: tmp = [] - for val in self.instance.A['A']: + for val in self.instance.A["A"]: tmp.append(val) tmp.sort() except: self.fail("Problems getting a valid set from a set array") self.assertEqual(tmp, [1, 3, 5, 7]) try: - tmp = self.instance.A['D'] + tmp = self.instance.A["D"] except KeyError: pass else: @@ -1131,15 +1131,15 @@ def test_getitem(self): def test_setitem(self): """Check the access to items""" - self.model.Z = Set(initialize=['A', 'C']) - self.model.A = Set(self.model.Z, initialize={'A': [1]}) + self.model.Z = Set(initialize=["A", "C"]) + self.model.A = Set(self.model.Z, initialize={"A": [1]}) self.instance = self.model.create_instance() tmp = [1, 6, 9] - self.instance.A['A'] = tmp - self.instance.A['C'] = tmp + self.instance.A["A"] = tmp + self.instance.A["C"] = tmp try: - self.instance.A['D'] = tmp + self.instance.A["D"] = tmp except KeyError: pass else: @@ -1149,7 +1149,7 @@ def test_keys(self): """Check the keys for the array""" tmp = list(self.instance.A.keys()) tmp.sort() - self.assertEqual(tmp, ['A', 'C']) + self.assertEqual(tmp, ["A", "C"]) def test_len(self): """Check that a simple set of numeric elements has the right size""" @@ -1281,7 +1281,7 @@ def test_or(self): """Check that set union works""" with self.assertRaisesRegex( TypeError, - r'Cannot apply a Set operator to an indexed Set ' r'component \(A\)', + r"Cannot apply a Set operator to an indexed Set " r"component \(A\)", ): self.instance.A | self.instance.tmpset3 @@ -1289,7 +1289,7 @@ def test_and(self): """Check that set intersection works""" with self.assertRaisesRegex( TypeError, - r'Cannot apply a Set operator to an indexed Set ' r'component \(A\)', + r"Cannot apply a Set operator to an indexed Set " r"component \(A\)", ): self.instance.A & self.instance.tmpset3 @@ -1297,7 +1297,7 @@ def test_xor(self): """Check that set exclusive or works""" with self.assertRaisesRegex( TypeError, - r'Cannot apply a Set operator to an indexed Set ' r'component \(A\)', + r"Cannot apply a Set operator to an indexed Set " r"component \(A\)", ): self.instance.A ^ self.instance.tmpset3 @@ -1305,7 +1305,7 @@ def test_diff(self): """Check that set difference works""" with self.assertRaisesRegex( TypeError, - r'Cannot apply a Set operator to an indexed Set ' r'component \(A\)', + r"Cannot apply a Set operator to an indexed Set " r"component \(A\)", ): self.instance.A - self.instance.tmpset3 @@ -1313,7 +1313,7 @@ def test_mul(self): """Check that set cross-product works""" with self.assertRaisesRegex( TypeError, - r'Cannot apply a Set operator to an indexed Set ' r'component \(A\)', + r"Cannot apply a Set operator to an indexed Set " r"component \(A\)", ): self.instance.A * self.instance.tmpset3 @@ -1362,16 +1362,16 @@ def setUp(self): self.model.tmpset3 = Set() self.instance = self.model.create_instance(currdir + "setA.dat") - self.e1 = ('A1', 1) + self.e1 = ("A1", 1) def test_bounds(self): - self.assertEqual(self.instance.A['A', 1].bounds(), (1, 7)) + self.assertEqual(self.instance.A["A", 1].bounds(), (1, 7)) def test_getitem(self): """Check the access to items""" try: tmp = [] - for val in self.instance.A['A', 1]: + for val in self.instance.A["A", 1]: tmp.append(val) tmp.sort() except: @@ -1379,12 +1379,12 @@ def test_getitem(self): self.assertEqual(tmp, [1, 3, 5, 7]) try: - tmp = self.instance.A['A', 2] + tmp = self.instance.A["A", 2] except: self.fail("Problems getting a valid uninitialized subset from a set array") try: - tmp = self.instance.A['A', 3] + tmp = self.instance.A["A", 3] except KeyError: pass else: @@ -1394,16 +1394,16 @@ def Xtest_setitem(self): """Check the access to items""" try: self.model.Y = Set(initialize=[1, 2]) - self.model.Z = Set(initialize=['A', 'C']) - self.model.A = Set(self.model.Z, self.model.Y, initialize={'A': [1]}) + self.model.Z = Set(initialize=["A", "C"]) + self.model.A = Set(self.model.Z, self.model.Y, initialize={"A": [1]}) self.instance = self.model.create_instance() tmp = [1, 6, 9] - self.instance.A['A'] = tmp - self.instance.A['C'] = tmp + self.instance.A["A"] = tmp + self.instance.A["C"] = tmp except: self.fail("Problems setting a valid set into a set array") try: - self.instance.A['D'] = tmp + self.instance.A["D"] = tmp except KeyError: pass else: @@ -1413,7 +1413,7 @@ def Xtest_keys(self): """Check the keys for the array""" tmp = self.instance.A.keys() tmp.sort() - self.assertEqual(tmp, ['A', 'C']) + self.assertEqual(tmp, ["A", "C"]) def Xtest_len(self): """Check that a simple set of numeric elements has the right size""" @@ -1587,11 +1587,11 @@ def test_name(self): # # self.assertEqual(x.name, None) # self.assertTrue('RealSet' in str(x)) - self.assertEqual(x.name, 'Reals') - self.assertEqual('Reals', str(x)) + self.assertEqual(x.name, "Reals") + self.assertEqual("Reals", str(x)) x = RealSet(name="x") - self.assertEqual(x.name, 'x') - self.assertEqual(str(x), 'x') + self.assertEqual(x.name, "x") + self.assertEqual(str(x), "x") @unittest.skip("_VirtualSet was removed during the set rewrite") def test_contains(self): @@ -1779,11 +1779,11 @@ def test_name(self): # # self.assertEqual(x.name, None) # self.assertTrue('IntegerSet' in str(x)) - self.assertEqual(x.name, 'Integers') - self.assertEqual('Integers', str(x)) + self.assertEqual(x.name, "Integers") + self.assertEqual("Integers", str(x)) x = IntegerSet(name="x") - self.assertEqual(x.name, 'x') - self.assertEqual(str(x), 'x') + self.assertEqual(x.name, "x") + self.assertEqual(str(x), "x") def test_contains(self): x = IntegerSet() @@ -1934,11 +1934,11 @@ def test_name(self): # # self.assertEqual(x.name, None) # self.assertTrue('BooleanSet' in str(x)) - self.assertEqual(x.name, 'Boolean') - self.assertEqual('Boolean', str(x)) + self.assertEqual(x.name, "Boolean") + self.assertEqual("Boolean", str(x)) x = BooleanSet(name="x") - self.assertEqual(x.name, 'x') - self.assertEqual(str(x), 'x') + self.assertEqual(x.name, "x") + self.assertEqual(str(x), "x") def test_contains(self): x = BooleanSet() @@ -2008,15 +2008,15 @@ def setUp(self): # # Misc datasets # - self.model.tmpset1 = Set(initialize=[1, '3', 5, 7]) - self.model.tmpset2 = Set(initialize=[1, 2, '3', 5, 7]) - self.model.tmpset3 = Set(initialize=[2, '3', 5, 7, 9]) + self.model.tmpset1 = Set(initialize=[1, "3", 5, 7]) + self.model.tmpset2 = Set(initialize=[1, 2, "3", 5, 7]) + self.model.tmpset3 = Set(initialize=[2, "3", 5, 7, 9]) y = _AnySet() # y.concrete=True self.model.setunion = y y.concrete = False - self.model.setintersection = Set(initialize=[1, '3', 5, 7]) + self.model.setintersection = Set(initialize=[1, "3", 5, 7]) self.model.setxor = Set(initialize=[]) self.model.setdiff = Set(initialize=[]) self.model.setmul = None @@ -2024,7 +2024,7 @@ def setUp(self): self.instance = self.model.create_instance() self.e1 = 1 self.e2 = 2 - self.e3 = '3' + self.e3 = "3" self.e4 = 4 self.e5 = 5 self.e6 = 6 @@ -2037,7 +2037,7 @@ def test_contains(self): """Various checks for contains() method""" self.assertEqual(self.e1 in self.instance.A, True) self.assertEqual(self.e2 in self.instance.A, True) - self.assertEqual('2' in self.instance.A, True) + self.assertEqual("2" in self.instance.A, True) def test_None1(self): self.assertEqual(None in Any, True) @@ -2223,7 +2223,7 @@ def tearDown(self): PyomoModel.tearDown(self) def test_initialize1_list(self): - self.model.A = Set(initialize=[1, 2, 3, 'A']) + self.model.A = Set(initialize=[1, 2, 3, "A"]) self.instance = self.model.create_instance() self.assertEqual(len(self.instance.A), 4) @@ -2570,7 +2570,7 @@ def test_validation2(self): def test_other1(self): self.model.A = Set( - initialize=[1, 2, 3, 'A'], validate=lambda model, x: x in Integers + initialize=[1, 2, 3, "A"], validate=lambda model, x: x in Integers ) try: self.instance = self.model.create_instance() @@ -2580,7 +2580,7 @@ def test_other1(self): self.fail("fail test_other1") def test_other2(self): - self.model.A = Set(initialize=[1, 2, 3, 'A'], within=Integers) + self.model.A = Set(initialize=[1, 2, 3, "A"], within=Integers) try: self.instance = self.model.create_instance() except ValueError: @@ -2655,9 +2655,9 @@ def test_initialize(self): # Create model instance # self.model.Z = Set() - self.model.A = Set(self.model.Z, initialize={'A': [1, 2, 3, 'A']}) - self.instance = self.model.create_instance(currdir + 'setA.dat') - self.assertEqual(len(self.instance.A['A']), 4) + self.model.A = Set(self.model.Z, initialize={"A": [1, 2, 3, "A"]}) + self.instance = self.model.create_instance(currdir + "setA.dat") + self.assertEqual(len(self.instance.A["A"]), 4) def test_dimen(self): # @@ -2700,7 +2700,7 @@ def tmp_init(model, i): self.model.Z = Set() self.model.A = Set(self.model.Z, initialize=tmp_init) self.instance = self.model.create_instance(currdir + "setA.dat") - self.assertEqual(len(self.instance.A['A']), 5) + self.assertEqual(len(self.instance.A["A"]), 5) def test_rule2(self): # @@ -2720,7 +2720,7 @@ def tmp_rule2(model, z, i): self.model.Z = Set() self.model.A = Set(self.model.Z, initialize=tmp_rule2) self.instance = self.model.create_instance(currdir + "setA.dat") - self.assertEqual(len(self.instance.A['A']), 5) + self.assertEqual(len(self.instance.A["A"]), 5) def test_rule3(self): # @@ -2739,7 +2739,7 @@ def tmp_rule2(model, z, i): self.model.Z = Set() self.model.A = Set(self.model.Z, initialize=tmp_rule2) self.instance = self.model.create_instance(currdir + "setA.dat") - self.assertEqual(len(self.instance.A['A']), 5) + self.assertEqual(len(self.instance.A["A"]), 5) def test_within1(self): # @@ -2816,13 +2816,15 @@ def test_validation2(self): self.fail("fail test_within2") else: pass - + 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.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 @@ -2831,13 +2833,15 @@ def test_validation3_pass(self): 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.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 @@ -2851,32 +2855,36 @@ def test_validation3_fail(self): except ValueError: error_raised = True assert error_raised - + 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.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], + 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.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], + initialize={"A": [(1, 2), (3, 4), (5, 6)]}, + validate=lambda model, x, y, i: (x, y) in model.A[i], ) error_raised = False try: @@ -2884,35 +2892,43 @@ def test_validation4_fail(self): except ValueError: error_raised = True assert error_raised - + 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)]}) + 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)]}, + 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)]}) + 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)]}, + initialize={"A": [(1, 2), (3, 4), (5, 6)]}, validate=validate_B, ) error_raised = False @@ -2923,10 +2939,10 @@ def validate_B(m, e1, e2, i): assert error_raised def test_other1(self): - self.model.Z = Set(initialize=['A']) + self.model.Z = Set(initialize=["A"]) self.model.A = Set( self.model.Z, - initialize={'A': [1, 2, 3, 'A']}, + initialize={"A": [1, 2, 3, "A"]}, validate=lambda model, x, i: x in Integers, ) try: @@ -2937,9 +2953,9 @@ def test_other1(self): self.fail("fail test_other1") def test_other2(self): - self.model.Z = Set(initialize=['A']) + self.model.Z = Set(initialize=["A"]) self.model.A = Set( - self.model.Z, initialize={'A': [1, 2, 3, 'A']}, within=Integers + self.model.Z, initialize={"A": [1, 2, 3, "A"]}, within=Integers ) try: self.instance = self.model.create_instance() @@ -2956,9 +2972,11 @@ def tmp_init(model, i): return tmp self.model.n = Param(initialize=5) - self.model.Z = Set(initialize=['A']) + self.model.Z = Set(initialize=["A"]) self.model.A = Set( - self.model.Z, initialize=tmp_init, validate=lambda model, x, i: x in Integers + self.model.Z, + initialize=tmp_init, + validate=lambda model, x, i: x in Integers, ) try: self.instance = self.model.create_instance() @@ -2975,7 +2993,7 @@ def tmp_init(model, i): return tmp self.model.n = Param(initialize=5) - self.model.Z = Set(initialize=['A']) + 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: @@ -2996,7 +3014,7 @@ def setUp(self): # # self.model.A = Set(initialize=[1, 2, 3]) - self.model.B = Set(initialize=['a', 'b', 'c']) + self.model.B = Set(initialize=["a", "b", "c"]) self.model.C = Set(initialize=[4, 5, 6]) def tearDown(self): @@ -3035,8 +3053,8 @@ def test_pprint_mixed(self): # In Python3, sorting a mixed string fails. We have added a # fallback more "robust" sorter, and this exercises that code m = ConcreteModel() - m.Z = Set(initialize=['A', 'C']) - m.A = Set(m.Z, initialize={'A': [1, 2, 3, 'A']}) + m.Z = Set(initialize=["A", "C"]) + m.A = Set(m.Z, initialize={"A": [1, 2, 3, "A"]}) buf = StringIO() m.pprint(ostream=buf) ref = """2 Set Declarations @@ -3237,7 +3255,7 @@ def test_io7(self): self.instance = self.model.create_instance(currdir + "setA.dat") self.assertEqual(self.instance.F.dim(), 1) self.assertEqual(len(list(self.instance.F.keys())), 3) - self.assertEqual(len(self.instance.F['A1']), 3) + self.assertEqual(len(self.instance.F["A1"]), 3) def test_io8(self): OUTPUT = open(currdir + "setA.dat", "w") @@ -3270,7 +3288,7 @@ def test_io10(self): OUTPUT.write("data;\n") OUTPUT.write("set A := 'A1 x' ' A2' \"A3\";\n") OUTPUT.write("set F['A1 x'] := 1 3 5;\n") - OUTPUT.write("set F[\" A2\"] := 2 4 6;\n") + OUTPUT.write('set F[" A2"] := 2 4 6;\n') OUTPUT.write("set F['A3'] := 3 5 7;\n") OUTPUT.write("end;\n") OUTPUT.close() @@ -3279,7 +3297,7 @@ def test_io10(self): self.instance = self.model.create_instance(currdir + "setA.dat") self.assertEqual(self.instance.F.dim(), 1) self.assertEqual(len(list(self.instance.F.keys())), 3) - self.assertEqual(len(self.instance.F['A1 x']), 3) + self.assertEqual(len(self.instance.F["A1 x"]), 3) class TestSetErrors(PyomoModel): @@ -3852,7 +3870,7 @@ def test_union(self): model = AbstractModel() s1 = set([1, 2]) model.s1 = Set(initialize=s1) - s2 = set(['a', 'b']) + s2 = set(["a", "b"]) model.s2 = Set(initialize=s2) s3 = set([None, True]) model.s3 = Set(initialize=s3) @@ -3875,7 +3893,7 @@ def test_intersection(self): model = AbstractModel() s1 = set([1, 2]) model.s1 = Set(initialize=s1) - s2 = set(['a', 'b']) + s2 = set(["a", "b"]) model.s2 = Set(initialize=s2) s3 = set([None, True]) model.s3 = Set(initialize=s3) @@ -3914,7 +3932,7 @@ def test_difference(self): model = AbstractModel() s1 = set([1, 2]) model.s1 = Set(initialize=s1) - s2 = set(['a', 'b']) + s2 = set(["a", "b"]) model.s2 = Set(initialize=s2) s3 = set([None, True]) model.s3 = Set(initialize=s3) From ad01644d7c8548b800bfa884085f4be3d93371ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Tue, 2 Jul 2024 21:24:15 +0200 Subject: [PATCH 064/220] Revised tests. --- pyomo/core/tests/unit/test_sets.py | 175 ++++++----------------------- 1 file changed, 35 insertions(+), 140 deletions(-) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 856af1130a4..59387c324d0 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2396,24 +2396,16 @@ 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 +2414,14 @@ 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 +2480,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 +2491,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 +2502,13 @@ 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") + # pass - def test_validation1(self): + def test_validation_fail(self): # # Create Set 'A' data file # @@ -2543,14 +2519,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 +2530,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 +2560,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,13 +2576,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): def setUp(self): @@ -2666,24 +2616,16 @@ 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 +2695,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 +2706,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 +2724,8 @@ def test_validation1(self): # self.model.Z = Set() self.model.A = Set(self.model.Z, validate=lambda model, x, i: 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 +2735,11 @@ def test_validation2(self): OUTPUT.write("data; set Z := A C; set A[A] := 1 3 5 5.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, validate=lambda model, x, i: x < 6) - 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_validation3_pass(self): # @@ -2849,12 +2773,8 @@ def test_validation3_fail(self): 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]) - error_raised = False - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance(currdir + "setsAB.dat") - except ValueError: - error_raised = True - assert error_raised def test_validation4_pass(self): # @@ -2886,12 +2806,8 @@ def test_validation4_fail(self): initialize={"A": [(1, 2), (3, 4), (5, 6)]}, validate=lambda model, x, y, i: (x, y) in model.A[i], ) - error_raised = False - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - except ValueError: - error_raised = True - assert error_raised def test_validation5_pass(self): # @@ -2931,12 +2847,8 @@ def validate_B(m, e1, e2, i): initialize={"A": [(1, 2), (3, 4), (5, 6)]}, validate=validate_B, ) - error_raised = False - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - except ValueError: - error_raised = True - assert error_raised def test_other1(self): self.model.Z = Set(initialize=["A"]) @@ -2945,24 +2857,16 @@ def test_other1(self): initialize={"A": [1, 2, 3, "A"]}, validate=lambda model, x, i: 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): @@ -2978,12 +2882,8 @@ def tmp_init(model, i): initialize=tmp_init, validate=lambda model, x, i: 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): @@ -2996,13 +2896,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): def setUp(self): From b232a1143723e4c252fd318fcb35b242095928bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Tue, 2 Jul 2024 22:57:40 +0200 Subject: [PATCH 065/220] Applied black. --- pyomo/core/tests/unit/test_sets.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 59387c324d0..6381b1162c9 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2399,11 +2399,13 @@ def test_dimen1(self): 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() - + self.model.A = Set(initialize=[(1, 2), (2, 3), (3, 4)], dimen=2) self.instance = self.model.create_instance() # - with self.assertRaisesRegex(ValueError, ".*has dimension 2 and is not valid for "): + 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() @@ -2414,12 +2416,16 @@ def f(model): self.model.A = Set(initialize=f, dimen=2) self.instance = self.model.create_instance() # - with self.assertRaisesRegex(ValueError, ".*has dimension 2 and is not valid for "): + 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() def test_dimen2(self): - with self.assertRaisesRegex(ValueError, ".*has dimension 2 and is not valid for "): + 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() self.model.A = Set(dimen=None, initialize=[1, 2, (3, 4)]) @@ -2506,7 +2512,7 @@ def test_within_pass(self): # self.model.A = Set(within=Reals) self.instance = self.model.create_instance(currdir + "setA.dat") - # pass + # pass def test_validation_fail(self): # @@ -2579,6 +2585,7 @@ def tmp_init(model): with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance(currdir + "setA.dat") + class TestSetArgs2(PyomoModel): def setUp(self): # @@ -2621,7 +2628,9 @@ def test_dimen(self): self.instance = self.model.create_instance() self.model.A = Set(self.model.Z, initialize=[(1, 2), (2, 3), (3, 4)], dimen=2) self.instance = self.model.create_instance() - with self.assertRaisesRegex(ValueError, ".*has dimension 2 and is not valid for"): + 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 ) @@ -2899,6 +2908,7 @@ def tmp_init(model, i): with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance() + class TestMisc(PyomoModel): def setUp(self): # From c9cd2ea26519028914a3933c44a366d3a7900b8f Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 8 Jul 2024 21:52:12 -0400 Subject: [PATCH 066/220] Add `ParameterizedQuadraticRepn` and accompanying walker --- pyomo/repn/parameterized_quadratic.py | 458 ++++++++++++++++++++++++++ 1 file changed, 458 insertions(+) create mode 100644 pyomo/repn/parameterized_quadratic.py diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py new file mode 100644 index 00000000000..2c3c8839d99 --- /dev/null +++ b/pyomo/repn/parameterized_quadratic.py @@ -0,0 +1,458 @@ +# ___________________________________________________________________________ +# +# 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, + mutable_expression, + PowExpression, + ProductExpression, + Expr_ifExpression, +) +from pyomo.repn.linear import ( + ExitNodeDispatcher, + _initialize_exit_node_dispatcher, + _handle_product_ANY_constant, + _handle_product_constant_ANY, + _handle_division_ANY_constant, + _handle_pow_ANY_constant, + _handle_expr_if_const, +) +from pyomo.repn.parameterized_linear import ( + ParameterizedLinearRepnVisitor, + ParameterizedLinearBeforeChildDispatcher, + _handle_division_ANY_pseudo_constant, + define_exit_node_handlers as _param_linear_def_exit_node_handlers, +) +from pyomo.repn.quadratic import ( + QuadraticRepn, + _handle_product_linear_linear, + _handle_product_nonlinear, + _mul_linear_linear, +) +from pyomo.repn.util import ExprType + + +_FIXED = ExprType.FIXED +_CONSTANT = ExprType.CONSTANT +_LINEAR = ExprType.LINEAR +_GENERAL = ExprType.GENERAL +_QUADRATIC = ExprType.QUADRATIC + + +def _merge_dict(dest_dict, mult, src_dict): + if not is_equal_to(mult, 1): + for vid, coef in src_dict.items(): + if vid in dest_dict: + dest_dict[vid] += mult * coef + else: + dest_dict[vid] = mult * coef + else: + for vid, coef in src_dict.items(): + if vid in dest_dict: + dest_dict[vid] += coef + else: + dest_dict[vid] = coef + + +def to_expression(visitor, arg): + if arg[0] in (_CONSTANT, _FIXED): + return arg[1] + else: + return arg[1].to_expression(visitor) + + +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 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 + + +class ParameterizedQuadraticBeforeChildDispatcher(ParameterizedLinearBeforeChildDispatcher): + @staticmethod + def _before_linear(visitor, child): + return True, None + + @staticmethod + def _before_var(visitor, child): + _id = id(child) + if _id not in visitor.var_map: + if child.fixed: + return False, (_CONSTANT, visitor.check_constant(child.value, child)) + if child in visitor.wrt: + # pseudo-constant + # We aren't treating this Var as a Var for the purposes of this walker + return False, (_FIXED, child) + # This is a normal situation + ParameterizedLinearBeforeChildDispatcher._record_var(visitor, child) + ans = visitor.Result() + ans.linear[_id] = 1 + return False, (ExprType.LINEAR, ans) + + @staticmethod + def _before_param(visitor, child): + ans = visitor.Result() + ans.constant = child + return False, (_CONSTANT, ans) + + +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_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 + _linear = arg1.linear + for vid, coef in _linear.items(): + _linear[vid] = c * coef + if not is_zero(arg1.constant): + _merge_dict(arg1.linear, arg1.constant, arg2.linear) + + # Finally, the constant and multipliers + # TODO: what if arg1.constant or arg2.constant is nan? + if is_zero(arg1.constant) or is_zero(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 + + # We are multiplying (A + Bx + Cx^2 + D(x)) * (A + Bx + Cx^2 + Dx)) + _, x1 = arg1 + _, x2 = arg2 + ans.multiplier = x1.multiplier * x2.multiplier + x1.multiplier = x2.multiplier = 1 + # x1.const * x2.const [AA] + # TODO: what if either constant is NaN? + if is_zero(x1.constant) or is_zero(x2.constant): + ans.constant = 0 + else: + ans.constant = x1.constant * x2.constant + # linear & quadratic terms + if not is_zero(x2.constant): + # [BA], [CA] + c = x2.constant + if is_equal_to(c, 1): + ans.linear = dict(x1.linear) + if x1.quadratic: + ans.quadratic = dict(x1.quadratic) + else: + ans.linear = {vid: c * coef for vid, coef in x1.linear.items()} + if x1.quadratic: + ans.quadratic = {k: c * coef for k, coef in x1.quadratic.items()} + if not is_zero(x1.constant): + # [AB] + _merge_dict(ans.linear, x1.constant, x2.linear) + # [AC] + 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()} + # [BB] + 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 + # [DA] + [DB] + [DC] + [DD] + 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 = {} + # [CB] + [CC] + [CD] + if x1.quadratic: + ans.nonlinear += x1.to_expression(visitor) * x2.to_expression(visitor) + x1.quadratic = None + x2.linear = {} + # [BC] + [BD] + 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) + # [AD] + if not is_zero(x1_c) and x2.nonlinear is not None: + ans.nonlinear += x1_c * x2.nonlinear + return _GENERAL, ans + + +_before_child_dispatcher = ParameterizedLinearBeforeChildDispatcher() + + +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 beforeChild(self, node, child, child_idx): + return _before_child_dispatcher[child.__class__](self, child) + + 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 + 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] + + 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 + if ans.constant != ans.constant or any( + c != c for c in ans.linear.values() + ): + # 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, 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 From 8d1ed3c541e438cb7e505fc42a7d7f1ad8736d39 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 8 Jul 2024 22:01:39 -0400 Subject: [PATCH 067/220] Simplify imports --- pyomo/repn/parameterized_quadratic.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 2c3c8839d99..929340b4b9d 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -31,11 +31,10 @@ ParameterizedLinearBeforeChildDispatcher, _handle_division_ANY_pseudo_constant, define_exit_node_handlers as _param_linear_def_exit_node_handlers, + to_expression, ) from pyomo.repn.quadratic import ( QuadraticRepn, - _handle_product_linear_linear, - _handle_product_nonlinear, _mul_linear_linear, ) from pyomo.repn.util import ExprType @@ -49,6 +48,10 @@ def _merge_dict(dest_dict, mult, src_dict): + """ + Slightly different from `merge_dict` of + from the parameterized module. + """ if not is_equal_to(mult, 1): for vid, coef in src_dict.items(): if vid in dest_dict: @@ -63,13 +66,6 @@ def _merge_dict(dest_dict, mult, src_dict): dest_dict[vid] = coef -def to_expression(visitor, arg): - if arg[0] in (_CONSTANT, _FIXED): - return arg[1] - else: - return arg[1].to_expression(visitor) - - class ParameterizedQuadraticRepn(QuadraticRepn): def __str__(self): return ( @@ -192,7 +188,9 @@ def append(self, other): self.nonlinear += nl -class ParameterizedQuadraticBeforeChildDispatcher(ParameterizedLinearBeforeChildDispatcher): +class ParameterizedQuadraticBeforeChildDispatcher( + ParameterizedLinearBeforeChildDispatcher + ): @staticmethod def _before_linear(visitor, child): return True, None From d864e640eeda6fdccb5bdaa8f02df36000f546d0 Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 8 Jul 2024 22:02:43 -0400 Subject: [PATCH 068/220] Rearrange imports --- pyomo/repn/parameterized_quadratic.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 929340b4b9d..1890a13a837 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -12,26 +12,26 @@ from pyomo.common.numeric_types import native_numeric_types from pyomo.core.expr.numeric_expr import ( DivisionExpression, + Expr_ifExpression, mutable_expression, PowExpression, ProductExpression, - Expr_ifExpression, ) from pyomo.repn.linear import ( ExitNodeDispatcher, - _initialize_exit_node_dispatcher, - _handle_product_ANY_constant, - _handle_product_constant_ANY, _handle_division_ANY_constant, - _handle_pow_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 ( - ParameterizedLinearRepnVisitor, - ParameterizedLinearBeforeChildDispatcher, - _handle_division_ANY_pseudo_constant, define_exit_node_handlers as _param_linear_def_exit_node_handlers, + ParameterizedLinearBeforeChildDispatcher, + ParameterizedLinearRepnVisitor, to_expression, + _handle_division_ANY_pseudo_constant, ) from pyomo.repn.quadratic import ( QuadraticRepn, From 0803ad7cbd5c831936144cbabb0d8006d11a270d Mon Sep 17 00:00:00 2001 From: jasherma Date: Mon, 8 Jul 2024 22:03:06 -0400 Subject: [PATCH 069/220] Apply black --- pyomo/repn/parameterized_quadratic.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 1890a13a837..c25071dd806 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -33,10 +33,7 @@ to_expression, _handle_division_ANY_pseudo_constant, ) -from pyomo.repn.quadratic import ( - QuadraticRepn, - _mul_linear_linear, -) +from pyomo.repn.quadratic import QuadraticRepn, _mul_linear_linear from pyomo.repn.util import ExprType @@ -189,8 +186,8 @@ def append(self, other): class ParameterizedQuadraticBeforeChildDispatcher( - ParameterizedLinearBeforeChildDispatcher - ): + ParameterizedLinearBeforeChildDispatcher +): @staticmethod def _before_linear(visitor, child): return True, None @@ -419,9 +416,7 @@ def finalizeResult(self, result): return ans if mult == 1: linear_zeros = [ - (vid, coef) - for vid, coef in ans.linear.items() - if is_zero(coef) + (vid, coef) for vid, coef in ans.linear.items() if is_zero(coef) ] for vid, coef in linear_zeros: del ans.linear[vid] From 9b6f929a3734c53644841d50abe232e35148d9db Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 9 Jul 2024 17:45:45 -0400 Subject: [PATCH 070/220] Account for case where quadratic is None --- pyomo/repn/parameterized_quadratic.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index c25071dd806..599add407ef 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -421,13 +421,14 @@ def finalizeResult(self, result): for vid, coef in linear_zeros: del ans.linear[vid] - 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] + 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 From 8892c852f36e26cea83b7110a630f5190379c6c1 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 10 Jul 2024 15:34:19 -0600 Subject: [PATCH 071/220] replace vTBD with current dev version --- pyomo/contrib/pynumero/interfaces/cyipopt_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py index 77c5f5db2fa..0fdc4cfe19a 100644 --- a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py @@ -485,7 +485,7 @@ def intermediate( """ if self._intermediate_callback is not None: if self._use_13arg_callback: - # This is the callback signature expected as of Pyomo vTBD + # This is the callback signature expected as of Pyomo 6.7.4.dev0 return self._intermediate_callback( self._nlp, self, @@ -502,7 +502,7 @@ def intermediate( ls_trials, ) else: - # This is the callback signature expected pre-Pyomo vTBD and + # This is the callback signature expected pre-Pyomo 6.7.4.dev0 and # is supported for backwards compatibility. return self._intermediate_callback( self._nlp, From 2b8f4e09928d294f267b97cc31f0f182d9ba80d5 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 10 Jul 2024 15:35:35 -0600 Subject: [PATCH 072/220] spaces in error message --- pyomo/contrib/pynumero/interfaces/cyipopt_interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py index 0fdc4cfe19a..183249b3793 100644 --- a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py @@ -337,9 +337,9 @@ def __init__(self, nlp, intermediate_callback=None, halt_on_evaluation_error=Non 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." + "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): From 142341f32a5851c5f3c9893bb165c2e71ebd99be Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Wed, 10 Jul 2024 15:49:37 -0600 Subject: [PATCH 073/220] add comment attempting to explain myself --- .../contrib/pynumero/interfaces/cyipopt_interface.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py index 183249b3793..26b43c37d1c 100644 --- a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py @@ -310,6 +310,17 @@ def __init__(self, nlp, intermediate_callback=None, halt_on_evaluation_error=Non # cyipopt.Problem.__init__ super(CyIpoptNLP, self).__init__() + # Pre-Pyomo 6.7.4.dev0, 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 + # if the user asked for variable-length *args, we only pass the Problem as + # an argument if their callback asks for exactly 13 arguments. + # 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) From 73e1e9f9e7a8d308e998badbdeff7edd160e2c01 Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 12 Jul 2024 09:51:44 -0400 Subject: [PATCH 074/220] Account for case where quadratic is None --- pyomo/repn/parameterized_quadratic.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 599add407ef..7528b438d20 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -391,14 +391,15 @@ def _factor_multiplier_into_quadratic_terms(self, ans, mult): del linear[vid] quadratic = ans.quadratic - 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 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 From c6ddbc97ee5a08b465c875f86f6b6f09e7734084 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Mon, 15 Jul 2024 10:33:34 -0600 Subject: [PATCH 075/220] allow None to override variable value, but log warning --- pyomo/core/plugins/transform/scaling.py | 14 +++++++++++--- pyomo/core/tests/transform/test_scaling.py | 6 +++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 654903773bd..4c427e72b92 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." @@ -313,9 +316,14 @@ def propagate_solution(self, scaled_model, original_model): original_v = original_model.find_component(original_v_path) for k in scaled_v: - if scaled_v[k].value is not None: - # NOTE: if the variable is set to None in the scaled model, - # we don't attempt to change its value in the original model + 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, diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index e354916c309..cb31aaa33ec 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -703,9 +703,9 @@ def test_propagate_solution_uninitialized_variable(self): scaled_model, m ) self.assertAlmostEqual(m.x[1].value, 2.0, delta=1e-8) - # Note that because x[2] was None in the scaled model, its value is unchanged - # (and has not been overridden and set to None). - self.assertEqual(m.x[2].value, 1.0) + # Note that value of x[2] in original model *has* been overriddeen to None. + # In this case, a warning has been raised. + self.assertIs(m.x[2].value, None) if __name__ == "__main__": From 93e64f813fe17ac5bd5896ff0ec25ba2ebb38b16 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 16 Jul 2024 16:17:42 -0600 Subject: [PATCH 076/220] Tweak variable names and some comments --- pyomo/repn/parameterized_quadratic.py | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 7528b438d20..3694bbde0df 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -236,9 +236,8 @@ def _handle_product_linear_linear(visitor, node, arg1, arg2): arg1.linear = {} elif not is_equal_to(arg2.constant, 1): c = arg2.constant - _linear = arg1.linear - for vid, coef in _linear.items(): - _linear[vid] = c * coef + for vid, coef in arg1.linear.items(): + arg1.linear[vid] = c * coef if not is_zero(arg1.constant): _merge_dict(arg1.linear, arg1.constant, arg2.linear) @@ -258,33 +257,35 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): ans.nonlinear = to_expression(visitor, arg1) * to_expression(visitor, arg2) return _GENERAL, ans - # We are multiplying (A + Bx + Cx^2 + D(x)) * (A + Bx + Cx^2 + Dx)) + # 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 - # x1.const * x2.const [AA] + + # constant term [A1A2] # TODO: what if either constant is NaN? if is_zero(x1.constant) or is_zero(x2.constant): ans.constant = 0 else: ans.constant = x1.constant * x2.constant + # linear & quadratic terms if not is_zero(x2.constant): - # [BA], [CA] - c = x2.constant - if is_equal_to(c, 1): + # [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: c * coef for vid, coef in x1.linear.items()} + ans.linear = {vid: x2_c * coef for vid, coef in x1.linear.items()} if x1.quadratic: - ans.quadratic = {k: c * coef for k, coef in x1.quadratic.items()} + ans.quadratic = {k: x2_c * coef for k, coef in x1.quadratic.items()} if not is_zero(x1.constant): - # [AB] + # [A1B2] _merge_dict(ans.linear, x1.constant, x2.linear) - # [AC] + # [A1C2] if x2.quadratic: if ans.quadratic: _merge_dict(ans.quadratic, x1.constant, x2.quadratic) @@ -293,14 +294,16 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): else: c = x1.constant ans.quadratic = {k: c * coef for k, coef in x2.quadratic.items()} - # [BB] + # [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 - # [DA] + [DB] + [DC] + [DD] + + # nonlinear portion + # [D1A2] + [D1B2] + [D1C2] + [D1D2] ans.nonlinear = 0 if x1.nonlinear is not None: ans.nonlinear += x1.nonlinear * x2.to_expression(visitor) From 06ab631ec0b5d877fee983d32297093b8bdf21df Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 16 Jul 2024 16:27:43 -0600 Subject: [PATCH 077/220] Tweak variable names and some comments --- pyomo/repn/parameterized_quadratic.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 3694bbde0df..ac9e9247586 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -434,19 +434,26 @@ def finalizeResult(self, result): 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 - if ans.constant != ans.constant or any( - c != c for c in ans.linear.values() - ): + # 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, and nonlinear term + # linear coefficients, quadratic coefficients, + # and nonlinear term self._factor_multiplier_into_quadratic_terms(ans, mult) return ans From bb0199429c6f7909105b9923ce49981960a6a526 Mon Sep 17 00:00:00 2001 From: jasherma Date: Tue, 16 Jul 2024 18:28:10 -0600 Subject: [PATCH 078/220] Remove unused `beforeChild` dispatcher --- pyomo/repn/parameterized_quadratic.py | 30 --------------------------- 1 file changed, 30 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index ac9e9247586..356fc10c3f8 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -185,36 +185,6 @@ def append(self, other): self.nonlinear += nl -class ParameterizedQuadraticBeforeChildDispatcher( - ParameterizedLinearBeforeChildDispatcher -): - @staticmethod - def _before_linear(visitor, child): - return True, None - - @staticmethod - def _before_var(visitor, child): - _id = id(child) - if _id not in visitor.var_map: - if child.fixed: - return False, (_CONSTANT, visitor.check_constant(child.value, child)) - if child in visitor.wrt: - # pseudo-constant - # We aren't treating this Var as a Var for the purposes of this walker - return False, (_FIXED, child) - # This is a normal situation - ParameterizedLinearBeforeChildDispatcher._record_var(visitor, child) - ans = visitor.Result() - ans.linear[_id] = 1 - return False, (ExprType.LINEAR, ans) - - @staticmethod - def _before_param(visitor, child): - ans = visitor.Result() - ans.constant = child - return False, (_CONSTANT, ans) - - def is_zero(obj): """Return true if expression/constant is zero, False otherwise.""" return obj.__class__ in native_numeric_types and not obj From 3675202dd93f96a272bda8932a34e1da703644e8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 10:26:12 -0600 Subject: [PATCH 079/220] Account for zeros more carefully --- pyomo/repn/parameterized_quadratic.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 356fc10c3f8..af1c4de78ab 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -212,11 +212,7 @@ def _handle_product_linear_linear(visitor, node, arg1, arg2): _merge_dict(arg1.linear, arg1.constant, arg2.linear) # Finally, the constant and multipliers - # TODO: what if arg1.constant or arg2.constant is nan? - if is_zero(arg1.constant) or is_zero(arg2.constant): - arg1.constant = 0 - else: - arg1.constant *= arg2.constant + arg1.constant *= arg2.constant arg1.multiplier *= arg2.multiplier return _QUADRATIC, arg1 @@ -234,14 +230,13 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.multiplier = x2.multiplier = 1 # constant term [A1A2] - # TODO: what if either constant is NaN? - if is_zero(x1.constant) or is_zero(x2.constant): + if not x1.constant and not x2.constant: ans.constant = 0 else: ans.constant = x1.constant * x2.constant # linear & quadratic terms - if not is_zero(x2.constant): + if x2.constant: # [B1A2], [C1A2] x2_c = x2.constant if is_equal_to(x2_c, 1): @@ -252,7 +247,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): 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): + if x1.constant: # [A1B2] _merge_dict(ans.linear, x1.constant, x2.linear) # [A1C2] @@ -293,7 +288,8 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.linear = x1_lin ans.nonlinear += x1.to_expression(visitor) * x2.to_expression(visitor) # [AD] - if not is_zero(x1_c) and x2.nonlinear is not None: + if x1_c and x2.nonlinear is not None: + # TODO: what if nonlinear contains nan? ans.nonlinear += x1_c * x2.nonlinear return _GENERAL, ans From 0cf9c7ce88a3909397ef43f1657eadc366532588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Wed, 17 Jul 2024 18:38:43 +0200 Subject: [PATCH 080/220] Applied black. --- pyomo/core/base/set.py | 3 +- pyomo/core/tests/unit/test_set.py | 1 + pyomo/core/tests/unit/test_sets.py | 56 ++++++++++++++++++++---------- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 33737b130e8..c4a1866f13c 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -506,6 +506,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 @@ -1420,7 +1421,7 @@ def add(self, *values): # _value is not a tuple: no need to unpack it for the method arguments' tuple flag = self._validate(_block, (_value, self._index)) else: - # non-indexed set: only the tentative member is given + # non-indexed set: only the tentative member is given flag = self._validate(_block, _value) except: logger.error( diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 70dfeb26f74..e920493fcb9 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4349,6 +4349,7 @@ def _validate_I(model, i, j): # validot when it is called for the index. def _validate_J(model, i, j, index): return _validate_I(model, i, j) + m.J = Set([(0, 0), (2, 2)], validate=_validate_J) output = StringIO() with LoggingIntercept(output, 'pyomo.core'): diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 7595e1a2fe7..5167d55defc 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2816,13 +2816,15 @@ def test_validation2(self): self.fail("fail test_within2") else: pass - + 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.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 @@ -2831,13 +2833,15 @@ def test_validation3_pass(self): 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.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 @@ -2851,32 +2855,36 @@ def test_validation3_fail(self): except ValueError: error_raised = True assert error_raised - + 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.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], + 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.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], + validate=lambda model, x, y, i: (x, y) in model.A[i], ) error_raised = False try: @@ -2884,15 +2892,19 @@ def test_validation4_fail(self): except ValueError: error_raised = True assert error_raised - + 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)]}) + 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, @@ -2900,15 +2912,19 @@ def validate_B(m, e1, e2, i): 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)]}) + 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, @@ -2958,7 +2974,9 @@ def tmp_init(model, i): self.model.n = Param(initialize=5) self.model.Z = Set(initialize=['A']) self.model.A = Set( - self.model.Z, initialize=tmp_init, validate=lambda model, x, i: x in Integers + self.model.Z, + initialize=tmp_init, + validate=lambda model, x, i: x in Integers, ) try: self.instance = self.model.create_instance() From bc54e01ef87106b9e2c578e1ff66c041d4322b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Wed, 17 Jul 2024 19:23:13 +0200 Subject: [PATCH 081/220] Revised test statements. --- pyomo/core/tests/unit/test_sets.py | 185 +++++++---------------------- 1 file changed, 45 insertions(+), 140 deletions(-) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index 5167d55defc..e557a9c3487 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2396,24 +2396,19 @@ 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 +2417,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 +2488,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 +2499,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 +2510,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 +2526,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 +2537,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 +2567,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,13 +2583,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): def setUp(self): @@ -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, i: 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,11 @@ def test_validation2(self): OUTPUT.write("data; set Z := A C; set A[A] := 1 3 5 5.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, validate=lambda model, x, i: x < 6) - 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_validation3_pass(self): # @@ -2849,12 +2782,8 @@ def test_validation3_fail(self): 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]) - error_raised = False - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance(currdir + "setsAB.dat") - except ValueError: - error_raised = True - assert error_raised def test_validation4_pass(self): # @@ -2886,12 +2815,8 @@ def test_validation4_fail(self): initialize={'A': [(1, 2), (3, 4), (5, 6)]}, validate=lambda model, x, y, i: (x, y) in model.A[i], ) - error_raised = False - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - except ValueError: - error_raised = True - assert error_raised def test_validation5_pass(self): # @@ -2931,12 +2856,8 @@ def validate_B(m, e1, e2, i): initialize={'A': [(1, 2), (3, 4), (5, 6)]}, validate=validate_B, ) - error_raised = False - try: + with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - except ValueError: - error_raised = True - assert error_raised def test_other1(self): self.model.Z = Set(initialize=['A']) @@ -2945,24 +2866,16 @@ def test_other1(self): initialize={'A': [1, 2, 3, 'A']}, validate=lambda model, x, i: 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): @@ -2978,12 +2891,8 @@ def tmp_init(model, i): initialize=tmp_init, validate=lambda model, x, i: 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): @@ -2996,12 +2905,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): From 080cddd4550024ca18c032201575f590639d41d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Wed, 17 Jul 2024 19:33:58 +0200 Subject: [PATCH 082/220] Applied black again. --- pyomo/core/tests/unit/test_sets.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index e557a9c3487..bd168c7c279 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2399,14 +2399,13 @@ def test_dimen1(self): 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() - + self.model.A = Set(initialize=[(1, 2), (2, 3), (3, 4)], dimen=2) self.instance = self.model.create_instance() # with self.assertRaisesRegex( - ValueError, - ".*has dimension 2 and is not valid for " - ): + 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() @@ -2419,17 +2418,17 @@ def f(model): # 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() def test_dimen2(self): 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() - + self.model.A = Set(dimen=None, initialize=[1, 2, (3, 4)]) self.instance = self.model.create_instance() @@ -2586,6 +2585,7 @@ def tmp_init(model): with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance(currdir + "setA.dat") + class TestSetArgs2(PyomoModel): def setUp(self): # @@ -2630,7 +2630,7 @@ def test_dimen(self): self.instance = self.model.create_instance() 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 ) @@ -2868,7 +2868,7 @@ def test_other1(self): ) with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() - + def test_other2(self): self.model.Z = Set(initialize=['A']) self.model.A = Set( From 56f22d6154489679c94da9c4e353b0a3733ef954 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 13:43:53 -0600 Subject: [PATCH 083/220] Fix zero coefficient checks --- pyomo/repn/parameterized_quadratic.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index af1c4de78ab..f8c6aacfc6d 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -230,13 +230,13 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.multiplier = x2.multiplier = 1 # constant term [A1A2] - if not x1.constant and not x2.constant: + if is_zero(x1.constant) and is_zero(x2.constant): ans.constant = 0 else: ans.constant = x1.constant * x2.constant # linear & quadratic terms - if x2.constant: + if not is_zero(x2.constant): # [B1A2], [C1A2] x2_c = x2.constant if is_equal_to(x2_c, 1): @@ -247,7 +247,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): 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 x1.constant: + if not is_zero(x1.constant): # [A1B2] _merge_dict(ans.linear, x1.constant, x2.linear) # [A1C2] @@ -288,7 +288,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.linear = x1_lin ans.nonlinear += x1.to_expression(visitor) * x2.to_expression(visitor) # [AD] - if x1_c and x2.nonlinear is not None: + 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 From 8767c8b8bb4cdb79f473f24233443affbc570ba8 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 13:53:35 -0600 Subject: [PATCH 084/220] Implement `ParameterizedQuadraticRepn.__repn__` --- pyomo/repn/parameterized_quadratic.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index f8c6aacfc6d..2053b45bb74 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -74,6 +74,9 @@ def __str__(self): f"nonlinear={self.nonlinear})" ) + def __repr__(self): + return str(self) + def walker_exitNode(self): if self.nonlinear is not None: return _GENERAL, self From 57203c62187820637fe3514caf02dc9b55b4f5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Wed, 17 Jul 2024 22:01:35 +0200 Subject: [PATCH 085/220] Nothing. --- pyomo/core/tests/unit/test_sets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index bd168c7c279..d8f0a7ee4de 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2908,7 +2908,6 @@ def tmp_init(model, i): with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance() - class TestMisc(PyomoModel): def setUp(self): # From 7fdc2b5db3023c87dc5aa0869e9f43ade21e1007 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 14:15:28 -0600 Subject: [PATCH 086/220] Add comprehensive test suite --- .../tests/test_parameterized_quadratic.py | 1288 +++++++++++++++++ 1 file changed, 1288 insertions(+) create mode 100644 pyomo/repn/tests/test_parameterized_quadratic.py diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py new file mode 100644 index 00000000000..9480070d69b --- /dev/null +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -0,0 +1,1288 @@ +# ___________________________________________________________________________ +# +# 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), + ) + + 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_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_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) From bee1f566b6641c5f28b8799d65cf6385ede5f8c2 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 14:16:17 -0600 Subject: [PATCH 087/220] Apply black to new testing module --- .../tests/test_parameterized_quadratic.py | 214 +++++++----------- 1 file changed, 85 insertions(+), 129 deletions(-) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 9480070d69b..50d76771172 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -263,7 +263,7 @@ def test_quadratic(self): 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 + self, repn.to_expression(visitor), 4 * m.x**2 + 3 * m.x + 2 ) def test_product_quadratic_quadratic(self): @@ -290,7 +290,7 @@ def test_product_quadratic_quadratic(self): 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 + self, repn.to_expression(visitor), NL + 52 * m.x**2 + 27 * m.x + 10 ) def test_product_quadratic_quadratic_2(self): @@ -336,10 +336,7 @@ def test_product_linear_linear(self): 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 - ), + (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): @@ -354,9 +351,7 @@ def test_product_linear_linear_wrt_y(self): 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) - ) + 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 @@ -367,10 +362,10 @@ def test_product_linear_linear_wrt_y(self): self, repn.to_expression(visitor), ( - 10 * m.x ** 2 + 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): @@ -395,7 +390,7 @@ def test_product_linear_linear_const_0(self): 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) + 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): @@ -417,15 +412,15 @@ def test_product_linear_quadratic(self): {(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)]) + 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 + 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 @@ -447,23 +442,19 @@ def test_product_linear_quadratic_wrt_x(self): 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, 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, 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 + (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) + + 3 * m.x * (5 + 3 * m.x), ) def test_product_nonlinear_var_expand_false(self): @@ -548,7 +539,7 @@ def test_product_nonlinear_var_2_expand_true(self): 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 + self, repn.to_expression(visitor), m.x**2 + m.x * m.y + NL + 2 * m.x ) def test_zero_elimination(self): @@ -649,7 +640,7 @@ def test_square_linear(self): 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 + 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): @@ -675,7 +666,8 @@ def test_square_linear_wrt_y(self): self, repn.to_expression(visitor), ( - 9 * m.x ** 2 + ((1 + 4 * m.y) * 3 + (1 + 4 * m.y) * 3) * m.x + 9 * m.x**2 + + ((1 + 4 * m.y) * 3 + (1 + 4 * m.y) * 3) * m.x + ((1 + 4 * m.y) * (1 + 4 * m.y)) ), ) @@ -702,12 +694,12 @@ def test_square_linear_float(self): 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 + 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) + expr = (1 + 3 * m.x + 4 * log(m.x) * m.y + 4 * m.y**2) / (2 * m.x) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) @@ -722,17 +714,13 @@ def test_division_quadratic_nonlinear(self): 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, + (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) + 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]) @@ -749,22 +737,20 @@ def test_division_quadratic_nonlinear_wrt_x(self): ) self.assertEqual(len(repn.quadratic), 1) assertExpressionsEqual( - self, - repn.quadratic[id(m.y), id(m.y)], - (1 / (2 * m.x)) * 4, + 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) * m.y**2 + ((1 / (2 * m.x)) * (4 * log(m.x))) * m.y - + (1 + 3 * m.x) * (1 / (2 * m.x)) + + (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) + expr = 5 * (2 * m.x + m.x**2) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, []) @@ -778,13 +764,11 @@ def test_constant_expr_multiplier(self): 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 - ) + 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) + 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]) @@ -803,12 +787,12 @@ def test_0_mult_nan_linear_coeff(self): assertExpressionsEqual( self, repn.to_expression(visitor), - 0 * m.y * m.x ** 2 + (log(m.x)) * 0 + float("nan") * m.x + 0 * m.y + 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) + expr = 0 * (m.x + m.y + log(m.x) + float("nan") * m.x**2 + 2 * m.x) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) @@ -826,21 +810,20 @@ def test_0_mult_nan_quadratic_coeff(self): assertExpressionsEqual( self, repn.to_expression(visitor), - float("nan") * m.x ** 2 + (log(m.x)) * 0 + 0 * m.y + 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 + 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) - ) + 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}) @@ -850,31 +833,26 @@ def test_square_quadratic(self): 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, - }, + {(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 + 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 + 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]) - ) + 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) @@ -886,9 +864,7 @@ def test_square_quadratic_wrt_y(self): 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, repn.linear[id(m.x)], (1 + m.y) * (1 + m.y) + (1 + m.y) * (1 + m.y) ) self.assertEqual(len(repn.quadratic), 1) assertExpressionsEqual( @@ -898,9 +874,7 @@ def test_square_quadratic_wrt_y(self): ) assertExpressionsEqual(self, repn.nonlinear, NL) assertExpressionsEqual( - self, - repn.to_expression(visitor), - NL + QC * m.x ** 2 + LC * m.x + CON, + self, repn.to_expression(visitor), NL + QC * m.x**2 + LC * m.x + CON ) def test_cube_linear(self): @@ -926,7 +900,7 @@ 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) + expr = (1 + log(m.x)) * (log(m.x) + m.y**2) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.z]) @@ -940,19 +914,17 @@ def test_nonlinear_product_with_constant_terms(self): 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), + 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, + 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 + 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]) @@ -962,19 +934,15 @@ def test_finalize_simplify_coefficients(self): 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) + 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, - ) + 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) + 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]) @@ -989,14 +957,14 @@ def test_factor_multiplier_simplify_coefficients(self): 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.constant, (2 * m.y**2) * 2) assertExpressionsEqual( - self, repn.to_expression(visitor), -2 * m.z + (2 * m.y ** 2) * 2 + 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))) + expr = 2 * (1 + log(m.x)) + (2 * (m.y + m.y**2 + log(m.x))) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) @@ -1006,22 +974,14 @@ def test_sum_nonlinear_custom_multiplier(self): 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), - ) + 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.nonlinear, 2 * log(m.x) + 2 * log(m.x)) def test_negation_linear(self): m = build_test_model() - expr = - (2 + 3 * m.x + 5 * m.x * m.y) + expr = -(2 + 3 * m.x + 5 * m.x * m.y) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y]) @@ -1043,9 +1003,13 @@ def test_negation_linear(self): 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 + 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) ) @@ -1067,10 +1031,10 @@ def test_negation_nonlinear_wrt_y_fix_z(self): assertExpressionsEqual( self, repn.to_expression(visitor), - + (-5 * m.y) * (m.x ** 2) + +(-5 * m.y) * (m.x**2) + 2 * log(m.x) * -1 + (-3) * m.x - + (2 + 8 * m.y + 4) * (-1) + + (2 + 8 * m.y + 4) * (-1), ) def test_negation_product_linear_linear(self): @@ -1095,15 +1059,13 @@ def test_negation_product_linear_linear(self): (-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, - ) + 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 + -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) ), @@ -1131,7 +1093,7 @@ def test_sum_bilinear_terms_commute_product(self): def test_sum_nonlinear(self): m = build_test_model() - expr = (1 + log(m.x)) + (m.x + m.y + m.y ** 2 + log(m.x)) + expr = (1 + log(m.x)) + (m.x + m.y + m.y**2 + log(m.x)) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) @@ -1143,14 +1105,14 @@ def test_sum_nonlinear(self): 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) + 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, + log(m.x) + log(m.x) + m.x + (1 + m.y) + m.y**2, ) def test_product_linear_linear_0_nan(self): @@ -1172,17 +1134,14 @@ def test_product_linear_linear_0_nan(self): self.assertIsNone(repn.quadratic) self.assertIsNone(repn.nonlinear) assertExpressionsEqual( - self, - repn.to_expression(visitor), - float("nan") * m.x + float("nan"), + 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) + expr = (float("nan") + float("nan") * m.x + float("nan") * m.x**2) * ( + m.p + 0 * m.x + 0 * m.x**2 ) cfg = VisitorConfig() @@ -1202,15 +1161,14 @@ def test_product_quadratic_quadratic_nan_0(self): assertExpressionsEqual( self, repn.to_expression(visitor), - float("nan") * m.x ** 2 + float("nan") * m.x + float("nan"), + 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) + expr = (m.p + 0 * m.x + 0 * m.x**2) * ( + float("nan") + float("nan") * m.x + float("nan") * m.x**2 ) cfg = VisitorConfig() @@ -1230,14 +1188,14 @@ def test_product_quadratic_quadratic_0_nan(self): assertExpressionsEqual( self, repn.to_expression(visitor), - float("nan") * m.x ** 2 + float("nan") * m.x + float("nan"), + 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) + 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) ) @@ -1253,22 +1211,20 @@ def test_nary_sum_products(self): 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), + 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 + (m.z - 1) * m.x**2 + + (m.y**4 + 0.8 + 5 * m.y * m.z * (-1) + (m.y + 2)) * m.x, ) def test_repr_parameterized_quadratic_repn(self): m = build_test_model() - expr = 2 + m.x + m.x ** 2 + log(m.x) + expr = 2 + m.x + m.x**2 + log(m.x) cfg = VisitorConfig() visitor = ParameterizedQuadraticRepnVisitor(*cfg, [m.y, m.z]) From cf7af78b147dafb805ea4d19390847113e5d3e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Wed, 17 Jul 2024 22:42:14 +0200 Subject: [PATCH 088/220] Applied black yet again. --- pyomo/core/tests/unit/test_sets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index d8f0a7ee4de..bd168c7c279 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2908,6 +2908,7 @@ def tmp_init(model, i): with self.assertRaisesRegex(ValueError, ".*Cannot add value "): self.instance = self.model.create_instance() + class TestMisc(PyomoModel): def setUp(self): # From 330cac8e13c5768784f1d134a6eb3b55ef63f787 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 14:48:40 -0600 Subject: [PATCH 089/220] Fix comments --- pyomo/repn/parameterized_quadratic.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 2053b45bb74..dcd6a9e5364 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -281,16 +281,16 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.constant = 0 x1_lin = x1.linear x1.linear = {} - # [CB] + [CC] + [CD] + # [C1B2] + [C1C2] + [C1D2] if x1.quadratic: ans.nonlinear += x1.to_expression(visitor) * x2.to_expression(visitor) x1.quadratic = None x2.linear = {} - # [BC] + [BD] + # [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) - # [AD] + # [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 From 21df3d48a43ac802e55f7f88fd27455ffbfa22d3 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 15:11:02 -0600 Subject: [PATCH 090/220] Test ternary product of linear expressions --- .../tests/test_parameterized_quadratic.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 50d76771172..d96cfe112c5 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1222,6 +1222,38 @@ def test_nary_sum_products(self): + (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_repr_parameterized_quadratic_repn(self): m = build_test_model() expr = 2 + m.x + m.x**2 + log(m.x) From d82b9f373e8ca6f305bcd43f7c8f34154c15ed91 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 15:15:39 -0600 Subject: [PATCH 091/220] Add test for noninteger power of linear expression --- .../tests/test_parameterized_quadratic.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index d96cfe112c5..ebe3e21d502 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1254,6 +1254,26 @@ def test_ternary_product_linear(self): ), ) + 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_repr_parameterized_quadratic_repn(self): m = build_test_model() expr = 2 + m.x + m.x**2 + log(m.x) From e4fca5776bf8a8cbaefe1876c00245f71b0e8b23 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 15:24:07 -0600 Subject: [PATCH 092/220] Test linear expression raised to fixed integer power --- .../tests/test_parameterized_quadratic.py | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index ebe3e21d502..edc1d7729a2 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1274,6 +1274,56 @@ def test_noninteger_pow_linear(self): 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) From 7cca746a8884bd91f53a42c5782224d36f501d5e Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 15:28:01 -0600 Subject: [PATCH 093/220] Apply black --- pyomo/repn/tests/test_parameterized_quadratic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index edc1d7729a2..b0173416cd5 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1318,10 +1318,10 @@ def test_pow_integer_fixed_var(self): self, repn.to_expression(visitor), ( - 4 * m.x ** 2 + 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): From ada07b088ac512c2b2601b059229767f58264fde Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 17:37:30 -0600 Subject: [PATCH 094/220] Complete test of nonlinear sum --- pyomo/repn/tests/test_parameterized_quadratic.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index b0173416cd5..2f89720dfe6 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -978,6 +978,11 @@ def test_sum_nonlinear_custom_multiplier(self): 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() From dbbb0f109e7592dafb5de9a7020d47b1d93f0baf Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 17:40:22 -0600 Subject: [PATCH 095/220] Apply black --- pyomo/repn/tests/test_parameterized_quadratic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 2f89720dfe6..0fedd1b3260 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -981,7 +981,7 @@ def test_sum_nonlinear_custom_multiplier(self): assertExpressionsEqual( self, repn.to_expression(visitor), - 2 * log(m.x) + 2 * log(m.x) + 2 + 2 * (m.y + m.y ** 2) + 2 * log(m.x) + 2 * log(m.x) + 2 + 2 * (m.y + m.y**2), ) def test_negation_linear(self): From 3eee189135a20ff03d26c08d57cc6c4110ce61b2 Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 17:53:20 -0600 Subject: [PATCH 096/220] Test simple expanded square monomial --- .../tests/test_parameterized_quadratic.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 0fedd1b3260..42fc58b5b6c 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1076,6 +1076,28 @@ def test_negation_product_linear_linear(self): ), ) + 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 From 416287b39b6da4c6830042eb0d6219f250abe68d Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 17 Jul 2024 17:53:51 -0600 Subject: [PATCH 097/220] Blacken new monomial square test --- pyomo/repn/tests/test_parameterized_quadratic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 42fc58b5b6c..a9e1aebd9b2 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1095,7 +1095,7 @@ def test_expanded_monomial_square_term(self): 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]) + self, repn.to_expression(visitor), SumExpression([m.x**2]) ) def test_sum_bilinear_terms_commute_product(self): From 48118832f1e286b3be40c1347807d629c340384e Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Jul 2024 11:07:15 -0600 Subject: [PATCH 098/220] whoops, fixing typo from last commit --- pyomo/contrib/piecewise/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index e5a0a541657..67596a709e3 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -35,6 +35,7 @@ ) from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( NonlinearToPWL, +) from pyomo.contrib.piecewise.transform.nested_inner_repn import ( NestedInnerRepresentationGDPTransformation, ) From 02cef2d00b68b07be4bfae9090b07745363a6070 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Jul 2024 11:23:55 -0600 Subject: [PATCH 099/220] Putting common part of log x tests into helper function for tests --- .../piecewise/tests/test_nonlinear_to_pwl.py | 86 ++++++++++++------- 1 file changed, 55 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 020edee3924..d0f5acc2ff1 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -37,35 +37,14 @@ def make_model(self): return m - def check_pw_linear_log_x(self, m, points): - x1 = points[0][0] - x2 = points - - def test_log_constraint_uniform_grid(self): - m = self.make_model() - + def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): 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,)] + points = [(x1,), (x2,), (x3,)] self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) self.assertEqual(pwlf._points, points) self.assertEqual(len(pwlf._linear_functions), 2) - x1 = 1.0009 - x2 = 5.5 assertExpressionsStructurallyEqual( self, pwlf._linear_functions[0](m.x), @@ -73,13 +52,11 @@ def test_log_constraint_uniform_grid(self): (log(x2) - ((log(x2) - log(x1))/(x2 - x1))*x2), places=7 ) - x1 = 5.5 - x2 = 9.9991 assertExpressionsStructurallyEqual( self, pwlf._linear_functions[1](m.x), - ((log(x2) - log(x1))/(x2 - x1))*m.x + - (log(x2) - ((log(x2) - log(x1))/(x2 - x1))*x2), + ((log(x3) - log(x2))/(x3 - x2))*m.x + + (log(x3) - ((log(x3) - log(x2))/(x3 - x2))*x3), places=7 ) @@ -90,6 +67,28 @@ def test_log_constraint_uniform_grid(self): self.assertIsNone(new_cons.ub) self.assertEqual(new_cons.lb, 0.35) self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons) + + 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) def test_log_constraint_random_grid(self): m = self.make_model() @@ -110,9 +109,34 @@ def test_log_constraint_random_grid(self): 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) + + + def test_log_constraint_lmt_uniform_sample(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.LINEAR_MODEL_TREE_UNIFORM, + ) + + # 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] + set_trace() - points = [(4.370861069626263,), (7.587945476302646,), (9.556428757689245,)] - self.assertEqual(pwlf._simplices, [(0, 1), (1, 2)]) - self.assertEqual(pwlf._points, points) - self.assertEqual(len(pwlf._linear_functions), 2) + + x1 = 4.370861069626263 + x2 = 7.587945476302646 + x3 = 9.556428757689245 + self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) From a619cf68d6523366c9bfd44f9ad02a091be852f1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 18 Jul 2024 14:05:38 -0600 Subject: [PATCH 100/220] Removing hard dependencies --- .../piecewise/tests/test_nonlinear_to_pwl.py | 47 ++++++++++--------- .../piecewise/transform/nonlinear_to_pwl.py | 28 ++++------- 2 files changed, 34 insertions(+), 41 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index d0f5acc2ff1..6b1e3f9f89f 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -116,27 +116,28 @@ def test_log_constraint_random_grid(self): self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) - def test_log_constraint_lmt_uniform_sample(self): - m = self.make_model() + # def test_log_constraint_lmt_uniform_sample(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.LINEAR_MODEL_TREE_UNIFORM, - ) - - # 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] - - set_trace() - - x1 = 4.370861069626263 - x2 = 7.587945476302646 - x3 = 9.556428757689245 - self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + # n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + # n_to_pwl.apply_to( + # m, + # num_points=3, + # domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_UNIFORM, + # ) + + # # 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] + + # set_trace() + + # # TODO + # x1 = 4.370861069626263 + # x2 = 7.587945476302646 + # x3 = 9.556428757689245 + # self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index d819e893d5f..408efdb4b32 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -12,10 +12,7 @@ import enum import itertools -from lineartree import LinearTreeRegressor -import lineartree import logging -import numpy as np from pyomo.environ import ( TransformationFactory, @@ -41,6 +38,8 @@ 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 @@ -54,13 +53,12 @@ from pyomo.network import Port from pyomo.repn.quadratic import QuadraticRepnVisitor -from sklearn.linear_model import LinearRegression -import random -from sklearn.metrics import mean_squared_error -from sklearn.model_selection import train_test_split +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 @@ -117,22 +115,15 @@ def get_points_lmt(points, bounds, func, seed): y_list = [] for point in points: y_list.append(func(*point)) - regr = LinearTreeRegressor( - LinearRegression(), + # ESJ: Do we really need the sklearn dependency to get LinearRegression?? + regr = lineartree.LinearTreeRegressor( + sklearn_lm.LinearRegression(), criterion='mse', max_bins=120, min_samples_leaf=4, max_depth=5, ) - - # Using train_test_split is silly. TODO: remove this and just sample my own - # extra points if I want to estimate the error. - X_train, X_test, y_train, y_test = train_test_split( - x_list, y_list, test_size=0.2, random_state=seed - ) - regr.fit(X_train, y_train) - y_pred = regr.predict(X_test) - error = mean_squared_error(y_test, y_pred) + regr.fit(x_list, y_list) leaves, splits, ths = parse_linear_tree_regressor(regr, bounds) @@ -572,6 +563,7 @@ def _get_bounds_list(self, var_list, parent_component): bounds = [] for v in var_list: if None in v.bounds: + # ESJ TODO: Con is undefined--this is a bug! raise ValueError( "Cannot automatically approximate constraints with unbounded " "variables. Var '%s' appearining in component '%s' is missing " From a94b7a4fa86f41358746a0ef2cc390cbe1754bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= Date: Mon, 22 Jul 2024 22:12:56 +0200 Subject: [PATCH 101/220] Removed unnecessary pass statement. --- pyomo/core/base/set.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 8a6e4767f6a..27dd3b7c51c 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1413,7 +1413,6 @@ def add(self, *values): # differentiate between indexed and non-indexed sets if self._index is not None: # indexed set: the value and the index are given - pass if type(_value) == tuple: # _value is a tuple: unpack it for the method arguments' tuple flag = self._validate(_block, (*_value, self._index)) From 5ccd890d7b449bbe79fe7bf7704a2f90357ea4c5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 23 Jul 2024 15:53:01 -0600 Subject: [PATCH 102/220] Generalizing log tests, adding chaos test that doesn't do anything yet --- pyomo/contrib/piecewise/__init__.py | 4 +- .../piecewise/tests/test_nonlinear_to_pwl.py | 108 +++++++++++++----- .../piecewise/transform/nonlinear_to_pwl.py | 95 ++++++++------- 3 files changed, 137 insertions(+), 70 deletions(-) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 67596a709e3..5fc75dcd091 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -33,9 +33,7 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) -from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( - NonlinearToPWL, -) +from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import NonlinearToPWL from pyomo.contrib.piecewise.transform.nested_inner_repn import ( NestedInnerRepresentationGDPTransformation, ) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 6b1e3f9f89f..3a1b5270aea 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -13,22 +13,15 @@ from pyomo.contrib.piecewise import PiecewiseLinearFunction from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( NonlinearToPWL, - DomainPartitioningMethod -) -from pyomo.core.expr.compare import ( - assertExpressionsStructurallyEqual, -) -from pyomo.environ import ( - ConcreteModel, - Var, - Constraint, - TransformationFactory, - log, + DomainPartitioningMethod, ) +from pyomo.core.expr.compare import assertExpressionsStructurallyEqual +from pyomo.environ import ConcreteModel, Var, Constraint, TransformationFactory, log ## debug from pytest import set_trace + class TestNonlinearToPWL_1D(unittest.TestCase): def make_model(self): m = ConcreteModel() @@ -48,16 +41,16 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): 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 + ((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 + ((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) @@ -67,7 +60,7 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): self.assertIsNone(new_cons.ub) self.assertEqual(new_cons.lb, 0.35) self.assertIs(n_to_pwl.get_src_component(new_cons), m.cons) - + def test_log_constraint_uniform_grid(self): m = self.make_model() @@ -81,18 +74,19 @@ def test_log_constraint_uniform_grid(self): # cons is transformed self.assertFalse(m.cons.active) - pwlf = list(m.component_data_objects(PiecewiseLinearFunction, - descend_into=True)) + 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) 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. @@ -105,8 +99,9 @@ def test_log_constraint_random_grid(self): # cons is transformed self.assertFalse(m.cons.active) - pwlf = list(m.component_data_objects(PiecewiseLinearFunction, - descend_into=True)) + pwlf = list( + m.component_data_objects(PiecewiseLinearFunction, descend_into=True) + ) self.assertEqual(len(pwlf), 1) pwlf = pwlf[0] @@ -114,11 +109,10 @@ def test_log_constraint_random_grid(self): x2 = 7.587945476302646 x3 = 9.556428757689245 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) - # def test_log_constraint_lmt_uniform_sample(self): # m = self.make_model() - + # n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') # n_to_pwl.apply_to( # m, @@ -141,3 +135,65 @@ def test_log_constraint_random_grid(self): # x2 = 7.587945476302646 # x3 = 9.556428757689245 # self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + + +class TestNonlinearToPWLIntegration(unittest.TestCase): + def test_Ali_example(self): + m = ConcreteModel() + m.flow_super_heated_vapor = Var() + m.flow_super_heated_vapor.fix(0.4586949988166174) + m.super_heated_vapor_temperature = Var(bounds=(31, 200), initialize=45) + m.evaporator_condensate_temperature = Var( + bounds=(29, 120.8291392028045), initialize=30 + ) + m.LMTD = Var(bounds=(0, 130.61608989795093), initialize=1) + m.evaporator_condensate_enthalpy = Var( + bounds=(-15836.847, -15510.210751855624), initialize=100 + ) + m.evaporator_condensate_vapor_enthalpy = Var( + bounds=(-13416.64, -13247.674383866839), initialize=100 + ) + m.heat_transfer_coef = Var( + bounds=(1.9936854577372858, 5.995319594088982), initialize=0.1 + ) + m.evaporator_brine_temperature = Var( + bounds=(27, 118.82913920280366), initialize=35 + ) + m.each_evaporator_area = Var() + + m.c = Constraint( + expr=m.each_evaporator_area + == ( + 1.873 + * m.flow_super_heated_vapor + * ( + m.super_heated_vapor_temperature + - m.evaporator_condensate_temperature + ) + / (100 * m.LMTD) + + m.flow_super_heated_vapor + * ( + m.evaporator_condensate_vapor_enthalpy + - m.evaporator_condensate_enthalpy + ) + / ( + m.heat_transfer_coef + * ( + m.evaporator_condensate_temperature + - m.evaporator_brine_temperature + ) + ) + ) + ) + + n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') + n_to_pwl.apply_to( + m, + num_points=3, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + + m.pprint() + + from pyomo.environ import SolverFactory + SolverFactory('gurobi').solve(m, tee=True) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 408efdb4b32..798eaafdddf 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -33,7 +33,7 @@ Block, ExternalFunction, SortComponents, - LogicalConstraint + LogicalConstraint, ) from pyomo.common.autoslots import AutoSlots from pyomo.common.collections import ComponentMap, ComponentSet @@ -45,10 +45,7 @@ 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.contrib.piecewise import PiecewiseLinearExpression, PiecewiseLinearFunction from pyomo.gdp import Disjunct, Disjunction from pyomo.network import Port from pyomo.repn.quadratic import QuadraticRepnVisitor @@ -65,23 +62,28 @@ class DomainPartitioningMethod(enum.IntEnum): LINEAR_MODEL_TREE_UNIFORM = 3 LINEAR_MODEL_TREE_RANDOM = 4 + # This should be safe to use many times; declare it globally _quadratic_repn_visitor = QuadraticRepnVisitor( subexpression_cache={}, var_map={}, var_order={}, sorter=None ) + class _NonlinearToPWLTransformationData(AutoSlots.Mixin): __slots__ = ('transformed_component', 'src_component') def __init__(self): self.transformed_component = ComponentMap() self.src_component = ComponentMap() + + Block.register_private_data_initializer(_NonlinearToPWLTransformationData) + def get_random_point_grid(bounds, n, func, seed=42): # Generate randomized grid of points linspaces = [] - for (lb, ub) in bounds: + for lb, ub in bounds: np.random.seed(seed) linspaces.append(np.random.uniform(lb, ub, n)) return list(itertools.product(*linspaces)) @@ -90,7 +92,7 @@ def get_random_point_grid(bounds, n, func, seed=42): def get_uniform_point_grid(bounds, n, func): # Generate non-randomized grid of points linspaces = [] - for (lb, ub) in bounds: + for lb, ub in bounds: # Issues happen when exactly using the boundary nudge = (ub - lb) * 1e-4 linspaces.append( @@ -137,6 +139,7 @@ def get_points_lmt(points, bounds, func, seed): # here? return bound_point_list + _partition_method_dispatcher = { DomainPartitioningMethod.RANDOM_GRID: get_random_point_grid, DomainPartitioningMethod.UNIFORM_GRID: get_uniform_point_grid, @@ -144,6 +147,7 @@ def get_points_lmt(points, bounds, func, seed): DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM: get_points_lmt_random_sample, } + def get_pwl_function_approximation(func, method, n, bounds): """ Get a piecewise-linear approximation of a function, given: @@ -163,8 +167,10 @@ def get_pwl_function_approximation(func, method, n, bounds): # 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") + logger.debug( + f"Constructing PWLF with {len(points)} points, each of which " + f"are {dim}-dimensional" + ) return PiecewiseLinearFunction(points=points, function=func) @@ -183,7 +189,7 @@ def generate_bound_points(leaves, bounds): for var_bound in leaf['bounds'].values(): lower_corner_list.append(var_bound[0]) upper_corner_list.append(var_bound[1]) - + # Duct tape to fix issues from unknown bugs for pt in [lower_corner_list, upper_corner_list]: for i in range(len(pt)): @@ -228,12 +234,12 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): 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 + # 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 + # 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 @@ -319,6 +325,7 @@ class NonlinearToPWL(Transformation): """ Convert nonlinear constraints and objectives to piecewise-linear approximations. """ + CONFIG = ConfigDict('contrib.piecewise.nonlinear_to_pwl') CONFIG.declare( 'targets', @@ -422,6 +429,7 @@ class NonlinearToPWL(Transformation): points in order to partition the function domain.""", ), ) + def __init__(self): super(Transformation).__init__() self._handlers = { @@ -454,7 +462,7 @@ def _apply_to(self, instance, **kwds): self._transformation_blocks.clear() self._transformation_block_set.clear() - def _apply_to_impl( self, model, **kwds): + def _apply_to_impl(self, model, **kwds): config = self.CONFIG(kwds.pop('options', {})) config.set_value(kwds) @@ -480,23 +488,19 @@ 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' - ) + 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 + 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 @@ -519,8 +523,8 @@ def _transform_constraint(self, cons, config): constraints = cons.values() if cons.is_indexed() else (cons,) for c in constraints: pw_approx = self._approximate_expression( - c.body, c, trans_block, config, - config.approximate_quadratic_constraints) + c.body, c, trans_block, config, config.approximate_quadratic_constraints + ) if pw_approx is None: # Didn't need approximated, nothing to do @@ -531,7 +535,7 @@ def _transform_constraint(self, cons, config): 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() @@ -542,8 +546,12 @@ def _transform_objective(self, objective, config): src_data_dict = objective.parent_block().private_data() for obj in objectives: pw_approx = self._approximate_expression( - obj.expr, obj, trans_block, config, - config.approximate_quadratic_objectives) + obj.expr, + obj, + trans_block, + config, + config.approximate_quadratic_objectives, + ) if pw_approx is None: # Didn't need approximated, nothing to do @@ -551,12 +559,11 @@ def _transform_objective(self, objective, config): new_obj = Objective(expr=pw_approx, sense=obj.sense) trans_block.add_component( - unique_component_name(trans_block, obj.name), - new_obj + 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, parent_component): @@ -567,7 +574,8 @@ def _get_bounds_list(self, var_list, parent_component): raise ValueError( "Cannot automatically approximate constraints with unbounded " "variables. Var '%s' appearining in component '%s' is missing " - "at least one bound" % (con.name, v.name)) + "at least one bound" % (con.name, v.name) + ) else: bounds.append(v.bounds) return bounds @@ -584,15 +592,17 @@ def _needs_approximating(self, expr, approximate_quadratic): return False return True - def _approximate_expression(self, obj, parent_component, trans_block, - config, approximate_quadratic): + def _approximate_expression( + self, obj, parent_component, trans_block, config, approximate_quadratic + ): if not self._needs_approximating(obj, approximate_quadratic): return - + # Additively decompose obj and work on the pieces pwl_func = 0 - for k, expr in enumerate(_additively_decompose_expr(obj) if - config.additively_decompose else (obj,)): + for k, expr in enumerate( + _additively_decompose_expr(obj) if config.additively_decompose else (obj,) + ): # First check is this is a good idea expr_vars = list(identify_variables(expr, include_fixed=False)) orig_values = ComponentMap((v, v.value) for v in expr_vars) @@ -603,7 +613,8 @@ def _approximate_expression(self, obj, parent_component, trans_block, "Not approximating expression for component '%s' as " "it exceeds the maximum dimension of %s. Try increasing " "'max_dimension' or additively separating the expression." - % (parent_component.name, config.max_dimension)) + % (parent_component.name, config.max_dimension) + ) pwl_func += expr continue elif not self._needs_approximating(expr, approximate_quadratic): @@ -616,13 +627,13 @@ def eval_expr(*args): return value(expr) pwlf = get_pwl_function_approximation( - eval_expr, config.domain_partitioning_method, + eval_expr, + config.domain_partitioning_method, config.num_points, - self._get_bounds_list(expr_vars, parent_component) + self._get_bounds_list(expr_vars, parent_component), ) name = unique_component_name( - trans_block, - parent_component.getname(fully_qualified=False) + trans_block, parent_component.getname(fully_qualified=False) ) trans_block.add_component(f"_pwle_{name}_{k}", pwlf) pwl_func += pwlf(*expr_vars) @@ -640,7 +651,8 @@ def get_src_component(self, cons): else: raise ValueError( "It does not appear that '%s' is a transformed Constraint " - "created by the 'nonlinear_to_pwl' transformation." % cons.name) + "created by the 'nonlinear_to_pwl' transformation." % cons.name + ) def get_transformed_component(self, cons): data = cons.parent_block().private_data().transformed_component @@ -649,4 +661,5 @@ def get_transformed_component(self, cons): else: raise ValueError( "It does not appear that '%s' is a Constraint that was " - "transformed by the 'nonlinear_to_pwl' transformation." % cons.name) + "transformed by the 'nonlinear_to_pwl' transformation." % cons.name + ) From 5956843fca7353bb63f5ef0e7cba969f6eb4c5c4 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 24 Jul 2024 16:00:45 -0600 Subject: [PATCH 103/220] Remove unused imports --- .../piecewise/transform/incremental.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 59c9039fdc8..fb761c00ebb 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -9,7 +9,6 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib.piecewise.transform.piecewise_linear_transformation_base import ( PiecewiseLinearTransformationBase, ) @@ -17,26 +16,18 @@ from pyomo.core import ( Constraint, Binary, - NonNegativeIntegers, - Suffix, Var, RangeSet, Param, ) from pyomo.core.base import TransformationFactory -from pyomo.gdp import Disjunct, Disjunction -from pyomo.common.errors import DeveloperError -from pyomo.core.expr.visitor import SimpleExpressionVisitor -from pyomo.core.expr.current import identify_components -from math import ceil, log2 -import logging @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 + 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. @@ -98,8 +89,8 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc 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 + # 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) @@ -141,8 +132,8 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc 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" + # 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 ( From 79369ad1826b3299cf867d9844f8fe7ea9b1745a Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 24 Jul 2024 16:16:33 -0600 Subject: [PATCH 104/220] Add copyright notices and expand comment --- ...generate_ordered_3d_j1_triangulation_data.py | 11 +++++++++++ .../ordered_3d_j1_triangulation_data.py | 17 ++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py index 93a087d883f..58c68051cdb 100644 --- a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# 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 networkx as nx import itertools diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index a4e921f42d2..631b0b3d4ef 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -1,4 +1,19 @@ -# Generated using generate_ordered_3d_j1_triangulation_data.py +# ___________________________________________________________________________ +# +# 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. +# ___________________________________________________________________________ + +# 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. hamiltonian_paths = { (((-1, 0, 0), 1), ((0, -1, 0), 1)): [ ((-1, -1, -1), (1, 2, 3)), From c20e41a6f38393a29b880dfec350a344d5b83125 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Wed, 24 Jul 2024 17:30:06 -0600 Subject: [PATCH 105/220] Change PiecewiseLinearFunction `triangulation` argument and remove override argument --- .../piecewise/piecewise_linear_function.py | 79 +++++++++++-------- .../piecewise/tests/test_incremental.py | 3 +- .../tests/test_piecewise_linear_function.py | 22 ------ 3 files changed, 47 insertions(+), 57 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index 7edba3d41d2..e1374a78668 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -24,7 +24,7 @@ get_ordered_j1_triangulation, Triangulation, ) -from pyomo.core import Any, NonNegativeIntegers, value, Var +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 @@ -235,6 +235,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 @@ -261,8 +274,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', Triangulation.Delaunay) - _triangulation_override_rule_arg = kwargs.pop('override_triangulation', None) + _triangulation_rule_arg = kwargs.pop('triangulation', None) kwargs.setdefault('ctype', PiecewiseLinearFunction) Block.__init__(self, *args, **kwargs) @@ -284,9 +296,6 @@ def __init__(self, *args, **kwargs): self._triangulation_rule = Initializer( _triangulation_rule_arg, treat_sequences_as_mappings=False ) - self._triangulation_override_rule = Initializer( - _triangulation_override_rule_arg, treat_sequences_as_mappings=False - ) def _get_dimension_from_points(self, points): if len(points) < 1: @@ -305,7 +314,13 @@ def _get_dimension_from_points(self, points): def _construct_simplices_from_multivariate_points( self, obj, parent, points, dimension ): - tri = self._triangulation_rule(parent, obj._index) + 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) @@ -321,7 +336,7 @@ def _construct_simplices_from_multivariate_points( obj._triangulation = tri else: raise ValueError( - "Unrecognized triangulation specified for '%s': %s" % (obj, tri) + "Invalid or unrecognized triangulation specified for '%s': %s" % (obj, tri) ) # Get the points for the triangulation because they might not all be @@ -341,7 +356,7 @@ def _construct_simplices_from_multivariate_points( # 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 tri != Triangulation.Delaunay: + 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 @@ -365,6 +380,24 @@ def _construct_simplices_from_multivariate_points( "%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 = [] @@ -401,7 +434,7 @@ def _construct_from_univariate_function_and_segments( ): # We can trust they are nicely ordered if we made them, otherwise anything goes. if segments_are_user_defined: - obj._triangulation = Triangulation.Unknown + self._check_and_set_triangulation_from_user(parent, obj) else: obj._triangulation = Triangulation.AssumeValid @@ -439,9 +472,9 @@ def _construct_from_function_and_simplices( ) # If we triangulated, then this tag was already set. If they provided it, - # then it should be unknown. + # then check their arguments and set. if simplices_are_user_defined: - obj._triangulation = Triangulation.Unknown + self._check_and_set_triangulation_from_user(parent, obj) # evaluate the function at each of the points and form the homogeneous # system of equations @@ -494,7 +527,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)] - obj._triangulation = Triangulation.Unknown + self._check_and_set_triangulation_from_user(parent, obj) return obj @_define_handler(_handlers, False, False, False, False, True) @@ -543,17 +576,6 @@ def _getitem_when_not_present(self, index): elif self._func is not None: nonlinear_function = self._func - # If the user asked for a specific triangulation but passed simplices, - # warn them that we're going to use the simplices and ignore the triangulation. - if self._simplices_rule is not None: - tri = self._triangulation_rule(parent, obj._index) - if tri not in (None, Triangulation.Delaunay): - logger.warn( - f"Non-default triangulation request {tri} was ignored because the " - "simplices were provided. If you meant to override the tag, use " - "`override_triangulation` instead." - ) - handler = self._handlers.get( ( nonlinear_function is not None, @@ -575,15 +597,6 @@ def _getitem_when_not_present(self, index): ) obj = handler(self, obj, parent, nonlinear_function) - # If the user wanted to override the triangulation tag, do it after we - # are finished setting it ourselves. - if self._triangulation_override_rule is not None: - triangulation_override = self._triangulation_override_rule( - parent, obj._index - ) - if triangulation_override is not None: - obj._triangulation = triangulation_override - return obj diff --git a/pyomo/contrib/piecewise/tests/test_incremental.py b/pyomo/contrib/piecewise/tests/test_incremental.py index a3071982e6f..cfeab53ef0b 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -14,7 +14,6 @@ from pyomo.contrib.piecewise.triangulations import Triangulation from pyomo.core.base import TransformationFactory from pyomo.core.expr.compare import assertExpressionsEqual -from pyomo.gdp import Disjunct, Disjunction from pyomo.environ import ( Constraint, SolverFactory, @@ -236,7 +235,7 @@ def g2(x1, x2): m.pw_paraboloid = PiecewiseLinearFunction( simplices=simplices, linear_functions=[g1, g1, g2, g2], - override_triangulation=Triangulation.AssumeValid, + triangulation=Triangulation.AssumeValid, ) m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2) diff --git a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py index 022c82cb759..a49519ae25e 100644 --- a/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/tests/test_piecewise_linear_function.py @@ -340,28 +340,6 @@ def test_pw_linear_approx_of_paraboloid_j1(self): self.assertEqual(len(m.pw._simplices), 8) self.assertEqual(m.pw.triangulation, Triangulation.OrderedJ1) - @unittest.skipUnless(numpy_available, "numpy is not available") - def test_triangulation_override(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, - override_triangulation=Triangulation.AssumeValid, - ) - self.assertEqual(len(m.pw._simplices), 8) - self.assertEqual(m.pw.triangulation, Triangulation.AssumeValid) - @unittest.skipUnless(scipy_available, "scipy is not available") def test_pw_linear_approx_tabular_data(self): m = self.make_model() From 8bc8d8663a6b7ee7f5131a1792826d8b94d32b9c Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 25 Jul 2024 09:21:53 -0600 Subject: [PATCH 106/220] Deduplicate test file --- pyomo/contrib/piecewise/tests/models.py | 22 +++-- .../piecewise/tests/test_incremental.py | 96 +++++-------------- 2 files changed, 38 insertions(+), 80 deletions(-) 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 index cfeab53ef0b..8ca43df20f3 100644 --- a/pyomo/contrib/piecewise/tests/test_incremental.py +++ b/pyomo/contrib/piecewise/tests/test_incremental.py @@ -11,6 +11,7 @@ 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 @@ -132,42 +133,59 @@ def check_pw_paraboloid(self, m): 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_ordered() + 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_ordered() + 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_ordered() + 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_ordered() + 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_ordered() + 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_ordered() + 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 j1 vertex ordering got broken + # 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): @@ -184,67 +202,3 @@ def test_solve_product_model(self): TransformationFactory("contrib.piecewise.incremental").apply_to(m) SolverFactory('gurobi').solve(m) self.assertAlmostEqual(0.45, value(m.obj)) - - -# Make a version of the log_x model with the simplices properly ordered for the -# incremental transform -def make_log_x_model_ordered(): - m = ConcreteModel() - m.x = Var(bounds=(1, 10)) - m.pw_log = PiecewiseLinearFunction(points=[1, 3, 6, 10], function=log) - - # Here are the linear functions, for safe keeping. - def f1(x): - return (log(3) / 2) * x - log(3) / 2 - - m.f1 = f1 - - def f2(x): - return (log(2) / 3) * x + log(3 / 2) - - m.f2 = f2 - - def f3(x): - return (log(5 / 3) / 4) * x + log(6 / ((5 / 3) ** (3 / 2))) - - m.f3 = f3 - - m.log_expr = m.pw_log(m.x) - m.obj = Objective(expr=m.log_expr) - - m.x1 = Var(bounds=(0, 3)) - m.x2 = Var(bounds=(1, 7)) - - ## approximates paraboloid x1**2 + x2**2 - def g1(x1, x2): - return 3 * x1 + 5 * x2 - 4 - - m.g1 = g1 - - def g2(x1, x2): - return 3 * x1 + 11 * x2 - 28 - - m.g2 = g2 - # order for incremental transformation - 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)], - ] - m.pw_paraboloid = PiecewiseLinearFunction( - simplices=simplices, - linear_functions=[g1, g1, g2, g2], - triangulation=Triangulation.AssumeValid, - ) - m.paraboloid_expr = m.pw_paraboloid(m.x1, m.x2) - - def c_rule(m, i): - if i == 0: - return m.x >= m.paraboloid_expr - else: - return (1, m.x1, 2) - - m.indexed_c = Constraint([0, 1], rule=c_rule) - - return m From df55bd2118e7e8dc18552acd54ea86025f4f5591 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 25 Jul 2024 09:38:17 -0600 Subject: [PATCH 107/220] Make several functions private --- .../piecewise/tests/test_triangulations.py | 6 +- pyomo/contrib/piecewise/triangulations.py | 73 ++++++++++--------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 0576191fc7d..26b415f95c7 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -16,7 +16,7 @@ from pyomo.contrib.piecewise.triangulations import ( get_unordered_j1_triangulation, get_ordered_j1_triangulation, - get_Gn_hamiltonian, + _get_Gn_hamiltonian, get_grid_hamiltonian, ) from pyomo.common.dependencies import numpy as np, numpy_available @@ -67,7 +67,7 @@ def check_J1_ordered(self, points, num_points, 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) + # test property (2) which also guarantees property (1) (from Vielma 2010) self.assertEqual( first_simplex[-1], second_simplex[0], @@ -166,7 +166,7 @@ def test_J1_ordered_4d_and_above(self): ) def check_Gn_hamiltonian_path(self, n, start_permutation, target_symbol, last): - path = get_Gn_hamiltonian(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: diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 8696aa31144..c09417ee003 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -37,7 +37,7 @@ class Triangulation(Enum): 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 _FakeScipyTriangulation( + return _Triangulation( points=np.array(points), simplices=np.array(simplices_list), coplanar=np.array([]), @@ -67,7 +67,7 @@ def get_ordered_j1_triangulation(points, dimension): simplices_list = _get_ordered_j1_triangulation_4d_and_above( points_map, num_pts - 1, dimension ) - return _FakeScipyTriangulation( + return _Triangulation( points=np.array(points), simplices=np.array(simplices_list), coplanar=np.array([]), @@ -80,7 +80,7 @@ def get_ordered_j1_triangulation(points, dimension): # - 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 _FakeScipyTriangulation: +class _Triangulation: def __init__(self, points, simplices, coplanar): self.points = points self.simplices = simplices @@ -94,11 +94,13 @@ def _process_points_j1(points, 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" + "'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" + "'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 @@ -224,7 +226,7 @@ def add_top_left(): if is_turnaround(x, y): # finished; this case should always eventually be reached add_bottom_left() - fix_vertices_incremental_order(simplices) + _fix_vertices_incremental_order(simplices) return simplices else: if square_parity_tlbr(x, y): @@ -317,10 +319,13 @@ def add_top_left(): def _get_ordered_j1_triangulation_3d(points_map, num_pts): # 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 + 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 + # 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 = [] @@ -351,7 +356,7 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): for simplex_data in current_cube_path: simplices.append( - get_one_j1_simplex( + _get_one_j1_simplex( current_v_0, simplex_data[1], simplex_data[0], 3, points_map ) ) @@ -374,19 +379,19 @@ def _get_ordered_j1_triangulation_3d(points_map, num_pts): for simplex_data in current_cube_path: simplices.append( - get_one_j1_simplex( + _get_one_j1_simplex( current_v_0, simplex_data[1], simplex_data[0], 3, points_map ) ) - fix_vertices_incremental_order(simplices) + _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) + 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], @@ -405,34 +410,34 @@ def _get_ordered_j1_triangulation_4d_and_above(points_map, num_pts, dim): 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) + 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) + 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)) + simplices.append(_get_one_j1_simplex(v_0, pi, sign, dim, points_map)) else: - perm_sequence = get_Gn_hamiltonian(dim, start_perm, j, True) + 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)) + 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)) + 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) + _fix_vertices_incremental_order(simplices) return simplices -def get_one_j1_simplex(v_0, pi, sign, dim, points_map): +def _get_one_j1_simplex(v_0, pi, sign, dim, points_map): simplex = [] current = list(v_0) simplex.append(points_map[tuple(current)]) @@ -444,7 +449,7 @@ def get_one_j1_simplex(v_0, pi, sign, dim, points_map): # 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): +def _get_nearest_odd_and_sign_vec(corner): v_0 = [] sign = [] for x in corner: @@ -457,12 +462,12 @@ def get_nearest_odd_and_sign_vec(corner): return v_0, sign -def get_grid_hamiltonian(dim, length): +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) + 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. @@ -476,7 +481,7 @@ def get_grid_hamiltonian(dim, length): # Fix vertices (in place) when the simplices are right but vertices are not -def fix_vertices_incremental_order(simplices): +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 @@ -496,7 +501,7 @@ def fix_vertices_incremental_order(simplices): if simplex[n] in simplices[i + 1] and n != first: last = n break - if first == None or last == None: + if first is None or last is None: raise DeveloperError("Couldn't fix vertex ordering for incremental.") # reorder the simplex with the desired first and last @@ -520,7 +525,7 @@ def fix_vertices_incremental_order(simplices): # 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={}): +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: @@ -529,7 +534,7 @@ def get_Gn_hamiltonian(n, start_permutation, target_symbol, last, _cache={}): if last: ret = [ tuple(reversed(pi)) - for pi in get_Gn_hamiltonian( + for pi in _get_Gn_hamiltonian( n, tuple(reversed(start_permutation)), target_symbol, False ) ] @@ -544,19 +549,19 @@ def get_Gn_hamiltonian(n, start_permutation, target_symbol, last, _cache={}): ] # pi^-1(j) ret = [ tuple(start_permutation[pi[i] - 1] for i in range(n)) - for pi in _get_Gn_hamiltonian(n, new_target_symbol) + 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(n, target_symbol) + 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(n, target_symbol): +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: @@ -675,7 +680,7 @@ def _get_Gn_hamiltonian(n, target_symbol): idx = n - 1 facing = -1 ret = [] - for pi in _get_Gn_hamiltonian(n - 1, target_symbol): + for pi in _get_Gn_hamiltonian_impl(n - 1, target_symbol): for _ in range(n): l = list(pi) l.insert(idx, n) @@ -689,7 +694,7 @@ def _get_Gn_hamiltonian(n, target_symbol): idx = 0 facing = 1 ret = [] - for pi in _get_Gn_hamiltonian(n - 1, n - 1): + 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) From c9215711f3527243f62b5ff4c2bbf9b21d8e80aa Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 25 Jul 2024 09:41:49 -0600 Subject: [PATCH 108/220] finish renaming symbol --- pyomo/contrib/piecewise/tests/test_triangulations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index 26b415f95c7..db17738b242 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -17,7 +17,7 @@ get_unordered_j1_triangulation, get_ordered_j1_triangulation, _get_Gn_hamiltonian, - get_grid_hamiltonian, + _get_grid_hamiltonian, ) from pyomo.common.dependencies import numpy as np, numpy_available from math import factorial @@ -203,7 +203,7 @@ def test_Gn_hamiltonian_paths(self): 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) + 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) From 58462ae383a3a1a7167cf19bd3404a81d8a35ce7 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 25 Jul 2024 09:42:12 -0600 Subject: [PATCH 109/220] Any color you like --- pyomo/contrib/piecewise/piecewise_linear_function.py | 3 ++- pyomo/contrib/piecewise/transform/incremental.py | 8 +------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index e1374a78668..42538082d16 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -336,7 +336,8 @@ def _construct_simplices_from_multivariate_points( obj._triangulation = tri else: raise ValueError( - "Invalid or unrecognized triangulation specified for '%s': %s" % (obj, tri) + "Invalid or unrecognized triangulation specified for '%s': %s" + % (obj, tri) ) # Get the points for the triangulation because they might not all be diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index fb761c00ebb..4cfe16c7ac4 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -13,13 +13,7 @@ PiecewiseLinearTransformationBase, ) from pyomo.contrib.piecewise.triangulations import Triangulation -from pyomo.core import ( - Constraint, - Binary, - Var, - RangeSet, - Param, -) +from pyomo.core import Constraint, Binary, Var, RangeSet, Param from pyomo.core.base import TransformationFactory From 89366854d07f7293d4299232af30f8961341db71 Mon Sep 17 00:00:00 2001 From: Soren Davis Date: Thu, 25 Jul 2024 09:56:08 -0600 Subject: [PATCH 110/220] reword a comment due to changed PWLF constructor --- pyomo/contrib/piecewise/transform/incremental.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/transform/incremental.py b/pyomo/contrib/piecewise/transform/incremental.py index 4cfe16c7ac4..f7143676b58 100644 --- a/pyomo/contrib/piecewise/transform/incremental.py +++ b/pyomo/contrib/piecewise/transform/incremental.py @@ -57,7 +57,7 @@ def _transform_pw_linear_expr(self, pw_expr, pw_linear_func, transformation_bloc "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 overriding the triangulation tag to be " + "suppress this error by setting the triangulation tag to " "Triangulation.AssumeValid during PiecewiseLinearFunction construction." ) # Get a new Block() in transformation_block.transformed_functions, which From c9956acc760404defee1182ef6ade8c5c826d389 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Fri, 26 Jul 2024 16:48:05 -0600 Subject: [PATCH 111/220] grammar --- pyomo/contrib/pynumero/interfaces/cyipopt_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py index 26b43c37d1c..5187efadac9 100644 --- a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py @@ -316,8 +316,8 @@ def __init__(self, nlp, intermediate_callback=None, halt_on_evaluation_error=Non # 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 - # if the user asked for variable-length *args, we only pass the Problem as - # an argument if their callback asks for exactly 13 arguments. + # 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 From 9f843b4fe7f60e584abf53c80b06ca5f1a8971f8 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 30 Jul 2024 13:27:06 -0600 Subject: [PATCH 112/220] correct docstring --- pyomo/core/plugins/transform/scaling.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pyomo/core/plugins/transform/scaling.py b/pyomo/core/plugins/transform/scaling.py index 4c427e72b92..c15993524f4 100644 --- a/pyomo/core/plugins/transform/scaling.py +++ b/pyomo/core/plugins/transform/scaling.py @@ -29,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. @@ -38,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 -------- @@ -70,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): From 6b377da7611eb1f7bbe5c13bc138092111a89ed1 Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 30 Jul 2024 13:31:59 -0600 Subject: [PATCH 113/220] test warning --- pyomo/core/tests/transform/test_scaling.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index cb31aaa33ec..c1c8e36904d 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 +from pyomo.common.log import LoggingIntercept class TestScaleModelTransformation(unittest.TestCase): @@ -699,11 +701,15 @@ def test_propagate_solution_uninitialized_variable(self): scaled_model = pyo.TransformationFactory("core.scale_model").create_using(m) scaled_model.scaled_x[1] = 20.0 scaled_model.scaled_x[2] = None - pyo.TransformationFactory("core.scale_model").propagate_solution( - scaled_model, m - ) + + OUTPUT = io.StringIO() + with LoggingIntercept(OUTPUT, "pyomo.core.plugins.transform.scaling"): + pyo.TransformationFactory("core.scale_model").propagate_solution( + scaled_model, m + ) + self.assertIn("replacing value of variable", OUTPUT.getvalue()) self.assertAlmostEqual(m.x[1].value, 2.0, delta=1e-8) - # Note that value of x[2] in original model *has* been overriddeen to None. + # 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) From 2c6b76072150f344fc27089fdfd15c367e5dba2c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 30 Jul 2024 15:04:24 -0600 Subject: [PATCH 114/220] Adding in an API to get the transformed linear and general nonlinear constraints --- .../piecewise/tests/test_nonlinear_to_pwl.py | 6 +++ .../piecewise/transform/nonlinear_to_pwl.py | 50 ++++++++++++------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 3a1b5270aea..a06c74b2a51 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -61,6 +61,12 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): 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) + def test_log_constraint_uniform_grid(self): m = self.make_model() diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 798eaafdddf..bc6ea3a1f7d 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from collections import defaultdict import enum import itertools @@ -49,6 +50,7 @@ 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') @@ -70,11 +72,12 @@ class DomainPartitioningMethod(enum.IntEnum): class _NonlinearToPWLTransformationData(AutoSlots.Mixin): - __slots__ = ('transformed_component', 'src_component') + __slots__ = ('transformed_component', 'src_component', 'transformed_constraints') def __init__(self): self.transformed_component = ComponentMap() self.src_component = ComponentMap() + self.transformed_constraints = defaultdict(ComponentSet) Block.register_private_data_initializer(_NonlinearToPWLTransformationData) @@ -585,26 +588,33 @@ def _needs_approximating(self, expr, approximate_quadratic): if repn.nonlinear is None: if repn.quadratic is None: # Linear constraint. Always skip. - return False + return ExprType.LINEAR, False else: if not approximate_quadratic: # Didn't need approximated, nothing to do - return False - return True + return ExprType.QUADRATIC, False + return ExprType.QUADRATIC, True + return ExprType.GENERAL, True def _approximate_expression( - self, obj, parent_component, trans_block, config, approximate_quadratic + self, expr, obj, trans_block, config, approximate_quadratic ): - if not self._needs_approximating(obj, approximate_quadratic): + expr_type, needs_approximating = self._needs_approximating( + expr, + approximate_quadratic + ) + if not needs_approximating: return - # Additively decompose obj and work on the pieces + obj.model().private_data().transformed_constraints[expr_type].add(obj) + + # Additively decompose expr and work on the pieces pwl_func = 0 - for k, expr in enumerate( - _additively_decompose_expr(obj) if config.additively_decompose else (obj,) + for k, subexpr in enumerate( + _additively_decompose_expr(expr) if config.additively_decompose else (expr,) ): # First check is this is a good idea - expr_vars = list(identify_variables(expr, include_fixed=False)) + expr_vars = list(identify_variables(subexpr, include_fixed=False)) orig_values = ComponentMap((v, v.value) for v in expr_vars) dim = len(expr_vars) @@ -613,27 +623,27 @@ def _approximate_expression( "Not approximating expression for component '%s' as " "it exceeds the maximum dimension of %s. Try increasing " "'max_dimension' or additively separating the expression." - % (parent_component.name, config.max_dimension) + % (obj.name, config.max_dimension) ) - pwl_func += expr + pwl_func += subexpr continue - elif not self._needs_approximating(expr, approximate_quadratic): - pwl_func += expr + elif not self._needs_approximating(expr, approximate_quadratic)[1]: + pwl_func += subexpr continue def eval_expr(*args): for i, v in enumerate(expr_vars): v.value = args[i] - return value(expr) + return value(subexpr) pwlf = get_pwl_function_approximation( eval_expr, config.domain_partitioning_method, config.num_points, - self._get_bounds_list(expr_vars, parent_component), + self._get_bounds_list(expr_vars, obj), ) name = unique_component_name( - trans_block, parent_component.getname(fully_qualified=False) + trans_block, obj.getname(fully_qualified=False) ) trans_block.add_component(f"_pwle_{name}_{k}", pwlf) pwl_func += pwlf(*expr_vars) @@ -663,3 +673,9 @@ def get_transformed_component(self, cons): "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): + return model.private_data().transformed_constraints[ExprType.GENERAL] + + def get_transformed_quadratic_constraints(self, model): + return model.private_data().transformed_constraints[ExprType.QUADRATIC] From 5b85af7000b5a12b4c6153b6e16fd32d54df84d5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jul 2024 15:21:05 -0600 Subject: [PATCH 115/220] Adding APIs for getting transformed nonlinear and quadratic Constraints and Objectives, fixing a few bugs --- .../piecewise/transform/nonlinear_to_pwl.py | 57 ++++++++++++++----- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index bc6ea3a1f7d..0adca4c8d80 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -72,12 +72,14 @@ class DomainPartitioningMethod(enum.IntEnum): class _NonlinearToPWLTransformationData(AutoSlots.Mixin): - __slots__ = ('transformed_component', 'src_component', 'transformed_constraints') + __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) @@ -432,6 +434,10 @@ class NonlinearToPWL(Transformation): points in order to partition the function domain.""", ), ) + # TODO: Minimum dimension to additively decompose--(Only decompose if the + # dimension exceeds this.) + + # TODO: incorporate Bashar's linear tree changes. def __init__(self): super(Transformation).__init__() @@ -484,7 +490,7 @@ def _apply_to_impl(self, model, **kwds): raise ValueError( "Target '%s' is not a Block, Constraint, or Objective. It " "is of type '%s' and cannot be transformed." - % (target.name, type(t)) + % (target.name, type(target)) ) def _get_transformation_block(self, parent): @@ -525,13 +531,14 @@ def _transform_constraint(self, cons, config): src_data_dict = cons.parent_block().private_data() constraints = cons.values() if cons.is_indexed() else (cons,) for c in constraints: - pw_approx = self._approximate_expression( + 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) @@ -548,7 +555,7 @@ def _transform_objective(self, objective, config): objectives = objective.values() if objective.is_indexed() else (objective,) src_data_dict = objective.parent_block().private_data() for obj in objectives: - pw_approx = self._approximate_expression( + pw_approx, expr_type = self._approximate_expression( obj.expr, obj, trans_block, @@ -559,6 +566,7 @@ def _transform_objective(self, objective, config): 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( @@ -569,15 +577,14 @@ def _transform_objective(self, objective, config): obj.deactivate() - def _get_bounds_list(self, var_list, parent_component): + def _get_bounds_list(self, var_list, obj): bounds = [] for v in var_list: if None in v.bounds: - # ESJ TODO: Con is undefined--this is a bug! raise ValueError( "Cannot automatically approximate constraints with unbounded " - "variables. Var '%s' appearining in component '%s' is missing " - "at least one bound" % (con.name, v.name) + "variables. Var '%s' appearing in component '%s' is missing " + "at least one bound" % (v.name, obj.name) ) else: bounds.append(v.bounds) @@ -604,16 +611,14 @@ def _approximate_expression( approximate_quadratic ) if not needs_approximating: - return - - obj.model().private_data().transformed_constraints[expr_type].add(obj) + return None, expr_type # Additively decompose expr and work on the pieces pwl_func = 0 for k, subexpr in enumerate( _additively_decompose_expr(expr) if config.additively_decompose else (expr,) ): - # First check is this is a good idea + # 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) @@ -652,7 +657,7 @@ def eval_expr(*args): for v, val in orig_values.items(): v.value = val - return pwl_func + return pwl_func, expr_type def get_src_component(self, cons): data = cons.parent_block().private_data().src_component @@ -675,7 +680,33 @@ def get_transformed_component(self, cons): ) 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] From 1cc71bda35d1505d16a5b7c119326ecfeb956758 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jul 2024 15:21:20 -0600 Subject: [PATCH 116/220] Adding a lot of tests, including 3D --- .../piecewise/tests/test_nonlinear_to_pwl.py | 324 +++++++++++++++--- 1 file changed, 274 insertions(+), 50 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index a06c74b2a51..ef1a5a4e096 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -16,7 +16,9 @@ DomainPartitioningMethod, ) from pyomo.core.expr.compare import assertExpressionsStructurallyEqual -from pyomo.environ import ConcreteModel, Var, Constraint, TransformationFactory, log +from pyomo.environ import ( + ConcreteModel, Var, Constraint, TransformationFactory, log, Objective +) ## debug from pytest import set_trace @@ -116,6 +118,101 @@ def test_log_constraint_random_grid(self): x3 = 9.556428757689245 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + 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) + + 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_log_constraint_lmt_uniform_sample(self): # m = self.make_model() @@ -143,63 +240,190 @@ def test_log_constraint_random_grid(self): # self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) -class TestNonlinearToPWLIntegration(unittest.TestCase): - def test_Ali_example(self): +class TestNonlinearToPWL_2D(unittest.TestCase): + def make_paraboloid_model(self): m = ConcreteModel() - m.flow_super_heated_vapor = Var() - m.flow_super_heated_vapor.fix(0.4586949988166174) - m.super_heated_vapor_temperature = Var(bounds=(31, 200), initialize=45) - m.evaporator_condensate_temperature = Var( - bounds=(29, 120.8291392028045), initialize=30 - ) - m.LMTD = Var(bounds=(0, 130.61608989795093), initialize=1) - m.evaporator_condensate_enthalpy = Var( - bounds=(-15836.847, -15510.210751855624), initialize=100 - ) - m.evaporator_condensate_vapor_enthalpy = Var( - bounds=(-13416.64, -13247.674383866839), initialize=100 + 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[id(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) + + 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 ) - m.heat_transfer_coef = Var( - bounds=(1.9936854577372858, 5.995319594088982), initialize=0.1 + + # check obj is transformed + self.assertFalse(m.obj.active) + + pwlf = list( + m.component_data_objects(PiecewiseLinearFunction, descend_into=True) ) - m.evaporator_brine_temperature = Var( - bounds=(27, 118.82913920280366), initialize=35 + 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) + + 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] ) - m.each_evaporator_area = Var() - - m.c = Constraint( - expr=m.each_evaporator_area - == ( - 1.873 - * m.flow_super_heated_vapor - * ( - m.super_heated_vapor_temperature - - m.evaporator_condensate_temperature - ) - / (100 * m.LMTD) - + m.flow_super_heated_vapor - * ( - m.evaporator_condensate_vapor_enthalpy - - m.evaporator_condensate_enthalpy - ) - / ( - m.heat_transfer_coef - * ( - m.evaporator_condensate_temperature - - m.evaporator_brine_temperature - ) - ) - ) + + + # 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=3, + m, num_points=2, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + approximate_quadratic_objectives=False ) + + # check obj is *not* transformed + self.assertTrue(m.obj.active) - m.pprint() - - from pyomo.environ import SolverFactory - SolverFactory('gurobi').solve(m, tee=True) + 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) + + +# class TestNonlinearToPWLIntegration(unittest.TestCase): +# def test_Ali_example(self): +# m = ConcreteModel() +# m.flow_super_heated_vapor = Var() +# m.flow_super_heated_vapor.fix(0.4586949988166174) +# m.super_heated_vapor_temperature = Var(bounds=(31, 200), initialize=45) +# m.evaporator_condensate_temperature = Var( +# bounds=(29, 120.8291392028045), initialize=30 +# ) +# m.LMTD = Var(bounds=(0, 130.61608989795093), initialize=1) +# m.evaporator_condensate_enthalpy = Var( +# bounds=(-15836.847, -15510.210751855624), initialize=100 +# ) +# m.evaporator_condensate_vapor_enthalpy = Var( +# bounds=(-13416.64, -13247.674383866839), initialize=100 +# ) +# m.heat_transfer_coef = Var( +# bounds=(1.9936854577372858, 5.995319594088982), initialize=0.1 +# ) +# m.evaporator_brine_temperature = Var( +# bounds=(27, 118.82913920280366), initialize=35 +# ) +# m.each_evaporator_area = Var() + +# m.c = Constraint( +# expr=m.each_evaporator_area +# == ( +# 1.873 +# * m.flow_super_heated_vapor +# * ( +# m.super_heated_vapor_temperature +# - m.evaporator_condensate_temperature +# ) +# / (100 * m.LMTD) +# + m.flow_super_heated_vapor +# * ( +# m.evaporator_condensate_vapor_enthalpy +# - m.evaporator_condensate_enthalpy +# ) +# / ( +# m.heat_transfer_coef +# * ( +# m.evaporator_condensate_temperature +# - m.evaporator_brine_temperature +# ) +# ) +# ) +# ) + +# n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') +# n_to_pwl.apply_to( +# m, +# num_points=3, +# domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, +# ) + +# m.pprint() + +# from pyomo.environ import SolverFactory +# SolverFactory('gurobi').solve(m, tee=True) From 2a6ad4603ff6e3baf611dc96ddc09f42c43f13ae Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jul 2024 15:22:15 -0600 Subject: [PATCH 117/220] Incorporating black's opinions --- .../piecewise/tests/test_nonlinear_to_pwl.py | 81 ++++++++++--------- .../piecewise/transform/nonlinear_to_pwl.py | 15 ++-- 2 files changed, 50 insertions(+), 46 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index ef1a5a4e096..c9edabd00bd 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -17,7 +17,12 @@ ) from pyomo.core.expr.compare import assertExpressionsStructurallyEqual from pyomo.environ import ( - ConcreteModel, Var, Constraint, TransformationFactory, log, Objective + ConcreteModel, + Var, + Constraint, + TransformationFactory, + log, + Objective, ) ## debug @@ -120,7 +125,7 @@ def test_log_constraint_random_grid(self): def test_do_not_transform_quadratic_constraint(self): m = self.make_model() - m.quad = Constraint(expr=m.x ** 2 <= 9) + m.quad = Constraint(expr=m.x**2 <= 9) m.lin = Constraint(expr=m.x >= 2) n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') @@ -128,7 +133,7 @@ def test_do_not_transform_quadratic_constraint(self): m, num_points=3, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, - approximate_quadratic_constraints=False + approximate_quadratic_constraints=False, ) # cons is transformed @@ -151,14 +156,14 @@ def test_do_not_transform_quadratic_constraint(self): def test_constraint_target(self): m = self.make_model() - m.quad = Constraint(expr=m.x ** 2 <= 9) + 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] + targets=[m.cons], ) # cons is transformed @@ -179,32 +184,32 @@ def test_constraint_target(self): 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." + 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] + 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) + 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" + 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, @@ -212,7 +217,6 @@ def test_cannot_approximate_constraints_with_unbounded_vars(self): domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, ) - # def test_log_constraint_lmt_uniform_sample(self): # m = self.make_model() @@ -245,31 +249,26 @@ 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) + 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), - ] + 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[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.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) @@ -292,8 +291,9 @@ def test_paraboloid_objective_uniform_grid(self): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') n_to_pwl.apply_to( - m, num_points=2, - domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID + m, + num_points=2, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, ) # check obj is transformed @@ -315,16 +315,16 @@ def test_paraboloid_objective_uniform_grid(self): def test_objective_target(self): m = self.make_paraboloid_model() - m.some_other_nonlinear_constraint = Constraint(expr=m.x1 ** 3 + m.x2 <= 6) + 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, + m, + num_points=2, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, - targets=[m.obj] + targets=[m.obj], ) - # check obj is transformed self.assertFalse(m.obj.active) @@ -349,11 +349,12 @@ def test_do_not_transform_quadratic_objective(self): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') n_to_pwl.apply_to( - m, num_points=2, + m, + num_points=2, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, - approximate_quadratic_objectives=False + approximate_quadratic_objectives=False, ) - + # check obj is *not* transformed self.assertTrue(m.obj.active) @@ -365,7 +366,7 @@ def test_do_not_transform_quadratic_objective(self): self.assertEqual(len(quadratic), 0) nonlinear = n_to_pwl.get_transformed_nonlinear_objectives(m) self.assertEqual(len(nonlinear), 0) - + # class TestNonlinearToPWLIntegration(unittest.TestCase): # def test_Ali_example(self): diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 0adca4c8d80..fd11c67a17b 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -72,8 +72,12 @@ class DomainPartitioningMethod(enum.IntEnum): class _NonlinearToPWLTransformationData(AutoSlots.Mixin): - __slots__ = ('transformed_component', 'src_component', 'transformed_constraints', - 'transformed_objectives') + __slots__ = ( + 'transformed_component', + 'src_component', + 'transformed_constraints', + 'transformed_objectives', + ) def __init__(self): self.transformed_component = ComponentMap() @@ -607,8 +611,7 @@ def _approximate_expression( self, expr, obj, trans_block, config, approximate_quadratic ): expr_type, needs_approximating = self._needs_approximating( - expr, - approximate_quadratic + expr, approximate_quadratic ) if not needs_approximating: return None, expr_type @@ -690,7 +693,7 @@ def get_transformed_nonlinear_constraints(self, model): 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 + return the list of quadratic Constraints that were approximated with PiecewiseLinearFunctions """ return model.private_data().transformed_constraints[ExprType.QUADRATIC] @@ -706,7 +709,7 @@ def get_transformed_nonlinear_objectives(self, model): 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 + return the list of quadratic Constraints that were approximated with PiecewiseLinearFunctions """ return model.private_data().transformed_objectives[ExprType.QUADRATIC] From e3d3fd63a7b0368704fc08fe06b1d96a68982451 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 31 Jul 2024 17:51:31 -0600 Subject: [PATCH 118/220] Fixing some typos --- pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index fd11c67a17b..f88c83af17f 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -136,7 +136,8 @@ def get_points_lmt(points, bounds, func, seed): ) regr.fit(x_list, y_list) - leaves, splits, ths = parse_linear_tree_regressor(regr, bounds) + # ESJ TODO: we actually only needs leaves from here... + leaves, splits, thresholds = parse_linear_tree_regressor(regr, bounds) # This was originally part of the LMT_Model_component and used to calculate # avg_leaves for the output data. TODO: get this back @@ -419,7 +420,7 @@ class NonlinearToPWL(Transformation): 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 paritioning the domain is + the scalability of the approximation (since partitioning the domain is subject to the curse of dimensionality).""", ), ) @@ -433,9 +434,9 @@ class NonlinearToPWL(Transformation): 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 the default - (5), it is likely that this transformation will stall triangulating the - points in order to partition the function domain.""", + 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.""", ), ) # TODO: Minimum dimension to additively decompose--(Only decompose if the From 83c326a64009dfc76763a789017f8ba21ee381cb Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 2 Aug 2024 09:18:32 -0400 Subject: [PATCH 119/220] Fix private method docstring --- pyomo/repn/parameterized_quadratic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index dcd6a9e5364..7400985db51 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -46,8 +46,8 @@ def _merge_dict(dest_dict, mult, src_dict): """ - Slightly different from `merge_dict` of - from the parameterized module. + Slightly different from `merge_dict` + in the `parameterized_linear` module. """ if not is_equal_to(mult, 1): for vid, coef in src_dict.items(): From 228ec3e2bc13ec04d0909c7dd168bb5bc1ece55a Mon Sep 17 00:00:00 2001 From: jasherma Date: Fri, 2 Aug 2024 09:21:22 -0400 Subject: [PATCH 120/220] Simplify implementation of `beforeChild` --- pyomo/repn/parameterized_quadratic.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 7400985db51..1dd93955825 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -28,7 +28,6 @@ ) from pyomo.repn.parameterized_linear import ( define_exit_node_handlers as _param_linear_def_exit_node_handlers, - ParameterizedLinearBeforeChildDispatcher, ParameterizedLinearRepnVisitor, to_expression, _handle_division_ANY_pseudo_constant, @@ -297,9 +296,6 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): return _GENERAL, ans -_before_child_dispatcher = ParameterizedLinearBeforeChildDispatcher() - - def define_exit_node_handlers(exit_node_handlers=None): if exit_node_handlers is None: exit_node_handlers = {} @@ -348,9 +344,6 @@ class ParameterizedQuadraticRepnVisitor(ParameterizedLinearRepnVisitor): max_exponential_expansion = 2 expand_nonlinear_products = True - def beforeChild(self, node, child, child_idx): - return _before_child_dispatcher[child.__class__](self, child) - def _factor_multiplier_into_quadratic_terms(self, ans, mult): linear = ans.linear zeros = [] From 1fe0f000415e14bd791d7592594605a702127e70 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 2 Aug 2024 14:17:01 -0600 Subject: [PATCH 121/220] Fixing a couple bugs with additive decomposition in nonlinear-to-pwl --- .../piecewise/transform/nonlinear_to_pwl.py | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index f88c83af17f..4daf69e14ea 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -52,6 +52,7 @@ 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') @@ -124,6 +125,7 @@ def get_points_lmt_uniform_sample(bounds, n, func, seed=42): def get_points_lmt(points, bounds, func, seed): x_list = np.array(points) y_list = [] + for point in points: y_list.append(func(*point)) # ESJ: Do we really need the sklearn dependency to get LinearRegression?? @@ -132,7 +134,9 @@ def get_points_lmt(points, bounds, func, seed): criterion='mse', max_bins=120, min_samples_leaf=4, - max_depth=5, + max_depth=max(4, int(np.log2(len(points) / 4))), # Want the tree to grow + # with increasing points + # but not get too large. ) regr.fit(x_list, y_list) @@ -439,6 +443,20 @@ class NonlinearToPWL(Transformation): triangulating the points in order to partition the function domain.""", ), ) + CONFIG.declare( + 'min_additive_decomposition_dimension', + 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_additive_decomposition_dimension' the transformation will additively decompose + If a the dimension of an expression is less than the "min_additive_decomposition_dimension" + then, it will not be additively decomposed""", + ), + ) # TODO: Minimum dimension to additively decompose--(Only decompose if the # dimension exceeds this.) @@ -634,11 +652,12 @@ def _approximate_expression( "'max_dimension' or additively separating the expression." % (obj.name, config.max_dimension) ) - pwl_func += subexpr + pwl_func = pwl_func + subexpr continue - elif not self._needs_approximating(expr, approximate_quadratic)[1]: - pwl_func += subexpr + elif not self._needs_approximating(subexpr, approximate_quadratic)[1]: + pwl_func = pwl_func + subexpr continue + # else we approximate subexpr def eval_expr(*args): for i, v in enumerate(expr_vars): @@ -655,7 +674,11 @@ def eval_expr(*args): trans_block, obj.getname(fully_qualified=False) ) trans_block.add_component(f"_pwle_{name}_{k}", pwlf) - pwl_func += pwlf(*expr_vars) + # 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_func = pwl_func + pwlf(*expr_vars) # restore var values for v, val in orig_values.items(): From 9ae91fbcc514aeeec2ffec88c545e2dbfc9384cb Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Fri, 2 Aug 2024 14:17:38 -0600 Subject: [PATCH 122/220] Adding partial test of additive decomposition --- .../piecewise/tests/test_nonlinear_to_pwl.py | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index c9edabd00bd..5c352bb3052 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -15,7 +15,11 @@ NonlinearToPWL, DomainPartitioningMethod, ) -from pyomo.core.expr.compare import assertExpressionsStructurallyEqual +from pyomo.core.expr.compare import ( + assertExpressionsEqual, + assertExpressionsStructurallyEqual, +) +from pyomo.core.expr.numeric_expr import SumExpression from pyomo.environ import ( ConcreteModel, Var, @@ -23,6 +27,8 @@ TransformationFactory, log, Objective, + Reals, + SolverFactory, ) ## debug @@ -368,7 +374,53 @@ def test_do_not_transform_quadratic_objective(self): self.assertEqual(len(nonlinear), 0) -# class TestNonlinearToPWLIntegration(unittest.TestCase): +class TestNonlinearToPWLIntegration(unittest.TestCase): + def test_additively_decompose(self): + 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') + n_to_pwl.apply_to( + m, + num_points=4, + domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM, + additively_decompose=True, + ) + + self.assertFalse(m.obj.active) + new_obj = n_to_pwl.get_transformed_component(m.obj) + self.assertIs(n_to_pwl.get_src_component(new_obj), m.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( + m.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 * m.x1) + + objs = n_to_pwl.get_transformed_nonlinear_objectives(m) + self.assertEqual(len(objs), 0) + objs = n_to_pwl.get_transformed_quadratic_objectives(m) + self.assertEqual(len(objs), 1) + self.assertIn(m.obj, objs) + self.assertEqual(len(n_to_pwl.get_transformed_nonlinear_constraints(m)), 0) + self.assertEqual(len(n_to_pwl.get_transformed_quadratic_constraints(m)), 0) + + TransformationFactory('contrib.piecewise.outer_repn_gdp').apply_to(m) + TransformationFactory('gdp.bigm').apply_to(m) + SolverFactory('gurobi').solve(m) + + # def test_Ali_example(self): # m = ConcreteModel() # m.flow_super_heated_vapor = Var() From 8074fe083b4325acc5f1dbd9831f288755ccccf2 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 11:53:11 -0600 Subject: [PATCH 123/220] NFC: Reformatting some docstrings --- .../piecewise/transform/nonlinear_to_pwl.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 4daf69e14ea..d1002930a26 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -413,8 +413,8 @@ class NonlinearToPWL(Transformation): ConfigValue( default=False, domain=bool, - description="Whether or not to additively decompose constraints and " - "approximate the summands separately.", + 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 @@ -444,17 +444,19 @@ class NonlinearToPWL(Transformation): ), ) CONFIG.declare( - 'min_additive_decomposition_dimension', + 'min_dimension_to_additively_decompose', ConfigValue( default=1, domain=PositiveInt, - description="The minimum dimension of functions that will be additively decomposed.", + 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_additive_decomposition_dimension' the transformation will additively decompose - If a the dimension of an expression is less than the "min_additive_decomposition_dimension" - then, it will not be additively decomposed""", + 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""", ), ) # TODO: Minimum dimension to additively decompose--(Only decompose if the From b78efe540752b2f15778568836f437fcc70875fa Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 13:36:40 -0600 Subject: [PATCH 124/220] Adding max depth argument to be used for the linear tree domain partitioning methods --- .../piecewise/transform/nonlinear_to_pwl.py | 81 +++++++++++-------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index d1002930a26..851c5a9e0f4 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -90,7 +90,7 @@ def __init__(self): Block.register_private_data_initializer(_NonlinearToPWLTransformationData) -def get_random_point_grid(bounds, n, func, seed=42): +def get_random_point_grid(bounds, n, func, config, seed=42): # Generate randomized grid of points linspaces = [] for lb, ub in bounds: @@ -99,7 +99,7 @@ def get_random_point_grid(bounds, n, func, seed=42): return list(itertools.product(*linspaces)) -def get_uniform_point_grid(bounds, n, func): +def get_uniform_point_grid(bounds, n, func, config): # Generate non-randomized grid of points linspaces = [] for lb, ub in bounds: @@ -112,31 +112,32 @@ def get_uniform_point_grid(bounds, n, func): return list(itertools.product(*linspaces)) -def get_points_lmt_random_sample(bounds, n, func, seed=42): +def get_points_lmt_random_sample(bounds, n, func, config, seed=42): points = get_random_point_grid(bounds, n, func, seed=seed) - return get_points_lmt(points, bounds, func, seed) + return get_points_lmt(points, bounds, func, config, seed) -def get_points_lmt_uniform_sample(bounds, n, func, seed=42): +def get_points_lmt_uniform_sample(bounds, n, func, config, seed=42): points = get_uniform_point_grid(bounds, n, func) - return get_points_lmt(points, bounds, func, seed) + return get_points_lmt(points, bounds, func, seed, config) -def get_points_lmt(points, bounds, func, seed): +def get_points_lmt(points, bounds, func, seed, config): x_list = np.array(points) y_list = [] for point in points: y_list.append(func(*point)) - # ESJ: Do we really need the sklearn dependency to get LinearRegression?? + 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(4, int(np.log2(len(points) / 4))), # Want the tree to grow - # with increasing points - # but not get too large. + max_depth=max_depth ) regr.fit(x_list, y_list) @@ -162,19 +163,21 @@ def get_points_lmt(points, bounds, func, seed): } -def get_pwl_function_approximation(func, method, n, bounds): +def _get_pwl_function_approximation(func, config, bounds): """ Get a piecewise-linear approximation of a function, given: func: function to approximate - method: method to use for the approximation, member of DomainPartitioningMethod - n: parameter controlling fineness of the approximation based on the specified method - bounds: list of tuples giving upper and lower bounds for each of func's arguments + 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 for each of func's + arguments """ - points = _partition_method_dispatcher[method](bounds, n, func) + method = config.domain_partitioning_method + n = config.num_points + points = _partition_method_dispatcher[method](bounds, n, func, config) - # DUCT TAPE WARNING: work around deficiency in PiecewiseLinearFunction - # constructor. TODO + # Don't confuse PiecewiseLinearFunction constructor... dim = len(points[0]) if dim == 1: points = [pt[0] for pt in points] @@ -188,10 +191,6 @@ def get_pwl_function_approximation(func, method, n, bounds): return PiecewiseLinearFunction(points=points, function=func) -# TODO: this is still horrible. Maybe I should put these back together into -# a wrapper class again, but better this time? - - # 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 @@ -288,9 +287,11 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): # This doesn't catch all additively separable expressions--we really need a # walker (as does gdp.partition_disjuncts) -def _additively_decompose_expr(input_expr): - if input_expr.__class__ is not SumExpression: - # This isn't separable, so we just have the one expression +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) @@ -459,10 +460,22 @@ class NonlinearToPWL(Transformation): decomposed""", ), ) - # TODO: Minimum dimension to additively decompose--(Only decompose if the - # dimension exceeds this.) - - # TODO: incorporate Bashar's linear tree changes. + 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__() @@ -640,7 +653,10 @@ def _approximate_expression( # Additively decompose expr and work on the pieces pwl_func = 0 for k, subexpr in enumerate( - _additively_decompose_expr(expr) if config.additively_decompose else (expr,) + _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)) @@ -666,10 +682,9 @@ def eval_expr(*args): v.value = args[i] return value(subexpr) - pwlf = get_pwl_function_approximation( + pwlf = _get_pwl_function_approximation( eval_expr, - config.domain_partitioning_method, - config.num_points, + config, self._get_bounds_list(expr_vars, obj), ) name = unique_component_name( From 31a95bab3b3894bdcaa27c4a0191a1e6fe77b117 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 14:38:31 -0600 Subject: [PATCH 125/220] Adding tests with absolute value for linear model tree partitioning --- .../piecewise/tests/test_nonlinear_to_pwl.py | 99 ++++++++++++++++++- .../piecewise/transform/nonlinear_to_pwl.py | 8 +- 2 files changed, 99 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 5c352bb3052..b779543cc9b 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -9,6 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ +from pyomo.common.dependencies import attempt_import, scipy_available import pyomo.common.unittest as unittest from pyomo.contrib.piecewise import PiecewiseLinearFunction from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( @@ -31,9 +32,15 @@ SolverFactory, ) -## debug -from pytest import set_trace +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] +## DEBUG +from pytest import set_trace class TestNonlinearToPWL_1D(unittest.TestCase): def make_model(self): @@ -374,8 +381,90 @@ def test_do_not_transform_quadratic_objective(self): 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 + ) + + class TestNonlinearToPWLIntegration(unittest.TestCase): - def test_additively_decompose(self): + @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) @@ -385,7 +474,7 @@ def test_additively_decompose(self): n_to_pwl.apply_to( m, num_points=4, - domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, additively_decompose=True, ) @@ -420,6 +509,8 @@ def test_additively_decompose(self): TransformationFactory('gdp.bigm').apply_to(m) SolverFactory('gurobi').solve(m) + # actually test the answer or something + self.assertTrue(False) # def test_Ali_example(self): # m = ConcreteModel() diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 851c5a9e0f4..59330a373eb 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -113,16 +113,16 @@ def get_uniform_point_grid(bounds, n, func, config): def get_points_lmt_random_sample(bounds, n, func, config, seed=42): - points = get_random_point_grid(bounds, n, func, seed=seed) + 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) - return get_points_lmt(points, bounds, func, seed, config) + 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, seed, config): +def get_points_lmt(points, bounds, func, config, seed): x_list = np.array(points) y_list = [] From c89ddbf454819acf4fef59d37696b858f09bf16c Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 14:39:38 -0600 Subject: [PATCH 126/220] NFC: black --- .../piecewise/tests/test_nonlinear_to_pwl.py | 30 ++++++++----------- .../piecewise/transform/nonlinear_to_pwl.py | 13 ++++---- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index b779543cc9b..7e8669e209a 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -42,6 +42,7 @@ ## DEBUG from pytest import set_trace + class TestNonlinearToPWL_1D(unittest.TestCase): def make_model(self): m = ConcreteModel() @@ -396,9 +397,9 @@ def test_linear_model_tree_uniform(self): 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 + 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 + linear_tree_max_depth=1, # force parsimony ) transformed_obj = n_to_pwl.get_transformed_component(m.obj) @@ -408,18 +409,14 @@ def test_linear_model_tree_uniform(self): 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 - ) + 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 + 0.9833360108369479 * m.x + 0.16663989163052034, + places=7, ) def test_linear_model_tree_random(self): @@ -427,9 +424,9 @@ def test_linear_model_tree_random(self): 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 + 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 + linear_tree_max_depth=1, # force parsimony ) transformed_obj = n_to_pwl.get_transformed_component(m.obj) @@ -439,18 +436,14 @@ def test_linear_model_tree_random(self): 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 - ) + 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 + 0.9927503741388829 * m.x + 0.07249625861117256, + places=7, ) @@ -512,6 +505,7 @@ def test_transform_and_solve_additively_decomposes_model(self): # actually test the answer or something self.assertTrue(False) + # def test_Ali_example(self): # m = ConcreteModel() # m.flow_super_heated_vapor = Var() diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 59330a373eb..4b549c0be8d 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -137,7 +137,7 @@ def get_points_lmt(points, bounds, func, config, seed): criterion='mse', max_bins=120, min_samples_leaf=4, - max_depth=max_depth + max_depth=max_depth, ) regr.fit(x_list, y_list) @@ -654,9 +654,10 @@ def _approximate_expression( pwl_func = 0 for k, subexpr in enumerate( _additively_decompose_expr( - expr, - config.min_dimension_to_additively_decompose) - if config.additively_decompose else (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)) @@ -683,9 +684,7 @@ def eval_expr(*args): return value(subexpr) pwlf = _get_pwl_function_approximation( - eval_expr, - config, - self._get_bounds_list(expr_vars, obj), + eval_expr, config, self._get_bounds_list(expr_vars, obj) ) name = unique_component_name( trans_block, obj.getname(fully_qualified=False) From e4bfca0af063f78e9aa50e0c3f758200b98fddbd Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:16:20 -0600 Subject: [PATCH 127/220] Finishing integration test, adding bigger linear model tree test --- .../piecewise/tests/test_nonlinear_to_pwl.py | 118 +++++++++++------- 1 file changed, 73 insertions(+), 45 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 7e8669e209a..074ef64b12d 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -30,6 +30,8 @@ Objective, Reals, SolverFactory, + TerminationCondition, + value ) gurobi_available = ( @@ -231,32 +233,6 @@ def test_cannot_approximate_constraints_with_unbounded_vars(self): domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, ) - # def test_log_constraint_lmt_uniform_sample(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.LINEAR_MODEL_TREE_UNIFORM, - # ) - - # # 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] - - # set_trace() - - # # TODO - # x1 = 4.370861069626263 - # x2 = 7.587945476302646 - # x3 = 9.556428757689245 - # self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) - class TestNonlinearToPWL_2D(unittest.TestCase): def make_paraboloid_model(self): @@ -446,6 +422,46 @@ def test_linear_model_tree_random(self): 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") @@ -464,16 +480,16 @@ def test_transform_and_solve_additively_decomposes_model(self): 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') - n_to_pwl.apply_to( + xm = n_to_pwl.create_using( m, num_points=4, domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, additively_decompose=True, ) - self.assertFalse(m.obj.active) - new_obj = n_to_pwl.get_transformed_component(m.obj) - self.assertIs(n_to_pwl.get_src_component(new_obj), m.obj) + 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) @@ -481,30 +497,42 @@ def test_transform_and_solve_additively_decomposes_model(self): first = new_obj.expr.args[0] pwlf = first.expr.pw_linear_function all_pwlf = list( - m.component_data_objects(PiecewiseLinearFunction, descend_into=True) + 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 * m.x1) + assertExpressionsEqual(self, second, 5.04 * xm.x1) - objs = n_to_pwl.get_transformed_nonlinear_objectives(m) + objs = n_to_pwl.get_transformed_nonlinear_objectives(xm) self.assertEqual(len(objs), 0) - objs = n_to_pwl.get_transformed_quadratic_objectives(m) + objs = n_to_pwl.get_transformed_quadratic_objectives(xm) self.assertEqual(len(objs), 1) - self.assertIn(m.obj, objs) - self.assertEqual(len(n_to_pwl.get_transformed_nonlinear_constraints(m)), 0) - self.assertEqual(len(n_to_pwl.get_transformed_quadratic_constraints(m)), 0) - - TransformationFactory('contrib.piecewise.outer_repn_gdp').apply_to(m) - TransformationFactory('gdp.bigm').apply_to(m) - SolverFactory('gurobi').solve(m) - - # actually test the answer or something - self.assertTrue(False) - + 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) # def test_Ali_example(self): # m = ConcreteModel() From 96b9aeecf37a9d3f954dc2c08b2f37b61ca84fa4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:16:59 -0600 Subject: [PATCH 128/220] NFC: black --- .../piecewise/tests/test_nonlinear_to_pwl.py | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 074ef64b12d..2a3a9821173 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -31,7 +31,7 @@ Reals, SolverFactory, TerminationCondition, - value + value, ) gurobi_available = ( @@ -428,7 +428,7 @@ def test_linear_model_tree_random_auto_depth_tree(self): n_to_pwl.apply_to( m, num_points=100, # sample a lot but not too many because this one is - # more prone to overfitting + # more prone to overfitting domain_partitioning_method=DomainPartitioningMethod.LINEAR_MODEL_TREE_RANDOM, ) @@ -443,11 +443,24 @@ def test_linear_model_tree_random_auto_depth_tree(self): # 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( + 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) @@ -456,7 +469,7 @@ def test_linear_model_tree_random_auto_depth_tree(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, + -0.9851979299618323 * m.x + 0.12006477080409184, places=7, ) for i in range(4, 8): @@ -518,14 +531,16 @@ def test_transform_and_solve_additively_decomposes_model(self): TransformationFactory('gdp.bigm').apply_to(xm) opt = SolverFactory('gurobi') results = opt.solve(xm) - self.assertEqual(results.solver.termination_condition, - TerminationCondition.optimal) + 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) + self.assertEqual( + results.solver.termination_condition, TerminationCondition.optimal + ) # Not a bad approximation: self.assertAlmostEqual(value(xm.obj), value(m.obj), places=2) @@ -534,6 +549,7 @@ def test_transform_and_solve_additively_decomposes_model(self): self.assertAlmostEqual(value(xm.x7), value(m.x7), places=4) self.assertAlmostEqual(value(xm.x1), value(m.x1), places=7) + # def test_Ali_example(self): # m = ConcreteModel() # m.flow_super_heated_vapor = Var() From 76459a2ed5923a6ad83d48b6bbeeb0b1f82a0fc0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:34:46 -0600 Subject: [PATCH 129/220] nonlinear_to_pwl.py --- .../piecewise/tests/test_nonlinear_to_pwl.py | 102 +++++++----------- .../piecewise/transform/nonlinear_to_pwl.py | 2 +- 2 files changed, 39 insertions(+), 65 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 2a3a9821173..36b646c6420 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -16,6 +16,7 @@ NonlinearToPWL, DomainPartitioningMethod, ) +from pyomo.core.base.expression import _ExpressionData from pyomo.core.expr.compare import ( assertExpressionsEqual, assertExpressionsStructurallyEqual, @@ -41,9 +42,6 @@ lineartree_available = attempt_import('lineartree')[1] sklearn_available = attempt_import('sklearn.linear_model')[1] -## DEBUG -from pytest import set_trace - class TestNonlinearToPWL_1D(unittest.TestCase): def make_model(self): @@ -233,6 +231,43 @@ def test_cannot_approximate_constraints_with_unbounded_vars(self): 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 + ) + + 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) + class TestNonlinearToPWL_2D(unittest.TestCase): def make_paraboloid_model(self): @@ -548,64 +583,3 @@ def test_transform_and_solve_additively_decomposes_model(self): 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) - - -# def test_Ali_example(self): -# m = ConcreteModel() -# m.flow_super_heated_vapor = Var() -# m.flow_super_heated_vapor.fix(0.4586949988166174) -# m.super_heated_vapor_temperature = Var(bounds=(31, 200), initialize=45) -# m.evaporator_condensate_temperature = Var( -# bounds=(29, 120.8291392028045), initialize=30 -# ) -# m.LMTD = Var(bounds=(0, 130.61608989795093), initialize=1) -# m.evaporator_condensate_enthalpy = Var( -# bounds=(-15836.847, -15510.210751855624), initialize=100 -# ) -# m.evaporator_condensate_vapor_enthalpy = Var( -# bounds=(-13416.64, -13247.674383866839), initialize=100 -# ) -# m.heat_transfer_coef = Var( -# bounds=(1.9936854577372858, 5.995319594088982), initialize=0.1 -# ) -# m.evaporator_brine_temperature = Var( -# bounds=(27, 118.82913920280366), initialize=35 -# ) -# m.each_evaporator_area = Var() - -# m.c = Constraint( -# expr=m.each_evaporator_area -# == ( -# 1.873 -# * m.flow_super_heated_vapor -# * ( -# m.super_heated_vapor_temperature -# - m.evaporator_condensate_temperature -# ) -# / (100 * m.LMTD) -# + m.flow_super_heated_vapor -# * ( -# m.evaporator_condensate_vapor_enthalpy -# - m.evaporator_condensate_enthalpy -# ) -# / ( -# m.heat_transfer_coef -# * ( -# m.evaporator_condensate_temperature -# - m.evaporator_brine_temperature -# ) -# ) -# ) -# ) - -# n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') -# n_to_pwl.apply_to( -# m, -# num_points=3, -# domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, -# ) - -# m.pprint() - -# from pyomo.environ import SolverFactory -# SolverFactory('gurobi').solve(m, tee=True) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 4b549c0be8d..30797b5318e 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -665,7 +665,7 @@ def _approximate_expression( dim = len(expr_vars) if dim > config.max_dimension: - logger.warning( + 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." From c61a7052d680a57d5e67e24500359fdcf9ce2163 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:36:06 -0600 Subject: [PATCH 130/220] Black --- .../contrib/piecewise/tests/test_nonlinear_to_pwl.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 36b646c6420..f5f4ebc8009 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -238,23 +238,23 @@ def test_error_for_non_separable_exceeding_max_dimension(self): 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." + 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 + max_dimension=4, ) 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, From 4145590e1b80aeed282e8ab757b348bdb416a6f8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:56:21 -0600 Subject: [PATCH 131/220] Making a lot of helper functions private --- .../piecewise/transform/nonlinear_to_pwl.py | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 30797b5318e..8b73ec00678 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -90,7 +90,7 @@ def __init__(self): Block.register_private_data_initializer(_NonlinearToPWLTransformationData) -def get_random_point_grid(bounds, n, func, config, seed=42): +def _get_random_point_grid(bounds, n, func, config, seed=42): # Generate randomized grid of points linspaces = [] for lb, ub in bounds: @@ -99,7 +99,7 @@ def get_random_point_grid(bounds, n, func, config, seed=42): return list(itertools.product(*linspaces)) -def get_uniform_point_grid(bounds, n, func, config): +def _get_uniform_point_grid(bounds, n, func, config): # Generate non-randomized grid of points linspaces = [] for lb, ub in bounds: @@ -112,17 +112,17 @@ def get_uniform_point_grid(bounds, n, func, config): 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_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_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): +def _get_points_lmt(points, bounds, func, config, seed): x_list = np.array(points) y_list = [] @@ -142,24 +142,24 @@ def get_points_lmt(points, bounds, func, config, seed): regr.fit(x_list, y_list) # ESJ TODO: we actually only needs leaves from here... - leaves, splits, thresholds = parse_linear_tree_regressor(regr, bounds) + leaves, splits, thresholds = _parse_linear_tree_regressor(regr, bounds) # This was originally part of the LMT_Model_component and used to calculate # avg_leaves for the output data. TODO: get this back # self.total_leaves += len(leaves) # bound_point_list = lmt.generate_bound(leaves) - bound_point_list = generate_bound_points(leaves, bounds) + bound_point_list = _generate_bound_points(leaves, bounds) # duct tape to fix possible issues from unknown bugs. TODO should this go # here? 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, + 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, } @@ -194,7 +194,7 @@ def _get_pwl_function_approximation(func, config, bounds): # 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): +def _generate_bound_points(leaves, bounds): bound_points = [] for leaf in leaves.values(): lower_corner_list = [] @@ -226,7 +226,7 @@ def generate_bound_points(leaves, bounds): # 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): +def _parse_linear_tree_regressor(linear_tree_regressor, bounds): leaves = linear_tree_regressor.summary(only_leaves=True) splits = linear_tree_regressor.summary() @@ -248,12 +248,14 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): 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]) + 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]) + 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: @@ -267,7 +269,7 @@ def parse_linear_tree_regressor(linear_tree_regressor, bounds): for leaf in splits[split]['right_leaves']: leaves[leaf]['bounds'][var][0] = splits[split]['th'] - leaves_new = reassign_none_bounds(leaves, bounds) + leaves_new = _reassign_none_bounds(leaves, bounds) splitting_thresholds = {} for split in splits: var = splits[split]['col'] @@ -299,7 +301,7 @@ def _additively_decompose_expr(input_expr, min_dimension): # Populate the "None" bounds with the bounding box bounds for a leaves-dict-tree # amalgamation. -def reassign_none_bounds(leaves, input_bounds): +def _reassign_none_bounds(leaves, input_bounds): L = np.array(list(leaves.keys())) features = np.arange(0, len(leaves[L[0]]['slope'])) @@ -312,7 +314,7 @@ def reassign_none_bounds(leaves, input_bounds): return leaves -def find_leaves(splits, leaves, input_node): +def _find_leaves(splits, leaves, input_node): root_node = input_node leaves_list = [] queue = [root_node] From 9e56e1dfa63c9e4248dc82079a1ae9a8d21af97b Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:58:10 -0600 Subject: [PATCH 132/220] NFC: removing some comments --- pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 8b73ec00678..8e5dd9d8219 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -106,7 +106,6 @@ def _get_uniform_point_grid(bounds, n, func, config): # Issues happen when exactly using the boundary nudge = (ub - lb) * 1e-4 linspaces.append( - # np.linspace(b[0], b[1], n) np.linspace(lb + nudge, ub - nudge, n) ) return list(itertools.product(*linspaces)) @@ -141,17 +140,9 @@ def _get_points_lmt(points, bounds, func, config, seed): ) regr.fit(x_list, y_list) - # ESJ TODO: we actually only needs leaves from here... leaves, splits, thresholds = _parse_linear_tree_regressor(regr, bounds) - # This was originally part of the LMT_Model_component and used to calculate - # avg_leaves for the output data. TODO: get this back - # self.total_leaves += len(leaves) - - # bound_point_list = lmt.generate_bound(leaves) bound_point_list = _generate_bound_points(leaves, bounds) - # duct tape to fix possible issues from unknown bugs. TODO should this go - # here? return bound_point_list @@ -203,7 +194,6 @@ def _generate_bound_points(leaves, bounds): lower_corner_list.append(var_bound[0]) upper_corner_list.append(var_bound[1]) - # Duct tape to fix issues from unknown bugs for pt in [lower_corner_list, upper_corner_list]: for i in range(len(pt)): # clamp within bounds range From 738a8f8fba874cac313a662927e95d752a3102a9 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 15:58:38 -0600 Subject: [PATCH 133/220] Apparently black has an opinion about that --- pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 8e5dd9d8219..5755286eabe 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -105,9 +105,7 @@ def _get_uniform_point_grid(bounds, n, func, config): for lb, ub in bounds: # Issues happen when exactly using the boundary nudge = (ub - lb) * 1e-4 - linspaces.append( - np.linspace(lb + nudge, ub - nudge, n) - ) + linspaces.append(np.linspace(lb + nudge, ub - nudge, n)) return list(itertools.product(*linspaces)) From 92b8ba1beefdf03db846df191927e21f759912e1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 5 Aug 2024 16:04:19 -0600 Subject: [PATCH 134/220] DomainPartitioningMethod imports with the module --- pyomo/contrib/piecewise/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/__init__.py b/pyomo/contrib/piecewise/__init__.py index 5fc75dcd091..1bdb7a70676 100644 --- a/pyomo/contrib/piecewise/__init__.py +++ b/pyomo/contrib/piecewise/__init__.py @@ -33,7 +33,10 @@ from pyomo.contrib.piecewise.transform.convex_combination import ( ConvexCombinationTransformation, ) -from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import NonlinearToPWL +from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( + DomainPartitioningMethod, + NonlinearToPWL, +) from pyomo.contrib.piecewise.transform.nested_inner_repn import ( NestedInnerRepresentationGDPTransformation, ) From 953a39dd0cd354f4d129d84ab3f385d47adcb897 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 6 Aug 2024 09:41:48 -0600 Subject: [PATCH 135/220] Skipping tests when numpy not available --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index f5f4ebc8009..ed3dfd2129f 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -88,6 +88,7 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): self.assertEqual(len(nonlinear), 1) self.assertIn(m.cons, nonlinear) + @skipUnless(numpy_available, "Numpy is not available") def test_log_constraint_uniform_grid(self): m = self.make_model() @@ -111,6 +112,7 @@ def test_log_constraint_uniform_grid(self): (x1, x2, x3) = 1.0009, 5.5, 9.9991 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + @skipUnless(numpy_available, "Numpy is not available") def test_log_constraint_random_grid(self): m = self.make_model() @@ -137,6 +139,7 @@ def test_log_constraint_random_grid(self): x3 = 9.556428757689245 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) + @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) @@ -168,6 +171,7 @@ def test_do_not_transform_quadratic_constraint(self): # neither is the linear one self.assertTrue(m.lin.active) + @skipUnless(numpy_available, "Numpy is not available") def test_constraint_target(self): m = self.make_model() m.quad = Constraint(expr=m.x**2 <= 9) @@ -250,6 +254,7 @@ def test_error_for_non_separable_exceeding_max_dimension(self): max_dimension=4, ) + @skipUnless(numpy_available, "Numpy 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)) @@ -311,6 +316,7 @@ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): nonlinear = n_to_pwl.get_transformed_nonlinear_objectives(m) self.assertEqual(len(nonlinear), 0) + @skipUnless(numpy_available, "Numpy is not available") def test_paraboloid_objective_uniform_grid(self): m = self.make_paraboloid_model() @@ -337,6 +343,7 @@ def test_paraboloid_objective_uniform_grid(self): self.check_pw_linear_paraboloid(m, pwlf, x1, x2, y1, y2) + @skipUnless(numpy_available, "Numpy is not available") def test_objective_target(self): m = self.make_paraboloid_model() From 68b15a5bafc65b224d6661704f9f9fe6b64e22cf Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 6 Aug 2024 09:43:23 -0600 Subject: [PATCH 136/220] HA, she can code... Fixing dumb typos in previous commit --- .../piecewise/tests/test_nonlinear_to_pwl.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index ed3dfd2129f..fec3245edd3 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -9,7 +9,7 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pyomo.common.dependencies import attempt_import, scipy_available +from pyomo.common.dependencies import attempt_import, scipy_available, numpy_available import pyomo.common.unittest as unittest from pyomo.contrib.piecewise import PiecewiseLinearFunction from pyomo.contrib.piecewise.transform.nonlinear_to_pwl import ( @@ -88,7 +88,7 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): self.assertEqual(len(nonlinear), 1) self.assertIn(m.cons, nonlinear) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_log_constraint_uniform_grid(self): m = self.make_model() @@ -112,7 +112,7 @@ def test_log_constraint_uniform_grid(self): (x1, x2, x3) = 1.0009, 5.5, 9.9991 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_log_constraint_random_grid(self): m = self.make_model() @@ -139,7 +139,7 @@ def test_log_constraint_random_grid(self): x3 = 9.556428757689245 self.check_pw_linear_log_x(m, pwlf, x1, x2, x3) - @skipUnless(numpy_available, "Numpy is not available") + @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) @@ -171,7 +171,7 @@ def test_do_not_transform_quadratic_constraint(self): # neither is the linear one self.assertTrue(m.lin.active) - @skipUnless(numpy_available, "Numpy is not available") + @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) @@ -254,7 +254,7 @@ def test_error_for_non_separable_exceeding_max_dimension(self): max_dimension=4, ) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy 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)) @@ -316,7 +316,7 @@ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): nonlinear = n_to_pwl.get_transformed_nonlinear_objectives(m) self.assertEqual(len(nonlinear), 0) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_paraboloid_objective_uniform_grid(self): m = self.make_paraboloid_model() @@ -343,7 +343,7 @@ def test_paraboloid_objective_uniform_grid(self): self.check_pw_linear_paraboloid(m, pwlf, x1, x2, y1, y2) - @skipUnless(numpy_available, "Numpy is not available") + @unittest.skipUnless(numpy_available, "Numpy is not available") def test_objective_target(self): m = self.make_paraboloid_model() From dabafbf4a08ab5f72157dd1488b39c81a5a0b0c1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 6 Aug 2024 09:46:06 -0600 Subject: [PATCH 137/220] Skipping 3 more tests if scipy is not available --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index fec3245edd3..67d0bb9f098 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -255,6 +255,7 @@ def test_error_for_non_separable_exceeding_max_dimension(self): ) @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)) @@ -317,6 +318,7 @@ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): 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() @@ -344,6 +346,7 @@ def test_paraboloid_objective_uniform_grid(self): 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_objective_target(self): m = self.make_paraboloid_model() From 109c0e97352a669293ed97efc127fefe18f6ee66 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 6 Aug 2024 13:52:59 -0600 Subject: [PATCH 138/220] Moving the code to generate the hamiltonian paths data to a comment in its file, and wrapping it in a function so that it doesn't get built when pyomo.environ is imported --- ...nerate_ordered_3d_j1_triangulation_data.py | 93 - .../ordered_3d_j1_triangulation_data.py | 6095 +++++++++-------- pyomo/contrib/piecewise/triangulations.py | 3 +- 3 files changed, 3095 insertions(+), 3096 deletions(-) delete mode 100644 pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py diff --git a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py deleted file mode 100644 index 58c68051cdb..00000000000 --- a/pyomo/contrib/piecewise/generate_ordered_3d_j1_triangulation_data.py +++ /dev/null @@ -1,93 +0,0 @@ -# ___________________________________________________________________________ -# -# 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 networkx as nx -import itertools - -# 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__': - # 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) - - # 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 path from {i} to {(j[0], 1) if (i, (j[0], 1)) in 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) diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index 631b0b3d4ef..edc117df6d7 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -9,3010 +9,3101 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ + +""" +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: + +import networkx as nx +import itertools + +# 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__': + # 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) + + # 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 path from {i} to {(j[0], 1) if (i, (j[0], 1)) in 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. -hamiltonian_paths = { - (((-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)), - ], -} +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/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index c09417ee003..7fba49d6e8d 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -14,7 +14,7 @@ from pyomo.common.errors import DeveloperError from pyomo.common.dependencies import numpy as np from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import ( - hamiltonian_paths as incremental_3d_simplex_pair_to_path, + get_hamiltonian_paths#as incremental_3d_simplex_pair_to_path, ) @@ -317,6 +317,7 @@ def add_top_left(): 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 From 3a11b6cff5839ba3af998944640e4a6382f89a79 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 7 Aug 2024 10:21:15 -0600 Subject: [PATCH 139/220] Adding placeholder for hamiltonian paths tests --- pyomo/contrib/piecewise/tests/test_triangulations.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index db17738b242..a88630c17a2 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -13,6 +13,9 @@ 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 +) from pyomo.contrib.piecewise.triangulations import ( get_unordered_j1_triangulation, get_ordered_j1_triangulation, @@ -220,3 +223,7 @@ def test_grid_hamiltonian_paths(self): self.check_grid_hamiltonian(2, 8) self.check_grid_hamiltonian(3, 5) self.check_grid_hamiltonian(4, 3) + +class TestHamiltonianPaths(unittest.TestCase): + def test_hamiltonian_paths(self): + paths = get_hamiltonian_paths() From 257d24f858d2e740bcb1b6892e200df2c15b0823 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 7 Aug 2024 10:59:44 -0600 Subject: [PATCH 140/220] reverting original change to Set validation --- pyomo/core/base/set.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 27dd3b7c51c..d297ba890fb 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1410,18 +1410,7 @@ def add(self, *values): if self._validate is not None: try: - # differentiate between indexed and non-indexed sets - if self._index is not None: - # indexed set: the value and the index are given - if type(_value) == tuple: - # _value is a tuple: unpack it for the method arguments' tuple - flag = self._validate(_block, (*_value, self._index)) - else: - # _value is not a tuple: no need to unpack it for the method arguments' tuple - flag = self._validate(_block, (_value, self._index)) - else: - # non-indexed set: only the tentative member is given - flag = self._validate(_block, _value) + flag = self._validate(_block, _value) except: logger.error( "Exception raised while validating element '%s' " From 4143140163a5e1c787e5dcc03d894340450f4dee Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Aug 2024 21:17:16 -0400 Subject: [PATCH 141/220] Add further simplification of zero times expr --- pyomo/repn/parameterized_quadratic.py | 21 ++++- .../tests/test_parameterized_quadratic.py | 94 +++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 1dd93955825..edd662ec4a5 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -192,6 +192,17 @@ def is_zero(obj): 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 @@ -211,10 +222,16 @@ def _handle_product_linear_linear(visitor, node, arg1, arg2): 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 - arg1.constant *= arg2.constant + if is_zero_product(arg1.constant, arg2.constant): + arg1.constant = 0 + else: + arg1.constant *= arg2.constant + arg1.multiplier *= arg2.multiplier return _QUADRATIC, arg1 @@ -232,7 +249,7 @@ def _handle_product_nonlinear(visitor, node, arg1, arg2): x1.multiplier = x2.multiplier = 1 # constant term [A1A2] - if is_zero(x1.constant) and is_zero(x2.constant): + if is_zero_product(x1.constant, x2.constant): ans.constant = 0 else: ans.constant = x1.constant * x2.constant diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index a9e1aebd9b2..6a1f0ff0b92 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1371,3 +1371,97 @@ def test_repr_parameterized_quadratic_repn(self): ) 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, + ) From 91c59ade86c107aeef08ec6c5fe46e0f215b5d4a Mon Sep 17 00:00:00 2001 From: jasherma Date: Wed, 7 Aug 2024 21:38:49 -0400 Subject: [PATCH 142/220] Apply black --- pyomo/repn/parameterized_quadratic.py | 5 +---- pyomo/repn/tests/test_parameterized_quadratic.py | 12 ++++-------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index edd662ec4a5..0face2702c7 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -197,10 +197,7 @@ 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)) - ) + return (is_zero(e1) and e2 == e2) or (e1 == e1 and is_zero(e2)) def is_equal_to(obj, val): diff --git a/pyomo/repn/tests/test_parameterized_quadratic.py b/pyomo/repn/tests/test_parameterized_quadratic.py index 6a1f0ff0b92..38f5f8ec8ad 100644 --- a/pyomo/repn/tests/test_parameterized_quadratic.py +++ b/pyomo/repn/tests/test_parameterized_quadratic.py @@ -1394,14 +1394,12 @@ def test_product_var_linear_wrt_yz(self): 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 - ) + 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, + self, repn.to_expression(visitor), m.y * m.x**2 + (m.y + m.z) * m.x ) def test_product_linear_var_wrt_yz(self): @@ -1424,14 +1422,12 @@ def test_product_linear_var_wrt_yz(self): 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 - ) + 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, + self, repn.to_expression(visitor), m.y * m.x**2 + (m.y + m.z) * m.x ) def test_product_var_quadratic(self): From 12227d012b0346c78e64b6d58f5c55855573b684 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:12:43 -0600 Subject: [PATCH 143/220] Move InitializerBase to leverage AutoSlots --- pyomo/core/base/initializer.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index c87a4236abe..e498b711ce8 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 @@ -193,27 +194,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 From 52be7a7e6f550adf949cd3212ed649bcef99f412 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:13:23 -0600 Subject: [PATCH 144/220] Support parameterized initializer functions (that take additional arguments) --- pyomo/core/base/initializer.py | 92 ++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index e498b711ce8..946d6fd3167 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -38,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 @@ -70,9 +71,51 @@ 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 prassing 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 arg.__class__ in function_types: + # 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=arg, + 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: @@ -303,6 +346,18 @@ def __call__(self, parent, idx): return self._fcn(parent, idx) +class ParameterizedIndexedCallInitializer(IndexedCallInitializer): + """IndexedCallCallInitializer 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 @@ -429,6 +484,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. @@ -472,6 +536,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)(*args) + + _bound_sequence_types = collections.defaultdict(None.__class__) From 8f6b9ba570613e7d3c1a92abd7df27f7050b4568 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:14:32 -0600 Subject: [PATCH 145/220] Revert changes to old tests/examples --- examples/pyomo/tutorials/set.py | 2 +- pyomo/core/tests/unit/test_set.py | 4 ++-- pyomo/core/tests/unit/test_sets.py | 8 +++----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/examples/pyomo/tutorials/set.py b/examples/pyomo/tutorials/set.py index 78d5f78cda1..9acdf35460c 100644 --- a/examples/pyomo/tutorials/set.py +++ b/examples/pyomo/tutorials/set.py @@ -183,7 +183,7 @@ def P_init(model, i, j): # Validation of set arrays can also be performed with the _validate_ option. # This is applied to all sets in the array: # -def T_validate(model, value, index): +def T_validate(model, value): return value in model.A diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 37a78bc9a56..6b71201d287 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4328,7 +4328,7 @@ def _lt_3(model, i): m = ConcreteModel() - def _validate_I(model, i, j): + def _validate(model, i, j): self.assertIs(model, m) if i + j < 2: return True @@ -4336,7 +4336,7 @@ def _validate_I(model, i, j): return False raise RuntimeError("Bogus value") - m.I = Set(validate=_validate_I) + m.I = Set(validate=_validate) output = StringIO() with LoggingIntercept(output, 'pyomo.core'): self.assertTrue(m.I.add((0, 1))) diff --git a/pyomo/core/tests/unit/test_sets.py b/pyomo/core/tests/unit/test_sets.py index bd168c7c279..52c4523eaba 100644 --- a/pyomo/core/tests/unit/test_sets.py +++ b/pyomo/core/tests/unit/test_sets.py @@ -2732,7 +2732,7 @@ def test_validation1(self): # 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.A = Set(self.model.Z, validate=lambda model, x: x < 6) with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance(currdir + "setA.dat") @@ -2864,7 +2864,7 @@ def test_other1(self): self.model.A = Set( self.model.Z, initialize={'A': [1, 2, 3, 'A']}, - validate=lambda model, x, i: x in Integers, + validate=lambda model, x: x in Integers, ) with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() @@ -2887,9 +2887,7 @@ def tmp_init(model, i): self.model.n = Param(initialize=5) self.model.Z = Set(initialize=['A']) self.model.A = Set( - self.model.Z, - initialize=tmp_init, - validate=lambda model, x, i: x in Integers, + self.model.Z, initialize=tmp_init, validate=lambda model, x: x in Integers ) with self.assertRaisesRegex(ValueError, ".*violates the validation rule of"): self.instance = self.model.create_instance() From f2a67ee95c607f5fb9a45404d302a1dd61a3bc58 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:18:46 -0600 Subject: [PATCH 146/220] Switch Set to use Initializer to process validate callback --- pyomo/core/base/set.py | 164 +++++++++++++++++++++++++++-------------- 1 file changed, 109 insertions(+), 55 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 0b1aab643cd..1eccb6e3624 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, @@ -1428,7 +1431,7 @@ def update(self, values): val_iter = filter(partial(self._filter, self.parent_block()), val_iter) if self._validate is not None: - val_iter = self._cb_validate(self._validate, self.parent_block(), val_iter) + 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. @@ -1456,22 +1459,82 @@ 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): + failFalse = mode == 'validate' + fcn = getattr(self, '_' + mode) + block = self.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 " + "(block, value, *index).", + version='6.7.4.dev0' + ) + orig_fcn = fcn._fcn + fcn = ParameterizedScalarCallInitializer( + lambda m, v: orig_fcn(m, *v), True + ) + setattr(self, '_' + 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.7.4.dev0' + ) + 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 failFalse: + 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: @@ -2170,7 +2233,7 @@ def __init__(self, *args, **kwds): allow_generators=True, ) ) - self._init_validate = Initializer(kwds.pop('validate', None)) + self._validate = Initializer(kwds.pop('validate', None), additional_args=1) self._init_filter = Initializer(kwds.pop('filter', None)) if 'virtual' in kwds: @@ -2183,6 +2246,19 @@ def __init__(self, *args, **kwds): IndexedComponent.__init__(self, *args, **kwds) + if ( + self._validate.__class__ is ParameterizedIndexedCallInitializer + and not self.parent_component().is_indexed() + ): + # TBD [JDS: 8/2024]: should we deprecate the "expanded + # tuple" version of the validate callback for scalar sets? + # It is widely used and we can (reasonably reliably) map to + # the expected behavior. + orig_fcn = self._validate._fcn + self._validate = ParameterizedScalarCallInitializer( + lambda m, v: orig_fcn(m, *v), True + ) + # HACK to make the "counted call" syntax work. We wait until # after the base class is set up so that is_indexed() is # reliable. @@ -2291,17 +2367,8 @@ 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._validate is not None: + obj._validate = self._validate if self._init_filter is not None: try: obj._filter = Initializer(self._init_filter(_block, index)) @@ -2997,7 +3064,7 @@ 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._validate = Initializer(kwds.pop('validate', None), additional_args=1) self._init_filter = Initializer(kwds.pop('filter', None)) self._init_bounds = kwds.pop('bounds', None) if self._init_bounds is not None: @@ -3192,38 +3259,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 From 9402a8e27d1ec3b5ea57969b30d46f57b6b2a71a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:19:17 -0600 Subject: [PATCH 147/220] Expand tutorial, update tests --- examples/pyomo/tutorials/set.dat | 6 +- examples/pyomo/tutorials/set.py | 10 +++ pyomo/core/tests/unit/test_set.py | 102 +++++++++++++++++++++++++----- 3 files changed, 102 insertions(+), 16 deletions(-) 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.py b/examples/pyomo/tutorials/set.py index 9acdf35460c..220bfbc82da 100644 --- a/examples/pyomo/tutorials/set.py +++ b/examples/pyomo/tutorials/set.py @@ -190,6 +190,16 @@ def T_validate(model, value): 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) + + ## ## Set options ## diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 6b71201d287..74847ad91e9 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4354,27 +4354,99 @@ def _validate(model, i, j): "Exception raised while validating element '(2, 2)' for Set I\n", ) - # Note: one of these indices will trigger the exception in the - # validot when it is called for the index. - def _validate_J(model, i, j, index): - return _validate_I(model, i, j) + 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", + ) - m.J = Set([(0, 0), (2, 2)], validate=_validate_J) - 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, 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=\(4, 1\) violates the validation rule of " r"Set J\[0,0\]", + r"The value=\(1, 0\) violates the validation rule of Set J2\[0,0\]", ): - m.J[0, 0].add((4, 1)) - self.assertEqual(output.getvalue(), "") + 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.J[2, 2].add((2, 2)) + m.J2[2, 2].add((2, 2)) self.assertEqual( - output.getvalue(), - "Exception raised while validating element '(2, 2)' for Set J[2,2]\n", + 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", ) def test_domain(self): From fc1d8eec24739df65b6033a8230379f46201ae1c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:30:27 -0600 Subject: [PATCH 148/220] Move Set filter callback to leverage Initializer / validate handler logic --- pyomo/core/base/set.py | 37 +++++++------------------------ pyomo/core/tests/unit/test_set.py | 6 ++--- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 1eccb6e3624..a66cc99c871 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1428,7 +1428,7 @@ def update(self, values): 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) + val_iter = self._cb_validate_filter('filter', val_iter) if self._validate is not None: val_iter = self._cb_validate_filter('validate', val_iter) @@ -2234,7 +2234,7 @@ def __init__(self, *args, **kwds): ) ) self._validate = Initializer(kwds.pop('validate', None), additional_args=1) - self._init_filter = Initializer(kwds.pop('filter', None)) + self._filter = Initializer(kwds.pop('filter', None), additional_args=1) if 'virtual' in kwds: deprecation_warning( @@ -2369,19 +2369,8 @@ def _getitem_when_not_present(self, index): obj._dimen = _d if self._validate is not None: obj._validate = self._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._filter is not None: + obj._filter = self._filter if self._init_values is not None: # record the user-provided dimen in the initializer self._init_values._dimen = _d @@ -3065,7 +3054,7 @@ def __init__(self, *args, **kwds): kwds.pop('finite', None) self._init_data = (args, kwds.pop('ranges', ())) self._validate = Initializer(kwds.pop('validate', None), additional_args=1) - self._init_filter = Initializer(kwds.pop('filter', None)) + 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) @@ -3216,23 +3205,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, @@ -3243,7 +3222,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]) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 74847ad91e9..41b88f3eb20 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, @@ -4293,8 +4294,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() @@ -5921,7 +5921,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" ) From 7bb1f2ff3f16ddb8408510ec9d6f9733878d51a0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 18:35:27 -0600 Subject: [PATCH 149/220] Remove _validate and _filter from SetData (and only store on container) --- pyomo/core/base/set.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index a66cc99c871..6cbd9f5d5ee 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1301,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) @@ -1310,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): @@ -1427,10 +1425,11 @@ 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: + 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: + 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 @@ -1461,8 +1460,9 @@ def _cb_check_set_end(self, val_iter): def _cb_validate_filter(self, mode, val_iter): failFalse = mode == 'validate' - fcn = getattr(self, '_' + mode) - block = self.parent_block() + comp = self.parent_component() + fcn = getattr(comp, '_' + mode) + block = comp.parent_block() idx = self.index() for value in val_iter: try: @@ -1495,7 +1495,7 @@ def _cb_validate_filter(self, mode, val_iter): fcn = ParameterizedScalarCallInitializer( lambda m, v: orig_fcn(m, *v), True ) - setattr(self, '_' + mode, fcn) + setattr(comp, '_' + mode, fcn) yield value continue except TypeError: @@ -2367,10 +2367,6 @@ def _getitem_when_not_present(self, index): obj._domain = domain if _d is not UnknownSetDimen: obj._dimen = _d - if self._validate is not None: - obj._validate = self._validate - if self._filter is not None: - obj._filter = self._filter if self._init_values is not None: # record the user-provided dimen in the initializer self._init_values._dimen = _d From aa3ea48b6680cfa4e2eee4aa7da2a133ac7d6963 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Fri, 9 Aug 2024 20:57:50 -0600 Subject: [PATCH 150/220] NF: apply black --- pyomo/core/base/set.py | 4 ++-- pyomo/core/tests/unit/test_set.py | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 6cbd9f5d5ee..26f44ffc08a 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1489,7 +1489,7 @@ def _cb_validate_filter(self, mode, val_iter): "callback signature matched (block, *value). " "Please update the callback to match the signature " "(block, value, *index).", - version='6.7.4.dev0' + version='6.7.4.dev0', ) orig_fcn = fcn._fcn fcn = ParameterizedScalarCallInitializer( @@ -1512,7 +1512,7 @@ def _cb_validate_filter(self, mode, val_iter): "callback signature matched (block, *value, *index). " "Please update the callback to match the signature " "(block, value, *index).", - version='6.7.4.dev0' + version='6.7.4.dev0', ) if fcn.__class__ is not ParameterizedInitializer: orig_fcn = fcn._fcn diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index 41b88f3eb20..17a9e254a65 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4361,7 +4361,7 @@ def _validate(model, i, j): 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\)" + r"callback to match the signature \(block, value, \*index\)", ) with LoggingIntercept() as OUT: with self.assertRaisesRegex( @@ -4391,7 +4391,7 @@ def _validate(model, i, j, ind1, ind2): 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\)" + r"callback to match the signature \(block, value, \*index\)", ) with LoggingIntercept() as OUT: @@ -4414,7 +4414,6 @@ def _validate(model, i, j, ind1, ind2): "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 From b24f0a4081d81304a9db9812ebfb644abc87e7d3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sat, 10 Aug 2024 11:11:24 -0600 Subject: [PATCH 151/220] Update baseline with additional Set example --- examples/pyomo/tutorials/set.out | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 From 95068733caf2dc09d2a63c10cf4d0793264b77aa Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 09:04:08 -0600 Subject: [PATCH 152/220] Provide implicit filter callback argument mapping for scalar Sets --- pyomo/core/base/set.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 26f44ffc08a..bfc5c7b6aa0 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -2246,19 +2246,6 @@ def __init__(self, *args, **kwds): IndexedComponent.__init__(self, *args, **kwds) - if ( - self._validate.__class__ is ParameterizedIndexedCallInitializer - and not self.parent_component().is_indexed() - ): - # TBD [JDS: 8/2024]: should we deprecate the "expanded - # tuple" version of the validate callback for scalar sets? - # It is widely used and we can (reasonably reliably) map to - # the expected behavior. - orig_fcn = self._validate._fcn - self._validate = ParameterizedScalarCallInitializer( - lambda m, v: orig_fcn(m, *v), True - ) - # HACK to make the "counted call" syntax work. We wait until # after the base class is set up so that is_indexed() is # reliable. @@ -2277,6 +2264,26 @@ def __init__(self, *args, **kwds): if self._init_dimen.constant(): self._dimen = self._init_dimen(self.parent_block(), None) + if self._validate.__class__ is ParameterizedIndexedCallInitializer: + # TBD [JDS: 8/2024]: should we deprecate the "expanded + # tuple" version of the validate callback for scalar sets? + # It is widely used and we can (reasonably reliably) map to + # the expected behavior... + orig_fcn = self._validate._fcn + self._validate = ParameterizedScalarCallInitializer( + lambda m, v: orig_fcn(m, *v), True + ) + + if self._filter.__class__ is ParameterizedIndexedCallInitializer: + # TBD [JDS: 8/2024]: should we deprecate the "expanded + # tuple" version of the filter callback for scalar sets? + # It is widely used and we can (reasonably reliably) map to + # the expected behavior... + orig_fcn = self._filter._fcn + self._filter = ParameterizedScalarCallInitializer( + lambda m, v: orig_fcn(m, *v), True + ) + @deprecated( "check_values() is deprecated: Sets only contain valid members", version='5.7' ) From f61a98ba4ef2f5fbbc4b2c436261480761cd6b8c Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 09:27:35 -0600 Subject: [PATCH 153/220] Fix typo, variable naming convention --- pyomo/core/base/initializer.py | 2 +- pyomo/core/base/set.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index 946d6fd3167..6e73cc176d1 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -83,7 +83,7 @@ def Initializer( if arg.__class__ in function_types: if allow_generators or inspect.isgeneratorfunction(arg): raise ValueError( - "Generator functions are not allowed when prassing additional args" + "Generator functions are not allowed when passing additional args" ) _args = inspect.getfullargspec(arg) _nargs = len(_args.args) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index bfc5c7b6aa0..7e4744bb9d1 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1459,7 +1459,7 @@ def _cb_check_set_end(self, val_iter): yield value def _cb_validate_filter(self, mode, val_iter): - failFalse = mode == 'validate' + fail_false = mode == 'validate' comp = self.parent_component() fcn = getattr(comp, '_' + mode) block = comp.parent_block() @@ -1524,7 +1524,7 @@ def _cb_validate_filter(self, mode, val_iter): except Exception as e: exc = e if flag is not None: - if failFalse: + if fail_false: raise ValueError( "The value=%s violates the validation rule of Set %s" % (value, self.name) From ead6f9385062f059318c90562dd8a3604a0bf8f1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 09:48:21 -0600 Subject: [PATCH 154/220] Adding a (currently failing) test for cloning a transformed model --- .../piecewise/tests/test_nonlinear_to_pwl.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 67d0bb9f098..81a1e186234 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -112,6 +112,33 @@ def test_log_constraint_uniform_grid(self): (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() From 61bc6fae037c7a44d1872bab79d7a004dc72e7a5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 09:51:44 -0600 Subject: [PATCH 155/220] NFC: removing a comment --- pyomo/contrib/piecewise/triangulations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 7fba49d6e8d..60d701a6be1 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -14,7 +14,7 @@ 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#as incremental_3d_simplex_pair_to_path, + get_hamiltonian_paths, ) From d467bd27009084ff0a51357a208a374e730f672d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 13:12:13 -0600 Subject: [PATCH 156/220] Adding tests for the 3D case Hamiltonian pathsu --- .../ordered_3d_j1_triangulation_data.py | 22 +++++++++++-------- .../piecewise/tests/test_triangulations.py | 19 ++++++++++++++-- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index edc117df6d7..7988ec159cb 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -9,17 +9,10 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - -""" -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: - -import networkx as nx +from pyomo.common.dependencies import networkx as nx import itertools -# 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__': +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)) @@ -46,6 +39,17 @@ 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. diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index a88630c17a2..ea0e287ac6d 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -14,7 +14,8 @@ from unittest import skipUnless import pyomo.common.unittest as unittest from pyomo.contrib.piecewise.ordered_3d_j1_triangulation_data import ( - get_hamiltonian_paths + get_hamiltonian_paths, + _get_double_cube_graph, ) from pyomo.contrib.piecewise.triangulations import ( get_unordered_j1_triangulation, @@ -22,7 +23,7 @@ _get_Gn_hamiltonian, _get_grid_hamiltonian, ) -from pyomo.common.dependencies import numpy as np, numpy_available +from pyomo.common.dependencies import numpy as np, numpy_available, networkx_available from math import factorial import itertools @@ -224,6 +225,20 @@ def test_grid_hamiltonian_paths(self): 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])) From 81d35f4f0e505debb14539dccfbedfc57bb2cac3 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 13:12:47 -0600 Subject: [PATCH 157/220] Black --- pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py | 3 +++ pyomo/contrib/piecewise/tests/test_triangulations.py | 1 + 2 files changed, 4 insertions(+) diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index 7988ec159cb..009107fd6ec 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -12,6 +12,7 @@ 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)) @@ -41,6 +42,7 @@ def _get_double_cube_graph(): 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: @@ -103,6 +105,7 @@ def _get_double_cube_graph(): """ + # 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 diff --git a/pyomo/contrib/piecewise/tests/test_triangulations.py b/pyomo/contrib/piecewise/tests/test_triangulations.py index ea0e287ac6d..7217750dfb1 100644 --- a/pyomo/contrib/piecewise/tests/test_triangulations.py +++ b/pyomo/contrib/piecewise/tests/test_triangulations.py @@ -225,6 +225,7 @@ def test_grid_hamiltonian_paths(self): 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): From 46ef920b3694559c702d70ffd01cdb3ca9eeffee Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 14:48:42 -0600 Subject: [PATCH 158/220] Resolve inconsistency in ParameterizedInitializer call API --- pyomo/core/base/initializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index 6e73cc176d1..3fba3d2b143 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -561,7 +561,7 @@ def indices(self): return self._base_initializer.indices() def __call__(self, parent, idx, *args): - return self._base_initializer(parent, idx)(*args) + return self._base_initializer(parent, idx)(parent, *args) _bound_sequence_types = collections.defaultdict(None.__class__) From 757babaf33d34b30c6feee8320aab56fb9102350 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 14:51:07 -0600 Subject: [PATCH 159/220] Resolve bug when wrapping new function types with additional_args --- pyomo/core/base/initializer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index 3fba3d2b143..072b064d425 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -102,14 +102,17 @@ def Initializer( treat_sequences_as_mappings=treat_sequences_as_mappings, arg_not_specified=arg_not_specified, ) - if arg.__class__ in function_types: + 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=arg, + arg=base_initializer._fcn, allow_generators=allow_generators, treat_sequences_as_mappings=treat_sequences_as_mappings, arg_not_specified=arg_not_specified, From 6a581b1f3f749e24bd7f6e42d71004c32c2e19b2 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 12 Aug 2024 14:52:35 -0600 Subject: [PATCH 160/220] Improve Initializer / Set unit test coverage --- pyomo/core/tests/unit/test_initializer.py | 137 ++++++++++++++++++++++ pyomo/core/tests/unit/test_set.py | 35 ++++++ 2 files changed, 172 insertions(+) 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 4e96e519820..a08202d7c50 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4456,6 +4456,41 @@ def _validate(model, v, ind1, ind2): "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() From 7bd2118ef42a33795cbbfd269cfdea6bd25e1ec5 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 20:32:09 -0600 Subject: [PATCH 161/220] Fixing two bugs with cloning transformed PiecewiseLinearFunctions --- .../piecewise/piecewise_linear_function.py | 16 +++++++++++++--- .../piecewise/tests/test_nonlinear_to_pwl.py | 4 ++-- .../piecewise/transform/nonlinear_to_pwl.py | 10 +++++----- .../transform/piecewise_to_mip_visitor.py | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/pyomo/contrib/piecewise/piecewise_linear_function.py b/pyomo/contrib/piecewise/piecewise_linear_function.py index e92edacc756..cdd977bbda1 100644 --- a/pyomo/contrib/piecewise/piecewise_linear_function.py +++ b/pyomo/contrib/piecewise/piecewise_linear_function.py @@ -43,6 +43,10 @@ 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 @@ -63,8 +67,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 +139,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 +169,7 @@ def __call__(self, x): class _multivariate_linear_functor(AutoSlots.Mixin): - __slots__ = 'normal' + __slots__ = ('normal',) def __init__(self, normal): self.normal = normal diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 81a1e186234..2530fda8fc2 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -77,7 +77,7 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): 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[id(new_cons.body.expr)]) + 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) @@ -331,7 +331,7 @@ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): 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[id(new_obj.expr.expr)]) + 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) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 5755286eabe..c4d7c801ba2 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -641,7 +641,7 @@ def _approximate_expression( return None, expr_type # Additively decompose expr and work on the pieces - pwl_func = 0 + pwl_summands = [] for k, subexpr in enumerate( _additively_decompose_expr( expr, config.min_dimension_to_additively_decompose @@ -661,10 +661,10 @@ def _approximate_expression( "'max_dimension' or additively separating the expression." % (obj.name, config.max_dimension) ) - pwl_func = pwl_func + subexpr + pwl_summands.append(subexpr) continue elif not self._needs_approximating(subexpr, approximate_quadratic)[1]: - pwl_func = pwl_func + subexpr + pwl_summands.append(subexpr) continue # else we approximate subexpr @@ -684,13 +684,13 @@ def eval_expr(*args): # implementation of iadd and dereference the ExpressionData holding # the PiecewiseLinearExpression that we later transform my remapping # it to a Var... - pwl_func = pwl_func + pwlf(*expr_vars) + pwl_summands.append(pwlf(*expr_vars)) # restore var values for v, val in orig_values.items(): v.value = val - return pwl_func, expr_type + return sum(pwl_summands), expr_type def get_src_component(self, cons): data = cons.parent_block().private_data().src_component 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 From 208c9d6ef2e6b464bbad45981e4ed954b42f9892 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 20:32:34 -0600 Subject: [PATCH 162/220] Black --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 2530fda8fc2..2f13532b63f 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -77,7 +77,9 @@ def check_pw_linear_log_x(self, m, pwlf, x1, x2, x3): 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.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) @@ -331,7 +333,9 @@ def check_pw_linear_paraboloid(self, m, pwlf, x1, x2, y1, y2): 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( + 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) From defc953b3a120cfbed1d44356e7a37381664ad12 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 12 Aug 2024 20:36:14 -0600 Subject: [PATCH 163/220] Adding a test for cloning a multivariate piecewise linear function too --- .../piecewise/tests/test_nonlinear_to_pwl.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 2f13532b63f..a42846c2802 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -376,6 +376,36 @@ def test_paraboloid_objective_uniform_grid(self): 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): From 46e5bfb16b8ffc86c19be2314b46233e64c6f0d1 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 09:52:36 -0600 Subject: [PATCH 164/220] Adding linear-tree to optional dependencies --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 6d28e4d184b..4e2b8bd6042 100644 --- a/setup.py +++ b/setup.py @@ -262,6 +262,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 From fc5ebc86cf940caf0579577a870f34866f64d0a2 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 09:58:56 -0600 Subject: [PATCH 165/220] Black being black --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4e2b8bd6042..b1f4a60c4c6 100644 --- a/setup.py +++ b/setup.py @@ -262,7 +262,7 @@ def __ne__(self, other): 'optional': [ 'dill', # No direct use, but improves lambda pickle 'ipython', # contrib.viewer - 'linear-tree', # contrib.piecewise + '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 From 58b5df8d74fa254e152adf1ef4b0b8515bb0e04d Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 13 Aug 2024 10:04:00 -0600 Subject: [PATCH 166/220] test ipopt version before trying get_current_iterate --- .../algorithms/solvers/tests/test_cyipopt_solver.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 60e3a72245e..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(): @@ -369,11 +370,12 @@ def intermediate( # 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+ + # 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, "cyipopt version < 1.3.0" + not cyipopt_available or not cyipopt_ge_1_3 or not ipopt_ge_3_14, + "cyipopt version < 1.3.0", ) - def test_solve_13arg_callback(self): + def test_solve_get_current_iterate(self): m = create_model1() iterate_data = [] From 3eb40d8841637368a5cc7988925d9b9e0ee7dc4f Mon Sep 17 00:00:00 2001 From: Robert Parker Date: Tue, 13 Aug 2024 10:15:50 -0600 Subject: [PATCH 167/220] more specific logging test --- pyomo/core/tests/transform/test_scaling.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyomo/core/tests/transform/test_scaling.py b/pyomo/core/tests/transform/test_scaling.py index c1c8e36904d..4609fef30f4 100644 --- a/pyomo/core/tests/transform/test_scaling.py +++ b/pyomo/core/tests/transform/test_scaling.py @@ -707,7 +707,11 @@ def test_propagate_solution_uninitialized_variable(self): pyo.TransformationFactory("core.scale_model").propagate_solution( scaled_model, m ) - self.assertIn("replacing value of variable", OUTPUT.getvalue()) + 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. From 844b09acad540ee914f866b662a2959ddfac46a7 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 10:16:27 -0600 Subject: [PATCH 168/220] Adding (failing) test for sampling discrete domains --- .../piecewise/tests/test_nonlinear_to_pwl.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index a42846c2802..1428fa4a810 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -9,7 +9,11 @@ # 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 ( @@ -23,9 +27,11 @@ ) from pyomo.core.expr.numeric_expr import SumExpression from pyomo.environ import ( + Binary, ConcreteModel, Var, Constraint, + Integers, TransformationFactory, log, Objective, @@ -303,6 +309,24 @@ def test_do_not_additively_decompose_below_min_dimension(self): # 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.contrib.piecewise', logging.WARNING): + n_to_pwl.apply_to( + m, + num_points=3, + additively_decompose=False, + domain_partitioning_method=DomainPartitioningMethod.UNIFORM_GRID, + ) + self.assertEqual(output.getvalue().strip()) + class TestNonlinearToPWL_2D(unittest.TestCase): def make_paraboloid_model(self): From 11326eb3fb73c43514c8be73b45f7a923dd46a23 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 10:23:21 -0600 Subject: [PATCH 169/220] Only use linear-tree for pypi, it appears to not be on conda --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index cc9760cbe5d..765e50826d0 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -29,7 +29,7 @@ defaults: env: PYTHONWARNINGS: ignore::UserWarning PYTHON_CORE_PKGS: wheel - PYPI_ONLY: z3-solver + PYPI_ONLY: z3-solver linear-tree PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels CACHE_VER: v221013.1 NEOS_EMAIL: tests@pyomo.org From 3c233e13a2a20db4b5fece30792fe3d82b536498 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 12:09:42 -0600 Subject: [PATCH 170/220] Whoops, actually intercepting the logging stream I want to make sure is empty... --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 1428fa4a810..76246688cdf 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -318,14 +318,14 @@ def test_uniform_sampling_discrete_vars(self): n_to_pwl = TransformationFactory('contrib.piecewise.nonlinear_to_pwl') output = StringIO() - with LoggingIntercept(output, 'pyomo.contrib.piecewise', logging.WARNING): + 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, ) - self.assertEqual(output.getvalue().strip()) + self.assertEqual(output.getvalue().strip(), "") class TestNonlinearToPWL_2D(unittest.TestCase): From df7f2b270be51d0b1e952fccbc88e1178bf2fe45 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 13:53:27 -0600 Subject: [PATCH 171/220] Clarify deprecation message --- pyomo/core/base/set.py | 4 ++-- pyomo/core/tests/unit/test_set.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 7e4744bb9d1..dad2feb25dc 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1485,7 +1485,7 @@ def _cb_validate_filter(self, mode, val_iter): flag = fcn(block, (), *vstar) if flag: deprecation_warning( - f"{self.__class__.__name__} {self.name}: {mode} " + f"{self.__class__.__name__} {self.name}: '{mode}=' " "callback signature matched (block, *value). " "Please update the callback to match the signature " "(block, value, *index).", @@ -1508,7 +1508,7 @@ def _cb_validate_filter(self, mode, val_iter): flag = fcn(block, idx, *value) if flag: deprecation_warning( - f"{self.__class__.__name__} {self.name}: {mode} " + f"{self.__class__.__name__} {self.name}: '{mode}=' " "callback signature matched (block, *value, *index). " "Please update the callback to match the signature " "(block, value, *index).", diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index a08202d7c50..b9030bb90f4 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -4367,7 +4367,7 @@ def _validate(model, i, j): self.assertTrue(m.J1[2, 2].add((0, 1))) self.assertRegex( OUT.getvalue().replace('\n', ' '), - r"DEPRECATED: InsertionOrderSetData J1\[2,2\]: validate callback " + 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\)", ) @@ -4397,7 +4397,7 @@ def _validate(model, i, j, ind1, ind2): self.assertTrue(m.J2[2, 2].add((0, 1))) self.assertRegex( OUT.getvalue().replace('\n', ' '), - r"DEPRECATED: InsertionOrderSetData J2\[2,2\]: validate callback " + 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\)", ) From 2d47311720ace0a9c16709e0b650c6d5b5ac07d3 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 13 Aug 2024 13:54:25 -0600 Subject: [PATCH 172/220] Deprecation warning for scalar sets with filter/validate callbacks expecting expanded values --- pyomo/core/base/set.py | 22 +-------------- pyomo/core/tests/unit/test_set.py | 47 ++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 28 deletions(-) diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index dad2feb25dc..964e83a4bba 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1488,7 +1488,7 @@ def _cb_validate_filter(self, mode, val_iter): f"{self.__class__.__name__} {self.name}: '{mode}=' " "callback signature matched (block, *value). " "Please update the callback to match the signature " - "(block, value, *index).", + f"(block, value{', *index' if comp.is_indexed() else ''}).", version='6.7.4.dev0', ) orig_fcn = fcn._fcn @@ -2264,26 +2264,6 @@ def __init__(self, *args, **kwds): if self._init_dimen.constant(): self._dimen = self._init_dimen(self.parent_block(), None) - if self._validate.__class__ is ParameterizedIndexedCallInitializer: - # TBD [JDS: 8/2024]: should we deprecate the "expanded - # tuple" version of the validate callback for scalar sets? - # It is widely used and we can (reasonably reliably) map to - # the expected behavior... - orig_fcn = self._validate._fcn - self._validate = ParameterizedScalarCallInitializer( - lambda m, v: orig_fcn(m, *v), True - ) - - if self._filter.__class__ is ParameterizedIndexedCallInitializer: - # TBD [JDS: 8/2024]: should we deprecate the "expanded - # tuple" version of the filter callback for scalar sets? - # It is widely used and we can (reasonably reliably) map to - # the expected behavior... - orig_fcn = self._filter._fcn - self._filter = ParameterizedScalarCallInitializer( - lambda m, v: orig_fcn(m, *v), True - ) - @deprecated( "check_values() is deprecated: Sets only contain valid members", version='5.7' ) diff --git a/pyomo/core/tests/unit/test_set.py b/pyomo/core/tests/unit/test_set.py index b9030bb90f4..6529c2b60a9 100644 --- a/pyomo/core/tests/unit/test_set.py +++ b/pyomo/core/tests/unit/test_set.py @@ -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,22 +4345,54 @@ 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 I1\n", + ) + + 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 I2", + ): + m.I2.add((4, 1)) + self.assertEqual(output.getvalue(), "") + with LoggingIntercept(module='pyomo.core') as output: + with self.assertRaisesRegex(RuntimeError, "Bogus value"): + m.I2.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 I2\n", ) m.J1 = Set([(0, 0), (2, 2)], validate=_validate) From e890a254bae81b0f9fb0a67a0d0b1d72c97e3ee0 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 17:44:11 -0600 Subject: [PATCH 173/220] Not sampling outside of discrete domains in uniform_grid and random_grid --- .../piecewise/tests/test_nonlinear_to_pwl.py | 78 +++++++++++++++++++ .../piecewise/transform/nonlinear_to_pwl.py | 40 ++++++---- 2 files changed, 103 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 76246688cdf..07a0f090cd6 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -325,8 +325,86 @@ def test_uniform_sampling_discrete_vars(self): 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") + 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") + 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): diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index c4d7c801ba2..03f006b66bc 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -93,19 +93,29 @@ def __init__(self): def _get_random_point_grid(bounds, n, func, config, seed=42): # Generate randomized grid of points linspaces = [] - for lb, ub in bounds: - np.random.seed(seed) - linspaces.append(np.random.uniform(lb, ub, n)) + 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 in bounds: - # Issues happen when exactly using the boundary - nudge = (ub - lb) * 1e-4 - linspaces.append(np.linspace(lb + nudge, ub - nudge, n)) + 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)) @@ -159,8 +169,8 @@ def _get_pwl_function_approximation(func, config, bounds): 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 for each of func's - arguments + 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 @@ -195,8 +205,8 @@ def _generate_bound_points(leaves, bounds): 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]) - pt[i] = min(pt[i], bounds[i][1]) + 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)) @@ -206,7 +216,7 @@ def _generate_bound_points(leaves, bounds): # 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(*bounds): + 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 @@ -296,9 +306,9 @@ def _reassign_none_bounds(leaves, input_bounds): for l in L: for f in features: if leaves[l]['bounds'][f][0] == None: - leaves[l]['bounds'][f][0] = input_bounds[f][0] + 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][1] + leaves[l]['bounds'][f][1] = input_bounds[f][0][1] return leaves @@ -615,7 +625,7 @@ def _get_bounds_list(self, var_list, obj): "at least one bound" % (v.name, obj.name) ) else: - bounds.append(v.bounds) + bounds.append((v.bounds, v.is_integer())) return bounds def _needs_approximating(self, expr, approximate_quadratic): From 10b902603511dba62e93d03286304dab170cd541 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Tue, 13 Aug 2024 17:44:33 -0600 Subject: [PATCH 174/220] black --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 2 +- pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index 07a0f090cd6..bc8d7a40027 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -371,7 +371,7 @@ def test_uniform_sampling_discrete_vars(self): for x in [0, 1]: for y in [0, 1]: for z in [0, 2, 5]: - self.assertIn((x, y, z), points) + self.assertIn((x, y, z), points) @unittest.skipUnless(numpy_available, "Numpy is not available") def test_random_sampling_discrete_vars(self): diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index 03f006b66bc..a35231dd890 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -99,8 +99,9 @@ def _get_random_point_grid(bounds, n, func, config, seed=42): 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)) + linspaces.append( + np.random.choice(range(lb, ub + 1), size=size, replace=False) + ) return list(itertools.product(*linspaces)) @@ -169,7 +170,7 @@ def _get_pwl_function_approximation(func, config, bounds): 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 + 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 From 86d4fffc2413d3109df74f0b40c69f3c37d5529d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Wed, 14 Aug 2024 16:48:56 -0600 Subject: [PATCH 175/220] not triggering scipy install on pypy --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 765e50826d0..2ca7e166fd8 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -30,7 +30,7 @@ env: PYTHONWARNINGS: ignore::UserWarning PYTHON_CORE_PKGS: wheel PYPI_ONLY: z3-solver linear-tree - PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels + PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels linear-tree CACHE_VER: v221013.1 NEOS_EMAIL: tests@pyomo.org SRC_REF: ${{ github.head_ref || github.ref }} From 5bda911a3f43739b2dc933fd350820521ce916d8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Thu, 15 Aug 2024 13:41:31 -0600 Subject: [PATCH 176/220] Skipping two more tests when scipy not available--this really should make the pypy tests pass --- pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py index bc8d7a40027..b937e09ce8b 100644 --- a/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/tests/test_nonlinear_to_pwl.py @@ -342,6 +342,7 @@ def test_uniform_sampling_discrete_vars(self): 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) @@ -374,6 +375,7 @@ def test_uniform_sampling_discrete_vars(self): 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) From 4ff37ce4ae79016815acff329365dd99e3bda5b5 Mon Sep 17 00:00:00 2001 From: jasherma Date: Thu, 15 Aug 2024 15:50:30 -0400 Subject: [PATCH 177/220] Simplify use of `_merge_dict` method --- pyomo/repn/parameterized_quadratic.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/pyomo/repn/parameterized_quadratic.py b/pyomo/repn/parameterized_quadratic.py index 0face2702c7..d818c7c3ed2 100644 --- a/pyomo/repn/parameterized_quadratic.py +++ b/pyomo/repn/parameterized_quadratic.py @@ -31,6 +31,7 @@ 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 @@ -43,25 +44,6 @@ _QUADRATIC = ExprType.QUADRATIC -def _merge_dict(dest_dict, mult, src_dict): - """ - Slightly different from `merge_dict` - in the `parameterized_linear` module. - """ - if not is_equal_to(mult, 1): - for vid, coef in src_dict.items(): - if vid in dest_dict: - dest_dict[vid] += mult * coef - else: - dest_dict[vid] = mult * coef - else: - for vid, coef in src_dict.items(): - if vid in dest_dict: - dest_dict[vid] += coef - else: - dest_dict[vid] = coef - - class ParameterizedQuadraticRepn(QuadraticRepn): def __str__(self): return ( From 804ad0480d91105234246fac50fb02a98c7df54f Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 19 Aug 2024 09:28:06 -0600 Subject: [PATCH 178/220] Moving quadratic repn visitor to instance attribute on nonlinear to pwl transformation --- pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py index a35231dd890..588fa8298f6 100644 --- a/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py +++ b/pyomo/contrib/piecewise/transform/nonlinear_to_pwl.py @@ -66,12 +66,6 @@ class DomainPartitioningMethod(enum.IntEnum): LINEAR_MODEL_TREE_RANDOM = 4 -# This should be safe to use many times; declare it globally -_quadratic_repn_visitor = QuadraticRepnVisitor( - subexpression_cache={}, var_map={}, var_order={}, sorter=None -) - - class _NonlinearToPWLTransformationData(AutoSlots.Mixin): __slots__ = ( 'transformed_component', @@ -502,6 +496,9 @@ def __init__(self): } 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: @@ -630,7 +627,7 @@ def _get_bounds_list(self, var_list, obj): return bounds def _needs_approximating(self, expr, approximate_quadratic): - repn = _quadratic_repn_visitor.walk_expression(expr) + repn = self._quadratic_repn_visitor.walk_expression(expr) if repn.nonlinear is None: if repn.quadratic is None: # Linear constraint. Always skip. From 5c9b6b33d373d8abd41534b8244b28f3862022e8 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 19 Aug 2024 14:19:22 -0600 Subject: [PATCH 179/220] NFC: Reformatting comments --- .../contrib/piecewise/ordered_3d_j1_triangulation_data.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py index 009107fd6ec..283c64cb27f 100644 --- a/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py +++ b/pyomo/contrib/piecewise/ordered_3d_j1_triangulation_data.py @@ -82,14 +82,17 @@ def _get_double_cube_graph(): 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 + # 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 path from {i} to {(j[0], 1) if (i, (j[0], 1)) in all_needed_hamiltonians.keys() else (j[0], 2)}" + 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}") From a477a02167d47714db451b0e790197e0609051a4 Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 19 Aug 2024 14:20:15 -0600 Subject: [PATCH 180/220] Moving direction Enum to module scope, rearranging --- pyomo/contrib/piecewise/triangulations.py | 40 ++++++++++++----------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 60d701a6be1..94c1129767a 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -26,6 +26,20 @@ class Triangulation(Enum): 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 @@ -74,19 +88,6 @@ def get_ordered_j1_triangulation(points, dimension): ) -# 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 - - # Does some validation but mostly assumes the user did the right thing def _process_points_j1(points, dimension): if not len(points[0]) == dimension: @@ -144,6 +145,13 @@ def _get_j1_triangulation(points_map, K, n): 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 @@ -154,12 +162,6 @@ def _get_ordered_j1_triangulation_2d(points_map, num_pts): # 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 - class Direction(Enum): - left = 0 - down = 1 - up = 2 - right = 3 - facing = None simplices = [] From 233b016c4090909cb4a2292d422975f247f5377d Mon Sep 17 00:00:00 2001 From: Emma Johnson Date: Mon, 19 Aug 2024 14:35:46 -0600 Subject: [PATCH 181/220] Emma learns what for-else does --- pyomo/contrib/piecewise/triangulations.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/piecewise/triangulations.py b/pyomo/contrib/piecewise/triangulations.py index 94c1129767a..8eb16a87d86 100644 --- a/pyomo/contrib/piecewise/triangulations.py +++ b/pyomo/contrib/piecewise/triangulations.py @@ -504,8 +504,11 @@ def _fix_vertices_incremental_order(simplices): if simplex[n] in simplices[i + 1] and n != first: last = n break - if first is None or last is None: - raise DeveloperError("Couldn't fix vertex ordering for incremental.") + 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) From e64143ddd27e738c623f9ef0c0c02d0de3ddf40c Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 19 Aug 2024 21:24:53 -0600 Subject: [PATCH 182/220] Update pyomo/core/base/initializer.py --- pyomo/core/base/initializer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/base/initializer.py b/pyomo/core/base/initializer.py index 072b064d425..c15e26855ae 100644 --- a/pyomo/core/base/initializer.py +++ b/pyomo/core/base/initializer.py @@ -350,7 +350,7 @@ def __call__(self, parent, idx): class ParameterizedIndexedCallInitializer(IndexedCallInitializer): - """IndexedCallCallInitializer that accepts additional arguments""" + """IndexedCallInitializer that accepts additional arguments""" __slots__ = () From 8ff7384efe2b2973817cde9db9109b9d66672222 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 19 Aug 2024 23:22:05 -0600 Subject: [PATCH 183/220] Updating the CHANGELOG in preparation for the 6.8.0 release --- CHANGELOG.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d1d1e45e3d..36cd15e8daf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,58 @@ Pyomo CHANGELOG =============== +------------------------------------------------------------------------------- +Pyomo 6.8.0 (20 Aug 2024) +------------------------------------------------------------------------------- + +- General + - Add ParameterizedQuadraticRepn and corresponding walker (#3324) + - Update Pyomo for NumPy 2.0 compatibility (#3292, #3353) + - Add ParameterizedLinearRepn and corresponding walker (#3268) +- 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) +- 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 SHA, repo owner, and branch name (#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 nonlinear-to-piecewise-linear transformation (#3333) + - 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) ------------------------------------------------------------------------------- From 9329e9096908a54397f18dff709b14764c17aec6 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Mon, 19 Aug 2024 23:26:44 -0600 Subject: [PATCH 184/220] More updates to the CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36cd15e8daf..4e24ef7dc94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Pyomo 6.8.0 (20 Aug 2024) 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 the index (#3338) - Solver Interfaces - Resolve NLv2 incompatibility with multithreading (#3332) - Resolve writer performance degradation (#3343) From ced0c8ca68dfc4d7cbbed1d2bdf5eab7f8738b83 Mon Sep 17 00:00:00 2001 From: Bethany Nicholson Date: Tue, 20 Aug 2024 00:07:17 -0600 Subject: [PATCH 185/220] More edits to the CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e24ef7dc94..0df034426a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,9 @@ Pyomo 6.8.0 (20 Aug 2024) - 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) From 4f9904d0f67d4fdf1be7cd23b7543b7686d019cc Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:14:01 -0600 Subject: [PATCH 186/220] Modify line-length --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0df034426a8..b193153fb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ Pyomo 6.8.0 (20 Aug 2024) 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 the index (#3338) + - Support validate/filter for IndexedSet components using index (#3338) - Solver Interfaces - Resolve NLv2 incompatibility with multithreading (#3332) - Resolve writer performance degradation (#3343) @@ -35,7 +35,7 @@ Pyomo 6.8.0 (20 Aug 2024) - Change BARON download URL (#3328) - Disable interface/testing for NEOS/octeract (#3322) - Fix typo in Jenkins driver (#3312) - - Jenkins: update logic for recording SHA, repo owner, and branch name (#3311) + - 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) @@ -47,7 +47,7 @@ Pyomo 6.8.0 (20 Aug 2024) 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 + - 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) From 608a68d10a5e6d642bc9e80b969078c8f1787d59 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:43:00 -0600 Subject: [PATCH 187/220] No cythonization for 3.8-3.10 --- .github/workflows/release_wheel_creation.yml | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 932b0d8eea6..f9d7568237e 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -14,9 +14,6 @@ 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 @@ -31,14 +28,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' + GLOBAL_OPTIONS: '' - wheel-version: 'cp312*' TARGET: 'py312' + GLOBAL_OPTIONS: '' steps: - uses: actions/checkout@v4 - name: Build wheels @@ -53,7 +55,7 @@ jobs: CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 - CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' + CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -72,14 +74,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' + GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' + GLOBAL_OPTIONS: '' - wheel-version: 'cp312*' TARGET: 'py312' + GLOBAL_OPTIONS: '' steps: - uses: actions/checkout@v4 - name: Set up QEMU @@ -97,7 +104,7 @@ jobs: CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 - CIBW_CONFIG_SETTINGS: '--global-option="--with-cython --with-distributable-extensions"' + CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From 76a670e5f275260bd5b2a5c9c26baf926f6420c1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 08:46:38 -0600 Subject: [PATCH 188/220] Change global options for 3.11/3.12 --- .github/workflows/release_wheel_creation.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index f9d7568237e..fe20b32cd29 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -37,10 +37,10 @@ jobs: GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '' + GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '' + GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Build wheels @@ -83,10 +83,10 @@ jobs: GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '' + GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '' + GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Set up QEMU From 4f1c31f494085a65c0aeae55b59f945302259d97 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:07:39 -0600 Subject: [PATCH 189/220] Turn on explicit PEP517 --- .github/workflows/release_wheel_creation.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index fe20b32cd29..cbb4203fcd6 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -28,19 +28,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Build wheels @@ -74,19 +74,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: '--global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '--global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Set up QEMU From 1fc00b535cbf4f3fd9d39f4a5d362d0abedc871c Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:18:55 -0600 Subject: [PATCH 190/220] Change args --- .github/workflows/release_wheel_creation.yml | 22 +++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index cbb4203fcd6..7c364af01f5 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -28,19 +28,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Build wheels @@ -54,6 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 + CIBW_BUILD_FRONTEND: "pip; args: --use-pep517" CIBW_BEFORE_BUILD: pip install cython pybind11 CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 @@ -74,19 +75,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: '--use-pep517 --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' steps: - uses: actions/checkout@v4 - name: Set up QEMU @@ -103,6 +104,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 + CIBW_BUILD_FRONTEND: "pip; args: --use-pep517" CIBW_BEFORE_BUILD: pip install cython pybind11 CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 From 234bd3032688674ec1cb9d80a3a220d5c0abfb2b Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:32:41 -0600 Subject: [PATCH 191/220] Downgrade pip to 23.0.1 --- .github/workflows/release_wheel_creation.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 7c364af01f5..f057fab8726 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -54,8 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BUILD_FRONTEND: "pip; args: --use-pep517" - CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_BEFORE_BUILD: pip install pip==23.0.1 cython pybind11 CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 with: @@ -104,8 +103,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BUILD_FRONTEND: "pip; args: --use-pep517" - CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_BEFORE_BUILD: pip install pip==23.0.1 cython pybind11 CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} - uses: actions/upload-artifact@v4 with: From 251939daa8ec2608f5300bf8a4cde43008129f04 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:42:10 -0600 Subject: [PATCH 192/220] Remove global options - attempting pyproject.toml --- .github/workflows/release_wheel_creation.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index f057fab8726..2ba1502e441 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -28,7 +28,7 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: ' --global-option=--with-cython --with-distributable-extensions"' - wheel-version: 'cp39*' TARGET: 'py39' GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' @@ -54,8 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install pip==23.0.1 cython pybind11 - CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} + CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -103,8 +102,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install pip==23.0.1 cython pybind11 - CIBW_CONFIG_SETTINGS: ${{ matrix.GLOBAL_OPTIONS }} + CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From f84c7cf8438d9780523a3dac9342d1139646aedc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 09:42:31 -0600 Subject: [PATCH 193/220] Add pyproject.toml --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..5d2d6b43154 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[tool.cibuildwheel.config-settings] +--build-option = "--with-distributable-extensions" +--build-option = "--with-cython" + From efe30f1f188a0a108ec9915630461b5b5850ebbc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 09:46:15 -0600 Subject: [PATCH 194/220] Remove pyproject.toml --- pyproject.toml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 5d2d6b43154..00000000000 --- a/pyproject.toml +++ /dev/null @@ -1,4 +0,0 @@ -[tool.cibuildwheel.config-settings] ---build-option = "--with-distributable-extensions" ---build-option = "--with-cython" - From 7868b04950561dceb06d68d0d8aabab6faa4c6df Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:51:33 -0600 Subject: [PATCH 195/220] Use global var instead - PIP_GLOBAL_OPTION --- .github/workflows/release_wheel_creation.yml | 26 ++++++++++++-------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 2ba1502e441..00931c253c1 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -28,21 +28,24 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: ' --global-option=--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 + - name: Set up global variable + run: | + echo "PIP_GLOBAL_OPTION=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 with: @@ -73,21 +76,24 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: ' --global-option="--with-cython --with-distributable-extensions"' + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' + PIP_GLOBAL_OPTION: "--without-cython --with-distributable-extensions" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: ' --global-option="--without-cython --with-distributable-extensions"' + PIP_GLOBAL_OPTION: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 + - name: Set up global variable + run: | + echo "PIP_GLOBAL_OPTION=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3 From de0fe4fc4c085fb1c7c607a26667c6b4009f7600 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 09:56:12 -0600 Subject: [PATCH 196/220] Set in action instead --- .github/workflows/release_wheel_creation.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 00931c253c1..c2b749817f1 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -43,9 +43,6 @@ jobs: GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 - - name: Set up global variable - run: | - echo "PIP_GLOBAL_OPTION=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 with: @@ -57,6 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 + CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: @@ -91,9 +89,6 @@ jobs: PIP_GLOBAL_OPTION: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 - - name: Set up global variable - run: | - echo "PIP_GLOBAL_OPTION=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3 @@ -108,6 +103,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 + CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: From 7183c47340a5c7405cebc9d19a47594cc9eb9658 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:00:02 -0600 Subject: [PATCH 197/220] Change order on env vars --- .github/workflows/release_wheel_creation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index c2b749817f1..64581db39af 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -54,8 +54,8 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -103,8 +103,8 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From a82eb9e43e4486a79b8a65b1593dc77387c314e9 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:03:15 -0600 Subject: [PATCH 198/220] Move env declaration to BEFORE_BUILD --- .github/workflows/release_wheel_creation.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 64581db39af..097d9d3994e 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -54,8 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 - CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" + CIBW_BEFORE_BUILD: pip install cython pybind11 && PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -103,8 +102,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 - CIBW_ENVIRONMENT: PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" + CIBW_BEFORE_BUILD: pip install cython pybind11 && PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From 597ccecb351be4f685823f5b46378397248e22c2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:07:49 -0600 Subject: [PATCH 199/220] Echo into GITHUB_ENV --- .github/workflows/release_wheel_creation.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 097d9d3994e..ccc78a52aec 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -54,7 +54,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 && PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" + CIBW_BEFORE_BUILD: pip install cython pybind11 && echo 'PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}"' >> $GITHUB_ENV - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -102,7 +102,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 && PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}" + CIBW_BEFORE_BUILD: pip install cython pybind11 && echo 'PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}"' >> $GITHUB_ENV - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From a0a37911fb65faf791d4c2297efc212b1d1a60d2 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:13:30 -0600 Subject: [PATCH 200/220] Introduce PYOMO_SETUP_ARGS instead --- .github/workflows/release_wheel_creation.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index ccc78a52aec..93574896f59 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -43,6 +43,9 @@ jobs: GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 + - name: Set up global variable + run: | + echo "PYOMO_SETUP_ARGS=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 with: @@ -54,7 +57,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 && echo 'PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}"' >> $GITHUB_ENV + CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -82,12 +85,15 @@ jobs: GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp311*' TARGET: 'py311' - PIP_GLOBAL_OPTION: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" - wheel-version: 'cp312*' TARGET: 'py312' - PIP_GLOBAL_OPTION: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 + - name: Set up global variable + run: | + echo "PYOMO_SETUP_ARGS=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3 @@ -102,7 +108,7 @@ jobs: CIBW_SKIP: "*-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 - CIBW_BEFORE_BUILD: pip install cython pybind11 && echo 'PIP_GLOBAL_OPTION="${{ matrix.GLOBAL_OPTIONS }}"' >> $GITHUB_ENV + CIBW_BEFORE_BUILD: pip install cython pybind11 - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From 5675db07cb289a56afcb802b904a687ee7ad3070 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Aug 2024 10:17:46 -0600 Subject: [PATCH 201/220] Reqork setup configuration to support arguments and PYOMO_SETUP_ARGS environment --- setup.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/setup.py b/setup.py index b1f4a60c4c6..735c2734594 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. From 02d01bbf81f0956d0f16228f264abd4628603fc9 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Aug 2024 10:33:32 -0600 Subject: [PATCH 202/220] debugging --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 735c2734594..071d673c5e6 100644 --- a/setup.py +++ b/setup.py @@ -58,7 +58,9 @@ def check_config_arg(name): sys.argv.remove(name) return True if name in os.getenv('PYOMO_SETUP_ARGS', "").split(): + print(f"FOUND {name}") return True + print(f"NOT FOUND {name}") return False @@ -134,6 +136,7 @@ def check_config_arg(name): ) ext_modules.append(appsi_extension) +print(f"\nEXTENSIONS: {ext_modules}\n") class DependenciesCommand(Command): """Custom setuptools command From 1f3d749f55ec7bc52af9ba05aba62649f2ef05ec Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Aug 2024 10:38:21 -0600 Subject: [PATCH 203/220] More debugging --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 071d673c5e6..193dc229233 100644 --- a/setup.py +++ b/setup.py @@ -54,9 +54,12 @@ def get_version(): def check_config_arg(name): + print(f"SEARCHING FOR '{name}' in '{sys.argv}'") if name in sys.argv: sys.argv.remove(name) return True + print(f"SEARCHING FOR '{name}' in '{os.getenv('PYOMO_SETUP_ARGS', "").split()}'") + print(" ", os.getenv('PYOMO_SETUP_ARGS', "")) if name in os.getenv('PYOMO_SETUP_ARGS', "").split(): print(f"FOUND {name}") return True From a258d7d82c6b8f36d86a98f4810bc370af2f3d1a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Aug 2024 10:40:25 -0600 Subject: [PATCH 204/220] Fix typo --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 193dc229233..30429a55f70 100644 --- a/setup.py +++ b/setup.py @@ -58,9 +58,9 @@ def check_config_arg(name): if name in sys.argv: sys.argv.remove(name) return True - print(f"SEARCHING FOR '{name}' in '{os.getenv('PYOMO_SETUP_ARGS', "").split()}'") + print(f"SEARCHING FOR '{name}' in '{os.getenv('PYOMO_SETUP_ARGS', '').split()}'") print(" ", os.getenv('PYOMO_SETUP_ARGS', "")) - if name in os.getenv('PYOMO_SETUP_ARGS', "").split(): + if name in os.getenv('PYOMO_SETUP_ARGS', '').split(): print(f"FOUND {name}") return True print(f"NOT FOUND {name}") From 8b7edf85c4100b46d221d80786f4ac447266195b Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:45:20 -0600 Subject: [PATCH 205/220] Move PYOMO_SETUP_ARGS to CIWB_ENVIRONMENT --- .github/workflows/release_wheel_creation.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 93574896f59..fad91c72ccc 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -43,9 +43,6 @@ jobs: GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 - - name: Set up global variable - run: | - echo "PYOMO_SETUP_ARGS=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Build wheels uses: pypa/cibuildwheel@v2.16.5 with: @@ -58,6 +55,7 @@ jobs: CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} @@ -91,9 +89,6 @@ jobs: GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 - - name: Set up global variable - run: | - echo "PYOMO_SETUP_ARGS=${{ matrix.GLOBAL_OPTIONS }}" >> $GITHUB_ENV - name: Set up QEMU if: runner.os == 'Linux' uses: docker/setup-qemu-action@v3 @@ -109,6 +104,7 @@ jobs: CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: name: alt_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From 10508b6bacec170677ff70ff468d8e95fe7e023a Mon Sep 17 00:00:00 2001 From: John Siirola Date: Tue, 20 Aug 2024 10:51:25 -0600 Subject: [PATCH 206/220] Remove debugging --- setup.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/setup.py b/setup.py index 30429a55f70..63c6891bda2 100644 --- a/setup.py +++ b/setup.py @@ -54,16 +54,11 @@ def get_version(): def check_config_arg(name): - print(f"SEARCHING FOR '{name}' in '{sys.argv}'") if name in sys.argv: sys.argv.remove(name) return True - print(f"SEARCHING FOR '{name}' in '{os.getenv('PYOMO_SETUP_ARGS', '').split()}'") - print(" ", os.getenv('PYOMO_SETUP_ARGS', "")) if name in os.getenv('PYOMO_SETUP_ARGS', '').split(): - print(f"FOUND {name}") return True - print(f"NOT FOUND {name}") return False @@ -139,7 +134,6 @@ def check_config_arg(name): ) ext_modules.append(appsi_extension) -print(f"\nEXTENSIONS: {ext_modules}\n") class DependenciesCommand(Command): """Custom setuptools command From 9b92cb4e01ce9809b4b78d13c1a96d46a9a4562b Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 10:55:41 -0600 Subject: [PATCH 207/220] Add skip test for win 3.11, 3.12 --- .github/workflows/release_wheel_creation.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 93574896f59..11c52b5fd38 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -58,6 +58,7 @@ jobs: CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 + CIBW_TEST_SKIP: "*{311,312}-win*" - uses: actions/upload-artifact@v4 with: name: native_wheels-${{ matrix.os }}-${{ matrix.TARGET }} From 6af5d7d9a1fccd06e0719cecb0d97eef03f0385a Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:08:13 -0600 Subject: [PATCH 208/220] Create two pure-python wheels --- .github/workflows/release_wheel_creation.yml | 29 +++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index e03082e964a..5af870da9b6 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -15,13 +15,40 @@ concurrency: cancel-in-progress: true jobs: + pure-python: + name: Build pure wheels (${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + python-version: ['3.11', '3.12'] + 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 generic tarball + run: | + python setup.py --without-cython sdist --format=gztar bdist_wheel + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: purepython_wheel-${{ matrix.python-version }} + path: dist/*.whl + overwrite: true + native_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture runs-on: ${{ matrix.os }} strategy: fail-fast: true matrix: - os: [ubuntu-22.04, windows-latest, macos-latest] + os: [ubuntu-22.04, windows-latest, macos-13] arch: [all] wheel-version: ['cp38*', 'cp39*', 'cp310*', 'cp311*', 'cp312*'] From 44da84e64d20f020879752a2b8f22712362ddaeb Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:09:04 -0600 Subject: [PATCH 209/220] Fix typo --- .github/workflows/release_wheel_creation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 5af870da9b6..6013437f5d4 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -15,7 +15,7 @@ concurrency: cancel-in-progress: true jobs: - pure-python: + pure_python: name: Build pure wheels (${{ matrix.python-version }}) runs-on: ubuntu-latest strategy: From 75d4f718d41b47d11692970337a86371527138f1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:11:05 -0600 Subject: [PATCH 210/220] YAML incorrectness --- .github/workflows/release_wheel_creation.yml | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 6013437f5d4..a903a91e077 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -22,25 +22,25 @@ jobs: fail-fast: true matrix: python-version: ['3.11', '3.12'] - 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 generic tarball - run: | - python setup.py --without-cython sdist --format=gztar bdist_wheel - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: purepython_wheel-${{ matrix.python-version }} - path: dist/*.whl - overwrite: true + 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 generic tarball + run: | + python setup.py --without-cython sdist --format=gztar bdist_wheel + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: purepython_wheel-${{ matrix.python-version }} + path: dist/*.whl + overwrite: true native_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture From 8e7003a9767f7eedb432900e386a31b7010cbdc4 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:14:02 -0600 Subject: [PATCH 211/220] Skip windows on 3.11, 3.12 --- .github/workflows/release_wheel_creation.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index a903a91e077..3b984b4f0c7 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -78,11 +78,10 @@ jobs: CIBW_ARCHS_LINUX: "native" CIBW_ARCHS_MACOS: "native arm64" CIBW_ARCHS_WINDOWS: "native ARM64" - CIBW_SKIP: "*-musllinux*" + CIBW_SKIP: "*-musllinux* *{311,312}-win*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 - CIBW_TEST_SKIP: "*{311,312}-win*" CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}" - uses: actions/upload-artifact@v4 with: From 0047b9b07553a247cd07e2855135d3fe4d1773e0 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:19:41 -0600 Subject: [PATCH 212/220] Turn off universal; change regex for win --- .github/workflows/release_wheel_creation.yml | 2 +- setup.cfg | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 3b984b4f0c7..1f95a06e670 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -78,7 +78,7 @@ jobs: CIBW_ARCHS_LINUX: "native" CIBW_ARCHS_MACOS: "native arm64" CIBW_ARCHS_WINDOWS: "native ARM64" - CIBW_SKIP: "*-musllinux* *{311,312}-win*" + CIBW_SKIP: "cp{311,312}-win32 cp{311,312}-win_amd64 cp{311,312}-win_arm64 *-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 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 From 1fd14660e1e9ed18782a7c607c9fa1c83d59acfe Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:27:04 -0600 Subject: [PATCH 213/220] Use exclude tag instead --- .github/workflows/release_wheel_creation.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 1f95a06e670..b3818b98d44 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: true matrix: - python-version: ['3.11', '3.12'] + python-version: ['3.11'] steps: - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} @@ -68,6 +68,13 @@ jobs: - 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 @@ -78,8 +85,8 @@ jobs: CIBW_ARCHS_LINUX: "native" CIBW_ARCHS_MACOS: "native arm64" CIBW_ARCHS_WINDOWS: "native ARM64" - CIBW_SKIP: "cp{311,312}-win32 cp{311,312}-win_amd64 cp{311,312}-win_arm64 *-musllinux*" CIBW_BUILD: ${{ matrix.wheel-version }} + CIBW_SKIP: "*-musllinux*" CIBW_BUILD_VERBOSITY: 1 CIBW_BEFORE_BUILD: pip install cython pybind11 CIBW_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}" @@ -127,8 +134,8 @@ 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_ENVIRONMENT: PYOMO_SETUP_ARGS="${{ matrix.GLOBAL_OPTIONS }}" From 6093f91930de9250cabedbf1fdda9a621bb3c5cc Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:34:41 -0600 Subject: [PATCH 214/220] Reduce repeat code --- .github/workflows/release_wheel_creation.yml | 76 ++++++++++---------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index b3818b98d44..7fe3811dc00 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -14,33 +14,11 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true +env: + WITH_CYTHON: "--with-cython --with-distributable-extensions" + WITHOUT_CYTHON: "--without-cython --with-distributable-extensions" + jobs: - pure_python: - name: Build pure wheels (${{ matrix.python-version }}) - runs-on: ubuntu-latest - strategy: - fail-fast: true - 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 generic tarball - run: | - python setup.py --without-cython sdist --format=gztar bdist_wheel - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: purepython_wheel-${{ matrix.python-version }} - path: dist/*.whl - overwrite: true native_wheels: name: Build wheels (${{ matrix.wheel-version }}) on ${{ matrix.os }} for native and cross-compiled architecture @@ -55,19 +33,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} exclude: - wheel-version: 'cp311*' @@ -108,19 +86,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" + GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} steps: - uses: actions/checkout@v4 - name: Set up QEMU @@ -145,6 +123,32 @@ jobs: path: dist/*.whl overwrite: true + pure_python: + name: Build pure wheels (${{ matrix.python-version }}) + 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: purepython_wheel-${{ matrix.python-version }} + path: dist/*.whl + overwrite: true + generictarball: name: ${{ matrix.TARGET }} runs-on: ${{ matrix.os }} From f1132e618f461dfafe4aae316b3d851595f6bf59 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:36:06 -0600 Subject: [PATCH 215/220] Not 'env' --- .github/workflows/release_wheel_creation.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 7fe3811dc00..436f1126bd1 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -33,19 +33,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} + GLOBAL_OPTIONS: $WITHOUT_CYTHON - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} + GLOBAL_OPTIONS: $WITHOUT_CYTHON exclude: - wheel-version: 'cp311*' @@ -86,19 +86,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: ${{ env.WITH_CYTHON }} + GLOBAL_OPTIONS: $WITH_CYTHON - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} + GLOBAL_OPTIONS: $WITHOUT_CYTHON - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: ${{ env.WITHOUT_CYTHON }} + GLOBAL_OPTIONS: $WITHOUT_CYTHON steps: - uses: actions/checkout@v4 - name: Set up QEMU From 2e9746c3e29ecb376579bd5f39eef57e1573b520 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:38:20 -0600 Subject: [PATCH 216/220] Quotes around var --- .github/workflows/release_wheel_creation.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 436f1126bd1..61441e80dbe 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -33,19 +33,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: $WITHOUT_CYTHON + GLOBAL_OPTIONS: "$WITHOUT_CYTHON" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: $WITHOUT_CYTHON + GLOBAL_OPTIONS: "$WITHOUT_CYTHON" exclude: - wheel-version: 'cp311*' @@ -86,19 +86,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: $WITH_CYTHON + GLOBAL_OPTIONS: "$WITH_CYTHON" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: $WITHOUT_CYTHON + GLOBAL_OPTIONS: "$WITHOUT_CYTHON" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: $WITHOUT_CYTHON + GLOBAL_OPTIONS: "$WITHOUT_CYTHON" steps: - uses: actions/checkout@v4 - name: Set up QEMU From d612cf65b8e2ba2dc311e41cf283032abf03cbc7 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 11:41:21 -0600 Subject: [PATCH 217/220] Revert env changes; rename pure python area --- .github/workflows/release_wheel_creation.yml | 28 +++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index 61441e80dbe..da17978a4a3 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -14,10 +14,6 @@ concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true -env: - WITH_CYTHON: "--with-cython --with-distributable-extensions" - WITHOUT_CYTHON: "--without-cython --with-distributable-extensions" - jobs: native_wheels: @@ -33,19 +29,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: "$WITHOUT_CYTHON" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: "$WITHOUT_CYTHON" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" exclude: - wheel-version: 'cp311*' @@ -86,19 +82,19 @@ jobs: include: - wheel-version: 'cp38*' TARGET: 'py38' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp39*' TARGET: 'py39' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp310*' TARGET: 'py310' - GLOBAL_OPTIONS: "$WITH_CYTHON" + GLOBAL_OPTIONS: "--with-cython --with-distributable-extensions" - wheel-version: 'cp311*' TARGET: 'py311' - GLOBAL_OPTIONS: "$WITHOUT_CYTHON" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" - wheel-version: 'cp312*' TARGET: 'py312' - GLOBAL_OPTIONS: "$WITHOUT_CYTHON" + GLOBAL_OPTIONS: "--without-cython --with-distributable-extensions" steps: - uses: actions/checkout@v4 - name: Set up QEMU @@ -124,7 +120,7 @@ jobs: overwrite: true pure_python: - name: Build pure wheels (${{ matrix.python-version }}) + name: pure_python_wheel runs-on: ubuntu-latest strategy: matrix: @@ -145,7 +141,7 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: purepython_wheel-${{ matrix.python-version }} + name: purepythonwheel path: dist/*.whl overwrite: true From cee756c0796ce636dfa378f5bfc7ea4e07b41822 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 12:03:42 -0600 Subject: [PATCH 218/220] Explicitly state the different archs --- .github/workflows/release_wheel_creation.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_wheel_creation.yml b/.github/workflows/release_wheel_creation.yml index da17978a4a3..d439dafaf0a 100644 --- a/.github/workflows/release_wheel_creation.yml +++ b/.github/workflows/release_wheel_creation.yml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: true matrix: - os: [ubuntu-22.04, windows-latest, macos-13] + os: [ubuntu-22.04, windows-latest, macos-latest] arch: [all] wheel-version: ['cp38*', 'cp39*', 'cp310*', 'cp311*', 'cp312*'] @@ -57,8 +57,8 @@ jobs: output-dir: dist env: CIBW_ARCHS_LINUX: "native" - CIBW_ARCHS_MACOS: "native arm64" - CIBW_ARCHS_WINDOWS: "native ARM64" + CIBW_ARCHS_MACOS: "x86_64 arm64" + CIBW_ARCHS_WINDOWS: "AMD64 ARM64" CIBW_BUILD: ${{ matrix.wheel-version }} CIBW_SKIP: "*-musllinux*" CIBW_BUILD_VERBOSITY: 1 From ee36b450f80cd016991808103dc999909daa69af Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 12:24:00 -0600 Subject: [PATCH 219/220] Update for 6.8.0 --- .coin-or/projDesc.xml | 4 +-- CHANGELOG.md | 8 ++++++ RELEASE.md | 27 ++++++++----------- pyomo/contrib/doe/__init__.py | 6 ++--- .../pynumero/interfaces/cyipopt_interface.py | 6 ++--- pyomo/core/base/set.py | 4 +-- pyomo/repn/plugins/nl_writer.py | 2 +- pyomo/version/info.py | 8 +++--- 8 files changed, 34 insertions(+), 31 deletions(-) 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/CHANGELOG.md b/CHANGELOG.md index b193153fb6e..5d16105d24b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,18 @@ 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) 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/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index ffb6df1a860..ef1207deab2 100644 --- a/pyomo/contrib/doe/__init__.py +++ b/pyomo/contrib/doe/__init__.py @@ -25,7 +25,7 @@ @deprecated( "Use of MeasurementVariables in Pyomo.DoE is no longer supported.", - version='6.7.4.dev0', + version='6.8.0', ) class MeasurementVariables: def __init__(self, *args): @@ -33,7 +33,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 +41,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/pynumero/interfaces/cyipopt_interface.py b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py index 5187efadac9..98916e11b48 100644 --- a/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py +++ b/pyomo/contrib/pynumero/interfaces/cyipopt_interface.py @@ -310,7 +310,7 @@ def __init__(self, nlp, intermediate_callback=None, halt_on_evaluation_error=Non # cyipopt.Problem.__init__ super(CyIpoptNLP, self).__init__() - # Pre-Pyomo 6.7.4.dev0, we had no way to pass the cyipopt.Problem object + # 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 @@ -496,7 +496,7 @@ def intermediate( """ if self._intermediate_callback is not None: if self._use_13arg_callback: - # This is the callback signature expected as of Pyomo 6.7.4.dev0 + # This is the callback signature expected as of Pyomo 6.8.0 return self._intermediate_callback( self._nlp, self, @@ -513,7 +513,7 @@ def intermediate( ls_trials, ) else: - # This is the callback signature expected pre-Pyomo 6.7.4.dev0 and + # This is the callback signature expected pre-Pyomo 6.8.0 and # is supported for backwards compatibility. return self._intermediate_callback( self._nlp, diff --git a/pyomo/core/base/set.py b/pyomo/core/base/set.py index 964e83a4bba..69b21c4d78b 100644 --- a/pyomo/core/base/set.py +++ b/pyomo/core/base/set.py @@ -1489,7 +1489,7 @@ def _cb_validate_filter(self, mode, val_iter): "callback signature matched (block, *value). " "Please update the callback to match the signature " f"(block, value{', *index' if comp.is_indexed() else ''}).", - version='6.7.4.dev0', + version='6.8.0', ) orig_fcn = fcn._fcn fcn = ParameterizedScalarCallInitializer( @@ -1512,7 +1512,7 @@ def _cb_validate_filter(self, mode, val_iter): "callback signature matched (block, *value, *index). " "Please update the callback to match the signature " "(block, value, *index).", - version='6.7.4.dev0', + version='6.8.0', ) if fcn.__class__ is not ParameterizedInitializer: orig_fcn = fcn._fcn 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/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': From feef6d905588f5dbd4624ec7094a3641e542d865 Mon Sep 17 00:00:00 2001 From: Miranda Mundt Date: Tue, 20 Aug 2024 12:32:16 -0600 Subject: [PATCH 220/220] Apply black --- pyomo/contrib/doe/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyomo/contrib/doe/__init__.py b/pyomo/contrib/doe/__init__.py index ef1207deab2..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.8.0', + "Use of MeasurementVariables in Pyomo.DoE is no longer supported.", version='6.8.0' ) class MeasurementVariables: def __init__(self, *args):