diff --git a/cpmpy/expressions/core.py b/cpmpy/expressions/core.py index cc1ac040d..149a3fee8 100644 --- a/cpmpy/expressions/core.py +++ b/cpmpy/expressions/core.py @@ -829,7 +829,7 @@ def _wsum_should(arg): all substractions are transformed into less readable wsums) """ return isinstance(arg, Operator) and \ - (arg.name == 'wsum' or \ + (arg.name == 'sum' or arg.name == 'wsum' or \ (arg.name == 'mul' and len(arg.args) == 2 and \ any(is_num(a) for a in arg.args) ) ) diff --git a/cpmpy/expressions/globalconstraints.py b/cpmpy/expressions/globalconstraints.py index 0715b7fff..0a318b385 100644 --- a/cpmpy/expressions/globalconstraints.py +++ b/cpmpy/expressions/globalconstraints.py @@ -194,7 +194,18 @@ def __init__(self, *args): def decompose(self): """Returns the decomposition """ - return [var1 != var2 for var1, var2 in all_pairs(self.args)], [] + if False: + return [var1 != var2 for var1, var2 in all_pairs(self.args)], [] + else: + # DO NOT COMMIT IN MAINLINE yet + # switch to ILP friendly decomposition + cons = [] + lbs, ubs = get_bounds(self.args) + M = [cp.MapDomain(var) for var in self.args if isinstance(var, Expression)] + for val in range(min(lbs), max(ubs)+1): + # each value can be taken at most once (not necessarily exactly once) + cons.append(cp.sum(x == val for x in self.args) <= 1) + return cons, M def value(self): return len(set(argvals(self.args))) == len(self.args) @@ -210,8 +221,20 @@ def __init__(self, arr, n): super().__init__("alldifferent_except_n", [flatarr, n]) def decompose(self): - # equivalent to (var1 == n) | (var2 == n) | (var1 != var2) - return [(var1 == var2).implies(cp.any(var1 == a for a in self.args[1])) for var1, var2 in all_pairs(self.args[0])], [] + if False: + # equivalent to (var1 == n) | (var2 == n) | (var1 != var2) + return [(var1 == var2).implies(cp.any(var1 == a for a in self.args[1])) for var1, var2 in all_pairs(self.args[0])], [] + else: + # DO NOT COMMIT IN MAINLINE yet + # switch to ILP friendly decomposition + cons = [] + arr, n = self.args + lbs, ubs = get_bounds(arr) + for val in range(min(lbs), max(ubs)+1): + if val != n: + # each value can be taken at most once (not necessarily exactly once) + cons.append(cp.sum(x == val for x in arr) <= 1) + return cons, [MapDomain(x) for x in arr] def value(self): vals = [argval(a) for a in self.args[0] if argval(a) not in argvals(self.args[1])] @@ -246,8 +269,19 @@ def __init__(self, *args): def decompose(self): """Returns the decomposition """ - # arg0 == arg1, arg1 == arg2, arg2 == arg3... no need to post n^2 equalities - return [var1 == var2 for var1, var2 in zip(self.args[:-1], self.args[1:])], [] + # Not sure we need the Boolean version here... + if False: + # arg0 == arg1, arg1 == arg2, arg2 == arg3... no need to post n^2 equalities + return [var1 == var2 for var1, var2 in zip(self.args[:-1], self.args[1:])], [] + else: + # DO NOT COMMIT IN MAINLINE yet + # switch to ILP friendly decomposition + cons = [] + lbs, ubs = get_bounds(self.args) + for val in range(min(lbs), max(ubs)+1): + # each value can be taken at most once (not necessarily exactly once) + cons.extend((x1 == val) == (x2 == val) for x1,x2 in all_pairs(self.args)) + return cons, [MapDomain(x) for x in self.args] def value(self): return len(set(argvals(self.args))) == 1 @@ -265,8 +299,21 @@ def __init__(self, arr, n): super().__init__("allequal_except_n", [flatarr, n]) def decompose(self): - return [(cp.any(var1 == a for a in self.args[1]) | (var1 == var2) | cp.any(var2 == a for a in self.args[1])) - for var1, var2 in all_pairs(self.args[0])], [] + # Not sure we need the Boolean version here... + if False: + return [(cp.any(var1 == a for a in self.args[1]) | (var1 == var2) | cp.any(var2 == a for a in self.args[1])) + for var1, var2 in all_pairs(self.args[0])], [] + else: + # DO NOT COMMIT IN MAINLINE yet + # switch to ILP friendly decomposition + cons = [] + arr, n = self.args + lbs, ubs = get_bounds(arr) + for val in range(min(lbs), max(ubs)+1): + if val != n: + # each value can be taken at most once (not necessarily exactly once) + cons.append((x1 == val) == (x2 == val) for x1,x2 in all_pairs(arr)) + return cons, [MapDomain(x) for x in arr] def value(self): vals = [argval(a) for a in self.args[0] if argval(a) not in argvals(self.args[1])] @@ -303,6 +350,7 @@ def decompose(self): MiniZinc has slightly different one: https://github.com/MiniZinc/libminizinc/blob/master/share/minizinc/std/fzn_circuit.mzn """ + # XXX Right, so for ILP solvers, we would ideally do lazy subcircuit elimination (otherwise MTZ)... succ = cpm_array(self.args) n = len(succ) order = intvar(0,n-1, shape=n) @@ -386,6 +434,7 @@ def decompose(self): lb, ub = get_bounds(x) if lb >= 0 and ub < len(rev): # safe, index is within bounds + # XXX Both rev and x are variables... is there an ILP friendly decomp? constraining.append(rev[x] == i) else: # partial! need safening here is_defined, total_expr, toplevel = cp.transformations.safening._safen_range(rev[x], (0, len(rev)-1), 1) @@ -620,7 +669,10 @@ def decompose(self): if expressions: return [cp.any(expr == a for a in arr)], defining else: - return [expr != val for val in range(lb, ub + 1) if val not in arr], defining + # XXX do we properly capture that x!=v with b:=x==v should be ~b? + # Can't do MapDomain, would need to check if `expr` is a variable... + # TODO is this even efficient when there are many gaps? (similar to NotInDomain) + return [expr != val for val in range(lb, ub + 1) if val not in arr] + [expr >= min(arr), expr <= max(arr)], defining def value(self): @@ -629,6 +681,62 @@ def value(self): def __repr__(self): return "{} in {}".format(self.args[0], self.args[1]) +class MapDomain(GlobalConstraint): + """ + Maps an integer decision variable to an array of Boolean variables, + one for each value in the domain, e.g. |ub+1 - lb| Boolean variables. + + No Boolean variables are returned or accessible; + after declaring this constraint, just + use 'ivar == v' and it will be replaced by the correct Boolean ?in flatten()?. + + Note: the `decompose()` takes an optional `csemap` and `is_supported` argument! + + TODO: what with solvers (e.g. SMT) that do not need flatten? + (they also don't need us to do CSE, so might be fine?) + + TODO: multiple (e.g. by global constraints) MapDomain's can be declared + for the same integer variable... + """ + def __init__(self, ivar): + super().__init__("mapdomain", [ivar]) + + def decompose(self, is_supported=False, csemap=None): + """ + is_supported: if True, only the CSE map is filled + (yes, ortools declares a global for this, called... MapDomain ) + csemap: if given, will populate the csemap with the Boolean variables + """ + ivar = self.args[0] + lb, ub = get_bounds(ivar) + + bvs = cp.boolvar(shape=(ub+1-lb,), name=f"B#{ivar.name}") + all_in_csemap = True + if csemap is not None: + for i,v in enumerate(range(lb, ub+1)): + expr = (ivar == v) + if expr in csemap: + bvs[i] = csemap[expr] # overwrite with pre-created one + else: + all_in_csemap = False + csemap[expr] = bvs[i] + + if is_supported: + # TRICKY HACK to use 2nd argument as all_in_csemap... + return [], all_in_csemap + + # ILP friendly decomposition + # TODO: if the 'ivar' is eliminated from the model, no need for 2nd constraint... + # TRICKY HACK to use 2nd argument as all_in_csemap... + return [cp.sum(bvs) == 1, + cp.sum(bvs[i]*v for i,v in enumerate(range(lb, ub+1))) == ivar], all_in_csemap + + def value(self): + # not much to say... + return True + + def __repr__(self): + return f"MapDomain({self.args[0]})" class Xor(GlobalConstraint): """ @@ -648,6 +756,7 @@ def __init__(self, arg_list): def decompose(self): # there are multiple decompositions possible, Recursively using sum allows it to be efficient for all solvers. + # and ILP friendly... decomp = [sum(self.args[:2]) == 1] if len(self.args) > 2: decomp = Xor([decomp,self.args[2:]]).decompose()[0] diff --git a/cpmpy/expressions/globalfunctions.py b/cpmpy/expressions/globalfunctions.py index dba6b0f19..1eb32f397 100644 --- a/cpmpy/expressions/globalfunctions.py +++ b/cpmpy/expressions/globalfunctions.py @@ -326,6 +326,16 @@ def decompose_comparison(self, cmp_op, cmp_rhs): arr, val = self.args return [eval_comparison(cmp_op, Operator('sum',[ai==val for ai in arr]), cmp_rhs)], [] + def decompose_numerical(self): + """ + Return a numerical expression to replace the array loopup with in the expression tree + XXX DOES NOT work automatically, needs hacking into decompose_global.py + """ + arr, val = self.args + expr = cp.sum(ai == val for ai in arr) + return expr, []# [cp.expressions.globalconstraints.MapDomain(ai) for ai in arr] <- somehow breaks the constraint in the setting "count(..., ...) not in [...]"" + + def value(self): arr, val = self.args val = argval(val) diff --git a/cpmpy/solvers/choco.py b/cpmpy/solvers/choco.py index 2eedcaf53..07139d38b 100644 --- a/cpmpy/solvers/choco.py +++ b/cpmpy/solvers/choco.py @@ -346,7 +346,7 @@ def objective(self, expr, minimize): # make objective function non-nested obj_var = intvar(*get_bounds(expr)) - self += obj_var == expr + self.add(obj_var == expr, internal=True) self.obj = obj_var self.minimize_obj = minimize # Choco has as default to maximize @@ -406,7 +406,7 @@ def transform(self, cpm_expr): return cpm_cons - def add(self, cpm_expr): + def add(self, cpm_expr, internal:bool=False): """ Eagerly add a constraint to the underlying solver. @@ -425,7 +425,8 @@ def add(self, cpm_expr): :return: self """ # add new user vars to the set - get_variables(cpm_expr, collect=self.user_vars) + if not internal: + get_variables(cpm_expr, collect=self.user_vars) # ensure all vars are known to solver # transform and post the constraints @@ -596,7 +597,7 @@ def _get_constraint(self, cpm_expr): table = table.astype(float) # nan's require float dtype # Choco requires a wildcard value not present in dom of args, # take value lower than anything else - chc_star = min(np.nanmin(table), *get_bounds(array)[0]) -1 + chc_star = int(min(np.nanmin(table), *get_bounds(array)[0]) -1) # should be an int chc_table = np.nan_to_num(table, nan=chc_star).astype(int).tolist() return self.chc_model.table(self.solver_vars(array), chc_table, universal_value=chc_star, algo="STR2+") elif cpm_expr.name == "regular": diff --git a/cpmpy/solvers/cpo.py b/cpmpy/solvers/cpo.py index 1dcdfea93..2001183f7 100644 --- a/cpmpy/solvers/cpo.py +++ b/cpmpy/solvers/cpo.py @@ -42,7 +42,6 @@ CPM_cpo """ -import shutil import time import warnings @@ -346,6 +345,8 @@ def objective(self, expr, minimize=True): technical side note: any constraints created during conversion of the objective are permanently posted to the solver """ + get_variables(expr, collect=self.user_vars) + dom = self.get_docp().modeler if self.has_objective(): self.cpo_model.remove(self.cpo_model.get_objective_expression()) diff --git a/cpmpy/solvers/exact.py b/cpmpy/solvers/exact.py index bbb811144..441dedb40 100644 --- a/cpmpy/solvers/exact.py +++ b/cpmpy/solvers/exact.py @@ -216,6 +216,11 @@ def solve(self, time_limit=None, assumptions=None, **kwargs): self.cpm_status.runtime = end - start self.objective_value_ = None + + self._fillVars() + if self.has_objective(): + self.objective_value_ = self.objective_.value() + # translate exit status # see 'toOptimum' documentation: # https://gitlab.com/nonfiction-software/exact/-/blob/main/src/interface/IntProg.cpp#L877 @@ -235,19 +240,13 @@ def solve(self, time_limit=None, assumptions=None, **kwargs): elif my_status == "INCONSISTENT": # found inconsistency over assumptions self.cpm_status.exitstatus = ExitStatus.UNSATISFIABLE elif my_status == "TIMEOUT": # found timeout - if self.xct_solver.hasSolution(): # found a (sub-)optimal solution + if self.objective_value_ is not None: # found a (sub-)optimal solution self.cpm_status.exitstatus = ExitStatus.FEASIBLE else: # no solution found self.cpm_status.exitstatus = ExitStatus.UNKNOWN else: raise NotImplementedError(my_status) # a new status type was introduced, please report on github - self._fillVars() - if self.has_objective(): - if self.objective_is_min_: - self.objective_value_ = obj_val - else: # maximize, so actually negative value - self.objective_value_ = -obj_val # True/False depending on self.cpm_status return self._solve_return(self.cpm_status) @@ -423,10 +422,10 @@ def objective(self, expr, minimize): self.objective_is_min_ = minimize # make objective function non-nested and with positive BoolVars only + get_variables(expr, collect=self.user_vars) # add objvars to vars (flat_obj, flat_cons) = flatten_objective(expr) flat_obj = only_positive_bv_wsum(flat_obj) # remove negboolviews - self.user_vars.update(get_variables(flat_obj)) # add objvars to vars - self += flat_cons # add potentially created constraints + self.add(flat_cons, internal=True) # add potentially created constraints # make objective function or variable and post xct_cfvars,xct_rhs = self._make_numexpr(flat_obj,0) @@ -517,7 +516,7 @@ def _add_xct_reif_right(self, head, sign, xct_cfvars, xct_rhs): def is_multiplication(cpm_expr): # helper function return isinstance(cpm_expr, Operator) and cpm_expr.name == 'mul' - def add(self, cpm_expr_orig): + def add(self, cpm_expr_orig, internal:bool=False): """ Eagerly add a constraint to the underlying solver. @@ -537,7 +536,8 @@ def add(self, cpm_expr_orig): """ # add new user vars to the set - get_variables(cpm_expr_orig, collect=self.user_vars) + if not internal: + get_variables(cpm_expr_orig, collect=self.user_vars) # transform and post the constraints for cpm_expr in self.transform(cpm_expr_orig): @@ -552,7 +552,7 @@ def add(self, cpm_expr_orig): assert pkg_resources.require("exact>=2.1.0"), f"Multiplication constraint {cpm_expr} " \ f"only supported by Exact version 2.1.0 and above" if is_num(rhs): # make dummy var - rhs = intvar(rhs, rhs) + rhs = cp.intvar(rhs, rhs) xct_rhs = self.solver_var(rhs) assert all(isinstance(v, _IntVarImpl) for v in lhs.args), "constant * var should be " \ "rewritten by linearize" diff --git a/cpmpy/solvers/gcs.py b/cpmpy/solvers/gcs.py index 3ff3f0d88..d1d236a3f 100644 --- a/cpmpy/solvers/gcs.py +++ b/cpmpy/solvers/gcs.py @@ -364,11 +364,11 @@ def objective(self, expr, minimize=True): """ # make objective function non-nested (flat_obj, flat_cons) = flatten_objective(expr) - self += flat_cons # add potentially created constraints + self.add(flat_cons, internal=True) # add potentially created constraints self.user_vars.update(get_variables(flat_obj)) # add objvars to vars (obj, obj_cons) = get_or_make_var(flat_obj, csemap=self._csemap) - self += obj_cons + self.add(obj_cons, internal=True) self.objective_var = obj @@ -471,7 +471,7 @@ def verify(self, name=None, location=".", time_limit=None, display_output=False, return self.veripb_return_code - def add(self, cpm_cons): + def add(self, cpm_cons, internal:bool=False): """ Post a (list of) CPMpy constraints(=expressions) to the solver Note that we don't store the constraints in a cpm_model, @@ -482,7 +482,8 @@ def add(self, cpm_cons): """ # add new user vars to the set # add new user vars to the set - get_variables(cpm_cons, collect=self.user_vars) + if not internal: + get_variables(cpm_cons, collect=self.user_vars) for con in self.transform(cpm_cons): cpm_expr = con @@ -551,10 +552,10 @@ def add(self, cpm_cons): # lt == x < y # gt == x > y lt_bool, gt_bool = boolvar(shape=2) - self += (lhs < rhs) == lt_bool - self += (lhs > rhs) == gt_bool + self.add( (lhs < rhs) == lt_bool, internal=True ) + self.add( (lhs > rhs) == gt_bool, internal=True ) if fully_reify: - self += (~bool_lhs).implies(lhs == rhs) + self.add( (~bool_lhs).implies(lhs == rhs), internal=True ) self.gcs.post_or_reif(self.solver_vars([lt_bool, gt_bool]), reif_var, False) else: raise NotImplementedError("Not currently supported by Glasgow Constraint Solver API '{}' {}".format) @@ -665,7 +666,7 @@ def add(self, cpm_cons): elif isinstance(cpm_expr, GlobalConstraint): # GCS also has SmartTable, Regular Language Membership, Knapsack constraints # which could be added in future. - self += cpm_expr.decompose() # assumes a decomposition exists... + self.add(cpm_expr.decompose(), internal=True) # assumes a decomposition exists... else: # Hopefully we don't end up here. raise NotImplementedError(cpm_expr) diff --git a/cpmpy/solvers/gurobi.py b/cpmpy/solvers/gurobi.py index 70472c230..8f57589d9 100644 --- a/cpmpy/solvers/gurobi.py +++ b/cpmpy/solvers/gurobi.py @@ -270,10 +270,10 @@ def objective(self, expr, minimize=True): from gurobipy import GRB # make objective function non-nested + get_variables(expr, collect=self.user_vars) # add potentially created variables (flat_obj, flat_cons) = flatten_objective(expr) flat_obj = only_positive_bv_wsum(flat_obj) # remove negboolviews - get_variables(flat_obj, collect=self.user_vars) # add potentially created variables - self += flat_cons + self.add(flat_cons, internal=True) # make objective function or variable and post obj = self._make_numexpr(flat_obj) @@ -344,7 +344,7 @@ def transform(self, cpm_expr): cpm_cons = only_positive_bv(cpm_cons, csemap=self._csemap) # after linearization, rewrite ~bv into 1-bv return cpm_cons - def add(self, cpm_expr_orig): + def add(self, cpm_expr_orig, internal:bool=False): """ Eagerly add a constraint to the underlying solver. @@ -365,7 +365,8 @@ def add(self, cpm_expr_orig): from gurobipy import GRB # add new user vars to the set - get_variables(cpm_expr_orig, collect=self.user_vars) + if not internal: + get_variables(cpm_expr_orig, collect=self.user_vars) # transform and post the constraints for cpm_expr in self.transform(cpm_expr_orig): diff --git a/cpmpy/solvers/minizinc.py b/cpmpy/solvers/minizinc.py index ceff42f51..4ac75ac15 100644 --- a/cpmpy/solvers/minizinc.py +++ b/cpmpy/solvers/minizinc.py @@ -437,13 +437,13 @@ def solver_var(self, cpm_var) -> str: if cpm_var not in self._varmap: # clean the varname varname = cpm_var.name - mzn_var = varname.replace(',', '_').replace('.', '_').replace(' ', '_').replace('[', '_').replace(']', '') + mzn_var = varname.replace(',', '_').replace('.', '_').replace(' ', '_').replace('[', '_').replace(']', '').replace('#', '_') # test if the name is a valid minizinc identifier if not self.mzn_name_pattern.search(mzn_var): raise MinizincNameException("Minizinc only accept names with alphabetic characters, " "digits and underscores. " - "First character must be an alphabetic character") + f"First character must be an alphabetic character: {mzn_var}") if mzn_var in self.keywords: raise MinizincNameException(f"This variable name is a disallowed keyword in MiniZinc: {mzn_var}") @@ -467,7 +467,7 @@ def objective(self, expr, minimize): 'objective()' can be called multiple times, only the last one is stored """ - # get_variables(expr, collect=self.user_vars) # add objvars to vars # all are user vars + get_variables(expr, collect=self.user_vars) # add objvars to vars # all are user vars # make objective function or variable and post obj = self._convert_expression(expr) @@ -712,11 +712,14 @@ def zero_based(array): vars = self._convert_expression(vars) vals = self._convert_expression(vals).replace("[", "{").replace("]", "}") # convert to set return "among({},{})".format(vars, vals) - + # a direct constraint, treat differently for MiniZinc, a text-based language # use the name as, unpack the arguments from the argument tuple + # elif isinstance(expr, DirectConstraint): + # return "{}({})".format(expr.name, ",".join(args_str)) + elif isinstance(expr, DirectConstraint): - return "{}({})".format(expr.name, ",".join(args_str)) + return expr.callSolver(self, self.mzn_model) print_map = {"allequal": "all_equal", "xor": "xorall"} if expr.name in print_map: diff --git a/cpmpy/solvers/ortools.py b/cpmpy/solvers/ortools.py index 14d2b1f4d..7e4334f5c 100644 --- a/cpmpy/solvers/ortools.py +++ b/cpmpy/solvers/ortools.py @@ -43,7 +43,7 @@ Module details ============== """ -import sys # for stdout checking +import sys import numpy as np from .solver_interface import SolverInterface, SolverStatus, ExitStatus @@ -55,7 +55,7 @@ from ..expressions.utils import is_num, is_int, eval_comparison, flatlist, argval, argvals, get_bounds from ..transformations.decompose_global import decompose_in_tree from ..transformations.get_variables import get_variables -from ..transformations.flatten_model import flatten_constraint, flatten_objective, get_or_make_var +from ..transformations.flatten_model import POSITIVE, flatten_constraint, flatten_objective, get_or_make_var from ..transformations.normalize import toplevel_list from ..transformations.reification import only_implies, reify_rewrite, only_bv_reifies from ..transformations.comparison import only_numexpr_equality @@ -327,8 +327,8 @@ def objective(self, expr, minimize): """ # make objective function non-nested (flat_obj, flat_cons) = flatten_objective(expr) - self += flat_cons # add potentially created constraints - get_variables(flat_obj, collect=self.user_vars) # add objvars to vars + get_variables(expr, collect=self.user_vars) # add objvars to vars <- XCSP3 needs also unused vars to be posted, added to user vars + self.add(flat_cons, internal=True) # add potentially created constraints # make objective function or variable and post obj = self._make_numexpr(flat_obj) @@ -389,18 +389,19 @@ def transform(self, cpm_expr): :return: list of Expression """ cpm_cons = toplevel_list(cpm_expr) - supported = {"min", "max", "abs", "element", "alldifferent", "xor", "table", "negative_table", "cumulative", "circuit", "inverse", "no_overlap", "regular"} + supported = {"min", "max", "abs", "element", "alldifferent", "xor", "table", "negative_table", "cumulative", "circuit", "inverse", "no_overlap", "regular", "mapdomain"} cpm_cons = no_partial_functions(cpm_cons, safen_toplevel=frozenset({"div", "mod"})) # before decompose, assumes total decomposition for partial functions cpm_cons = decompose_in_tree(cpm_cons, supported, csemap=self._csemap) - cpm_cons = flatten_constraint(cpm_cons, csemap=self._csemap) # flat normal form + cpm_cons = flatten_constraint(cpm_cons, csemap=self._csemap, context=POSITIVE) # flat normal form cpm_cons = reify_rewrite(cpm_cons, supported=frozenset(['sum', 'wsum']), csemap=self._csemap) # constraints that support reification cpm_cons = only_numexpr_equality(cpm_cons, supported=frozenset(["sum", "wsum", "sub"]), csemap=self._csemap) # supports >, <, != cpm_cons = only_bv_reifies(cpm_cons, csemap=self._csemap) - cpm_cons = only_implies(cpm_cons, csemap=self._csemap) # everything that can create + cpm_cons = only_implies(cpm_cons, csemap=self._csemap, rewrite_bool_eq=False) # everything that can create # reified expr must go before this + return cpm_cons - def add(self, cpm_expr): + def add(self, cpm_expr, internal:bool=False): """ Eagerly add a constraint to the underlying solver. @@ -419,7 +420,8 @@ def add(self, cpm_expr): :return: self """ # add new user vars to the set - get_variables(cpm_expr, collect=self.user_vars) + if not internal: + get_variables(cpm_expr, collect=self.user_vars) # transform and post the constraints for con in self.transform(cpm_expr): @@ -513,7 +515,7 @@ def _post_constraint(self, cpm_expr, reifiable=False): x,y = lhs.args if get_bounds(y)[0] <= 0: # not supported, but result of modulo is agnositic to sign of second arg y, link = get_or_make_var(-lhs.args[1], csemap=self._csemap) - self += link + self.add(link, internal=True) return self.ort_model.AddModuloEquality(ortrhs, *self.solver_vars([x,y])) elif lhs.name == 'pow': # only `POW(b,2) == IV` supported, post as b*b == IV @@ -527,7 +529,7 @@ def _post_constraint(self, cpm_expr, reifiable=False): new_lhs = 1 for exp in range(n): new_lhs, new_cons = get_or_make_var(b * new_lhs, csemap=self._csemap) - self += new_cons + self.add(new_cons, internal=True) return self.ort_model.Add(eval_comparison("==", self.solver_var(new_lhs), ortrhs)) @@ -571,7 +573,7 @@ def _post_constraint(self, cpm_expr, reifiable=False): N = len(x) arcvars = boolvar(shape=(N,N)) # post channeling constraints from int to bool - self += [b == (x[i] == j) for (i,j),b in np.ndenumerate(arcvars)] + self.add([b == (x[i] == j) for (i,j),b in np.ndenumerate(arcvars)], internal=True) # post the global constraint # when posting arcs on diagonal (i==j), it would do subcircuit ort_arcs = [(i,j,self.solver_var(b)) for (i,j),b in np.ndenumerate(arcvars) if i != j] @@ -582,6 +584,13 @@ def _post_constraint(self, cpm_expr, reifiable=False): return self.ort_model.AddInverse(fwd, rev) elif cpm_expr.name == 'xor': return self.ort_model.AddBoolXOr(self.solver_vars(cpm_expr.args)) + elif cpm_expr.name == 'mapdomain': + ivar = cpm_expr.args[0] + # extract boolvars from csemap + lb, ub = get_bounds(ivar) + bvs = [self._csemap[ivar == v] for v in range(lb, ub+1)] + self.add(sum(bvs) == 1, internal=True) # not covered by AddMapDomain... + return self.ort_model.add_map_domain(self.solver_var(ivar), self.solver_vars(bvs), offset=lb) else: raise NotImplementedError(f"Unknown global constraint {cpm_expr}, should be decomposed! " f"If you reach this, please report on github.") diff --git a/cpmpy/solvers/z3.py b/cpmpy/solvers/z3.py index 8f786fca0..e985f803a 100644 --- a/cpmpy/solvers/z3.py +++ b/cpmpy/solvers/z3.py @@ -45,6 +45,7 @@ Module details ============== """ +import cpmpy as cp from cpmpy.transformations.get_variables import get_variables from .solver_interface import SolverInterface, SolverStatus, ExitStatus from ..exceptions import NotSupportedError @@ -293,15 +294,17 @@ def objective(self, expr, minimize=True): technical side note: any constraints created during conversion of the objective are premanently posted to the solver """ + get_variables(expr, collect=self.user_vars) + import z3 # objective can be a nested expression for z3 if not isinstance(self.z3_solver, z3.Optimize): raise NotSupportedError("Use the z3 optimizer for optimization problems") - if isinstance(expr, GlobalFunction): # not supported by Z3 - obj_var = intvar(*expr.get_bounds()) - self += expr == obj_var - expr = obj_var + # if isinstance(expr, GlobalFunction): # not supported by Z3 + obj_var = intvar(*expr.get_bounds()) + self.add(expr == obj_var, internal=True) + expr = obj_var obj = self._z3_expr(expr) if minimize: @@ -327,11 +330,11 @@ def transform(self, cpm_expr): cpm_cons = toplevel_list(cpm_expr) cpm_cons = no_partial_functions(cpm_cons, safen_toplevel={"div", "mod"}) - supported = {"alldifferent", "xor", "ite"} # z3 accepts these reified too + supported = {"alldifferent", "xor", "ite", "mapdomain", "table"} # z3 accepts these reified too, TODO mapdomain is a hack to prevent posting it cpm_cons = decompose_in_tree(cpm_cons, supported, supported, csemap=self._csemap) return cpm_cons - def add(self, cpm_expr): + def add(self, cpm_expr, internal:bool=False): """ Z3 supports nested expressions so translate expression tree and post to solver API directly @@ -351,13 +354,15 @@ def add(self, cpm_expr): """ # all variables are user variables, handled in `solver_var()` # unless their constraint gets simplified away, so lets collect them anyway - get_variables(cpm_expr, collect=self.user_vars) + if not internal: + get_variables(cpm_expr, collect=self.user_vars) # transform and post the constraints for cpm_con in self.transform(cpm_expr): # translate each expression tree, then post straight away z3_con = self._z3_expr(cpm_con) - self.z3_solver.add(z3_con) + if z3_con is not None: + self.z3_solver.add(z3_con) return self __add__ = add # avoid redirect in superclass @@ -496,6 +501,26 @@ def _z3_expr(self, cpm_con): elif cpm_con.name == 'ite': return z3.If(self._z3_expr(cpm_con.args[0]), self._z3_expr(cpm_con.args[1]), self._z3_expr(cpm_con.args[2])) + elif cpm_con.name == "mapdomain": + # Hack: dummy native as to prevent posting MapDomain to Z3 + return None + elif cpm_con.name == "table": + # Hack: dummy native at to have unique Z3 decomposition for table (even if result of decomposition of another constraint) + arr, tab = cpm_con.args + if len(tab) == 1: + self.add([x == v for x,v in zip(arr, tab[0])], internal=True) + return None + + row_selected = cp.boolvar(shape=len(tab)) + + cons = [Operator("or", row_selected)] + for i, row in enumerate(tab): + # lets already flatten it a bit + cons += [Operator("->", [row_selected[i], x == v]) for x,v in zip(arr, row)] + self.add(cons, internal=True) + return None + + raise ValueError(f"Global constraint {cpm_con} should be decomposed already, please report on github.") diff --git a/cpmpy/tools/__init__.py b/cpmpy/tools/__init__.py index 15fc0fdf0..e718f5c39 100644 --- a/cpmpy/tools/__init__.py +++ b/cpmpy/tools/__init__.py @@ -12,7 +12,9 @@ dimacs maximal_propagate tune_solver + xcsp3 """ from .tune_solver import ParameterTuner, GridSearchTuner from .explain import * +from .xcsp3 import * \ No newline at end of file diff --git a/cpmpy/tools/xcsp3/README.md b/cpmpy/tools/xcsp3/README.md new file mode 100644 index 000000000..dda3732fd --- /dev/null +++ b/cpmpy/tools/xcsp3/README.md @@ -0,0 +1,72 @@ +# CPMpy XCSP3 tools + +This directory contains a collection of tools for reading, loading and solving XCSP3 instances in CPMpy. + +What is included: +- utilities for reading and loading XCSP3 instances +- a PyTorch-compatible dataset class for loading (and downloading) XCSP3 instances +- cli for solving individual XCSP3 instances and outputting the result in the competition format +- cli for benchmarking CPMpy across a large collection of XCSP3 instances +- cli for processing and visualizing benchmark results + +Only the XCSP3-Core specification version 3.2 is currently supported. + +## Utilities + +We provide a basic utility for parsing and loading a XCSP3 `.xml` or compressed `.xml.lzma` file into a CPMpy model: + +```python +from cpmpy.tools.xcsp3 import read_xcsp3 + +model = read_xcsp3("") +``` + + +## Dataset + +We provide a PyTorch compatible dataset class (`XCSP3Dataset`) to easily work with XCSP3 competition instances. As an example, use the following to install the 2024 instances from the COP track: + +```python +from cpmpy.tools.xcsp3 import XCSP3Dataset + +dataset = XCSP3Dataset(year=2024, track="COP", download=True) +``` + +This will install the instances under `/2024/COP` as `.xml.lzma` compressed files. + +You can now iterate over the dataset and load the instances as CPMpy models: + +```python +from cpmpy.tools.xcsp3 import XCSP3Dataset, read_xcsp3 + +for filename, metadata in XCSP3Dataset(year=2024, track="COP", download=True): # auto download dataset and iterate over its instances + # Do whatever you want here, e.g. reading to a CPMpy model and solving it: + model = read_xcsp3(filename) + model.solve() + print(model.status()) +``` + +## Solving single instance + +To parse, load and solve a single XCSP3 instance, we provide the `xcsp3_cpmpy` CLI. + +```python +python xcsp3_cpmpy.py --solver [-s SEED] [-l TIME_LIMIT] [-m MEM_LIMIT] [-t TMPDIR] [-c CORES] [--time-buffer TIME_BUFFER] [--intermediate] +``` + + + +## Benchmarking + +For benchmarking CPMpy / a backend solver on XCSP3, we provide a CLI to run against a complete competition dataset. + +```python +python xcsp3_benchmark.py --year --track --solver [--workers WORKERS] [--time-limit TIME_LIMIT] [--mem-limit MEM_LIMIT] [--output-dir OUTPUT_DIR] [--verbose] [--intermediate] +``` + +This will create a `.csv` file containing (performance) measurements for each of the instances. To compare the results of different solvers: + +```python +python xcsp3_analyze.py [--time_limit TIME_LIMIT] [--output OUTPUT] +``` + diff --git a/cpmpy/tools/xcsp3/__init__.py b/cpmpy/tools/xcsp3/__init__.py new file mode 100644 index 000000000..fca5f72cb --- /dev/null +++ b/cpmpy/tools/xcsp3/__init__.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- +## +## __init__.py +## +""" + Set of utilities for working with XCSP3-formatted CP models. + + + ================= + List of functions + ================= + + .. autosummary:: + :nosignatures: + + read_xcsp3 + + ======================== + List of helper functions + ======================== + + .. autosummary:: + :nosignatures: + + _parse_xcsp3 + _load_xcsp3 + + ================== + List of submodules + ================== + + .. autosummary:: + :nosignatures: + + parser_callbacks + xcsp3_analyze + xcsp3_benchmark + xcsp3_cpmpy + xcsp3_dataset + xcsp3_globals + xcsp3_natives + xcsp3_solution +""" +from io import StringIO +import lzma +import os +import cpmpy as cp + +# Prevent pycsp3 from complaining on exit + breaking docs +# import sys +# sys.argv = ["-nocompile"] + +from pycsp3.parser.xparser import CallbackerXCSP3, ParserXCSP3 +from .parser_callbacks import CallbacksCPMPy +from .xcsp3_dataset import XCSP3Dataset + + +def _parse_xcsp3(path: os.PathLike) -> ParserXCSP3: + """ + Parses an XCSP3 instance file (.xml) and returns a `ParserXCSP3` instance. + + Arguments: + path: location of the XCSP3 instance to read (expects a .xml file). + + Returns: + A parser object. + """ + parser = ParserXCSP3(path) + return parser + +def _load_xcsp3(parser: ParserXCSP3) -> cp.Model: + """ + Takes in a `ParserXCSP3` instance and loads its captured model as a CPMpy model. + + Arguments: + parser (ParserXCSP3): A parser object to load from. + + Returns: + The XCSP3 instance loaded as a CPMpy model. + """ + callbacks = CallbacksCPMPy() + callbacks.force_exit = True + callbacker = CallbackerXCSP3(parser, callbacks) + callbacker.load_instance() + model = callbacks.cpm_model + + return model + + +def read_xcsp3(path: os.PathLike) -> cp.Model: + """ + Reads in an XCSP3 instance (.xml or .xml.lzma) and returns its matching CPMpy model. + + Arguments: + path: location of the XCSP3 instance to read (expects a .xml or .xml.lzma file). + + Returns: + The XCSP3 instance loaded as a CPMpy model. + """ + # Decompress on the fly if still in .lzma format + if str(path).endswith(".lzma"): + path = decompress_lzma(path) + + # Parse and create CPMpy model + parser = _parse_xcsp3(path) + model = _load_xcsp3(parser) + return model + +def decompress_lzma(path: os.PathLike) -> StringIO: + """ + Decompresses a .lzma file. + + Arguments: + path: Location of .lzma file + + Returns: + Memory-mapped decompressed file + """ + # Decompress the XZ file + with lzma.open(path, 'rt', encoding='utf-8') as f: + return StringIO(f.read()) # read to memory-mapped file + + + \ No newline at end of file diff --git a/cpmpy/tools/xcsp3/competition.md b/cpmpy/tools/xcsp3/competition.md new file mode 100644 index 000000000..4bd00290e --- /dev/null +++ b/cpmpy/tools/xcsp3/competition.md @@ -0,0 +1,228 @@ +# XCSP3 competition 2025 + +This document contains the installation and usage instructions for the CPMpy submission to the XCSP3 2025 competition. + +## Submission + +This submission is the basis for multiple submissions with different solver backends. CPMpy is a modelling system which can translate to many different solvers, seven of which have been chosen for the XCSP3 competition. The data files and install instructions are shared (some solvers have additional installation steps). From the executable's point of view, the major difference between the submissions is the actual command used to run the executable, where the correct solver must be set. These commands are listed later on. Internally, different solver-tailored backends will be used, where the COP and CSP models get transformed as to satisfy the modelling capabilities of the selected solver target. + +The CPMpy modelling system will compete in the following tracks, using the following solver backends: + +| CPMpy_backend | CSP sequential | COP sequential (3') | COP sequential (30') | COP parallel | +| - | - | - | - | - | +| **cpmpy_ortools** | yes | yes | yes | yes | +| **cpmpy_exact** | yes | yes | yes | no | +| **cpmpy_z3** | yes | yes | yes | no | +| **cpmpy_gurobi** | yes | yes | yes | yes | +| **cpmpy_cpo** | yes | yes | yes | yes | +| **cpmpy_mnz_gecode** | yes | yes | yes | no | +| **cpmpy_mnz_chuffed** | yes | yes | yes | no | + + +## Setup + +Since the competition will be run on a cluster of Rocky Linux 9.5 servers, the installation steps have been talored to that particular OS (but for version 9.6, unsure if there will be any difference). Our submission is not inherently dependant on any particular OS, but some dependencies might be missing on a clean install (which are for example included in a standard Ubuntu install). + +### Dependencies + +CPMpy is a python library, and thus python is the main dependency. But many of the libraries on which it depends for communicating with the different solvers, have their own dependencies, often requirering certain C libraries. The next steps should be done in order, since they need to be available when compiling python3.10 from source. When installing python3.10 through any other means, there is no guarantee that it has been build with these dependencies included (The anaconda builds do seem to include everything). + + +1) C compiler & other dev tools + + Some python libraries have C dependencies and need to be able to compile them + upon setup (when pip installing). The following installs various development tools (like g++) : + + ```bash + sudo dnf group install "Development Tools" + ``` + +2) libffi-devel + + To get `_ctypes` in python. Similarly as the previous dependency, it is required to + build some of the python libraries. Must be installed before compiling python3.10 (if python3.10 is not already installed) + ```bash + sudo dnf install -y libffi-devel + ``` + +3) openssl-devel + + To get pip working with locations that require TLS/SSL. + ```bash + sudo dnf install -y openssl-devel + ``` + +5) ncurses-devel + + Needed for building the "readline" python library, otherwise linker will complain. + ```bash + sudo dnf install -y ncurses-devel + ``` + +5) boost-devel + + Needed for the `Exact` solver. + ```bash + sudo dnf install boost-devel + ``` + +6) libGL divers + + The `GeCode` solver requires certain graphics drivers to be installed on the system (probably uses them for vector operations?). + Without them, GeCode will crash (when running, not when in installing) on a missing shared object file: `libEGL.so.1`. + This might not be present on a headless install of the OS. + + ```bash + sudo dnf install mesa-libGL mesa-dri-drivers libselinux libXdamage libXxf86vm libXext + sudo dnf install mesa-libEGL + ``` + +7) Python + - version: 3.10(.16) + + These are the steps we used to install it: + ```bash + curl -O https://www.python.org/ftp/python/3.10.16/Python-3.10.16.tgz + tar xzf Python-3.10.16.tgz + cd Python-3.10.16 + ./configure --enable-optimizations + make altinstall + ``` + Python should now be available with the command `python3.10` + + > [!WARNING] + > If the above dependencies are not installed at the time of building python, later installation steps for some of the solvers will fail. + +### Solvers + +1) Minizinc + + To download Minizinc, run the following: + ```bash + curl -LO https://github.com/MiniZinc/MiniZincIDE/releases/download/2.9.0/MiniZincIDE-2.9.0-bundle-linux-x86_64.tgz + tar zxvf MiniZincIDE-2.9.0-bundle-linux-x86_64.tgz + ``` + Now add the `/bin` directory inside the extracted directory to `PATH`. + + E.g.: + ```bash + export PATH="$HOME/MiniZincIDE-2.9.0-bundle-linux-x86_64/bin/:$PATH" + ``` + +2) Gurobi licence + + Gurobi is a commercial MIP solver that requires a licence to run at its full potential. + Depending on the type of licence, the installation instructions differ. + The following instructions are tailored to installing a "Academic Named-User License", which can be aquired on the [Gurobi licence portal](https://www.gurobi.com/academia/academic-program-and-licenses). + + First, get a licence from Gurobi's site. It should give you a command looking like: `grbgetkey ` + + Next, get the license installer: + + ```bash + curl -O https://packages.gurobi.com/lictools/licensetools11.0.2_linux64.tar.gz + tar zxvf licensetools11.0.2_linux64.tar.gz + ``` + + Now install the license: + ```bash + ./grbgetkey + ``` + It will ask where you would like to install the license. As long as Gurobi can find the license again, + the exact location does not matter for CPMpy. + +3) CP Optimizer + + CP Optimizer is a commercial CP solver that requires a licence to run at its full potential. + + First get a license from the CP Optimizer's website (academic license is available): + https://www.ibm.com/products/ilog-cplex-optimization-studio + (for academic license: https://www.ibm.com/academic/) + + The license will be downloadable as a binary, e.g. "cplex_studio2211.linux_x86_64.bin". + + Run the installer inside the binary (you can install it anywhere): + ```bash + chmod +x cplex_studio2211.linux_x86_64.bin + ./cplex_studio2211.linux_x86_64.bin + ``` + + Make sure all dependencies are installed in your python environment: + ```bash + python /python/setup.py install + ``` + + As a last step, you'll need to edit a config file to point to your cplex studio install. A sample for this config has already been provided: + ```bash + cp cpmpy/tools/xcsp3/cpo_config.py.sample cpo_config.py + ``` + Now fill in the install location in `cpo_config.py`. + + + +### Installation + +These are the final steps to install everything from CPMpy's side. We will create a python virtual environment and install all libraries inside it. + + +1) Create python virtual environment + ```bash + python3.10 -m venv .venv + ``` + +2) Activate python environment + + Everything should be installed in the python virtual environment. Either activate the environment using: + ```bash + source .venv/bin/activate + ``` + or replace `python` with the path to the python executable and `pip` with ` -m pip` in each of the following steps. + +3) Navigate to the root directory of this submission. + +4) Install python libraries + + ```bash + pip install .[exact,z3,gurobi,minizinc,cpo,xcsp3] + pip install -r ./cpmpy/tools/xcsp3/requirements.txt + ``` +Now we should be all set up! + + + +## Running code + +This section will explain how to run the executable on problem instances. + +The interface of the executable is as follows: +```bash +python ./cpmpy/tools/xcsp3/xcsp3_cpmpy.py + [-s/--seed ] + [-l/--time-limit=] + [-m/--mem-limit ] + [-c/--cores ] + [--solver ] # Name of solver, can be solver:subsolver + [--intermediate] # If intermediate results should be reported (only for COP and a subset of solvers) +``` + +The same executable supports multiple solver backends and is used for all of the submissions to the competition. The submitted cpmpy + backends are: +- `cpmpy_ortools` +- `cpmpy_exact` +- `cpmpy_z3` +- `cpmpy_gurobi` +- `cpmpy_cpo` +- `cpmpy_mnz_chuffed` +- `cpmpy_mnz_gecode` + +The commands are as follows: + +| Submission | Command | +| - | - | +| **cpmpy_ortools** | python ./cpmpy/tools/xcsp3/xcsp3_cpmpy.py BENCHNAME --intermediate --cores=NBCORES --solver=ortools --mem-limit=MEMLIMIT --time-limit=TIMELIMIT --seed=RANDOMSEED | +| **cpmpy_exact** | python ./cpmpy/tools/xcsp3/xcsp3_cpmpy.py BENCHNAME --intermediate --cores=NBCORES --solver=exact --mem-limit=MEMLIMIT --time-limit=TIMELIMIT --seed=RANDOMSEED | +| **cpmpy_z3** | python ./cpmpy/tools/xcsp3/xcsp3_cpmpy.py BENCHNAME --intermediate --cores=NBCORES --solver=z3 --mem-limit=MEMLIMIT --time-limit=TIMELIMIT --seed=RANDOMSEED | +| **cpmpy_gurobi** | python ./cpmpy/tools/xcsp3/xcsp3_cpmpy.py BENCHNAME --intermediate --cores=NBCORES --solver=gurobi --mem-limit=MEMLIMIT --time-limit=TIMELIMIT --seed=RANDOMSEED | +| **cpmpy_cpo** | python ./cpmpy/tools/xcsp3/xcsp3_cpmpy.py BENCHNAME --intermediate --cores=NBCORES --solver=cpo --mem-limit=MEMLIMIT --time-limit=TIMELIMIT --seed=RANDOMSEED | +| **cpmpy_mnz_chuffed** | python ./cpmpy/tools/xcsp3/xcsp3_cpmpy.py BENCHNAME --intermediate --cores=NBCORES --solver=minizinc:chuffed --mem-limit=MEMLIMIT --time-limit=TIMELIMIT --seed=RANDOMSEED | +| **cpmpy_mnz_gecode** | python ./cpmpy/tools/xcsp3/xcsp3_cpmpy.py BENCHNAME --intermediate --cores=NBCORES --solver=minizinc:gecode --mem-limit=MEMLIMIT --time-limit=TIMELIMIT --seed=RANDOMSEED | + diff --git a/cpmpy/tools/xcsp3/cpo_config.py.sample b/cpmpy/tools/xcsp3/cpo_config.py.sample new file mode 100644 index 000000000..fa846fcd5 --- /dev/null +++ b/cpmpy/tools/xcsp3/cpo_config.py.sample @@ -0,0 +1,2 @@ +context.solver.agent = 'local' +context.solver.local.execfile = "/cpoptimizer/bin/x86-64_linux/cpoptimizer" \ No newline at end of file diff --git a/cpmpy/tools/xcsp3/models/Fillomino-mini-5-0_c24.xml b/cpmpy/tools/xcsp3/models/Fillomino-mini-5-0_c24.xml new file mode 100644 index 000000000..d951f5b78 --- /dev/null +++ b/cpmpy/tools/xcsp3/models/Fillomino-mini-5-0_c24.xml @@ -0,0 +1,547 @@ + + + + -1 + 0..9 + + + -1 + 4 + 1 2 3 4 5 6 + 3 + 5 + 2 + 6 + 1 + + + -1 + 0..6 + + 0 1 2 3 4 5 6 + 0 1 + + + + eq(%0,%1) + x[1][1] 0 + x[1][4] 1 + x[2][2] 2 + x[2][5] 3 + x[3][2] 4 + x[3][3] 5 + x[3][5] 6 + y[1][1] 4 + y[1][4] 3 + y[2][2] 5 + y[2][5] 2 + y[3][2] 6 + y[3][3] 1 + y[3][5] 1 + s[0] 4 + s[1] 3 + s[2] 5 + s[3] 2 + s[4] 6 + s[5] 1 + s[6] 1 + + + + s[] + %0 + %1 + + x[1][2] y[1][2] + x[1][3] y[1][3] + x[1][5] y[1][5] + x[2][1] y[2][1] + x[2][4] y[2][4] + x[3][1] y[3][1] + x[3][4] y[3][4] + x[4][1] y[4][1] + x[4][2] y[4][2] + x[4][3] y[4][3] + x[4][5] y[4][5] + x[5][2] y[5][2] + x[5][3] y[5][3] + x[5][5] y[5][5] + + + + %0 %1 + (0,1)(0,2)(0,3)(0,4)(0,5)(0,6)(0,7)(0,8)(0,9)(1,0) + + a[0][0][0] x[1][1] + a[0][1][0] x[1][2] + a[0][2][0] x[1][3] + a[0][3][0] x[1][4] + a[0][4][0] x[1][5] + a[1][0][0] x[2][1] + a[1][1][0] x[2][2] + a[1][2][0] x[2][3] + a[1][3][0] x[2][4] + a[1][4][0] x[2][5] + a[2][0][0] x[3][1] + a[2][1][0] x[3][2] + a[2][2][0] x[3][3] + a[2][3][0] x[3][4] + a[2][4][0] x[3][5] + a[3][0][0] x[4][1] + a[3][1][0] x[4][2] + a[3][2][0] x[4][3] + a[3][3][0] x[4][4] + a[3][4][0] x[4][5] + a[4][0][0] x[5][1] + a[4][1][0] x[5][2] + a[4][2][0] x[5][3] + a[4][3][0] x[5][4] + a[4][4][0] x[5][5] + + + + %0 %1 + (0,0)(0,2)(0,3)(0,4)(0,5)(0,6)(0,7)(0,8)(0,9)(1,1) + + a[0][0][1] x[1][1] + a[0][1][1] x[1][2] + a[0][2][1] x[1][3] + a[0][3][1] x[1][4] + a[0][4][1] x[1][5] + a[1][0][1] x[2][1] + a[1][1][1] x[2][2] + a[1][2][1] x[2][3] + a[1][3][1] x[2][4] + a[1][4][1] x[2][5] + a[2][0][1] x[3][1] + a[2][1][1] x[3][2] + a[2][2][1] x[3][3] + a[2][3][1] x[3][4] + a[2][4][1] x[3][5] + a[3][0][1] x[4][1] + a[3][1][1] x[4][2] + a[3][2][1] x[4][3] + a[3][3][1] x[4][4] + a[3][4][1] x[4][5] + a[4][0][1] x[5][1] + a[4][1][1] x[5][2] + a[4][2][1] x[5][3] + a[4][3][1] x[5][4] + a[4][4][1] x[5][5] + + + + %0 %1 + (0,0)(0,1)(0,3)(0,4)(0,5)(0,6)(0,7)(0,8)(0,9)(1,2) + + a[0][0][2] x[1][1] + a[0][1][2] x[1][2] + a[0][2][2] x[1][3] + a[0][3][2] x[1][4] + a[0][4][2] x[1][5] + a[1][0][2] x[2][1] + a[1][1][2] x[2][2] + a[1][2][2] x[2][3] + a[1][3][2] x[2][4] + a[1][4][2] x[2][5] + a[2][0][2] x[3][1] + a[2][1][2] x[3][2] + a[2][2][2] x[3][3] + a[2][3][2] x[3][4] + a[2][4][2] x[3][5] + a[3][0][2] x[4][1] + a[3][1][2] x[4][2] + a[3][2][2] x[4][3] + a[3][3][2] x[4][4] + a[3][4][2] x[4][5] + a[4][0][2] x[5][1] + a[4][1][2] x[5][2] + a[4][2][2] x[5][3] + a[4][3][2] x[5][4] + a[4][4][2] x[5][5] + + + + %0 %1 + (0,0)(0,1)(0,2)(0,4)(0,5)(0,6)(0,7)(0,8)(0,9)(1,3) + + a[0][0][3] x[1][1] + a[0][1][3] x[1][2] + a[0][2][3] x[1][3] + a[0][3][3] x[1][4] + a[0][4][3] x[1][5] + a[1][0][3] x[2][1] + a[1][1][3] x[2][2] + a[1][2][3] x[2][3] + a[1][3][3] x[2][4] + a[1][4][3] x[2][5] + a[2][0][3] x[3][1] + a[2][1][3] x[3][2] + a[2][2][3] x[3][3] + a[2][3][3] x[3][4] + a[2][4][3] x[3][5] + a[3][0][3] x[4][1] + a[3][1][3] x[4][2] + a[3][2][3] x[4][3] + a[3][3][3] x[4][4] + a[3][4][3] x[4][5] + a[4][0][3] x[5][1] + a[4][1][3] x[5][2] + a[4][2][3] x[5][3] + a[4][3][3] x[5][4] + a[4][4][3] x[5][5] + + + + %0 %1 + (0,0)(0,1)(0,2)(0,3)(0,5)(0,6)(0,7)(0,8)(0,9)(1,4) + + a[0][0][4] x[1][1] + a[0][1][4] x[1][2] + a[0][2][4] x[1][3] + a[0][3][4] x[1][4] + a[0][4][4] x[1][5] + a[1][0][4] x[2][1] + a[1][1][4] x[2][2] + a[1][2][4] x[2][3] + a[1][3][4] x[2][4] + a[1][4][4] x[2][5] + a[2][0][4] x[3][1] + a[2][1][4] x[3][2] + a[2][2][4] x[3][3] + a[2][3][4] x[3][4] + a[2][4][4] x[3][5] + a[3][0][4] x[4][1] + a[3][1][4] x[4][2] + a[3][2][4] x[4][3] + a[3][3][4] x[4][4] + a[3][4][4] x[4][5] + a[4][0][4] x[5][1] + a[4][1][4] x[5][2] + a[4][2][4] x[5][3] + a[4][3][4] x[5][4] + a[4][4][4] x[5][5] + + + + %0 %1 + (0,0)(0,1)(0,2)(0,3)(0,4)(0,6)(0,7)(0,8)(0,9)(1,5) + + a[0][0][5] x[1][1] + a[0][1][5] x[1][2] + a[0][2][5] x[1][3] + a[0][3][5] x[1][4] + a[0][4][5] x[1][5] + a[1][0][5] x[2][1] + a[1][1][5] x[2][2] + a[1][2][5] x[2][3] + a[1][3][5] x[2][4] + a[1][4][5] x[2][5] + a[2][0][5] x[3][1] + a[2][1][5] x[3][2] + a[2][2][5] x[3][3] + a[2][3][5] x[3][4] + a[2][4][5] x[3][5] + a[3][0][5] x[4][1] + a[3][1][5] x[4][2] + a[3][2][5] x[4][3] + a[3][3][5] x[4][4] + a[3][4][5] x[4][5] + a[4][0][5] x[5][1] + a[4][1][5] x[5][2] + a[4][2][5] x[5][3] + a[4][3][5] x[5][4] + a[4][4][5] x[5][5] + + + + %0 %1 + (0,0)(0,1)(0,2)(0,3)(0,4)(0,5)(0,7)(0,8)(0,9)(1,6) + + a[0][0][6] x[1][1] + a[0][1][6] x[1][2] + a[0][2][6] x[1][3] + a[0][3][6] x[1][4] + a[0][4][6] x[1][5] + a[1][0][6] x[2][1] + a[1][1][6] x[2][2] + a[1][2][6] x[2][3] + a[1][3][6] x[2][4] + a[1][4][6] x[2][5] + a[2][0][6] x[3][1] + a[2][1][6] x[3][2] + a[2][2][6] x[3][3] + a[2][3][6] x[3][4] + a[2][4][6] x[3][5] + a[3][0][6] x[4][1] + a[3][1][6] x[4][2] + a[3][2][6] x[4][3] + a[3][3][6] x[4][4] + a[3][4][6] x[4][5] + a[4][0][6] x[5][1] + a[4][1][6] x[5][2] + a[4][2][6] x[5][3] + a[4][3][6] x[5][4] + a[4][4][6] x[5][5] + + + + %0 %1 + (0,0)(0,1)(0,2)(0,3)(0,4)(0,5)(0,6)(0,8)(0,9)(1,7) + + a[0][0][7] x[1][1] + a[0][1][7] x[1][2] + a[0][2][7] x[1][3] + a[0][3][7] x[1][4] + a[0][4][7] x[1][5] + a[1][0][7] x[2][1] + a[1][1][7] x[2][2] + a[1][2][7] x[2][3] + a[1][3][7] x[2][4] + a[1][4][7] x[2][5] + a[2][0][7] x[3][1] + a[2][1][7] x[3][2] + a[2][2][7] x[3][3] + a[2][3][7] x[3][4] + a[2][4][7] x[3][5] + a[3][0][7] x[4][1] + a[3][1][7] x[4][2] + a[3][2][7] x[4][3] + a[3][3][7] x[4][4] + a[3][4][7] x[4][5] + a[4][0][7] x[5][1] + a[4][1][7] x[5][2] + a[4][2][7] x[5][3] + a[4][3][7] x[5][4] + a[4][4][7] x[5][5] + + + + %0 %1 + (0,0)(0,1)(0,2)(0,3)(0,4)(0,5)(0,6)(0,7)(0,9)(1,8) + + a[0][0][8] x[1][1] + a[0][1][8] x[1][2] + a[0][2][8] x[1][3] + a[0][3][8] x[1][4] + a[0][4][8] x[1][5] + a[1][0][8] x[2][1] + a[1][1][8] x[2][2] + a[1][2][8] x[2][3] + a[1][3][8] x[2][4] + a[1][4][8] x[2][5] + a[2][0][8] x[3][1] + a[2][1][8] x[3][2] + a[2][2][8] x[3][3] + a[2][3][8] x[3][4] + a[2][4][8] x[3][5] + a[3][0][8] x[4][1] + a[3][1][8] x[4][2] + a[3][2][8] x[4][3] + a[3][3][8] x[4][4] + a[3][4][8] x[4][5] + a[4][0][8] x[5][1] + a[4][1][8] x[5][2] + a[4][2][8] x[5][3] + a[4][3][8] x[5][4] + a[4][4][8] x[5][5] + + + + %0 %1 + (0,0)(0,1)(0,2)(0,3)(0,4)(0,5)(0,6)(0,7)(0,8)(1,9) + + a[0][0][9] x[1][1] + a[0][1][9] x[1][2] + a[0][2][9] x[1][3] + a[0][3][9] x[1][4] + a[0][4][9] x[1][5] + a[1][0][9] x[2][1] + a[1][1][9] x[2][2] + a[1][2][9] x[2][3] + a[1][3][9] x[2][4] + a[1][4][9] x[2][5] + a[2][0][9] x[3][1] + a[2][1][9] x[3][2] + a[2][2][9] x[3][3] + a[2][3][9] x[3][4] + a[2][4][9] x[3][5] + a[3][0][9] x[4][1] + a[3][1][9] x[4][2] + a[3][2][9] x[4][3] + a[3][3][9] x[4][4] + a[3][4][9] x[4][5] + a[4][0][9] x[5][1] + a[4][1][9] x[5][2] + a[4][2][9] x[5][3] + a[4][3][9] x[5][4] + a[4][4][9] x[5][5] + + + + %... + (eq,%0) + + s[0] a[][][0] + s[1] a[][][1] + s[2] a[][][2] + s[3] a[][][3] + s[4] a[][][4] + s[5] a[][][5] + s[6] a[][][6] + s[7] a[][][7] + s[8] a[][][8] + s[9] a[][][9] + + + + + %... + (1,*,*,*,*,*,0,*,*,*,*)(4,0,0,*,*,*,1,0,*,*,*)(4,0,0,*,*,*,2,1,*,*,*)(4,0,0,*,*,*,3,2,*,*,*)(4,0,0,*,*,*,4,3,*,*,*)(4,0,0,*,*,*,5,4,*,*,*)(4,0,0,*,*,*,6,5,*,*,*)(4,0,*,0,*,*,0,*,1,*,*)(4,0,*,0,*,*,1,*,0,*,*)(4,0,*,0,*,*,2,*,1,*,*)(4,0,*,0,*,*,3,*,2,*,*)(4,0,*,0,*,*,4,*,3,*,*)(4,0,*,0,*,*,5,*,4,*,*)(4,0,*,0,*,*,6,*,5,*,*)(4,0,*,*,0,*,1,*,*,0,*)(4,0,*,*,0,*,2,*,*,1,*)(4,0,*,*,0,*,3,*,*,2,*)(4,0,*,*,0,*,4,*,*,3,*)(4,0,*,*,0,*,5,*,*,4,*)(4,0,*,*,0,*,6,*,*,5,*)(4,0,*,*,*,0,0,*,*,*,1)(4,0,*,*,*,0,1,*,*,*,0)(4,0,*,*,*,0,2,*,*,*,1)(4,0,*,*,*,0,3,*,*,*,2)(4,0,*,*,*,0,4,*,*,*,3)(4,0,*,*,*,0,5,*,*,*,4)(4,0,*,*,*,0,6,*,*,*,5)(4,1,1,*,*,*,1,0,*,*,*)(4,1,1,*,*,*,2,1,*,*,*)(4,1,1,*,*,*,3,2,*,*,*)(4,1,1,*,*,*,4,3,*,*,*)(4,1,1,*,*,*,5,4,*,*,*)(4,1,1,*,*,*,6,5,*,*,*)(4,1,*,1,*,*,0,*,1,*,*)(4,1,*,1,*,*,1,*,0,*,*)(4,1,*,1,*,*,2,*,1,*,*)(4,1,*,1,*,*,3,*,2,*,*)(4,1,*,1,*,*,4,*,3,*,*)(4,1,*,1,*,*,5,*,4,*,*)(4,1,*,1,*,*,6,*,5,*,*)(4,1,*,*,1,*,1,*,*,0,*)(4,1,*,*,1,*,2,*,*,1,*)(4,1,*,*,1,*,3,*,*,2,*)(4,1,*,*,1,*,4,*,*,3,*)(4,1,*,*,1,*,5,*,*,4,*)(4,1,*,*,1,*,6,*,*,5,*)(4,1,*,*,*,1,0,*,*,*,1)(4,1,*,*,*,1,1,*,*,*,0)(4,1,*,*,*,1,2,*,*,*,1)(4,1,*,*,*,1,3,*,*,*,2)(4,1,*,*,*,1,4,*,*,*,3)(4,1,*,*,*,1,5,*,*,*,4)(4,1,*,*,*,1,6,*,*,*,5)(4,2,2,*,*,*,1,0,*,*,*)(4,2,2,*,*,*,2,1,*,*,*)(4,2,2,*,*,*,3,2,*,*,*)(4,2,2,*,*,*,4,3,*,*,*)(4,2,2,*,*,*,5,4,*,*,*)(4,2,2,*,*,*,6,5,*,*,*)(4,2,*,2,*,*,0,*,1,*,*)(4,2,*,2,*,*,1,*,0,*,*)(4,2,*,2,*,*,2,*,1,*,*)(4,2,*,2,*,*,3,*,2,*,*)(4,2,*,2,*,*,4,*,3,*,*)(4,2,*,2,*,*,5,*,4,*,*)(4,2,*,2,*,*,6,*,5,*,*)(4,2,*,*,2,*,1,*,*,0,*)(4,2,*,*,2,*,2,*,*,1,*)(4,2,*,*,2,*,3,*,*,2,*)(4,2,*,*,2,*,4,*,*,3,*)(4,2,*,*,2,*,5,*,*,4,*)(4,2,*,*,2,*,6,*,*,5,*)(4,2,*,*,*,2,0,*,*,*,1)(4,2,*,*,*,2,1,*,*,*,0)(4,2,*,*,*,2,2,*,*,*,1)(4,2,*,*,*,2,3,*,*,*,2)(4,2,*,*,*,2,4,*,*,*,3)(4,2,*,*,*,2,5,*,*,*,4)(4,2,*,*,*,2,6,*,*,*,5)(4,3,3,*,*,*,1,0,*,*,*)(4,3,3,*,*,*,2,1,*,*,*)(4,3,3,*,*,*,3,2,*,*,*)(4,3,3,*,*,*,4,3,*,*,*)(4,3,3,*,*,*,5,4,*,*,*)(4,3,3,*,*,*,6,5,*,*,*)(4,3,*,3,*,*,0,*,1,*,*)(4,3,*,3,*,*,1,*,0,*,*)(4,3,*,3,*,*,2,*,1,*,*)(4,3,*,3,*,*,3,*,2,*,*)(4,3,*,3,*,*,4,*,3,*,*)(4,3,*,3,*,*,5,*,4,*,*)(4,3,*,3,*,*,6,*,5,*,*)(4,3,*,*,3,*,1,*,*,0,*)(4,3,*,*,3,*,2,*,*,1,*)(4,3,*,*,3,*,3,*,*,2,*)(4,3,*,*,3,*,4,*,*,3,*)(4,3,*,*,3,*,5,*,*,4,*)(4,3,*,*,3,*,6,*,*,5,*)(4,3,*,*,*,3,0,*,*,*,1)(4,3,*,*,*,3,1,*,*,*,0)(4,3,*,*,*,3,2,*,*,*,1)(4,3,*,*,*,3,3,*,*,*,2)(4,3,*,*,*,3,4,*,*,*,3)(4,3,*,*,*,3,5,*,*,*,4)(4,3,*,*,*,3,6,*,*,*,5)(4,4,4,*,*,*,1,0,*,*,*)(4,4,4,*,*,*,2,1,*,*,*)(4,4,4,*,*,*,3,2,*,*,*)(4,4,4,*,*,*,4,3,*,*,*)(4,4,4,*,*,*,5,4,*,*,*)(4,4,4,*,*,*,6,5,*,*,*)(4,4,*,4,*,*,0,*,1,*,*)(4,4,*,4,*,*,1,*,0,*,*)(4,4,*,4,*,*,2,*,1,*,*)(4,4,*,4,*,*,3,*,2,*,*)(4,4,*,4,*,*,4,*,3,*,*)(4,4,*,4,*,*,5,*,4,*,*)(4,4,*,4,*,*,6,*,5,*,*)(4,4,*,*,4,*,1,*,*,0,*)(4,4,*,*,4,*,2,*,*,1,*)(4,4,*,*,4,*,3,*,*,2,*)(4,4,*,*,4,*,4,*,*,3,*)(4,4,*,*,4,*,5,*,*,4,*)(4,4,*,*,4,*,6,*,*,5,*)(4,4,*,*,*,4,0,*,*,*,1)(4,4,*,*,*,4,1,*,*,*,0)(4,4,*,*,*,4,2,*,*,*,1)(4,4,*,*,*,4,3,*,*,*,2)(4,4,*,*,*,4,4,*,*,*,3)(4,4,*,*,*,4,5,*,*,*,4)(4,4,*,*,*,4,6,*,*,*,5)(4,5,5,*,*,*,1,0,*,*,*)(4,5,5,*,*,*,2,1,*,*,*)(4,5,5,*,*,*,3,2,*,*,*)(4,5,5,*,*,*,4,3,*,*,*)(4,5,5,*,*,*,5,4,*,*,*)(4,5,5,*,*,*,6,5,*,*,*)(4,5,*,5,*,*,0,*,1,*,*)(4,5,*,5,*,*,1,*,0,*,*)(4,5,*,5,*,*,2,*,1,*,*)(4,5,*,5,*,*,3,*,2,*,*)(4,5,*,5,*,*,4,*,3,*,*)(4,5,*,5,*,*,5,*,4,*,*)(4,5,*,5,*,*,6,*,5,*,*)(4,5,*,*,5,*,1,*,*,0,*)(4,5,*,*,5,*,2,*,*,1,*)(4,5,*,*,5,*,3,*,*,2,*)(4,5,*,*,5,*,4,*,*,3,*)(4,5,*,*,5,*,5,*,*,4,*)(4,5,*,*,5,*,6,*,*,5,*)(4,5,*,*,*,5,0,*,*,*,1)(4,5,*,*,*,5,1,*,*,*,0)(4,5,*,*,*,5,2,*,*,*,1)(4,5,*,*,*,5,3,*,*,*,2)(4,5,*,*,*,5,4,*,*,*,3)(4,5,*,*,*,5,5,*,*,*,4)(4,5,*,*,*,5,6,*,*,*,5)(4,6,6,*,*,*,1,0,*,*,*)(4,6,6,*,*,*,2,1,*,*,*)(4,6,6,*,*,*,3,2,*,*,*)(4,6,6,*,*,*,4,3,*,*,*)(4,6,6,*,*,*,5,4,*,*,*)(4,6,6,*,*,*,6,5,*,*,*)(4,6,*,6,*,*,0,*,1,*,*)(4,6,*,6,*,*,1,*,0,*,*)(4,6,*,6,*,*,2,*,1,*,*)(4,6,*,6,*,*,3,*,2,*,*)(4,6,*,6,*,*,4,*,3,*,*)(4,6,*,6,*,*,5,*,4,*,*)(4,6,*,6,*,*,6,*,5,*,*)(4,6,*,*,6,*,1,*,*,0,*)(4,6,*,*,6,*,2,*,*,1,*)(4,6,*,*,6,*,3,*,*,2,*)(4,6,*,*,6,*,4,*,*,3,*)(4,6,*,*,6,*,5,*,*,4,*)(4,6,*,*,6,*,6,*,*,5,*)(4,6,*,*,*,6,0,*,*,*,1)(4,6,*,*,*,6,1,*,*,*,0)(4,6,*,*,*,6,2,*,*,*,1)(4,6,*,*,*,6,3,*,*,*,2)(4,6,*,*,*,6,4,*,*,*,3)(4,6,*,*,*,6,5,*,*,*,4)(4,6,*,*,*,6,6,*,*,*,5)(4,7,7,*,*,*,1,0,*,*,*)(4,7,7,*,*,*,2,1,*,*,*)(4,7,7,*,*,*,3,2,*,*,*)(4,7,7,*,*,*,4,3,*,*,*)(4,7,7,*,*,*,5,4,*,*,*)(4,7,7,*,*,*,6,5,*,*,*)(4,7,*,7,*,*,0,*,1,*,*)(4,7,*,7,*,*,1,*,0,*,*)(4,7,*,7,*,*,2,*,1,*,*)(4,7,*,7,*,*,3,*,2,*,*)(4,7,*,7,*,*,4,*,3,*,*)(4,7,*,7,*,*,5,*,4,*,*)(4,7,*,7,*,*,6,*,5,*,*)(4,7,*,*,7,*,1,*,*,0,*)(4,7,*,*,7,*,2,*,*,1,*)(4,7,*,*,7,*,3,*,*,2,*)(4,7,*,*,7,*,4,*,*,3,*)(4,7,*,*,7,*,5,*,*,4,*)(4,7,*,*,7,*,6,*,*,5,*)(4,7,*,*,*,7,0,*,*,*,1)(4,7,*,*,*,7,1,*,*,*,0)(4,7,*,*,*,7,2,*,*,*,1)(4,7,*,*,*,7,3,*,*,*,2)(4,7,*,*,*,7,4,*,*,*,3)(4,7,*,*,*,7,5,*,*,*,4)(4,7,*,*,*,7,6,*,*,*,5)(4,8,8,*,*,*,1,0,*,*,*)(4,8,8,*,*,*,2,1,*,*,*)(4,8,8,*,*,*,3,2,*,*,*)(4,8,8,*,*,*,4,3,*,*,*)(4,8,8,*,*,*,5,4,*,*,*)(4,8,8,*,*,*,6,5,*,*,*)(4,8,*,8,*,*,0,*,1,*,*)(4,8,*,8,*,*,1,*,0,*,*)(4,8,*,8,*,*,2,*,1,*,*)(4,8,*,8,*,*,3,*,2,*,*)(4,8,*,8,*,*,4,*,3,*,*)(4,8,*,8,*,*,5,*,4,*,*)(4,8,*,8,*,*,6,*,5,*,*)(4,8,*,*,8,*,1,*,*,0,*)(4,8,*,*,8,*,2,*,*,1,*)(4,8,*,*,8,*,3,*,*,2,*)(4,8,*,*,8,*,4,*,*,3,*)(4,8,*,*,8,*,5,*,*,4,*)(4,8,*,*,8,*,6,*,*,5,*)(4,8,*,*,*,8,0,*,*,*,1)(4,8,*,*,*,8,1,*,*,*,0)(4,8,*,*,*,8,2,*,*,*,1)(4,8,*,*,*,8,3,*,*,*,2)(4,8,*,*,*,8,4,*,*,*,3)(4,8,*,*,*,8,5,*,*,*,4)(4,8,*,*,*,8,6,*,*,*,5)(4,9,9,*,*,*,1,0,*,*,*)(4,9,9,*,*,*,2,1,*,*,*)(4,9,9,*,*,*,3,2,*,*,*)(4,9,9,*,*,*,4,3,*,*,*)(4,9,9,*,*,*,5,4,*,*,*)(4,9,9,*,*,*,6,5,*,*,*)(4,9,*,9,*,*,0,*,1,*,*)(4,9,*,9,*,*,1,*,0,*,*)(4,9,*,9,*,*,2,*,1,*,*)(4,9,*,9,*,*,3,*,2,*,*)(4,9,*,9,*,*,4,*,3,*,*)(4,9,*,9,*,*,5,*,4,*,*)(4,9,*,9,*,*,6,*,5,*,*)(4,9,*,*,9,*,1,*,*,0,*)(4,9,*,*,9,*,2,*,*,1,*)(4,9,*,*,9,*,3,*,*,2,*)(4,9,*,*,9,*,4,*,*,3,*)(4,9,*,*,9,*,5,*,*,4,*)(4,9,*,*,9,*,6,*,*,5,*)(4,9,*,*,*,9,0,*,*,*,1)(4,9,*,*,*,9,1,*,*,*,0)(4,9,*,*,*,9,2,*,*,*,1)(4,9,*,*,*,9,3,*,*,*,2)(4,9,*,*,*,9,4,*,*,*,3)(4,9,*,*,*,9,5,*,*,*,4)(4,9,*,*,*,9,6,*,*,*,5) + + y[1][1] x[1][1] x[1][0] x[1][2] x[0][1] x[2][1] d[1][1] d[1][0] d[1][2] d[0][1] d[2][1] + y[2][3] x[2][3] x[2][2] x[2][4] x[1][3] x[3][3] d[2][3] d[2][2] d[2][4] d[1][3] d[3][3] + + + + %... + (1,*,*,*,*,*,0,*,*,*,*)(2,0,0,*,*,*,1,0,*,*,*)(2,0,0,*,*,*,2,1,*,*,*)(2,0,0,*,*,*,3,2,*,*,*)(2,0,0,*,*,*,4,3,*,*,*)(2,0,0,*,*,*,5,4,*,*,*)(2,0,0,*,*,*,6,5,*,*,*)(2,0,*,0,*,*,0,*,1,*,*)(2,0,*,0,*,*,1,*,0,*,*)(2,0,*,0,*,*,2,*,1,*,*)(2,0,*,0,*,*,3,*,2,*,*)(2,0,*,0,*,*,4,*,3,*,*)(2,0,*,0,*,*,5,*,4,*,*)(2,0,*,0,*,*,6,*,5,*,*)(2,0,*,*,0,*,1,*,*,0,*)(2,0,*,*,0,*,2,*,*,1,*)(2,0,*,*,0,*,3,*,*,2,*)(2,0,*,*,0,*,4,*,*,3,*)(2,0,*,*,0,*,5,*,*,4,*)(2,0,*,*,0,*,6,*,*,5,*)(2,0,*,*,*,0,0,*,*,*,1)(2,0,*,*,*,0,1,*,*,*,0)(2,0,*,*,*,0,2,*,*,*,1)(2,0,*,*,*,0,3,*,*,*,2)(2,0,*,*,*,0,4,*,*,*,3)(2,0,*,*,*,0,5,*,*,*,4)(2,0,*,*,*,0,6,*,*,*,5)(2,1,1,*,*,*,1,0,*,*,*)(2,1,1,*,*,*,2,1,*,*,*)(2,1,1,*,*,*,3,2,*,*,*)(2,1,1,*,*,*,4,3,*,*,*)(2,1,1,*,*,*,5,4,*,*,*)(2,1,1,*,*,*,6,5,*,*,*)(2,1,*,1,*,*,0,*,1,*,*)(2,1,*,1,*,*,1,*,0,*,*)(2,1,*,1,*,*,2,*,1,*,*)(2,1,*,1,*,*,3,*,2,*,*)(2,1,*,1,*,*,4,*,3,*,*)(2,1,*,1,*,*,5,*,4,*,*)(2,1,*,1,*,*,6,*,5,*,*)(2,1,*,*,1,*,1,*,*,0,*)(2,1,*,*,1,*,2,*,*,1,*)(2,1,*,*,1,*,3,*,*,2,*)(2,1,*,*,1,*,4,*,*,3,*)(2,1,*,*,1,*,5,*,*,4,*)(2,1,*,*,1,*,6,*,*,5,*)(2,1,*,*,*,1,0,*,*,*,1)(2,1,*,*,*,1,1,*,*,*,0)(2,1,*,*,*,1,2,*,*,*,1)(2,1,*,*,*,1,3,*,*,*,2)(2,1,*,*,*,1,4,*,*,*,3)(2,1,*,*,*,1,5,*,*,*,4)(2,1,*,*,*,1,6,*,*,*,5)(2,2,2,*,*,*,1,0,*,*,*)(2,2,2,*,*,*,2,1,*,*,*)(2,2,2,*,*,*,3,2,*,*,*)(2,2,2,*,*,*,4,3,*,*,*)(2,2,2,*,*,*,5,4,*,*,*)(2,2,2,*,*,*,6,5,*,*,*)(2,2,*,2,*,*,0,*,1,*,*)(2,2,*,2,*,*,1,*,0,*,*)(2,2,*,2,*,*,2,*,1,*,*)(2,2,*,2,*,*,3,*,2,*,*)(2,2,*,2,*,*,4,*,3,*,*)(2,2,*,2,*,*,5,*,4,*,*)(2,2,*,2,*,*,6,*,5,*,*)(2,2,*,*,2,*,1,*,*,0,*)(2,2,*,*,2,*,2,*,*,1,*)(2,2,*,*,2,*,3,*,*,2,*)(2,2,*,*,2,*,4,*,*,3,*)(2,2,*,*,2,*,5,*,*,4,*)(2,2,*,*,2,*,6,*,*,5,*)(2,2,*,*,*,2,0,*,*,*,1)(2,2,*,*,*,2,1,*,*,*,0)(2,2,*,*,*,2,2,*,*,*,1)(2,2,*,*,*,2,3,*,*,*,2)(2,2,*,*,*,2,4,*,*,*,3)(2,2,*,*,*,2,5,*,*,*,4)(2,2,*,*,*,2,6,*,*,*,5)(2,3,3,*,*,*,1,0,*,*,*)(2,3,3,*,*,*,2,1,*,*,*)(2,3,3,*,*,*,3,2,*,*,*)(2,3,3,*,*,*,4,3,*,*,*)(2,3,3,*,*,*,5,4,*,*,*)(2,3,3,*,*,*,6,5,*,*,*)(2,3,*,3,*,*,0,*,1,*,*)(2,3,*,3,*,*,1,*,0,*,*)(2,3,*,3,*,*,2,*,1,*,*)(2,3,*,3,*,*,3,*,2,*,*)(2,3,*,3,*,*,4,*,3,*,*)(2,3,*,3,*,*,5,*,4,*,*)(2,3,*,3,*,*,6,*,5,*,*)(2,3,*,*,3,*,1,*,*,0,*)(2,3,*,*,3,*,2,*,*,1,*)(2,3,*,*,3,*,3,*,*,2,*)(2,3,*,*,3,*,4,*,*,3,*)(2,3,*,*,3,*,5,*,*,4,*)(2,3,*,*,3,*,6,*,*,5,*)(2,3,*,*,*,3,0,*,*,*,1)(2,3,*,*,*,3,1,*,*,*,0)(2,3,*,*,*,3,2,*,*,*,1)(2,3,*,*,*,3,3,*,*,*,2)(2,3,*,*,*,3,4,*,*,*,3)(2,3,*,*,*,3,5,*,*,*,4)(2,3,*,*,*,3,6,*,*,*,5)(2,4,4,*,*,*,1,0,*,*,*)(2,4,4,*,*,*,2,1,*,*,*)(2,4,4,*,*,*,3,2,*,*,*)(2,4,4,*,*,*,4,3,*,*,*)(2,4,4,*,*,*,5,4,*,*,*)(2,4,4,*,*,*,6,5,*,*,*)(2,4,*,4,*,*,0,*,1,*,*)(2,4,*,4,*,*,1,*,0,*,*)(2,4,*,4,*,*,2,*,1,*,*)(2,4,*,4,*,*,3,*,2,*,*)(2,4,*,4,*,*,4,*,3,*,*)(2,4,*,4,*,*,5,*,4,*,*)(2,4,*,4,*,*,6,*,5,*,*)(2,4,*,*,4,*,1,*,*,0,*)(2,4,*,*,4,*,2,*,*,1,*)(2,4,*,*,4,*,3,*,*,2,*)(2,4,*,*,4,*,4,*,*,3,*)(2,4,*,*,4,*,5,*,*,4,*)(2,4,*,*,4,*,6,*,*,5,*)(2,4,*,*,*,4,0,*,*,*,1)(2,4,*,*,*,4,1,*,*,*,0)(2,4,*,*,*,4,2,*,*,*,1)(2,4,*,*,*,4,3,*,*,*,2)(2,4,*,*,*,4,4,*,*,*,3)(2,4,*,*,*,4,5,*,*,*,4)(2,4,*,*,*,4,6,*,*,*,5)(2,5,5,*,*,*,1,0,*,*,*)(2,5,5,*,*,*,2,1,*,*,*)(2,5,5,*,*,*,3,2,*,*,*)(2,5,5,*,*,*,4,3,*,*,*)(2,5,5,*,*,*,5,4,*,*,*)(2,5,5,*,*,*,6,5,*,*,*)(2,5,*,5,*,*,0,*,1,*,*)(2,5,*,5,*,*,1,*,0,*,*)(2,5,*,5,*,*,2,*,1,*,*)(2,5,*,5,*,*,3,*,2,*,*)(2,5,*,5,*,*,4,*,3,*,*)(2,5,*,5,*,*,5,*,4,*,*)(2,5,*,5,*,*,6,*,5,*,*)(2,5,*,*,5,*,1,*,*,0,*)(2,5,*,*,5,*,2,*,*,1,*)(2,5,*,*,5,*,3,*,*,2,*)(2,5,*,*,5,*,4,*,*,3,*)(2,5,*,*,5,*,5,*,*,4,*)(2,5,*,*,5,*,6,*,*,5,*)(2,5,*,*,*,5,0,*,*,*,1)(2,5,*,*,*,5,1,*,*,*,0)(2,5,*,*,*,5,2,*,*,*,1)(2,5,*,*,*,5,3,*,*,*,2)(2,5,*,*,*,5,4,*,*,*,3)(2,5,*,*,*,5,5,*,*,*,4)(2,5,*,*,*,5,6,*,*,*,5)(2,6,6,*,*,*,1,0,*,*,*)(2,6,6,*,*,*,2,1,*,*,*)(2,6,6,*,*,*,3,2,*,*,*)(2,6,6,*,*,*,4,3,*,*,*)(2,6,6,*,*,*,5,4,*,*,*)(2,6,6,*,*,*,6,5,*,*,*)(2,6,*,6,*,*,0,*,1,*,*)(2,6,*,6,*,*,1,*,0,*,*)(2,6,*,6,*,*,2,*,1,*,*)(2,6,*,6,*,*,3,*,2,*,*)(2,6,*,6,*,*,4,*,3,*,*)(2,6,*,6,*,*,5,*,4,*,*)(2,6,*,6,*,*,6,*,5,*,*)(2,6,*,*,6,*,1,*,*,0,*)(2,6,*,*,6,*,2,*,*,1,*)(2,6,*,*,6,*,3,*,*,2,*)(2,6,*,*,6,*,4,*,*,3,*)(2,6,*,*,6,*,5,*,*,4,*)(2,6,*,*,6,*,6,*,*,5,*)(2,6,*,*,*,6,0,*,*,*,1)(2,6,*,*,*,6,1,*,*,*,0)(2,6,*,*,*,6,2,*,*,*,1)(2,6,*,*,*,6,3,*,*,*,2)(2,6,*,*,*,6,4,*,*,*,3)(2,6,*,*,*,6,5,*,*,*,4)(2,6,*,*,*,6,6,*,*,*,5)(2,7,7,*,*,*,1,0,*,*,*)(2,7,7,*,*,*,2,1,*,*,*)(2,7,7,*,*,*,3,2,*,*,*)(2,7,7,*,*,*,4,3,*,*,*)(2,7,7,*,*,*,5,4,*,*,*)(2,7,7,*,*,*,6,5,*,*,*)(2,7,*,7,*,*,0,*,1,*,*)(2,7,*,7,*,*,1,*,0,*,*)(2,7,*,7,*,*,2,*,1,*,*)(2,7,*,7,*,*,3,*,2,*,*)(2,7,*,7,*,*,4,*,3,*,*)(2,7,*,7,*,*,5,*,4,*,*)(2,7,*,7,*,*,6,*,5,*,*)(2,7,*,*,7,*,1,*,*,0,*)(2,7,*,*,7,*,2,*,*,1,*)(2,7,*,*,7,*,3,*,*,2,*)(2,7,*,*,7,*,4,*,*,3,*)(2,7,*,*,7,*,5,*,*,4,*)(2,7,*,*,7,*,6,*,*,5,*)(2,7,*,*,*,7,0,*,*,*,1)(2,7,*,*,*,7,1,*,*,*,0)(2,7,*,*,*,7,2,*,*,*,1)(2,7,*,*,*,7,3,*,*,*,2)(2,7,*,*,*,7,4,*,*,*,3)(2,7,*,*,*,7,5,*,*,*,4)(2,7,*,*,*,7,6,*,*,*,5)(2,8,8,*,*,*,1,0,*,*,*)(2,8,8,*,*,*,2,1,*,*,*)(2,8,8,*,*,*,3,2,*,*,*)(2,8,8,*,*,*,4,3,*,*,*)(2,8,8,*,*,*,5,4,*,*,*)(2,8,8,*,*,*,6,5,*,*,*)(2,8,*,8,*,*,0,*,1,*,*)(2,8,*,8,*,*,1,*,0,*,*)(2,8,*,8,*,*,2,*,1,*,*)(2,8,*,8,*,*,3,*,2,*,*)(2,8,*,8,*,*,4,*,3,*,*)(2,8,*,8,*,*,5,*,4,*,*)(2,8,*,8,*,*,6,*,5,*,*)(2,8,*,*,8,*,1,*,*,0,*)(2,8,*,*,8,*,2,*,*,1,*)(2,8,*,*,8,*,3,*,*,2,*)(2,8,*,*,8,*,4,*,*,3,*)(2,8,*,*,8,*,5,*,*,4,*)(2,8,*,*,8,*,6,*,*,5,*)(2,8,*,*,*,8,0,*,*,*,1)(2,8,*,*,*,8,1,*,*,*,0)(2,8,*,*,*,8,2,*,*,*,1)(2,8,*,*,*,8,3,*,*,*,2)(2,8,*,*,*,8,4,*,*,*,3)(2,8,*,*,*,8,5,*,*,*,4)(2,8,*,*,*,8,6,*,*,*,5)(2,9,9,*,*,*,1,0,*,*,*)(2,9,9,*,*,*,2,1,*,*,*)(2,9,9,*,*,*,3,2,*,*,*)(2,9,9,*,*,*,4,3,*,*,*)(2,9,9,*,*,*,5,4,*,*,*)(2,9,9,*,*,*,6,5,*,*,*)(2,9,*,9,*,*,0,*,1,*,*)(2,9,*,9,*,*,1,*,0,*,*)(2,9,*,9,*,*,2,*,1,*,*)(2,9,*,9,*,*,3,*,2,*,*)(2,9,*,9,*,*,4,*,3,*,*)(2,9,*,9,*,*,5,*,4,*,*)(2,9,*,9,*,*,6,*,5,*,*)(2,9,*,*,9,*,1,*,*,0,*)(2,9,*,*,9,*,2,*,*,1,*)(2,9,*,*,9,*,3,*,*,2,*)(2,9,*,*,9,*,4,*,*,3,*)(2,9,*,*,9,*,5,*,*,4,*)(2,9,*,*,9,*,6,*,*,5,*)(2,9,*,*,*,9,0,*,*,*,1)(2,9,*,*,*,9,1,*,*,*,0)(2,9,*,*,*,9,2,*,*,*,1)(2,9,*,*,*,9,3,*,*,*,2)(2,9,*,*,*,9,4,*,*,*,3)(2,9,*,*,*,9,5,*,*,*,4)(2,9,*,*,*,9,6,*,*,*,5)(3,0,0,*,*,*,1,0,*,*,*)(3,0,0,*,*,*,2,1,*,*,*)(3,0,0,*,*,*,3,2,*,*,*)(3,0,0,*,*,*,4,3,*,*,*)(3,0,0,*,*,*,5,4,*,*,*)(3,0,0,*,*,*,6,5,*,*,*)(3,0,*,0,*,*,0,*,1,*,*)(3,0,*,0,*,*,1,*,0,*,*)(3,0,*,0,*,*,2,*,1,*,*)(3,0,*,0,*,*,3,*,2,*,*)(3,0,*,0,*,*,4,*,3,*,*)(3,0,*,0,*,*,5,*,4,*,*)(3,0,*,0,*,*,6,*,5,*,*)(3,0,*,*,0,*,1,*,*,0,*)(3,0,*,*,0,*,2,*,*,1,*)(3,0,*,*,0,*,3,*,*,2,*)(3,0,*,*,0,*,4,*,*,3,*)(3,0,*,*,0,*,5,*,*,4,*)(3,0,*,*,0,*,6,*,*,5,*)(3,0,*,*,*,0,0,*,*,*,1)(3,0,*,*,*,0,1,*,*,*,0)(3,0,*,*,*,0,2,*,*,*,1)(3,0,*,*,*,0,3,*,*,*,2)(3,0,*,*,*,0,4,*,*,*,3)(3,0,*,*,*,0,5,*,*,*,4)(3,0,*,*,*,0,6,*,*,*,5)(3,1,1,*,*,*,1,0,*,*,*)(3,1,1,*,*,*,2,1,*,*,*)(3,1,1,*,*,*,3,2,*,*,*)(3,1,1,*,*,*,4,3,*,*,*)(3,1,1,*,*,*,5,4,*,*,*)(3,1,1,*,*,*,6,5,*,*,*)(3,1,*,1,*,*,0,*,1,*,*)(3,1,*,1,*,*,1,*,0,*,*)(3,1,*,1,*,*,2,*,1,*,*)(3,1,*,1,*,*,3,*,2,*,*)(3,1,*,1,*,*,4,*,3,*,*)(3,1,*,1,*,*,5,*,4,*,*)(3,1,*,1,*,*,6,*,5,*,*)(3,1,*,*,1,*,1,*,*,0,*)(3,1,*,*,1,*,2,*,*,1,*)(3,1,*,*,1,*,3,*,*,2,*)(3,1,*,*,1,*,4,*,*,3,*)(3,1,*,*,1,*,5,*,*,4,*)(3,1,*,*,1,*,6,*,*,5,*)(3,1,*,*,*,1,0,*,*,*,1)(3,1,*,*,*,1,1,*,*,*,0)(3,1,*,*,*,1,2,*,*,*,1)(3,1,*,*,*,1,3,*,*,*,2)(3,1,*,*,*,1,4,*,*,*,3)(3,1,*,*,*,1,5,*,*,*,4)(3,1,*,*,*,1,6,*,*,*,5)(3,2,2,*,*,*,1,0,*,*,*)(3,2,2,*,*,*,2,1,*,*,*)(3,2,2,*,*,*,3,2,*,*,*)(3,2,2,*,*,*,4,3,*,*,*)(3,2,2,*,*,*,5,4,*,*,*)(3,2,2,*,*,*,6,5,*,*,*)(3,2,*,2,*,*,0,*,1,*,*)(3,2,*,2,*,*,1,*,0,*,*)(3,2,*,2,*,*,2,*,1,*,*)(3,2,*,2,*,*,3,*,2,*,*)(3,2,*,2,*,*,4,*,3,*,*)(3,2,*,2,*,*,5,*,4,*,*)(3,2,*,2,*,*,6,*,5,*,*)(3,2,*,*,2,*,1,*,*,0,*)(3,2,*,*,2,*,2,*,*,1,*)(3,2,*,*,2,*,3,*,*,2,*)(3,2,*,*,2,*,4,*,*,3,*)(3,2,*,*,2,*,5,*,*,4,*)(3,2,*,*,2,*,6,*,*,5,*)(3,2,*,*,*,2,0,*,*,*,1)(3,2,*,*,*,2,1,*,*,*,0)(3,2,*,*,*,2,2,*,*,*,1)(3,2,*,*,*,2,3,*,*,*,2)(3,2,*,*,*,2,4,*,*,*,3)(3,2,*,*,*,2,5,*,*,*,4)(3,2,*,*,*,2,6,*,*,*,5)(3,3,3,*,*,*,1,0,*,*,*)(3,3,3,*,*,*,2,1,*,*,*)(3,3,3,*,*,*,3,2,*,*,*)(3,3,3,*,*,*,4,3,*,*,*)(3,3,3,*,*,*,5,4,*,*,*)(3,3,3,*,*,*,6,5,*,*,*)(3,3,*,3,*,*,0,*,1,*,*)(3,3,*,3,*,*,1,*,0,*,*)(3,3,*,3,*,*,2,*,1,*,*)(3,3,*,3,*,*,3,*,2,*,*)(3,3,*,3,*,*,4,*,3,*,*)(3,3,*,3,*,*,5,*,4,*,*)(3,3,*,3,*,*,6,*,5,*,*)(3,3,*,*,3,*,1,*,*,0,*)(3,3,*,*,3,*,2,*,*,1,*)(3,3,*,*,3,*,3,*,*,2,*)(3,3,*,*,3,*,4,*,*,3,*)(3,3,*,*,3,*,5,*,*,4,*)(3,3,*,*,3,*,6,*,*,5,*)(3,3,*,*,*,3,0,*,*,*,1)(3,3,*,*,*,3,1,*,*,*,0)(3,3,*,*,*,3,2,*,*,*,1)(3,3,*,*,*,3,3,*,*,*,2)(3,3,*,*,*,3,4,*,*,*,3)(3,3,*,*,*,3,5,*,*,*,4)(3,3,*,*,*,3,6,*,*,*,5)(3,4,4,*,*,*,1,0,*,*,*)(3,4,4,*,*,*,2,1,*,*,*)(3,4,4,*,*,*,3,2,*,*,*)(3,4,4,*,*,*,4,3,*,*,*)(3,4,4,*,*,*,5,4,*,*,*)(3,4,4,*,*,*,6,5,*,*,*)(3,4,*,4,*,*,0,*,1,*,*)(3,4,*,4,*,*,1,*,0,*,*)(3,4,*,4,*,*,2,*,1,*,*)(3,4,*,4,*,*,3,*,2,*,*)(3,4,*,4,*,*,4,*,3,*,*)(3,4,*,4,*,*,5,*,4,*,*)(3,4,*,4,*,*,6,*,5,*,*)(3,4,*,*,4,*,1,*,*,0,*)(3,4,*,*,4,*,2,*,*,1,*)(3,4,*,*,4,*,3,*,*,2,*)(3,4,*,*,4,*,4,*,*,3,*)(3,4,*,*,4,*,5,*,*,4,*)(3,4,*,*,4,*,6,*,*,5,*)(3,4,*,*,*,4,0,*,*,*,1)(3,4,*,*,*,4,1,*,*,*,0)(3,4,*,*,*,4,2,*,*,*,1)(3,4,*,*,*,4,3,*,*,*,2)(3,4,*,*,*,4,4,*,*,*,3)(3,4,*,*,*,4,5,*,*,*,4)(3,4,*,*,*,4,6,*,*,*,5)(3,5,5,*,*,*,1,0,*,*,*)(3,5,5,*,*,*,2,1,*,*,*)(3,5,5,*,*,*,3,2,*,*,*)(3,5,5,*,*,*,4,3,*,*,*)(3,5,5,*,*,*,5,4,*,*,*)(3,5,5,*,*,*,6,5,*,*,*)(3,5,*,5,*,*,0,*,1,*,*)(3,5,*,5,*,*,1,*,0,*,*)(3,5,*,5,*,*,2,*,1,*,*)(3,5,*,5,*,*,3,*,2,*,*)(3,5,*,5,*,*,4,*,3,*,*)(3,5,*,5,*,*,5,*,4,*,*)(3,5,*,5,*,*,6,*,5,*,*)(3,5,*,*,5,*,1,*,*,0,*)(3,5,*,*,5,*,2,*,*,1,*)(3,5,*,*,5,*,3,*,*,2,*)(3,5,*,*,5,*,4,*,*,3,*)(3,5,*,*,5,*,5,*,*,4,*)(3,5,*,*,5,*,6,*,*,5,*)(3,5,*,*,*,5,0,*,*,*,1)(3,5,*,*,*,5,1,*,*,*,0)(3,5,*,*,*,5,2,*,*,*,1)(3,5,*,*,*,5,3,*,*,*,2)(3,5,*,*,*,5,4,*,*,*,3)(3,5,*,*,*,5,5,*,*,*,4)(3,5,*,*,*,5,6,*,*,*,5)(3,6,6,*,*,*,1,0,*,*,*)(3,6,6,*,*,*,2,1,*,*,*)(3,6,6,*,*,*,3,2,*,*,*)(3,6,6,*,*,*,4,3,*,*,*)(3,6,6,*,*,*,5,4,*,*,*)(3,6,6,*,*,*,6,5,*,*,*)(3,6,*,6,*,*,0,*,1,*,*)(3,6,*,6,*,*,1,*,0,*,*)(3,6,*,6,*,*,2,*,1,*,*)(3,6,*,6,*,*,3,*,2,*,*)(3,6,*,6,*,*,4,*,3,*,*)(3,6,*,6,*,*,5,*,4,*,*)(3,6,*,6,*,*,6,*,5,*,*)(3,6,*,*,6,*,1,*,*,0,*)(3,6,*,*,6,*,2,*,*,1,*)(3,6,*,*,6,*,3,*,*,2,*)(3,6,*,*,6,*,4,*,*,3,*)(3,6,*,*,6,*,5,*,*,4,*)(3,6,*,*,6,*,6,*,*,5,*)(3,6,*,*,*,6,0,*,*,*,1)(3,6,*,*,*,6,1,*,*,*,0)(3,6,*,*,*,6,2,*,*,*,1)(3,6,*,*,*,6,3,*,*,*,2)(3,6,*,*,*,6,4,*,*,*,3)(3,6,*,*,*,6,5,*,*,*,4)(3,6,*,*,*,6,6,*,*,*,5)(3,7,7,*,*,*,1,0,*,*,*)(3,7,7,*,*,*,2,1,*,*,*)(3,7,7,*,*,*,3,2,*,*,*)(3,7,7,*,*,*,4,3,*,*,*)(3,7,7,*,*,*,5,4,*,*,*)(3,7,7,*,*,*,6,5,*,*,*)(3,7,*,7,*,*,0,*,1,*,*)(3,7,*,7,*,*,1,*,0,*,*)(3,7,*,7,*,*,2,*,1,*,*)(3,7,*,7,*,*,3,*,2,*,*)(3,7,*,7,*,*,4,*,3,*,*)(3,7,*,7,*,*,5,*,4,*,*)(3,7,*,7,*,*,6,*,5,*,*)(3,7,*,*,7,*,1,*,*,0,*)(3,7,*,*,7,*,2,*,*,1,*)(3,7,*,*,7,*,3,*,*,2,*)(3,7,*,*,7,*,4,*,*,3,*)(3,7,*,*,7,*,5,*,*,4,*)(3,7,*,*,7,*,6,*,*,5,*)(3,7,*,*,*,7,0,*,*,*,1)(3,7,*,*,*,7,1,*,*,*,0)(3,7,*,*,*,7,2,*,*,*,1)(3,7,*,*,*,7,3,*,*,*,2)(3,7,*,*,*,7,4,*,*,*,3)(3,7,*,*,*,7,5,*,*,*,4)(3,7,*,*,*,7,6,*,*,*,5)(3,8,8,*,*,*,1,0,*,*,*)(3,8,8,*,*,*,2,1,*,*,*)(3,8,8,*,*,*,3,2,*,*,*)(3,8,8,*,*,*,4,3,*,*,*)(3,8,8,*,*,*,5,4,*,*,*)(3,8,8,*,*,*,6,5,*,*,*)(3,8,*,8,*,*,0,*,1,*,*)(3,8,*,8,*,*,1,*,0,*,*)(3,8,*,8,*,*,2,*,1,*,*)(3,8,*,8,*,*,3,*,2,*,*)(3,8,*,8,*,*,4,*,3,*,*)(3,8,*,8,*,*,5,*,4,*,*)(3,8,*,8,*,*,6,*,5,*,*)(3,8,*,*,8,*,1,*,*,0,*)(3,8,*,*,8,*,2,*,*,1,*)(3,8,*,*,8,*,3,*,*,2,*)(3,8,*,*,8,*,4,*,*,3,*)(3,8,*,*,8,*,5,*,*,4,*)(3,8,*,*,8,*,6,*,*,5,*)(3,8,*,*,*,8,0,*,*,*,1)(3,8,*,*,*,8,1,*,*,*,0)(3,8,*,*,*,8,2,*,*,*,1)(3,8,*,*,*,8,3,*,*,*,2)(3,8,*,*,*,8,4,*,*,*,3)(3,8,*,*,*,8,5,*,*,*,4)(3,8,*,*,*,8,6,*,*,*,5)(3,9,9,*,*,*,1,0,*,*,*)(3,9,9,*,*,*,2,1,*,*,*)(3,9,9,*,*,*,3,2,*,*,*)(3,9,9,*,*,*,4,3,*,*,*)(3,9,9,*,*,*,5,4,*,*,*)(3,9,9,*,*,*,6,5,*,*,*)(3,9,*,9,*,*,0,*,1,*,*)(3,9,*,9,*,*,1,*,0,*,*)(3,9,*,9,*,*,2,*,1,*,*)(3,9,*,9,*,*,3,*,2,*,*)(3,9,*,9,*,*,4,*,3,*,*)(3,9,*,9,*,*,5,*,4,*,*)(3,9,*,9,*,*,6,*,5,*,*)(3,9,*,*,9,*,1,*,*,0,*)(3,9,*,*,9,*,2,*,*,1,*)(3,9,*,*,9,*,3,*,*,2,*)(3,9,*,*,9,*,4,*,*,3,*)(3,9,*,*,9,*,5,*,*,4,*)(3,9,*,*,9,*,6,*,*,5,*)(3,9,*,*,*,9,0,*,*,*,1)(3,9,*,*,*,9,1,*,*,*,0)(3,9,*,*,*,9,2,*,*,*,1)(3,9,*,*,*,9,3,*,*,*,2)(3,9,*,*,*,9,4,*,*,*,3)(3,9,*,*,*,9,5,*,*,*,4)(3,9,*,*,*,9,6,*,*,*,5)(4,0,0,*,*,*,1,0,*,*,*)(4,0,0,*,*,*,2,1,*,*,*)(4,0,0,*,*,*,3,2,*,*,*)(4,0,0,*,*,*,4,3,*,*,*)(4,0,0,*,*,*,5,4,*,*,*)(4,0,0,*,*,*,6,5,*,*,*)(4,0,*,0,*,*,0,*,1,*,*)(4,0,*,0,*,*,1,*,0,*,*)(4,0,*,0,*,*,2,*,1,*,*)(4,0,*,0,*,*,3,*,2,*,*)(4,0,*,0,*,*,4,*,3,*,*)(4,0,*,0,*,*,5,*,4,*,*)(4,0,*,0,*,*,6,*,5,*,*)(4,0,*,*,0,*,1,*,*,0,*)(4,0,*,*,0,*,2,*,*,1,*)(4,0,*,*,0,*,3,*,*,2,*)(4,0,*,*,0,*,4,*,*,3,*)(4,0,*,*,0,*,5,*,*,4,*)(4,0,*,*,0,*,6,*,*,5,*)(4,0,*,*,*,0,0,*,*,*,1)(4,0,*,*,*,0,1,*,*,*,0)(4,0,*,*,*,0,2,*,*,*,1)(4,0,*,*,*,0,3,*,*,*,2)(4,0,*,*,*,0,4,*,*,*,3)(4,0,*,*,*,0,5,*,*,*,4)(4,0,*,*,*,0,6,*,*,*,5)(4,1,1,*,*,*,1,0,*,*,*)(4,1,1,*,*,*,2,1,*,*,*)(4,1,1,*,*,*,3,2,*,*,*)(4,1,1,*,*,*,4,3,*,*,*)(4,1,1,*,*,*,5,4,*,*,*)(4,1,1,*,*,*,6,5,*,*,*)(4,1,*,1,*,*,0,*,1,*,*)(4,1,*,1,*,*,1,*,0,*,*)(4,1,*,1,*,*,2,*,1,*,*)(4,1,*,1,*,*,3,*,2,*,*)(4,1,*,1,*,*,4,*,3,*,*)(4,1,*,1,*,*,5,*,4,*,*)(4,1,*,1,*,*,6,*,5,*,*)(4,1,*,*,1,*,1,*,*,0,*)(4,1,*,*,1,*,2,*,*,1,*)(4,1,*,*,1,*,3,*,*,2,*)(4,1,*,*,1,*,4,*,*,3,*)(4,1,*,*,1,*,5,*,*,4,*)(4,1,*,*,1,*,6,*,*,5,*)(4,1,*,*,*,1,0,*,*,*,1)(4,1,*,*,*,1,1,*,*,*,0)(4,1,*,*,*,1,2,*,*,*,1)(4,1,*,*,*,1,3,*,*,*,2)(4,1,*,*,*,1,4,*,*,*,3)(4,1,*,*,*,1,5,*,*,*,4)(4,1,*,*,*,1,6,*,*,*,5)(4,2,2,*,*,*,1,0,*,*,*)(4,2,2,*,*,*,2,1,*,*,*)(4,2,2,*,*,*,3,2,*,*,*)(4,2,2,*,*,*,4,3,*,*,*)(4,2,2,*,*,*,5,4,*,*,*)(4,2,2,*,*,*,6,5,*,*,*)(4,2,*,2,*,*,0,*,1,*,*)(4,2,*,2,*,*,1,*,0,*,*)(4,2,*,2,*,*,2,*,1,*,*)(4,2,*,2,*,*,3,*,2,*,*)(4,2,*,2,*,*,4,*,3,*,*)(4,2,*,2,*,*,5,*,4,*,*)(4,2,*,2,*,*,6,*,5,*,*)(4,2,*,*,2,*,1,*,*,0,*)(4,2,*,*,2,*,2,*,*,1,*)(4,2,*,*,2,*,3,*,*,2,*)(4,2,*,*,2,*,4,*,*,3,*)(4,2,*,*,2,*,5,*,*,4,*)(4,2,*,*,2,*,6,*,*,5,*)(4,2,*,*,*,2,0,*,*,*,1)(4,2,*,*,*,2,1,*,*,*,0)(4,2,*,*,*,2,2,*,*,*,1)(4,2,*,*,*,2,3,*,*,*,2)(4,2,*,*,*,2,4,*,*,*,3)(4,2,*,*,*,2,5,*,*,*,4)(4,2,*,*,*,2,6,*,*,*,5)(4,3,3,*,*,*,1,0,*,*,*)(4,3,3,*,*,*,2,1,*,*,*)(4,3,3,*,*,*,3,2,*,*,*)(4,3,3,*,*,*,4,3,*,*,*)(4,3,3,*,*,*,5,4,*,*,*)(4,3,3,*,*,*,6,5,*,*,*)(4,3,*,3,*,*,0,*,1,*,*)(4,3,*,3,*,*,1,*,0,*,*)(4,3,*,3,*,*,2,*,1,*,*)(4,3,*,3,*,*,3,*,2,*,*)(4,3,*,3,*,*,4,*,3,*,*)(4,3,*,3,*,*,5,*,4,*,*)(4,3,*,3,*,*,6,*,5,*,*)(4,3,*,*,3,*,1,*,*,0,*)(4,3,*,*,3,*,2,*,*,1,*)(4,3,*,*,3,*,3,*,*,2,*)(4,3,*,*,3,*,4,*,*,3,*)(4,3,*,*,3,*,5,*,*,4,*)(4,3,*,*,3,*,6,*,*,5,*)(4,3,*,*,*,3,0,*,*,*,1)(4,3,*,*,*,3,1,*,*,*,0)(4,3,*,*,*,3,2,*,*,*,1)(4,3,*,*,*,3,3,*,*,*,2)(4,3,*,*,*,3,4,*,*,*,3)(4,3,*,*,*,3,5,*,*,*,4)(4,3,*,*,*,3,6,*,*,*,5)(4,4,4,*,*,*,1,0,*,*,*)(4,4,4,*,*,*,2,1,*,*,*)(4,4,4,*,*,*,3,2,*,*,*)(4,4,4,*,*,*,4,3,*,*,*)(4,4,4,*,*,*,5,4,*,*,*)(4,4,4,*,*,*,6,5,*,*,*)(4,4,*,4,*,*,0,*,1,*,*)(4,4,*,4,*,*,1,*,0,*,*)(4,4,*,4,*,*,2,*,1,*,*)(4,4,*,4,*,*,3,*,2,*,*)(4,4,*,4,*,*,4,*,3,*,*)(4,4,*,4,*,*,5,*,4,*,*)(4,4,*,4,*,*,6,*,5,*,*)(4,4,*,*,4,*,1,*,*,0,*)(4,4,*,*,4,*,2,*,*,1,*)(4,4,*,*,4,*,3,*,*,2,*)(4,4,*,*,4,*,4,*,*,3,*)(4,4,*,*,4,*,5,*,*,4,*)(4,4,*,*,4,*,6,*,*,5,*)(4,4,*,*,*,4,0,*,*,*,1)(4,4,*,*,*,4,1,*,*,*,0)(4,4,*,*,*,4,2,*,*,*,1)(4,4,*,*,*,4,3,*,*,*,2)(4,4,*,*,*,4,4,*,*,*,3)(4,4,*,*,*,4,5,*,*,*,4)(4,4,*,*,*,4,6,*,*,*,5)(4,5,5,*,*,*,1,0,*,*,*)(4,5,5,*,*,*,2,1,*,*,*)(4,5,5,*,*,*,3,2,*,*,*)(4,5,5,*,*,*,4,3,*,*,*)(4,5,5,*,*,*,5,4,*,*,*)(4,5,5,*,*,*,6,5,*,*,*)(4,5,*,5,*,*,0,*,1,*,*)(4,5,*,5,*,*,1,*,0,*,*)(4,5,*,5,*,*,2,*,1,*,*)(4,5,*,5,*,*,3,*,2,*,*)(4,5,*,5,*,*,4,*,3,*,*)(4,5,*,5,*,*,5,*,4,*,*)(4,5,*,5,*,*,6,*,5,*,*)(4,5,*,*,5,*,1,*,*,0,*)(4,5,*,*,5,*,2,*,*,1,*)(4,5,*,*,5,*,3,*,*,2,*)(4,5,*,*,5,*,4,*,*,3,*)(4,5,*,*,5,*,5,*,*,4,*)(4,5,*,*,5,*,6,*,*,5,*)(4,5,*,*,*,5,0,*,*,*,1)(4,5,*,*,*,5,1,*,*,*,0)(4,5,*,*,*,5,2,*,*,*,1)(4,5,*,*,*,5,3,*,*,*,2)(4,5,*,*,*,5,4,*,*,*,3)(4,5,*,*,*,5,5,*,*,*,4)(4,5,*,*,*,5,6,*,*,*,5)(4,6,6,*,*,*,1,0,*,*,*)(4,6,6,*,*,*,2,1,*,*,*)(4,6,6,*,*,*,3,2,*,*,*)(4,6,6,*,*,*,4,3,*,*,*)(4,6,6,*,*,*,5,4,*,*,*)(4,6,6,*,*,*,6,5,*,*,*)(4,6,*,6,*,*,0,*,1,*,*)(4,6,*,6,*,*,1,*,0,*,*)(4,6,*,6,*,*,2,*,1,*,*)(4,6,*,6,*,*,3,*,2,*,*)(4,6,*,6,*,*,4,*,3,*,*)(4,6,*,6,*,*,5,*,4,*,*)(4,6,*,6,*,*,6,*,5,*,*)(4,6,*,*,6,*,1,*,*,0,*)(4,6,*,*,6,*,2,*,*,1,*)(4,6,*,*,6,*,3,*,*,2,*)(4,6,*,*,6,*,4,*,*,3,*)(4,6,*,*,6,*,5,*,*,4,*)(4,6,*,*,6,*,6,*,*,5,*)(4,6,*,*,*,6,0,*,*,*,1)(4,6,*,*,*,6,1,*,*,*,0)(4,6,*,*,*,6,2,*,*,*,1)(4,6,*,*,*,6,3,*,*,*,2)(4,6,*,*,*,6,4,*,*,*,3)(4,6,*,*,*,6,5,*,*,*,4)(4,6,*,*,*,6,6,*,*,*,5)(4,7,7,*,*,*,1,0,*,*,*)(4,7,7,*,*,*,2,1,*,*,*)(4,7,7,*,*,*,3,2,*,*,*)(4,7,7,*,*,*,4,3,*,*,*)(4,7,7,*,*,*,5,4,*,*,*)(4,7,7,*,*,*,6,5,*,*,*)(4,7,*,7,*,*,0,*,1,*,*)(4,7,*,7,*,*,1,*,0,*,*)(4,7,*,7,*,*,2,*,1,*,*)(4,7,*,7,*,*,3,*,2,*,*)(4,7,*,7,*,*,4,*,3,*,*)(4,7,*,7,*,*,5,*,4,*,*)(4,7,*,7,*,*,6,*,5,*,*)(4,7,*,*,7,*,1,*,*,0,*)(4,7,*,*,7,*,2,*,*,1,*)(4,7,*,*,7,*,3,*,*,2,*)(4,7,*,*,7,*,4,*,*,3,*)(4,7,*,*,7,*,5,*,*,4,*)(4,7,*,*,7,*,6,*,*,5,*)(4,7,*,*,*,7,0,*,*,*,1)(4,7,*,*,*,7,1,*,*,*,0)(4,7,*,*,*,7,2,*,*,*,1)(4,7,*,*,*,7,3,*,*,*,2)(4,7,*,*,*,7,4,*,*,*,3)(4,7,*,*,*,7,5,*,*,*,4)(4,7,*,*,*,7,6,*,*,*,5)(4,8,8,*,*,*,1,0,*,*,*)(4,8,8,*,*,*,2,1,*,*,*)(4,8,8,*,*,*,3,2,*,*,*)(4,8,8,*,*,*,4,3,*,*,*)(4,8,8,*,*,*,5,4,*,*,*)(4,8,8,*,*,*,6,5,*,*,*)(4,8,*,8,*,*,0,*,1,*,*)(4,8,*,8,*,*,1,*,0,*,*)(4,8,*,8,*,*,2,*,1,*,*)(4,8,*,8,*,*,3,*,2,*,*)(4,8,*,8,*,*,4,*,3,*,*)(4,8,*,8,*,*,5,*,4,*,*)(4,8,*,8,*,*,6,*,5,*,*)(4,8,*,*,8,*,1,*,*,0,*)(4,8,*,*,8,*,2,*,*,1,*)(4,8,*,*,8,*,3,*,*,2,*)(4,8,*,*,8,*,4,*,*,3,*)(4,8,*,*,8,*,5,*,*,4,*)(4,8,*,*,8,*,6,*,*,5,*)(4,8,*,*,*,8,0,*,*,*,1)(4,8,*,*,*,8,1,*,*,*,0)(4,8,*,*,*,8,2,*,*,*,1)(4,8,*,*,*,8,3,*,*,*,2)(4,8,*,*,*,8,4,*,*,*,3)(4,8,*,*,*,8,5,*,*,*,4)(4,8,*,*,*,8,6,*,*,*,5)(4,9,9,*,*,*,1,0,*,*,*)(4,9,9,*,*,*,2,1,*,*,*)(4,9,9,*,*,*,3,2,*,*,*)(4,9,9,*,*,*,4,3,*,*,*)(4,9,9,*,*,*,5,4,*,*,*)(4,9,9,*,*,*,6,5,*,*,*)(4,9,*,9,*,*,0,*,1,*,*)(4,9,*,9,*,*,1,*,0,*,*)(4,9,*,9,*,*,2,*,1,*,*)(4,9,*,9,*,*,3,*,2,*,*)(4,9,*,9,*,*,4,*,3,*,*)(4,9,*,9,*,*,5,*,4,*,*)(4,9,*,9,*,*,6,*,5,*,*)(4,9,*,*,9,*,1,*,*,0,*)(4,9,*,*,9,*,2,*,*,1,*)(4,9,*,*,9,*,3,*,*,2,*)(4,9,*,*,9,*,4,*,*,3,*)(4,9,*,*,9,*,5,*,*,4,*)(4,9,*,*,9,*,6,*,*,5,*)(4,9,*,*,*,9,0,*,*,*,1)(4,9,*,*,*,9,1,*,*,*,0)(4,9,*,*,*,9,2,*,*,*,1)(4,9,*,*,*,9,3,*,*,*,2)(4,9,*,*,*,9,4,*,*,*,3)(4,9,*,*,*,9,5,*,*,*,4)(4,9,*,*,*,9,6,*,*,*,5)(5,0,0,*,*,*,1,0,*,*,*)(5,0,0,*,*,*,2,1,*,*,*)(5,0,0,*,*,*,3,2,*,*,*)(5,0,0,*,*,*,4,3,*,*,*)(5,0,0,*,*,*,5,4,*,*,*)(5,0,0,*,*,*,6,5,*,*,*)(5,0,*,0,*,*,0,*,1,*,*)(5,0,*,0,*,*,1,*,0,*,*)(5,0,*,0,*,*,2,*,1,*,*)(5,0,*,0,*,*,3,*,2,*,*)(5,0,*,0,*,*,4,*,3,*,*)(5,0,*,0,*,*,5,*,4,*,*)(5,0,*,0,*,*,6,*,5,*,*)(5,0,*,*,0,*,1,*,*,0,*)(5,0,*,*,0,*,2,*,*,1,*)(5,0,*,*,0,*,3,*,*,2,*)(5,0,*,*,0,*,4,*,*,3,*)(5,0,*,*,0,*,5,*,*,4,*)(5,0,*,*,0,*,6,*,*,5,*)(5,0,*,*,*,0,0,*,*,*,1)(5,0,*,*,*,0,1,*,*,*,0)(5,0,*,*,*,0,2,*,*,*,1)(5,0,*,*,*,0,3,*,*,*,2)(5,0,*,*,*,0,4,*,*,*,3)(5,0,*,*,*,0,5,*,*,*,4)(5,0,*,*,*,0,6,*,*,*,5)(5,1,1,*,*,*,1,0,*,*,*)(5,1,1,*,*,*,2,1,*,*,*)(5,1,1,*,*,*,3,2,*,*,*)(5,1,1,*,*,*,4,3,*,*,*)(5,1,1,*,*,*,5,4,*,*,*)(5,1,1,*,*,*,6,5,*,*,*)(5,1,*,1,*,*,0,*,1,*,*)(5,1,*,1,*,*,1,*,0,*,*)(5,1,*,1,*,*,2,*,1,*,*)(5,1,*,1,*,*,3,*,2,*,*)(5,1,*,1,*,*,4,*,3,*,*)(5,1,*,1,*,*,5,*,4,*,*)(5,1,*,1,*,*,6,*,5,*,*)(5,1,*,*,1,*,1,*,*,0,*)(5,1,*,*,1,*,2,*,*,1,*)(5,1,*,*,1,*,3,*,*,2,*)(5,1,*,*,1,*,4,*,*,3,*)(5,1,*,*,1,*,5,*,*,4,*)(5,1,*,*,1,*,6,*,*,5,*)(5,1,*,*,*,1,0,*,*,*,1)(5,1,*,*,*,1,1,*,*,*,0)(5,1,*,*,*,1,2,*,*,*,1)(5,1,*,*,*,1,3,*,*,*,2)(5,1,*,*,*,1,4,*,*,*,3)(5,1,*,*,*,1,5,*,*,*,4)(5,1,*,*,*,1,6,*,*,*,5)(5,2,2,*,*,*,1,0,*,*,*)(5,2,2,*,*,*,2,1,*,*,*)(5,2,2,*,*,*,3,2,*,*,*)(5,2,2,*,*,*,4,3,*,*,*)(5,2,2,*,*,*,5,4,*,*,*)(5,2,2,*,*,*,6,5,*,*,*)(5,2,*,2,*,*,0,*,1,*,*)(5,2,*,2,*,*,1,*,0,*,*)(5,2,*,2,*,*,2,*,1,*,*)(5,2,*,2,*,*,3,*,2,*,*)(5,2,*,2,*,*,4,*,3,*,*)(5,2,*,2,*,*,5,*,4,*,*)(5,2,*,2,*,*,6,*,5,*,*)(5,2,*,*,2,*,1,*,*,0,*)(5,2,*,*,2,*,2,*,*,1,*)(5,2,*,*,2,*,3,*,*,2,*)(5,2,*,*,2,*,4,*,*,3,*)(5,2,*,*,2,*,5,*,*,4,*)(5,2,*,*,2,*,6,*,*,5,*)(5,2,*,*,*,2,0,*,*,*,1)(5,2,*,*,*,2,1,*,*,*,0)(5,2,*,*,*,2,2,*,*,*,1)(5,2,*,*,*,2,3,*,*,*,2)(5,2,*,*,*,2,4,*,*,*,3)(5,2,*,*,*,2,5,*,*,*,4)(5,2,*,*,*,2,6,*,*,*,5)(5,3,3,*,*,*,1,0,*,*,*)(5,3,3,*,*,*,2,1,*,*,*)(5,3,3,*,*,*,3,2,*,*,*)(5,3,3,*,*,*,4,3,*,*,*)(5,3,3,*,*,*,5,4,*,*,*)(5,3,3,*,*,*,6,5,*,*,*)(5,3,*,3,*,*,0,*,1,*,*)(5,3,*,3,*,*,1,*,0,*,*)(5,3,*,3,*,*,2,*,1,*,*)(5,3,*,3,*,*,3,*,2,*,*)(5,3,*,3,*,*,4,*,3,*,*)(5,3,*,3,*,*,5,*,4,*,*)(5,3,*,3,*,*,6,*,5,*,*)(5,3,*,*,3,*,1,*,*,0,*)(5,3,*,*,3,*,2,*,*,1,*)(5,3,*,*,3,*,3,*,*,2,*)(5,3,*,*,3,*,4,*,*,3,*)(5,3,*,*,3,*,5,*,*,4,*)(5,3,*,*,3,*,6,*,*,5,*)(5,3,*,*,*,3,0,*,*,*,1)(5,3,*,*,*,3,1,*,*,*,0)(5,3,*,*,*,3,2,*,*,*,1)(5,3,*,*,*,3,3,*,*,*,2)(5,3,*,*,*,3,4,*,*,*,3)(5,3,*,*,*,3,5,*,*,*,4)(5,3,*,*,*,3,6,*,*,*,5)(5,4,4,*,*,*,1,0,*,*,*)(5,4,4,*,*,*,2,1,*,*,*)(5,4,4,*,*,*,3,2,*,*,*)(5,4,4,*,*,*,4,3,*,*,*)(5,4,4,*,*,*,5,4,*,*,*)(5,4,4,*,*,*,6,5,*,*,*)(5,4,*,4,*,*,0,*,1,*,*)(5,4,*,4,*,*,1,*,0,*,*)(5,4,*,4,*,*,2,*,1,*,*)(5,4,*,4,*,*,3,*,2,*,*)(5,4,*,4,*,*,4,*,3,*,*)(5,4,*,4,*,*,5,*,4,*,*)(5,4,*,4,*,*,6,*,5,*,*)(5,4,*,*,4,*,1,*,*,0,*)(5,4,*,*,4,*,2,*,*,1,*)(5,4,*,*,4,*,3,*,*,2,*)(5,4,*,*,4,*,4,*,*,3,*)(5,4,*,*,4,*,5,*,*,4,*)(5,4,*,*,4,*,6,*,*,5,*)(5,4,*,*,*,4,0,*,*,*,1)(5,4,*,*,*,4,1,*,*,*,0)(5,4,*,*,*,4,2,*,*,*,1)(5,4,*,*,*,4,3,*,*,*,2)(5,4,*,*,*,4,4,*,*,*,3)(5,4,*,*,*,4,5,*,*,*,4)(5,4,*,*,*,4,6,*,*,*,5)(5,5,5,*,*,*,1,0,*,*,*)(5,5,5,*,*,*,2,1,*,*,*)(5,5,5,*,*,*,3,2,*,*,*)(5,5,5,*,*,*,4,3,*,*,*)(5,5,5,*,*,*,5,4,*,*,*)(5,5,5,*,*,*,6,5,*,*,*)(5,5,*,5,*,*,0,*,1,*,*)(5,5,*,5,*,*,1,*,0,*,*)(5,5,*,5,*,*,2,*,1,*,*)(5,5,*,5,*,*,3,*,2,*,*)(5,5,*,5,*,*,4,*,3,*,*)(5,5,*,5,*,*,5,*,4,*,*)(5,5,*,5,*,*,6,*,5,*,*)(5,5,*,*,5,*,1,*,*,0,*)(5,5,*,*,5,*,2,*,*,1,*)(5,5,*,*,5,*,3,*,*,2,*)(5,5,*,*,5,*,4,*,*,3,*)(5,5,*,*,5,*,5,*,*,4,*)(5,5,*,*,5,*,6,*,*,5,*)(5,5,*,*,*,5,0,*,*,*,1)(5,5,*,*,*,5,1,*,*,*,0)(5,5,*,*,*,5,2,*,*,*,1)(5,5,*,*,*,5,3,*,*,*,2)(5,5,*,*,*,5,4,*,*,*,3)(5,5,*,*,*,5,5,*,*,*,4)(5,5,*,*,*,5,6,*,*,*,5)(5,6,6,*,*,*,1,0,*,*,*)(5,6,6,*,*,*,2,1,*,*,*)(5,6,6,*,*,*,3,2,*,*,*)(5,6,6,*,*,*,4,3,*,*,*)(5,6,6,*,*,*,5,4,*,*,*)(5,6,6,*,*,*,6,5,*,*,*)(5,6,*,6,*,*,0,*,1,*,*)(5,6,*,6,*,*,1,*,0,*,*)(5,6,*,6,*,*,2,*,1,*,*)(5,6,*,6,*,*,3,*,2,*,*)(5,6,*,6,*,*,4,*,3,*,*)(5,6,*,6,*,*,5,*,4,*,*)(5,6,*,6,*,*,6,*,5,*,*)(5,6,*,*,6,*,1,*,*,0,*)(5,6,*,*,6,*,2,*,*,1,*)(5,6,*,*,6,*,3,*,*,2,*)(5,6,*,*,6,*,4,*,*,3,*)(5,6,*,*,6,*,5,*,*,4,*)(5,6,*,*,6,*,6,*,*,5,*)(5,6,*,*,*,6,0,*,*,*,1)(5,6,*,*,*,6,1,*,*,*,0)(5,6,*,*,*,6,2,*,*,*,1)(5,6,*,*,*,6,3,*,*,*,2)(5,6,*,*,*,6,4,*,*,*,3)(5,6,*,*,*,6,5,*,*,*,4)(5,6,*,*,*,6,6,*,*,*,5)(5,7,7,*,*,*,1,0,*,*,*)(5,7,7,*,*,*,2,1,*,*,*)(5,7,7,*,*,*,3,2,*,*,*)(5,7,7,*,*,*,4,3,*,*,*)(5,7,7,*,*,*,5,4,*,*,*)(5,7,7,*,*,*,6,5,*,*,*)(5,7,*,7,*,*,0,*,1,*,*)(5,7,*,7,*,*,1,*,0,*,*)(5,7,*,7,*,*,2,*,1,*,*)(5,7,*,7,*,*,3,*,2,*,*)(5,7,*,7,*,*,4,*,3,*,*)(5,7,*,7,*,*,5,*,4,*,*)(5,7,*,7,*,*,6,*,5,*,*)(5,7,*,*,7,*,1,*,*,0,*)(5,7,*,*,7,*,2,*,*,1,*)(5,7,*,*,7,*,3,*,*,2,*)(5,7,*,*,7,*,4,*,*,3,*)(5,7,*,*,7,*,5,*,*,4,*)(5,7,*,*,7,*,6,*,*,5,*)(5,7,*,*,*,7,0,*,*,*,1)(5,7,*,*,*,7,1,*,*,*,0)(5,7,*,*,*,7,2,*,*,*,1)(5,7,*,*,*,7,3,*,*,*,2)(5,7,*,*,*,7,4,*,*,*,3)(5,7,*,*,*,7,5,*,*,*,4)(5,7,*,*,*,7,6,*,*,*,5)(5,8,8,*,*,*,1,0,*,*,*)(5,8,8,*,*,*,2,1,*,*,*)(5,8,8,*,*,*,3,2,*,*,*)(5,8,8,*,*,*,4,3,*,*,*)(5,8,8,*,*,*,5,4,*,*,*)(5,8,8,*,*,*,6,5,*,*,*)(5,8,*,8,*,*,0,*,1,*,*)(5,8,*,8,*,*,1,*,0,*,*)(5,8,*,8,*,*,2,*,1,*,*)(5,8,*,8,*,*,3,*,2,*,*)(5,8,*,8,*,*,4,*,3,*,*)(5,8,*,8,*,*,5,*,4,*,*)(5,8,*,8,*,*,6,*,5,*,*)(5,8,*,*,8,*,1,*,*,0,*)(5,8,*,*,8,*,2,*,*,1,*)(5,8,*,*,8,*,3,*,*,2,*)(5,8,*,*,8,*,4,*,*,3,*)(5,8,*,*,8,*,5,*,*,4,*)(5,8,*,*,8,*,6,*,*,5,*)(5,8,*,*,*,8,0,*,*,*,1)(5,8,*,*,*,8,1,*,*,*,0)(5,8,*,*,*,8,2,*,*,*,1)(5,8,*,*,*,8,3,*,*,*,2)(5,8,*,*,*,8,4,*,*,*,3)(5,8,*,*,*,8,5,*,*,*,4)(5,8,*,*,*,8,6,*,*,*,5)(5,9,9,*,*,*,1,0,*,*,*)(5,9,9,*,*,*,2,1,*,*,*)(5,9,9,*,*,*,3,2,*,*,*)(5,9,9,*,*,*,4,3,*,*,*)(5,9,9,*,*,*,5,4,*,*,*)(5,9,9,*,*,*,6,5,*,*,*)(5,9,*,9,*,*,0,*,1,*,*)(5,9,*,9,*,*,1,*,0,*,*)(5,9,*,9,*,*,2,*,1,*,*)(5,9,*,9,*,*,3,*,2,*,*)(5,9,*,9,*,*,4,*,3,*,*)(5,9,*,9,*,*,5,*,4,*,*)(5,9,*,9,*,*,6,*,5,*,*)(5,9,*,*,9,*,1,*,*,0,*)(5,9,*,*,9,*,2,*,*,1,*)(5,9,*,*,9,*,3,*,*,2,*)(5,9,*,*,9,*,4,*,*,3,*)(5,9,*,*,9,*,5,*,*,4,*)(5,9,*,*,9,*,6,*,*,5,*)(5,9,*,*,*,9,0,*,*,*,1)(5,9,*,*,*,9,1,*,*,*,0)(5,9,*,*,*,9,2,*,*,*,1)(5,9,*,*,*,9,3,*,*,*,2)(5,9,*,*,*,9,4,*,*,*,3)(5,9,*,*,*,9,5,*,*,*,4)(5,9,*,*,*,9,6,*,*,*,5)(6,0,0,*,*,*,1,0,*,*,*)(6,0,0,*,*,*,2,1,*,*,*)(6,0,0,*,*,*,3,2,*,*,*)(6,0,0,*,*,*,4,3,*,*,*)(6,0,0,*,*,*,5,4,*,*,*)(6,0,0,*,*,*,6,5,*,*,*)(6,0,*,0,*,*,0,*,1,*,*)(6,0,*,0,*,*,1,*,0,*,*)(6,0,*,0,*,*,2,*,1,*,*)(6,0,*,0,*,*,3,*,2,*,*)(6,0,*,0,*,*,4,*,3,*,*)(6,0,*,0,*,*,5,*,4,*,*)(6,0,*,0,*,*,6,*,5,*,*)(6,0,*,*,0,*,1,*,*,0,*)(6,0,*,*,0,*,2,*,*,1,*)(6,0,*,*,0,*,3,*,*,2,*)(6,0,*,*,0,*,4,*,*,3,*)(6,0,*,*,0,*,5,*,*,4,*)(6,0,*,*,0,*,6,*,*,5,*)(6,0,*,*,*,0,0,*,*,*,1)(6,0,*,*,*,0,1,*,*,*,0)(6,0,*,*,*,0,2,*,*,*,1)(6,0,*,*,*,0,3,*,*,*,2)(6,0,*,*,*,0,4,*,*,*,3)(6,0,*,*,*,0,5,*,*,*,4)(6,0,*,*,*,0,6,*,*,*,5)(6,1,1,*,*,*,1,0,*,*,*)(6,1,1,*,*,*,2,1,*,*,*)(6,1,1,*,*,*,3,2,*,*,*)(6,1,1,*,*,*,4,3,*,*,*)(6,1,1,*,*,*,5,4,*,*,*)(6,1,1,*,*,*,6,5,*,*,*)(6,1,*,1,*,*,0,*,1,*,*)(6,1,*,1,*,*,1,*,0,*,*)(6,1,*,1,*,*,2,*,1,*,*)(6,1,*,1,*,*,3,*,2,*,*)(6,1,*,1,*,*,4,*,3,*,*)(6,1,*,1,*,*,5,*,4,*,*)(6,1,*,1,*,*,6,*,5,*,*)(6,1,*,*,1,*,1,*,*,0,*)(6,1,*,*,1,*,2,*,*,1,*)(6,1,*,*,1,*,3,*,*,2,*)(6,1,*,*,1,*,4,*,*,3,*)(6,1,*,*,1,*,5,*,*,4,*)(6,1,*,*,1,*,6,*,*,5,*)(6,1,*,*,*,1,0,*,*,*,1)(6,1,*,*,*,1,1,*,*,*,0)(6,1,*,*,*,1,2,*,*,*,1)(6,1,*,*,*,1,3,*,*,*,2)(6,1,*,*,*,1,4,*,*,*,3)(6,1,*,*,*,1,5,*,*,*,4)(6,1,*,*,*,1,6,*,*,*,5)(6,2,2,*,*,*,1,0,*,*,*)(6,2,2,*,*,*,2,1,*,*,*)(6,2,2,*,*,*,3,2,*,*,*)(6,2,2,*,*,*,4,3,*,*,*)(6,2,2,*,*,*,5,4,*,*,*)(6,2,2,*,*,*,6,5,*,*,*)(6,2,*,2,*,*,0,*,1,*,*)(6,2,*,2,*,*,1,*,0,*,*)(6,2,*,2,*,*,2,*,1,*,*)(6,2,*,2,*,*,3,*,2,*,*)(6,2,*,2,*,*,4,*,3,*,*)(6,2,*,2,*,*,5,*,4,*,*)(6,2,*,2,*,*,6,*,5,*,*)(6,2,*,*,2,*,1,*,*,0,*)(6,2,*,*,2,*,2,*,*,1,*)(6,2,*,*,2,*,3,*,*,2,*)(6,2,*,*,2,*,4,*,*,3,*)(6,2,*,*,2,*,5,*,*,4,*)(6,2,*,*,2,*,6,*,*,5,*)(6,2,*,*,*,2,0,*,*,*,1)(6,2,*,*,*,2,1,*,*,*,0)(6,2,*,*,*,2,2,*,*,*,1)(6,2,*,*,*,2,3,*,*,*,2)(6,2,*,*,*,2,4,*,*,*,3)(6,2,*,*,*,2,5,*,*,*,4)(6,2,*,*,*,2,6,*,*,*,5)(6,3,3,*,*,*,1,0,*,*,*)(6,3,3,*,*,*,2,1,*,*,*)(6,3,3,*,*,*,3,2,*,*,*)(6,3,3,*,*,*,4,3,*,*,*)(6,3,3,*,*,*,5,4,*,*,*)(6,3,3,*,*,*,6,5,*,*,*)(6,3,*,3,*,*,0,*,1,*,*)(6,3,*,3,*,*,1,*,0,*,*)(6,3,*,3,*,*,2,*,1,*,*)(6,3,*,3,*,*,3,*,2,*,*)(6,3,*,3,*,*,4,*,3,*,*)(6,3,*,3,*,*,5,*,4,*,*)(6,3,*,3,*,*,6,*,5,*,*)(6,3,*,*,3,*,1,*,*,0,*)(6,3,*,*,3,*,2,*,*,1,*)(6,3,*,*,3,*,3,*,*,2,*)(6,3,*,*,3,*,4,*,*,3,*)(6,3,*,*,3,*,5,*,*,4,*)(6,3,*,*,3,*,6,*,*,5,*)(6,3,*,*,*,3,0,*,*,*,1)(6,3,*,*,*,3,1,*,*,*,0)(6,3,*,*,*,3,2,*,*,*,1)(6,3,*,*,*,3,3,*,*,*,2)(6,3,*,*,*,3,4,*,*,*,3)(6,3,*,*,*,3,5,*,*,*,4)(6,3,*,*,*,3,6,*,*,*,5)(6,4,4,*,*,*,1,0,*,*,*)(6,4,4,*,*,*,2,1,*,*,*)(6,4,4,*,*,*,3,2,*,*,*)(6,4,4,*,*,*,4,3,*,*,*)(6,4,4,*,*,*,5,4,*,*,*)(6,4,4,*,*,*,6,5,*,*,*)(6,4,*,4,*,*,0,*,1,*,*)(6,4,*,4,*,*,1,*,0,*,*)(6,4,*,4,*,*,2,*,1,*,*)(6,4,*,4,*,*,3,*,2,*,*)(6,4,*,4,*,*,4,*,3,*,*)(6,4,*,4,*,*,5,*,4,*,*)(6,4,*,4,*,*,6,*,5,*,*)(6,4,*,*,4,*,1,*,*,0,*)(6,4,*,*,4,*,2,*,*,1,*)(6,4,*,*,4,*,3,*,*,2,*)(6,4,*,*,4,*,4,*,*,3,*)(6,4,*,*,4,*,5,*,*,4,*)(6,4,*,*,4,*,6,*,*,5,*)(6,4,*,*,*,4,0,*,*,*,1)(6,4,*,*,*,4,1,*,*,*,0)(6,4,*,*,*,4,2,*,*,*,1)(6,4,*,*,*,4,3,*,*,*,2)(6,4,*,*,*,4,4,*,*,*,3)(6,4,*,*,*,4,5,*,*,*,4)(6,4,*,*,*,4,6,*,*,*,5)(6,5,5,*,*,*,1,0,*,*,*)(6,5,5,*,*,*,2,1,*,*,*)(6,5,5,*,*,*,3,2,*,*,*)(6,5,5,*,*,*,4,3,*,*,*)(6,5,5,*,*,*,5,4,*,*,*)(6,5,5,*,*,*,6,5,*,*,*)(6,5,*,5,*,*,0,*,1,*,*)(6,5,*,5,*,*,1,*,0,*,*)(6,5,*,5,*,*,2,*,1,*,*)(6,5,*,5,*,*,3,*,2,*,*)(6,5,*,5,*,*,4,*,3,*,*)(6,5,*,5,*,*,5,*,4,*,*)(6,5,*,5,*,*,6,*,5,*,*)(6,5,*,*,5,*,1,*,*,0,*)(6,5,*,*,5,*,2,*,*,1,*)(6,5,*,*,5,*,3,*,*,2,*)(6,5,*,*,5,*,4,*,*,3,*)(6,5,*,*,5,*,5,*,*,4,*)(6,5,*,*,5,*,6,*,*,5,*)(6,5,*,*,*,5,0,*,*,*,1)(6,5,*,*,*,5,1,*,*,*,0)(6,5,*,*,*,5,2,*,*,*,1)(6,5,*,*,*,5,3,*,*,*,2)(6,5,*,*,*,5,4,*,*,*,3)(6,5,*,*,*,5,5,*,*,*,4)(6,5,*,*,*,5,6,*,*,*,5)(6,6,6,*,*,*,1,0,*,*,*)(6,6,6,*,*,*,2,1,*,*,*)(6,6,6,*,*,*,3,2,*,*,*)(6,6,6,*,*,*,4,3,*,*,*)(6,6,6,*,*,*,5,4,*,*,*)(6,6,6,*,*,*,6,5,*,*,*)(6,6,*,6,*,*,0,*,1,*,*)(6,6,*,6,*,*,1,*,0,*,*)(6,6,*,6,*,*,2,*,1,*,*)(6,6,*,6,*,*,3,*,2,*,*)(6,6,*,6,*,*,4,*,3,*,*)(6,6,*,6,*,*,5,*,4,*,*)(6,6,*,6,*,*,6,*,5,*,*)(6,6,*,*,6,*,1,*,*,0,*)(6,6,*,*,6,*,2,*,*,1,*)(6,6,*,*,6,*,3,*,*,2,*)(6,6,*,*,6,*,4,*,*,3,*)(6,6,*,*,6,*,5,*,*,4,*)(6,6,*,*,6,*,6,*,*,5,*)(6,6,*,*,*,6,0,*,*,*,1)(6,6,*,*,*,6,1,*,*,*,0)(6,6,*,*,*,6,2,*,*,*,1)(6,6,*,*,*,6,3,*,*,*,2)(6,6,*,*,*,6,4,*,*,*,3)(6,6,*,*,*,6,5,*,*,*,4)(6,6,*,*,*,6,6,*,*,*,5)(6,7,7,*,*,*,1,0,*,*,*)(6,7,7,*,*,*,2,1,*,*,*)(6,7,7,*,*,*,3,2,*,*,*)(6,7,7,*,*,*,4,3,*,*,*)(6,7,7,*,*,*,5,4,*,*,*)(6,7,7,*,*,*,6,5,*,*,*)(6,7,*,7,*,*,0,*,1,*,*)(6,7,*,7,*,*,1,*,0,*,*)(6,7,*,7,*,*,2,*,1,*,*)(6,7,*,7,*,*,3,*,2,*,*)(6,7,*,7,*,*,4,*,3,*,*)(6,7,*,7,*,*,5,*,4,*,*)(6,7,*,7,*,*,6,*,5,*,*)(6,7,*,*,7,*,1,*,*,0,*)(6,7,*,*,7,*,2,*,*,1,*)(6,7,*,*,7,*,3,*,*,2,*)(6,7,*,*,7,*,4,*,*,3,*)(6,7,*,*,7,*,5,*,*,4,*)(6,7,*,*,7,*,6,*,*,5,*)(6,7,*,*,*,7,0,*,*,*,1)(6,7,*,*,*,7,1,*,*,*,0)(6,7,*,*,*,7,2,*,*,*,1)(6,7,*,*,*,7,3,*,*,*,2)(6,7,*,*,*,7,4,*,*,*,3)(6,7,*,*,*,7,5,*,*,*,4)(6,7,*,*,*,7,6,*,*,*,5)(6,8,8,*,*,*,1,0,*,*,*)(6,8,8,*,*,*,2,1,*,*,*)(6,8,8,*,*,*,3,2,*,*,*)(6,8,8,*,*,*,4,3,*,*,*)(6,8,8,*,*,*,5,4,*,*,*)(6,8,8,*,*,*,6,5,*,*,*)(6,8,*,8,*,*,0,*,1,*,*)(6,8,*,8,*,*,1,*,0,*,*)(6,8,*,8,*,*,2,*,1,*,*)(6,8,*,8,*,*,3,*,2,*,*)(6,8,*,8,*,*,4,*,3,*,*)(6,8,*,8,*,*,5,*,4,*,*)(6,8,*,8,*,*,6,*,5,*,*)(6,8,*,*,8,*,1,*,*,0,*)(6,8,*,*,8,*,2,*,*,1,*)(6,8,*,*,8,*,3,*,*,2,*)(6,8,*,*,8,*,4,*,*,3,*)(6,8,*,*,8,*,5,*,*,4,*)(6,8,*,*,8,*,6,*,*,5,*)(6,8,*,*,*,8,0,*,*,*,1)(6,8,*,*,*,8,1,*,*,*,0)(6,8,*,*,*,8,2,*,*,*,1)(6,8,*,*,*,8,3,*,*,*,2)(6,8,*,*,*,8,4,*,*,*,3)(6,8,*,*,*,8,5,*,*,*,4)(6,8,*,*,*,8,6,*,*,*,5)(6,9,9,*,*,*,1,0,*,*,*)(6,9,9,*,*,*,2,1,*,*,*)(6,9,9,*,*,*,3,2,*,*,*)(6,9,9,*,*,*,4,3,*,*,*)(6,9,9,*,*,*,5,4,*,*,*)(6,9,9,*,*,*,6,5,*,*,*)(6,9,*,9,*,*,0,*,1,*,*)(6,9,*,9,*,*,1,*,0,*,*)(6,9,*,9,*,*,2,*,1,*,*)(6,9,*,9,*,*,3,*,2,*,*)(6,9,*,9,*,*,4,*,3,*,*)(6,9,*,9,*,*,5,*,4,*,*)(6,9,*,9,*,*,6,*,5,*,*)(6,9,*,*,9,*,1,*,*,0,*)(6,9,*,*,9,*,2,*,*,1,*)(6,9,*,*,9,*,3,*,*,2,*)(6,9,*,*,9,*,4,*,*,3,*)(6,9,*,*,9,*,5,*,*,4,*)(6,9,*,*,9,*,6,*,*,5,*)(6,9,*,*,*,9,0,*,*,*,1)(6,9,*,*,*,9,1,*,*,*,0)(6,9,*,*,*,9,2,*,*,*,1)(6,9,*,*,*,9,3,*,*,*,2)(6,9,*,*,*,9,4,*,*,*,3)(6,9,*,*,*,9,5,*,*,*,4)(6,9,*,*,*,9,6,*,*,*,5) + + y[1][2] x[1][2] x[1][1] x[1][3] x[0][2] x[2][2] d[1][2] d[1][1] d[1][3] d[0][2] d[2][2] + y[1][3] x[1][3] x[1][2] x[1][4] x[0][3] x[2][3] d[1][3] d[1][2] d[1][4] d[0][3] d[2][3] + y[1][5] x[1][5] x[1][4] x[1][6] x[0][5] x[2][5] d[1][5] d[1][4] d[1][6] d[0][5] d[2][5] + y[2][1] x[2][1] x[2][0] x[2][2] x[1][1] x[3][1] d[2][1] d[2][0] d[2][2] d[1][1] d[3][1] + y[2][4] x[2][4] x[2][3] x[2][5] x[1][4] x[3][4] d[2][4] d[2][3] d[2][5] d[1][4] d[3][4] + y[3][1] x[3][1] x[3][0] x[3][2] x[2][1] x[4][1] d[3][1] d[3][0] d[3][2] d[2][1] d[4][1] + y[3][4] x[3][4] x[3][3] x[3][5] x[2][4] x[4][4] d[3][4] d[3][3] d[3][5] d[2][4] d[4][4] + y[4][1] x[4][1] x[4][0] x[4][2] x[3][1] x[5][1] d[4][1] d[4][0] d[4][2] d[3][1] d[5][1] + y[4][2] x[4][2] x[4][1] x[4][3] x[3][2] x[5][2] d[4][2] d[4][1] d[4][3] d[3][2] d[5][2] + y[4][3] x[4][3] x[4][2] x[4][4] x[3][3] x[5][3] d[4][3] d[4][2] d[4][4] d[3][3] d[5][3] + y[4][5] x[4][5] x[4][4] x[4][6] x[3][5] x[5][5] d[4][5] d[4][4] d[4][6] d[3][5] d[5][5] + y[5][2] x[5][2] x[5][1] x[5][3] x[4][2] x[6][2] d[5][2] d[5][1] d[5][3] d[4][2] d[6][2] + y[5][3] x[5][3] x[5][2] x[5][4] x[4][3] x[6][3] d[5][3] d[5][2] d[5][4] d[4][3] d[6][3] + y[5][5] x[5][5] x[5][4] x[5][6] x[4][5] x[6][5] d[5][5] d[5][4] d[5][6] d[4][5] d[6][5] + + + + %... + (1,*,*,*,*,*,0,*,*,*,*)(3,0,0,*,*,*,1,0,*,*,*)(3,0,0,*,*,*,2,1,*,*,*)(3,0,0,*,*,*,3,2,*,*,*)(3,0,0,*,*,*,4,3,*,*,*)(3,0,0,*,*,*,5,4,*,*,*)(3,0,0,*,*,*,6,5,*,*,*)(3,0,*,0,*,*,0,*,1,*,*)(3,0,*,0,*,*,1,*,0,*,*)(3,0,*,0,*,*,2,*,1,*,*)(3,0,*,0,*,*,3,*,2,*,*)(3,0,*,0,*,*,4,*,3,*,*)(3,0,*,0,*,*,5,*,4,*,*)(3,0,*,0,*,*,6,*,5,*,*)(3,0,*,*,0,*,1,*,*,0,*)(3,0,*,*,0,*,2,*,*,1,*)(3,0,*,*,0,*,3,*,*,2,*)(3,0,*,*,0,*,4,*,*,3,*)(3,0,*,*,0,*,5,*,*,4,*)(3,0,*,*,0,*,6,*,*,5,*)(3,0,*,*,*,0,0,*,*,*,1)(3,0,*,*,*,0,1,*,*,*,0)(3,0,*,*,*,0,2,*,*,*,1)(3,0,*,*,*,0,3,*,*,*,2)(3,0,*,*,*,0,4,*,*,*,3)(3,0,*,*,*,0,5,*,*,*,4)(3,0,*,*,*,0,6,*,*,*,5)(3,1,1,*,*,*,1,0,*,*,*)(3,1,1,*,*,*,2,1,*,*,*)(3,1,1,*,*,*,3,2,*,*,*)(3,1,1,*,*,*,4,3,*,*,*)(3,1,1,*,*,*,5,4,*,*,*)(3,1,1,*,*,*,6,5,*,*,*)(3,1,*,1,*,*,0,*,1,*,*)(3,1,*,1,*,*,1,*,0,*,*)(3,1,*,1,*,*,2,*,1,*,*)(3,1,*,1,*,*,3,*,2,*,*)(3,1,*,1,*,*,4,*,3,*,*)(3,1,*,1,*,*,5,*,4,*,*)(3,1,*,1,*,*,6,*,5,*,*)(3,1,*,*,1,*,1,*,*,0,*)(3,1,*,*,1,*,2,*,*,1,*)(3,1,*,*,1,*,3,*,*,2,*)(3,1,*,*,1,*,4,*,*,3,*)(3,1,*,*,1,*,5,*,*,4,*)(3,1,*,*,1,*,6,*,*,5,*)(3,1,*,*,*,1,0,*,*,*,1)(3,1,*,*,*,1,1,*,*,*,0)(3,1,*,*,*,1,2,*,*,*,1)(3,1,*,*,*,1,3,*,*,*,2)(3,1,*,*,*,1,4,*,*,*,3)(3,1,*,*,*,1,5,*,*,*,4)(3,1,*,*,*,1,6,*,*,*,5)(3,2,2,*,*,*,1,0,*,*,*)(3,2,2,*,*,*,2,1,*,*,*)(3,2,2,*,*,*,3,2,*,*,*)(3,2,2,*,*,*,4,3,*,*,*)(3,2,2,*,*,*,5,4,*,*,*)(3,2,2,*,*,*,6,5,*,*,*)(3,2,*,2,*,*,0,*,1,*,*)(3,2,*,2,*,*,1,*,0,*,*)(3,2,*,2,*,*,2,*,1,*,*)(3,2,*,2,*,*,3,*,2,*,*)(3,2,*,2,*,*,4,*,3,*,*)(3,2,*,2,*,*,5,*,4,*,*)(3,2,*,2,*,*,6,*,5,*,*)(3,2,*,*,2,*,1,*,*,0,*)(3,2,*,*,2,*,2,*,*,1,*)(3,2,*,*,2,*,3,*,*,2,*)(3,2,*,*,2,*,4,*,*,3,*)(3,2,*,*,2,*,5,*,*,4,*)(3,2,*,*,2,*,6,*,*,5,*)(3,2,*,*,*,2,0,*,*,*,1)(3,2,*,*,*,2,1,*,*,*,0)(3,2,*,*,*,2,2,*,*,*,1)(3,2,*,*,*,2,3,*,*,*,2)(3,2,*,*,*,2,4,*,*,*,3)(3,2,*,*,*,2,5,*,*,*,4)(3,2,*,*,*,2,6,*,*,*,5)(3,3,3,*,*,*,1,0,*,*,*)(3,3,3,*,*,*,2,1,*,*,*)(3,3,3,*,*,*,3,2,*,*,*)(3,3,3,*,*,*,4,3,*,*,*)(3,3,3,*,*,*,5,4,*,*,*)(3,3,3,*,*,*,6,5,*,*,*)(3,3,*,3,*,*,0,*,1,*,*)(3,3,*,3,*,*,1,*,0,*,*)(3,3,*,3,*,*,2,*,1,*,*)(3,3,*,3,*,*,3,*,2,*,*)(3,3,*,3,*,*,4,*,3,*,*)(3,3,*,3,*,*,5,*,4,*,*)(3,3,*,3,*,*,6,*,5,*,*)(3,3,*,*,3,*,1,*,*,0,*)(3,3,*,*,3,*,2,*,*,1,*)(3,3,*,*,3,*,3,*,*,2,*)(3,3,*,*,3,*,4,*,*,3,*)(3,3,*,*,3,*,5,*,*,4,*)(3,3,*,*,3,*,6,*,*,5,*)(3,3,*,*,*,3,0,*,*,*,1)(3,3,*,*,*,3,1,*,*,*,0)(3,3,*,*,*,3,2,*,*,*,1)(3,3,*,*,*,3,3,*,*,*,2)(3,3,*,*,*,3,4,*,*,*,3)(3,3,*,*,*,3,5,*,*,*,4)(3,3,*,*,*,3,6,*,*,*,5)(3,4,4,*,*,*,1,0,*,*,*)(3,4,4,*,*,*,2,1,*,*,*)(3,4,4,*,*,*,3,2,*,*,*)(3,4,4,*,*,*,4,3,*,*,*)(3,4,4,*,*,*,5,4,*,*,*)(3,4,4,*,*,*,6,5,*,*,*)(3,4,*,4,*,*,0,*,1,*,*)(3,4,*,4,*,*,1,*,0,*,*)(3,4,*,4,*,*,2,*,1,*,*)(3,4,*,4,*,*,3,*,2,*,*)(3,4,*,4,*,*,4,*,3,*,*)(3,4,*,4,*,*,5,*,4,*,*)(3,4,*,4,*,*,6,*,5,*,*)(3,4,*,*,4,*,1,*,*,0,*)(3,4,*,*,4,*,2,*,*,1,*)(3,4,*,*,4,*,3,*,*,2,*)(3,4,*,*,4,*,4,*,*,3,*)(3,4,*,*,4,*,5,*,*,4,*)(3,4,*,*,4,*,6,*,*,5,*)(3,4,*,*,*,4,0,*,*,*,1)(3,4,*,*,*,4,1,*,*,*,0)(3,4,*,*,*,4,2,*,*,*,1)(3,4,*,*,*,4,3,*,*,*,2)(3,4,*,*,*,4,4,*,*,*,3)(3,4,*,*,*,4,5,*,*,*,4)(3,4,*,*,*,4,6,*,*,*,5)(3,5,5,*,*,*,1,0,*,*,*)(3,5,5,*,*,*,2,1,*,*,*)(3,5,5,*,*,*,3,2,*,*,*)(3,5,5,*,*,*,4,3,*,*,*)(3,5,5,*,*,*,5,4,*,*,*)(3,5,5,*,*,*,6,5,*,*,*)(3,5,*,5,*,*,0,*,1,*,*)(3,5,*,5,*,*,1,*,0,*,*)(3,5,*,5,*,*,2,*,1,*,*)(3,5,*,5,*,*,3,*,2,*,*)(3,5,*,5,*,*,4,*,3,*,*)(3,5,*,5,*,*,5,*,4,*,*)(3,5,*,5,*,*,6,*,5,*,*)(3,5,*,*,5,*,1,*,*,0,*)(3,5,*,*,5,*,2,*,*,1,*)(3,5,*,*,5,*,3,*,*,2,*)(3,5,*,*,5,*,4,*,*,3,*)(3,5,*,*,5,*,5,*,*,4,*)(3,5,*,*,5,*,6,*,*,5,*)(3,5,*,*,*,5,0,*,*,*,1)(3,5,*,*,*,5,1,*,*,*,0)(3,5,*,*,*,5,2,*,*,*,1)(3,5,*,*,*,5,3,*,*,*,2)(3,5,*,*,*,5,4,*,*,*,3)(3,5,*,*,*,5,5,*,*,*,4)(3,5,*,*,*,5,6,*,*,*,5)(3,6,6,*,*,*,1,0,*,*,*)(3,6,6,*,*,*,2,1,*,*,*)(3,6,6,*,*,*,3,2,*,*,*)(3,6,6,*,*,*,4,3,*,*,*)(3,6,6,*,*,*,5,4,*,*,*)(3,6,6,*,*,*,6,5,*,*,*)(3,6,*,6,*,*,0,*,1,*,*)(3,6,*,6,*,*,1,*,0,*,*)(3,6,*,6,*,*,2,*,1,*,*)(3,6,*,6,*,*,3,*,2,*,*)(3,6,*,6,*,*,4,*,3,*,*)(3,6,*,6,*,*,5,*,4,*,*)(3,6,*,6,*,*,6,*,5,*,*)(3,6,*,*,6,*,1,*,*,0,*)(3,6,*,*,6,*,2,*,*,1,*)(3,6,*,*,6,*,3,*,*,2,*)(3,6,*,*,6,*,4,*,*,3,*)(3,6,*,*,6,*,5,*,*,4,*)(3,6,*,*,6,*,6,*,*,5,*)(3,6,*,*,*,6,0,*,*,*,1)(3,6,*,*,*,6,1,*,*,*,0)(3,6,*,*,*,6,2,*,*,*,1)(3,6,*,*,*,6,3,*,*,*,2)(3,6,*,*,*,6,4,*,*,*,3)(3,6,*,*,*,6,5,*,*,*,4)(3,6,*,*,*,6,6,*,*,*,5)(3,7,7,*,*,*,1,0,*,*,*)(3,7,7,*,*,*,2,1,*,*,*)(3,7,7,*,*,*,3,2,*,*,*)(3,7,7,*,*,*,4,3,*,*,*)(3,7,7,*,*,*,5,4,*,*,*)(3,7,7,*,*,*,6,5,*,*,*)(3,7,*,7,*,*,0,*,1,*,*)(3,7,*,7,*,*,1,*,0,*,*)(3,7,*,7,*,*,2,*,1,*,*)(3,7,*,7,*,*,3,*,2,*,*)(3,7,*,7,*,*,4,*,3,*,*)(3,7,*,7,*,*,5,*,4,*,*)(3,7,*,7,*,*,6,*,5,*,*)(3,7,*,*,7,*,1,*,*,0,*)(3,7,*,*,7,*,2,*,*,1,*)(3,7,*,*,7,*,3,*,*,2,*)(3,7,*,*,7,*,4,*,*,3,*)(3,7,*,*,7,*,5,*,*,4,*)(3,7,*,*,7,*,6,*,*,5,*)(3,7,*,*,*,7,0,*,*,*,1)(3,7,*,*,*,7,1,*,*,*,0)(3,7,*,*,*,7,2,*,*,*,1)(3,7,*,*,*,7,3,*,*,*,2)(3,7,*,*,*,7,4,*,*,*,3)(3,7,*,*,*,7,5,*,*,*,4)(3,7,*,*,*,7,6,*,*,*,5)(3,8,8,*,*,*,1,0,*,*,*)(3,8,8,*,*,*,2,1,*,*,*)(3,8,8,*,*,*,3,2,*,*,*)(3,8,8,*,*,*,4,3,*,*,*)(3,8,8,*,*,*,5,4,*,*,*)(3,8,8,*,*,*,6,5,*,*,*)(3,8,*,8,*,*,0,*,1,*,*)(3,8,*,8,*,*,1,*,0,*,*)(3,8,*,8,*,*,2,*,1,*,*)(3,8,*,8,*,*,3,*,2,*,*)(3,8,*,8,*,*,4,*,3,*,*)(3,8,*,8,*,*,5,*,4,*,*)(3,8,*,8,*,*,6,*,5,*,*)(3,8,*,*,8,*,1,*,*,0,*)(3,8,*,*,8,*,2,*,*,1,*)(3,8,*,*,8,*,3,*,*,2,*)(3,8,*,*,8,*,4,*,*,3,*)(3,8,*,*,8,*,5,*,*,4,*)(3,8,*,*,8,*,6,*,*,5,*)(3,8,*,*,*,8,0,*,*,*,1)(3,8,*,*,*,8,1,*,*,*,0)(3,8,*,*,*,8,2,*,*,*,1)(3,8,*,*,*,8,3,*,*,*,2)(3,8,*,*,*,8,4,*,*,*,3)(3,8,*,*,*,8,5,*,*,*,4)(3,8,*,*,*,8,6,*,*,*,5)(3,9,9,*,*,*,1,0,*,*,*)(3,9,9,*,*,*,2,1,*,*,*)(3,9,9,*,*,*,3,2,*,*,*)(3,9,9,*,*,*,4,3,*,*,*)(3,9,9,*,*,*,5,4,*,*,*)(3,9,9,*,*,*,6,5,*,*,*)(3,9,*,9,*,*,0,*,1,*,*)(3,9,*,9,*,*,1,*,0,*,*)(3,9,*,9,*,*,2,*,1,*,*)(3,9,*,9,*,*,3,*,2,*,*)(3,9,*,9,*,*,4,*,3,*,*)(3,9,*,9,*,*,5,*,4,*,*)(3,9,*,9,*,*,6,*,5,*,*)(3,9,*,*,9,*,1,*,*,0,*)(3,9,*,*,9,*,2,*,*,1,*)(3,9,*,*,9,*,3,*,*,2,*)(3,9,*,*,9,*,4,*,*,3,*)(3,9,*,*,9,*,5,*,*,4,*)(3,9,*,*,9,*,6,*,*,5,*)(3,9,*,*,*,9,0,*,*,*,1)(3,9,*,*,*,9,1,*,*,*,0)(3,9,*,*,*,9,2,*,*,*,1)(3,9,*,*,*,9,3,*,*,*,2)(3,9,*,*,*,9,4,*,*,*,3)(3,9,*,*,*,9,5,*,*,*,4)(3,9,*,*,*,9,6,*,*,*,5) + + y[1][4] x[1][4] x[1][3] x[1][5] x[0][4] x[2][4] d[1][4] d[1][3] d[1][5] d[0][4] d[2][4] + y[5][4] x[5][4] x[5][3] x[5][5] x[4][4] x[6][4] d[5][4] d[5][3] d[5][5] d[4][4] d[6][4] + + + + %... + (1,*,*,*,*,*,0,*,*,*,*)(5,0,0,*,*,*,1,0,*,*,*)(5,0,0,*,*,*,2,1,*,*,*)(5,0,0,*,*,*,3,2,*,*,*)(5,0,0,*,*,*,4,3,*,*,*)(5,0,0,*,*,*,5,4,*,*,*)(5,0,0,*,*,*,6,5,*,*,*)(5,0,*,0,*,*,0,*,1,*,*)(5,0,*,0,*,*,1,*,0,*,*)(5,0,*,0,*,*,2,*,1,*,*)(5,0,*,0,*,*,3,*,2,*,*)(5,0,*,0,*,*,4,*,3,*,*)(5,0,*,0,*,*,5,*,4,*,*)(5,0,*,0,*,*,6,*,5,*,*)(5,0,*,*,0,*,1,*,*,0,*)(5,0,*,*,0,*,2,*,*,1,*)(5,0,*,*,0,*,3,*,*,2,*)(5,0,*,*,0,*,4,*,*,3,*)(5,0,*,*,0,*,5,*,*,4,*)(5,0,*,*,0,*,6,*,*,5,*)(5,0,*,*,*,0,0,*,*,*,1)(5,0,*,*,*,0,1,*,*,*,0)(5,0,*,*,*,0,2,*,*,*,1)(5,0,*,*,*,0,3,*,*,*,2)(5,0,*,*,*,0,4,*,*,*,3)(5,0,*,*,*,0,5,*,*,*,4)(5,0,*,*,*,0,6,*,*,*,5)(5,1,1,*,*,*,1,0,*,*,*)(5,1,1,*,*,*,2,1,*,*,*)(5,1,1,*,*,*,3,2,*,*,*)(5,1,1,*,*,*,4,3,*,*,*)(5,1,1,*,*,*,5,4,*,*,*)(5,1,1,*,*,*,6,5,*,*,*)(5,1,*,1,*,*,0,*,1,*,*)(5,1,*,1,*,*,1,*,0,*,*)(5,1,*,1,*,*,2,*,1,*,*)(5,1,*,1,*,*,3,*,2,*,*)(5,1,*,1,*,*,4,*,3,*,*)(5,1,*,1,*,*,5,*,4,*,*)(5,1,*,1,*,*,6,*,5,*,*)(5,1,*,*,1,*,1,*,*,0,*)(5,1,*,*,1,*,2,*,*,1,*)(5,1,*,*,1,*,3,*,*,2,*)(5,1,*,*,1,*,4,*,*,3,*)(5,1,*,*,1,*,5,*,*,4,*)(5,1,*,*,1,*,6,*,*,5,*)(5,1,*,*,*,1,0,*,*,*,1)(5,1,*,*,*,1,1,*,*,*,0)(5,1,*,*,*,1,2,*,*,*,1)(5,1,*,*,*,1,3,*,*,*,2)(5,1,*,*,*,1,4,*,*,*,3)(5,1,*,*,*,1,5,*,*,*,4)(5,1,*,*,*,1,6,*,*,*,5)(5,2,2,*,*,*,1,0,*,*,*)(5,2,2,*,*,*,2,1,*,*,*)(5,2,2,*,*,*,3,2,*,*,*)(5,2,2,*,*,*,4,3,*,*,*)(5,2,2,*,*,*,5,4,*,*,*)(5,2,2,*,*,*,6,5,*,*,*)(5,2,*,2,*,*,0,*,1,*,*)(5,2,*,2,*,*,1,*,0,*,*)(5,2,*,2,*,*,2,*,1,*,*)(5,2,*,2,*,*,3,*,2,*,*)(5,2,*,2,*,*,4,*,3,*,*)(5,2,*,2,*,*,5,*,4,*,*)(5,2,*,2,*,*,6,*,5,*,*)(5,2,*,*,2,*,1,*,*,0,*)(5,2,*,*,2,*,2,*,*,1,*)(5,2,*,*,2,*,3,*,*,2,*)(5,2,*,*,2,*,4,*,*,3,*)(5,2,*,*,2,*,5,*,*,4,*)(5,2,*,*,2,*,6,*,*,5,*)(5,2,*,*,*,2,0,*,*,*,1)(5,2,*,*,*,2,1,*,*,*,0)(5,2,*,*,*,2,2,*,*,*,1)(5,2,*,*,*,2,3,*,*,*,2)(5,2,*,*,*,2,4,*,*,*,3)(5,2,*,*,*,2,5,*,*,*,4)(5,2,*,*,*,2,6,*,*,*,5)(5,3,3,*,*,*,1,0,*,*,*)(5,3,3,*,*,*,2,1,*,*,*)(5,3,3,*,*,*,3,2,*,*,*)(5,3,3,*,*,*,4,3,*,*,*)(5,3,3,*,*,*,5,4,*,*,*)(5,3,3,*,*,*,6,5,*,*,*)(5,3,*,3,*,*,0,*,1,*,*)(5,3,*,3,*,*,1,*,0,*,*)(5,3,*,3,*,*,2,*,1,*,*)(5,3,*,3,*,*,3,*,2,*,*)(5,3,*,3,*,*,4,*,3,*,*)(5,3,*,3,*,*,5,*,4,*,*)(5,3,*,3,*,*,6,*,5,*,*)(5,3,*,*,3,*,1,*,*,0,*)(5,3,*,*,3,*,2,*,*,1,*)(5,3,*,*,3,*,3,*,*,2,*)(5,3,*,*,3,*,4,*,*,3,*)(5,3,*,*,3,*,5,*,*,4,*)(5,3,*,*,3,*,6,*,*,5,*)(5,3,*,*,*,3,0,*,*,*,1)(5,3,*,*,*,3,1,*,*,*,0)(5,3,*,*,*,3,2,*,*,*,1)(5,3,*,*,*,3,3,*,*,*,2)(5,3,*,*,*,3,4,*,*,*,3)(5,3,*,*,*,3,5,*,*,*,4)(5,3,*,*,*,3,6,*,*,*,5)(5,4,4,*,*,*,1,0,*,*,*)(5,4,4,*,*,*,2,1,*,*,*)(5,4,4,*,*,*,3,2,*,*,*)(5,4,4,*,*,*,4,3,*,*,*)(5,4,4,*,*,*,5,4,*,*,*)(5,4,4,*,*,*,6,5,*,*,*)(5,4,*,4,*,*,0,*,1,*,*)(5,4,*,4,*,*,1,*,0,*,*)(5,4,*,4,*,*,2,*,1,*,*)(5,4,*,4,*,*,3,*,2,*,*)(5,4,*,4,*,*,4,*,3,*,*)(5,4,*,4,*,*,5,*,4,*,*)(5,4,*,4,*,*,6,*,5,*,*)(5,4,*,*,4,*,1,*,*,0,*)(5,4,*,*,4,*,2,*,*,1,*)(5,4,*,*,4,*,3,*,*,2,*)(5,4,*,*,4,*,4,*,*,3,*)(5,4,*,*,4,*,5,*,*,4,*)(5,4,*,*,4,*,6,*,*,5,*)(5,4,*,*,*,4,0,*,*,*,1)(5,4,*,*,*,4,1,*,*,*,0)(5,4,*,*,*,4,2,*,*,*,1)(5,4,*,*,*,4,3,*,*,*,2)(5,4,*,*,*,4,4,*,*,*,3)(5,4,*,*,*,4,5,*,*,*,4)(5,4,*,*,*,4,6,*,*,*,5)(5,5,5,*,*,*,1,0,*,*,*)(5,5,5,*,*,*,2,1,*,*,*)(5,5,5,*,*,*,3,2,*,*,*)(5,5,5,*,*,*,4,3,*,*,*)(5,5,5,*,*,*,5,4,*,*,*)(5,5,5,*,*,*,6,5,*,*,*)(5,5,*,5,*,*,0,*,1,*,*)(5,5,*,5,*,*,1,*,0,*,*)(5,5,*,5,*,*,2,*,1,*,*)(5,5,*,5,*,*,3,*,2,*,*)(5,5,*,5,*,*,4,*,3,*,*)(5,5,*,5,*,*,5,*,4,*,*)(5,5,*,5,*,*,6,*,5,*,*)(5,5,*,*,5,*,1,*,*,0,*)(5,5,*,*,5,*,2,*,*,1,*)(5,5,*,*,5,*,3,*,*,2,*)(5,5,*,*,5,*,4,*,*,3,*)(5,5,*,*,5,*,5,*,*,4,*)(5,5,*,*,5,*,6,*,*,5,*)(5,5,*,*,*,5,0,*,*,*,1)(5,5,*,*,*,5,1,*,*,*,0)(5,5,*,*,*,5,2,*,*,*,1)(5,5,*,*,*,5,3,*,*,*,2)(5,5,*,*,*,5,4,*,*,*,3)(5,5,*,*,*,5,5,*,*,*,4)(5,5,*,*,*,5,6,*,*,*,5)(5,6,6,*,*,*,1,0,*,*,*)(5,6,6,*,*,*,2,1,*,*,*)(5,6,6,*,*,*,3,2,*,*,*)(5,6,6,*,*,*,4,3,*,*,*)(5,6,6,*,*,*,5,4,*,*,*)(5,6,6,*,*,*,6,5,*,*,*)(5,6,*,6,*,*,0,*,1,*,*)(5,6,*,6,*,*,1,*,0,*,*)(5,6,*,6,*,*,2,*,1,*,*)(5,6,*,6,*,*,3,*,2,*,*)(5,6,*,6,*,*,4,*,3,*,*)(5,6,*,6,*,*,5,*,4,*,*)(5,6,*,6,*,*,6,*,5,*,*)(5,6,*,*,6,*,1,*,*,0,*)(5,6,*,*,6,*,2,*,*,1,*)(5,6,*,*,6,*,3,*,*,2,*)(5,6,*,*,6,*,4,*,*,3,*)(5,6,*,*,6,*,5,*,*,4,*)(5,6,*,*,6,*,6,*,*,5,*)(5,6,*,*,*,6,0,*,*,*,1)(5,6,*,*,*,6,1,*,*,*,0)(5,6,*,*,*,6,2,*,*,*,1)(5,6,*,*,*,6,3,*,*,*,2)(5,6,*,*,*,6,4,*,*,*,3)(5,6,*,*,*,6,5,*,*,*,4)(5,6,*,*,*,6,6,*,*,*,5)(5,7,7,*,*,*,1,0,*,*,*)(5,7,7,*,*,*,2,1,*,*,*)(5,7,7,*,*,*,3,2,*,*,*)(5,7,7,*,*,*,4,3,*,*,*)(5,7,7,*,*,*,5,4,*,*,*)(5,7,7,*,*,*,6,5,*,*,*)(5,7,*,7,*,*,0,*,1,*,*)(5,7,*,7,*,*,1,*,0,*,*)(5,7,*,7,*,*,2,*,1,*,*)(5,7,*,7,*,*,3,*,2,*,*)(5,7,*,7,*,*,4,*,3,*,*)(5,7,*,7,*,*,5,*,4,*,*)(5,7,*,7,*,*,6,*,5,*,*)(5,7,*,*,7,*,1,*,*,0,*)(5,7,*,*,7,*,2,*,*,1,*)(5,7,*,*,7,*,3,*,*,2,*)(5,7,*,*,7,*,4,*,*,3,*)(5,7,*,*,7,*,5,*,*,4,*)(5,7,*,*,7,*,6,*,*,5,*)(5,7,*,*,*,7,0,*,*,*,1)(5,7,*,*,*,7,1,*,*,*,0)(5,7,*,*,*,7,2,*,*,*,1)(5,7,*,*,*,7,3,*,*,*,2)(5,7,*,*,*,7,4,*,*,*,3)(5,7,*,*,*,7,5,*,*,*,4)(5,7,*,*,*,7,6,*,*,*,5)(5,8,8,*,*,*,1,0,*,*,*)(5,8,8,*,*,*,2,1,*,*,*)(5,8,8,*,*,*,3,2,*,*,*)(5,8,8,*,*,*,4,3,*,*,*)(5,8,8,*,*,*,5,4,*,*,*)(5,8,8,*,*,*,6,5,*,*,*)(5,8,*,8,*,*,0,*,1,*,*)(5,8,*,8,*,*,1,*,0,*,*)(5,8,*,8,*,*,2,*,1,*,*)(5,8,*,8,*,*,3,*,2,*,*)(5,8,*,8,*,*,4,*,3,*,*)(5,8,*,8,*,*,5,*,4,*,*)(5,8,*,8,*,*,6,*,5,*,*)(5,8,*,*,8,*,1,*,*,0,*)(5,8,*,*,8,*,2,*,*,1,*)(5,8,*,*,8,*,3,*,*,2,*)(5,8,*,*,8,*,4,*,*,3,*)(5,8,*,*,8,*,5,*,*,4,*)(5,8,*,*,8,*,6,*,*,5,*)(5,8,*,*,*,8,0,*,*,*,1)(5,8,*,*,*,8,1,*,*,*,0)(5,8,*,*,*,8,2,*,*,*,1)(5,8,*,*,*,8,3,*,*,*,2)(5,8,*,*,*,8,4,*,*,*,3)(5,8,*,*,*,8,5,*,*,*,4)(5,8,*,*,*,8,6,*,*,*,5)(5,9,9,*,*,*,1,0,*,*,*)(5,9,9,*,*,*,2,1,*,*,*)(5,9,9,*,*,*,3,2,*,*,*)(5,9,9,*,*,*,4,3,*,*,*)(5,9,9,*,*,*,5,4,*,*,*)(5,9,9,*,*,*,6,5,*,*,*)(5,9,*,9,*,*,0,*,1,*,*)(5,9,*,9,*,*,1,*,0,*,*)(5,9,*,9,*,*,2,*,1,*,*)(5,9,*,9,*,*,3,*,2,*,*)(5,9,*,9,*,*,4,*,3,*,*)(5,9,*,9,*,*,5,*,4,*,*)(5,9,*,9,*,*,6,*,5,*,*)(5,9,*,*,9,*,1,*,*,0,*)(5,9,*,*,9,*,2,*,*,1,*)(5,9,*,*,9,*,3,*,*,2,*)(5,9,*,*,9,*,4,*,*,3,*)(5,9,*,*,9,*,5,*,*,4,*)(5,9,*,*,9,*,6,*,*,5,*)(5,9,*,*,*,9,0,*,*,*,1)(5,9,*,*,*,9,1,*,*,*,0)(5,9,*,*,*,9,2,*,*,*,1)(5,9,*,*,*,9,3,*,*,*,2)(5,9,*,*,*,9,4,*,*,*,3)(5,9,*,*,*,9,5,*,*,*,4)(5,9,*,*,*,9,6,*,*,*,5) + + y[2][2] x[2][2] x[2][1] x[2][3] x[1][2] x[3][2] d[2][2] d[2][1] d[2][3] d[1][2] d[3][2] + y[5][1] x[5][1] x[5][0] x[5][2] x[4][1] x[6][1] d[5][1] d[5][0] d[5][2] d[4][1] d[6][1] + + + y[2][5] x[2][5] x[2][4] x[2][6] x[1][5] x[3][5] d[2][5] d[2][4] d[2][6] d[1][5] d[3][5] + (1,*,*,*,*,*,0,*,*,*,*)(2,0,0,*,*,*,1,0,*,*,*)(2,0,0,*,*,*,2,1,*,*,*)(2,0,0,*,*,*,3,2,*,*,*)(2,0,0,*,*,*,4,3,*,*,*)(2,0,0,*,*,*,5,4,*,*,*)(2,0,0,*,*,*,6,5,*,*,*)(2,0,*,0,*,*,0,*,1,*,*)(2,0,*,0,*,*,1,*,0,*,*)(2,0,*,0,*,*,2,*,1,*,*)(2,0,*,0,*,*,3,*,2,*,*)(2,0,*,0,*,*,4,*,3,*,*)(2,0,*,0,*,*,5,*,4,*,*)(2,0,*,0,*,*,6,*,5,*,*)(2,0,*,*,0,*,1,*,*,0,*)(2,0,*,*,0,*,2,*,*,1,*)(2,0,*,*,0,*,3,*,*,2,*)(2,0,*,*,0,*,4,*,*,3,*)(2,0,*,*,0,*,5,*,*,4,*)(2,0,*,*,0,*,6,*,*,5,*)(2,0,*,*,*,0,0,*,*,*,1)(2,0,*,*,*,0,1,*,*,*,0)(2,0,*,*,*,0,2,*,*,*,1)(2,0,*,*,*,0,3,*,*,*,2)(2,0,*,*,*,0,4,*,*,*,3)(2,0,*,*,*,0,5,*,*,*,4)(2,0,*,*,*,0,6,*,*,*,5)(2,1,1,*,*,*,1,0,*,*,*)(2,1,1,*,*,*,2,1,*,*,*)(2,1,1,*,*,*,3,2,*,*,*)(2,1,1,*,*,*,4,3,*,*,*)(2,1,1,*,*,*,5,4,*,*,*)(2,1,1,*,*,*,6,5,*,*,*)(2,1,*,1,*,*,0,*,1,*,*)(2,1,*,1,*,*,1,*,0,*,*)(2,1,*,1,*,*,2,*,1,*,*)(2,1,*,1,*,*,3,*,2,*,*)(2,1,*,1,*,*,4,*,3,*,*)(2,1,*,1,*,*,5,*,4,*,*)(2,1,*,1,*,*,6,*,5,*,*)(2,1,*,*,1,*,1,*,*,0,*)(2,1,*,*,1,*,2,*,*,1,*)(2,1,*,*,1,*,3,*,*,2,*)(2,1,*,*,1,*,4,*,*,3,*)(2,1,*,*,1,*,5,*,*,4,*)(2,1,*,*,1,*,6,*,*,5,*)(2,1,*,*,*,1,0,*,*,*,1)(2,1,*,*,*,1,1,*,*,*,0)(2,1,*,*,*,1,2,*,*,*,1)(2,1,*,*,*,1,3,*,*,*,2)(2,1,*,*,*,1,4,*,*,*,3)(2,1,*,*,*,1,5,*,*,*,4)(2,1,*,*,*,1,6,*,*,*,5)(2,2,2,*,*,*,1,0,*,*,*)(2,2,2,*,*,*,2,1,*,*,*)(2,2,2,*,*,*,3,2,*,*,*)(2,2,2,*,*,*,4,3,*,*,*)(2,2,2,*,*,*,5,4,*,*,*)(2,2,2,*,*,*,6,5,*,*,*)(2,2,*,2,*,*,0,*,1,*,*)(2,2,*,2,*,*,1,*,0,*,*)(2,2,*,2,*,*,2,*,1,*,*)(2,2,*,2,*,*,3,*,2,*,*)(2,2,*,2,*,*,4,*,3,*,*)(2,2,*,2,*,*,5,*,4,*,*)(2,2,*,2,*,*,6,*,5,*,*)(2,2,*,*,2,*,1,*,*,0,*)(2,2,*,*,2,*,2,*,*,1,*)(2,2,*,*,2,*,3,*,*,2,*)(2,2,*,*,2,*,4,*,*,3,*)(2,2,*,*,2,*,5,*,*,4,*)(2,2,*,*,2,*,6,*,*,5,*)(2,2,*,*,*,2,0,*,*,*,1)(2,2,*,*,*,2,1,*,*,*,0)(2,2,*,*,*,2,2,*,*,*,1)(2,2,*,*,*,2,3,*,*,*,2)(2,2,*,*,*,2,4,*,*,*,3)(2,2,*,*,*,2,5,*,*,*,4)(2,2,*,*,*,2,6,*,*,*,5)(2,3,3,*,*,*,1,0,*,*,*)(2,3,3,*,*,*,2,1,*,*,*)(2,3,3,*,*,*,3,2,*,*,*)(2,3,3,*,*,*,4,3,*,*,*)(2,3,3,*,*,*,5,4,*,*,*)(2,3,3,*,*,*,6,5,*,*,*)(2,3,*,3,*,*,0,*,1,*,*)(2,3,*,3,*,*,1,*,0,*,*)(2,3,*,3,*,*,2,*,1,*,*)(2,3,*,3,*,*,3,*,2,*,*)(2,3,*,3,*,*,4,*,3,*,*)(2,3,*,3,*,*,5,*,4,*,*)(2,3,*,3,*,*,6,*,5,*,*)(2,3,*,*,3,*,1,*,*,0,*)(2,3,*,*,3,*,2,*,*,1,*)(2,3,*,*,3,*,3,*,*,2,*)(2,3,*,*,3,*,4,*,*,3,*)(2,3,*,*,3,*,5,*,*,4,*)(2,3,*,*,3,*,6,*,*,5,*)(2,3,*,*,*,3,0,*,*,*,1)(2,3,*,*,*,3,1,*,*,*,0)(2,3,*,*,*,3,2,*,*,*,1)(2,3,*,*,*,3,3,*,*,*,2)(2,3,*,*,*,3,4,*,*,*,3)(2,3,*,*,*,3,5,*,*,*,4)(2,3,*,*,*,3,6,*,*,*,5)(2,4,4,*,*,*,1,0,*,*,*)(2,4,4,*,*,*,2,1,*,*,*)(2,4,4,*,*,*,3,2,*,*,*)(2,4,4,*,*,*,4,3,*,*,*)(2,4,4,*,*,*,5,4,*,*,*)(2,4,4,*,*,*,6,5,*,*,*)(2,4,*,4,*,*,0,*,1,*,*)(2,4,*,4,*,*,1,*,0,*,*)(2,4,*,4,*,*,2,*,1,*,*)(2,4,*,4,*,*,3,*,2,*,*)(2,4,*,4,*,*,4,*,3,*,*)(2,4,*,4,*,*,5,*,4,*,*)(2,4,*,4,*,*,6,*,5,*,*)(2,4,*,*,4,*,1,*,*,0,*)(2,4,*,*,4,*,2,*,*,1,*)(2,4,*,*,4,*,3,*,*,2,*)(2,4,*,*,4,*,4,*,*,3,*)(2,4,*,*,4,*,5,*,*,4,*)(2,4,*,*,4,*,6,*,*,5,*)(2,4,*,*,*,4,0,*,*,*,1)(2,4,*,*,*,4,1,*,*,*,0)(2,4,*,*,*,4,2,*,*,*,1)(2,4,*,*,*,4,3,*,*,*,2)(2,4,*,*,*,4,4,*,*,*,3)(2,4,*,*,*,4,5,*,*,*,4)(2,4,*,*,*,4,6,*,*,*,5)(2,5,5,*,*,*,1,0,*,*,*)(2,5,5,*,*,*,2,1,*,*,*)(2,5,5,*,*,*,3,2,*,*,*)(2,5,5,*,*,*,4,3,*,*,*)(2,5,5,*,*,*,5,4,*,*,*)(2,5,5,*,*,*,6,5,*,*,*)(2,5,*,5,*,*,0,*,1,*,*)(2,5,*,5,*,*,1,*,0,*,*)(2,5,*,5,*,*,2,*,1,*,*)(2,5,*,5,*,*,3,*,2,*,*)(2,5,*,5,*,*,4,*,3,*,*)(2,5,*,5,*,*,5,*,4,*,*)(2,5,*,5,*,*,6,*,5,*,*)(2,5,*,*,5,*,1,*,*,0,*)(2,5,*,*,5,*,2,*,*,1,*)(2,5,*,*,5,*,3,*,*,2,*)(2,5,*,*,5,*,4,*,*,3,*)(2,5,*,*,5,*,5,*,*,4,*)(2,5,*,*,5,*,6,*,*,5,*)(2,5,*,*,*,5,0,*,*,*,1)(2,5,*,*,*,5,1,*,*,*,0)(2,5,*,*,*,5,2,*,*,*,1)(2,5,*,*,*,5,3,*,*,*,2)(2,5,*,*,*,5,4,*,*,*,3)(2,5,*,*,*,5,5,*,*,*,4)(2,5,*,*,*,5,6,*,*,*,5)(2,6,6,*,*,*,1,0,*,*,*)(2,6,6,*,*,*,2,1,*,*,*)(2,6,6,*,*,*,3,2,*,*,*)(2,6,6,*,*,*,4,3,*,*,*)(2,6,6,*,*,*,5,4,*,*,*)(2,6,6,*,*,*,6,5,*,*,*)(2,6,*,6,*,*,0,*,1,*,*)(2,6,*,6,*,*,1,*,0,*,*)(2,6,*,6,*,*,2,*,1,*,*)(2,6,*,6,*,*,3,*,2,*,*)(2,6,*,6,*,*,4,*,3,*,*)(2,6,*,6,*,*,5,*,4,*,*)(2,6,*,6,*,*,6,*,5,*,*)(2,6,*,*,6,*,1,*,*,0,*)(2,6,*,*,6,*,2,*,*,1,*)(2,6,*,*,6,*,3,*,*,2,*)(2,6,*,*,6,*,4,*,*,3,*)(2,6,*,*,6,*,5,*,*,4,*)(2,6,*,*,6,*,6,*,*,5,*)(2,6,*,*,*,6,0,*,*,*,1)(2,6,*,*,*,6,1,*,*,*,0)(2,6,*,*,*,6,2,*,*,*,1)(2,6,*,*,*,6,3,*,*,*,2)(2,6,*,*,*,6,4,*,*,*,3)(2,6,*,*,*,6,5,*,*,*,4)(2,6,*,*,*,6,6,*,*,*,5)(2,7,7,*,*,*,1,0,*,*,*)(2,7,7,*,*,*,2,1,*,*,*)(2,7,7,*,*,*,3,2,*,*,*)(2,7,7,*,*,*,4,3,*,*,*)(2,7,7,*,*,*,5,4,*,*,*)(2,7,7,*,*,*,6,5,*,*,*)(2,7,*,7,*,*,0,*,1,*,*)(2,7,*,7,*,*,1,*,0,*,*)(2,7,*,7,*,*,2,*,1,*,*)(2,7,*,7,*,*,3,*,2,*,*)(2,7,*,7,*,*,4,*,3,*,*)(2,7,*,7,*,*,5,*,4,*,*)(2,7,*,7,*,*,6,*,5,*,*)(2,7,*,*,7,*,1,*,*,0,*)(2,7,*,*,7,*,2,*,*,1,*)(2,7,*,*,7,*,3,*,*,2,*)(2,7,*,*,7,*,4,*,*,3,*)(2,7,*,*,7,*,5,*,*,4,*)(2,7,*,*,7,*,6,*,*,5,*)(2,7,*,*,*,7,0,*,*,*,1)(2,7,*,*,*,7,1,*,*,*,0)(2,7,*,*,*,7,2,*,*,*,1)(2,7,*,*,*,7,3,*,*,*,2)(2,7,*,*,*,7,4,*,*,*,3)(2,7,*,*,*,7,5,*,*,*,4)(2,7,*,*,*,7,6,*,*,*,5)(2,8,8,*,*,*,1,0,*,*,*)(2,8,8,*,*,*,2,1,*,*,*)(2,8,8,*,*,*,3,2,*,*,*)(2,8,8,*,*,*,4,3,*,*,*)(2,8,8,*,*,*,5,4,*,*,*)(2,8,8,*,*,*,6,5,*,*,*)(2,8,*,8,*,*,0,*,1,*,*)(2,8,*,8,*,*,1,*,0,*,*)(2,8,*,8,*,*,2,*,1,*,*)(2,8,*,8,*,*,3,*,2,*,*)(2,8,*,8,*,*,4,*,3,*,*)(2,8,*,8,*,*,5,*,4,*,*)(2,8,*,8,*,*,6,*,5,*,*)(2,8,*,*,8,*,1,*,*,0,*)(2,8,*,*,8,*,2,*,*,1,*)(2,8,*,*,8,*,3,*,*,2,*)(2,8,*,*,8,*,4,*,*,3,*)(2,8,*,*,8,*,5,*,*,4,*)(2,8,*,*,8,*,6,*,*,5,*)(2,8,*,*,*,8,0,*,*,*,1)(2,8,*,*,*,8,1,*,*,*,0)(2,8,*,*,*,8,2,*,*,*,1)(2,8,*,*,*,8,3,*,*,*,2)(2,8,*,*,*,8,4,*,*,*,3)(2,8,*,*,*,8,5,*,*,*,4)(2,8,*,*,*,8,6,*,*,*,5)(2,9,9,*,*,*,1,0,*,*,*)(2,9,9,*,*,*,2,1,*,*,*)(2,9,9,*,*,*,3,2,*,*,*)(2,9,9,*,*,*,4,3,*,*,*)(2,9,9,*,*,*,5,4,*,*,*)(2,9,9,*,*,*,6,5,*,*,*)(2,9,*,9,*,*,0,*,1,*,*)(2,9,*,9,*,*,1,*,0,*,*)(2,9,*,9,*,*,2,*,1,*,*)(2,9,*,9,*,*,3,*,2,*,*)(2,9,*,9,*,*,4,*,3,*,*)(2,9,*,9,*,*,5,*,4,*,*)(2,9,*,9,*,*,6,*,5,*,*)(2,9,*,*,9,*,1,*,*,0,*)(2,9,*,*,9,*,2,*,*,1,*)(2,9,*,*,9,*,3,*,*,2,*)(2,9,*,*,9,*,4,*,*,3,*)(2,9,*,*,9,*,5,*,*,4,*)(2,9,*,*,9,*,6,*,*,5,*)(2,9,*,*,*,9,0,*,*,*,1)(2,9,*,*,*,9,1,*,*,*,0)(2,9,*,*,*,9,2,*,*,*,1)(2,9,*,*,*,9,3,*,*,*,2)(2,9,*,*,*,9,4,*,*,*,3)(2,9,*,*,*,9,5,*,*,*,4)(2,9,*,*,*,9,6,*,*,*,5) + + + + %... + (1,*,*,*,*,*,0,*,*,*,*)(6,0,0,*,*,*,1,0,*,*,*)(6,0,0,*,*,*,2,1,*,*,*)(6,0,0,*,*,*,3,2,*,*,*)(6,0,0,*,*,*,4,3,*,*,*)(6,0,0,*,*,*,5,4,*,*,*)(6,0,0,*,*,*,6,5,*,*,*)(6,0,*,0,*,*,0,*,1,*,*)(6,0,*,0,*,*,1,*,0,*,*)(6,0,*,0,*,*,2,*,1,*,*)(6,0,*,0,*,*,3,*,2,*,*)(6,0,*,0,*,*,4,*,3,*,*)(6,0,*,0,*,*,5,*,4,*,*)(6,0,*,0,*,*,6,*,5,*,*)(6,0,*,*,0,*,1,*,*,0,*)(6,0,*,*,0,*,2,*,*,1,*)(6,0,*,*,0,*,3,*,*,2,*)(6,0,*,*,0,*,4,*,*,3,*)(6,0,*,*,0,*,5,*,*,4,*)(6,0,*,*,0,*,6,*,*,5,*)(6,0,*,*,*,0,0,*,*,*,1)(6,0,*,*,*,0,1,*,*,*,0)(6,0,*,*,*,0,2,*,*,*,1)(6,0,*,*,*,0,3,*,*,*,2)(6,0,*,*,*,0,4,*,*,*,3)(6,0,*,*,*,0,5,*,*,*,4)(6,0,*,*,*,0,6,*,*,*,5)(6,1,1,*,*,*,1,0,*,*,*)(6,1,1,*,*,*,2,1,*,*,*)(6,1,1,*,*,*,3,2,*,*,*)(6,1,1,*,*,*,4,3,*,*,*)(6,1,1,*,*,*,5,4,*,*,*)(6,1,1,*,*,*,6,5,*,*,*)(6,1,*,1,*,*,0,*,1,*,*)(6,1,*,1,*,*,1,*,0,*,*)(6,1,*,1,*,*,2,*,1,*,*)(6,1,*,1,*,*,3,*,2,*,*)(6,1,*,1,*,*,4,*,3,*,*)(6,1,*,1,*,*,5,*,4,*,*)(6,1,*,1,*,*,6,*,5,*,*)(6,1,*,*,1,*,1,*,*,0,*)(6,1,*,*,1,*,2,*,*,1,*)(6,1,*,*,1,*,3,*,*,2,*)(6,1,*,*,1,*,4,*,*,3,*)(6,1,*,*,1,*,5,*,*,4,*)(6,1,*,*,1,*,6,*,*,5,*)(6,1,*,*,*,1,0,*,*,*,1)(6,1,*,*,*,1,1,*,*,*,0)(6,1,*,*,*,1,2,*,*,*,1)(6,1,*,*,*,1,3,*,*,*,2)(6,1,*,*,*,1,4,*,*,*,3)(6,1,*,*,*,1,5,*,*,*,4)(6,1,*,*,*,1,6,*,*,*,5)(6,2,2,*,*,*,1,0,*,*,*)(6,2,2,*,*,*,2,1,*,*,*)(6,2,2,*,*,*,3,2,*,*,*)(6,2,2,*,*,*,4,3,*,*,*)(6,2,2,*,*,*,5,4,*,*,*)(6,2,2,*,*,*,6,5,*,*,*)(6,2,*,2,*,*,0,*,1,*,*)(6,2,*,2,*,*,1,*,0,*,*)(6,2,*,2,*,*,2,*,1,*,*)(6,2,*,2,*,*,3,*,2,*,*)(6,2,*,2,*,*,4,*,3,*,*)(6,2,*,2,*,*,5,*,4,*,*)(6,2,*,2,*,*,6,*,5,*,*)(6,2,*,*,2,*,1,*,*,0,*)(6,2,*,*,2,*,2,*,*,1,*)(6,2,*,*,2,*,3,*,*,2,*)(6,2,*,*,2,*,4,*,*,3,*)(6,2,*,*,2,*,5,*,*,4,*)(6,2,*,*,2,*,6,*,*,5,*)(6,2,*,*,*,2,0,*,*,*,1)(6,2,*,*,*,2,1,*,*,*,0)(6,2,*,*,*,2,2,*,*,*,1)(6,2,*,*,*,2,3,*,*,*,2)(6,2,*,*,*,2,4,*,*,*,3)(6,2,*,*,*,2,5,*,*,*,4)(6,2,*,*,*,2,6,*,*,*,5)(6,3,3,*,*,*,1,0,*,*,*)(6,3,3,*,*,*,2,1,*,*,*)(6,3,3,*,*,*,3,2,*,*,*)(6,3,3,*,*,*,4,3,*,*,*)(6,3,3,*,*,*,5,4,*,*,*)(6,3,3,*,*,*,6,5,*,*,*)(6,3,*,3,*,*,0,*,1,*,*)(6,3,*,3,*,*,1,*,0,*,*)(6,3,*,3,*,*,2,*,1,*,*)(6,3,*,3,*,*,3,*,2,*,*)(6,3,*,3,*,*,4,*,3,*,*)(6,3,*,3,*,*,5,*,4,*,*)(6,3,*,3,*,*,6,*,5,*,*)(6,3,*,*,3,*,1,*,*,0,*)(6,3,*,*,3,*,2,*,*,1,*)(6,3,*,*,3,*,3,*,*,2,*)(6,3,*,*,3,*,4,*,*,3,*)(6,3,*,*,3,*,5,*,*,4,*)(6,3,*,*,3,*,6,*,*,5,*)(6,3,*,*,*,3,0,*,*,*,1)(6,3,*,*,*,3,1,*,*,*,0)(6,3,*,*,*,3,2,*,*,*,1)(6,3,*,*,*,3,3,*,*,*,2)(6,3,*,*,*,3,4,*,*,*,3)(6,3,*,*,*,3,5,*,*,*,4)(6,3,*,*,*,3,6,*,*,*,5)(6,4,4,*,*,*,1,0,*,*,*)(6,4,4,*,*,*,2,1,*,*,*)(6,4,4,*,*,*,3,2,*,*,*)(6,4,4,*,*,*,4,3,*,*,*)(6,4,4,*,*,*,5,4,*,*,*)(6,4,4,*,*,*,6,5,*,*,*)(6,4,*,4,*,*,0,*,1,*,*)(6,4,*,4,*,*,1,*,0,*,*)(6,4,*,4,*,*,2,*,1,*,*)(6,4,*,4,*,*,3,*,2,*,*)(6,4,*,4,*,*,4,*,3,*,*)(6,4,*,4,*,*,5,*,4,*,*)(6,4,*,4,*,*,6,*,5,*,*)(6,4,*,*,4,*,1,*,*,0,*)(6,4,*,*,4,*,2,*,*,1,*)(6,4,*,*,4,*,3,*,*,2,*)(6,4,*,*,4,*,4,*,*,3,*)(6,4,*,*,4,*,5,*,*,4,*)(6,4,*,*,4,*,6,*,*,5,*)(6,4,*,*,*,4,0,*,*,*,1)(6,4,*,*,*,4,1,*,*,*,0)(6,4,*,*,*,4,2,*,*,*,1)(6,4,*,*,*,4,3,*,*,*,2)(6,4,*,*,*,4,4,*,*,*,3)(6,4,*,*,*,4,5,*,*,*,4)(6,4,*,*,*,4,6,*,*,*,5)(6,5,5,*,*,*,1,0,*,*,*)(6,5,5,*,*,*,2,1,*,*,*)(6,5,5,*,*,*,3,2,*,*,*)(6,5,5,*,*,*,4,3,*,*,*)(6,5,5,*,*,*,5,4,*,*,*)(6,5,5,*,*,*,6,5,*,*,*)(6,5,*,5,*,*,0,*,1,*,*)(6,5,*,5,*,*,1,*,0,*,*)(6,5,*,5,*,*,2,*,1,*,*)(6,5,*,5,*,*,3,*,2,*,*)(6,5,*,5,*,*,4,*,3,*,*)(6,5,*,5,*,*,5,*,4,*,*)(6,5,*,5,*,*,6,*,5,*,*)(6,5,*,*,5,*,1,*,*,0,*)(6,5,*,*,5,*,2,*,*,1,*)(6,5,*,*,5,*,3,*,*,2,*)(6,5,*,*,5,*,4,*,*,3,*)(6,5,*,*,5,*,5,*,*,4,*)(6,5,*,*,5,*,6,*,*,5,*)(6,5,*,*,*,5,0,*,*,*,1)(6,5,*,*,*,5,1,*,*,*,0)(6,5,*,*,*,5,2,*,*,*,1)(6,5,*,*,*,5,3,*,*,*,2)(6,5,*,*,*,5,4,*,*,*,3)(6,5,*,*,*,5,5,*,*,*,4)(6,5,*,*,*,5,6,*,*,*,5)(6,6,6,*,*,*,1,0,*,*,*)(6,6,6,*,*,*,2,1,*,*,*)(6,6,6,*,*,*,3,2,*,*,*)(6,6,6,*,*,*,4,3,*,*,*)(6,6,6,*,*,*,5,4,*,*,*)(6,6,6,*,*,*,6,5,*,*,*)(6,6,*,6,*,*,0,*,1,*,*)(6,6,*,6,*,*,1,*,0,*,*)(6,6,*,6,*,*,2,*,1,*,*)(6,6,*,6,*,*,3,*,2,*,*)(6,6,*,6,*,*,4,*,3,*,*)(6,6,*,6,*,*,5,*,4,*,*)(6,6,*,6,*,*,6,*,5,*,*)(6,6,*,*,6,*,1,*,*,0,*)(6,6,*,*,6,*,2,*,*,1,*)(6,6,*,*,6,*,3,*,*,2,*)(6,6,*,*,6,*,4,*,*,3,*)(6,6,*,*,6,*,5,*,*,4,*)(6,6,*,*,6,*,6,*,*,5,*)(6,6,*,*,*,6,0,*,*,*,1)(6,6,*,*,*,6,1,*,*,*,0)(6,6,*,*,*,6,2,*,*,*,1)(6,6,*,*,*,6,3,*,*,*,2)(6,6,*,*,*,6,4,*,*,*,3)(6,6,*,*,*,6,5,*,*,*,4)(6,6,*,*,*,6,6,*,*,*,5)(6,7,7,*,*,*,1,0,*,*,*)(6,7,7,*,*,*,2,1,*,*,*)(6,7,7,*,*,*,3,2,*,*,*)(6,7,7,*,*,*,4,3,*,*,*)(6,7,7,*,*,*,5,4,*,*,*)(6,7,7,*,*,*,6,5,*,*,*)(6,7,*,7,*,*,0,*,1,*,*)(6,7,*,7,*,*,1,*,0,*,*)(6,7,*,7,*,*,2,*,1,*,*)(6,7,*,7,*,*,3,*,2,*,*)(6,7,*,7,*,*,4,*,3,*,*)(6,7,*,7,*,*,5,*,4,*,*)(6,7,*,7,*,*,6,*,5,*,*)(6,7,*,*,7,*,1,*,*,0,*)(6,7,*,*,7,*,2,*,*,1,*)(6,7,*,*,7,*,3,*,*,2,*)(6,7,*,*,7,*,4,*,*,3,*)(6,7,*,*,7,*,5,*,*,4,*)(6,7,*,*,7,*,6,*,*,5,*)(6,7,*,*,*,7,0,*,*,*,1)(6,7,*,*,*,7,1,*,*,*,0)(6,7,*,*,*,7,2,*,*,*,1)(6,7,*,*,*,7,3,*,*,*,2)(6,7,*,*,*,7,4,*,*,*,3)(6,7,*,*,*,7,5,*,*,*,4)(6,7,*,*,*,7,6,*,*,*,5)(6,8,8,*,*,*,1,0,*,*,*)(6,8,8,*,*,*,2,1,*,*,*)(6,8,8,*,*,*,3,2,*,*,*)(6,8,8,*,*,*,4,3,*,*,*)(6,8,8,*,*,*,5,4,*,*,*)(6,8,8,*,*,*,6,5,*,*,*)(6,8,*,8,*,*,0,*,1,*,*)(6,8,*,8,*,*,1,*,0,*,*)(6,8,*,8,*,*,2,*,1,*,*)(6,8,*,8,*,*,3,*,2,*,*)(6,8,*,8,*,*,4,*,3,*,*)(6,8,*,8,*,*,5,*,4,*,*)(6,8,*,8,*,*,6,*,5,*,*)(6,8,*,*,8,*,1,*,*,0,*)(6,8,*,*,8,*,2,*,*,1,*)(6,8,*,*,8,*,3,*,*,2,*)(6,8,*,*,8,*,4,*,*,3,*)(6,8,*,*,8,*,5,*,*,4,*)(6,8,*,*,8,*,6,*,*,5,*)(6,8,*,*,*,8,0,*,*,*,1)(6,8,*,*,*,8,1,*,*,*,0)(6,8,*,*,*,8,2,*,*,*,1)(6,8,*,*,*,8,3,*,*,*,2)(6,8,*,*,*,8,4,*,*,*,3)(6,8,*,*,*,8,5,*,*,*,4)(6,8,*,*,*,8,6,*,*,*,5)(6,9,9,*,*,*,1,0,*,*,*)(6,9,9,*,*,*,2,1,*,*,*)(6,9,9,*,*,*,3,2,*,*,*)(6,9,9,*,*,*,4,3,*,*,*)(6,9,9,*,*,*,5,4,*,*,*)(6,9,9,*,*,*,6,5,*,*,*)(6,9,*,9,*,*,0,*,1,*,*)(6,9,*,9,*,*,1,*,0,*,*)(6,9,*,9,*,*,2,*,1,*,*)(6,9,*,9,*,*,3,*,2,*,*)(6,9,*,9,*,*,4,*,3,*,*)(6,9,*,9,*,*,5,*,4,*,*)(6,9,*,9,*,*,6,*,5,*,*)(6,9,*,*,9,*,1,*,*,0,*)(6,9,*,*,9,*,2,*,*,1,*)(6,9,*,*,9,*,3,*,*,2,*)(6,9,*,*,9,*,4,*,*,3,*)(6,9,*,*,9,*,5,*,*,4,*)(6,9,*,*,9,*,6,*,*,5,*)(6,9,*,*,*,9,0,*,*,*,1)(6,9,*,*,*,9,1,*,*,*,0)(6,9,*,*,*,9,2,*,*,*,1)(6,9,*,*,*,9,3,*,*,*,2)(6,9,*,*,*,9,4,*,*,*,3)(6,9,*,*,*,9,5,*,*,*,4)(6,9,*,*,*,9,6,*,*,*,5) + + y[3][2] x[3][2] x[3][1] x[3][3] x[2][2] x[4][2] d[3][2] d[3][1] d[3][3] d[2][2] d[4][2] + y[4][4] x[4][4] x[4][3] x[4][5] x[3][4] x[5][4] d[4][4] d[4][3] d[4][5] d[3][4] d[5][4] + + + + %... + (1,*,*,*,*,*,0,*,*,*,*) + + y[3][3] x[3][3] x[3][2] x[3][4] x[2][3] x[4][3] d[3][3] d[3][2] d[3][4] d[2][3] d[4][3] + y[3][5] x[3][5] x[3][4] x[3][6] x[2][5] x[4][5] d[3][5] d[3][4] d[3][6] d[2][5] d[4][5] + + + + + + %... + (1,1,0,0)(1,1,1,1)(1,1,2,2)(1,1,3,3)(1,1,4,4)(1,1,5,5)(1,1,6,6)(1,1,7,7)(1,1,8,8)(1,1,9,9)(1,2,0,1)(1,2,0,2)(1,2,0,3)(1,2,0,4)(1,2,0,5)(1,2,0,6)(1,2,0,7)(1,2,0,8)(1,2,0,9)(1,2,1,0)(1,2,1,2)(1,2,1,3)(1,2,1,4)(1,2,1,5)(1,2,1,6)(1,2,1,7)(1,2,1,8)(1,2,1,9)(1,2,2,0)(1,2,2,1)(1,2,2,3)(1,2,2,4)(1,2,2,5)(1,2,2,6)(1,2,2,7)(1,2,2,8)(1,2,2,9)(1,2,3,0)(1,2,3,1)(1,2,3,2)(1,2,3,4)(1,2,3,5)(1,2,3,6)(1,2,3,7)(1,2,3,8)(1,2,3,9)(1,2,4,0)(1,2,4,1)(1,2,4,2)(1,2,4,3)(1,2,4,5)(1,2,4,6)(1,2,4,7)(1,2,4,8)(1,2,4,9)(1,2,5,0)(1,2,5,1)(1,2,5,2)(1,2,5,3)(1,2,5,4)(1,2,5,6)(1,2,5,7)(1,2,5,8)(1,2,5,9)(1,2,6,0)(1,2,6,1)(1,2,6,2)(1,2,6,3)(1,2,6,4)(1,2,6,5)(1,2,6,7)(1,2,6,8)(1,2,6,9)(1,2,7,0)(1,2,7,1)(1,2,7,2)(1,2,7,3)(1,2,7,4)(1,2,7,5)(1,2,7,6)(1,2,7,8)(1,2,7,9)(1,2,8,0)(1,2,8,1)(1,2,8,2)(1,2,8,3)(1,2,8,4)(1,2,8,5)(1,2,8,6)(1,2,8,7)(1,2,8,9)(1,2,9,0)(1,2,9,1)(1,2,9,2)(1,2,9,3)(1,2,9,4)(1,2,9,5)(1,2,9,6)(1,2,9,7)(1,2,9,8)(1,3,0,1)(1,3,0,2)(1,3,0,3)(1,3,0,4)(1,3,0,5)(1,3,0,6)(1,3,0,7)(1,3,0,8)(1,3,0,9)(1,3,1,0)(1,3,1,2)(1,3,1,3)(1,3,1,4)(1,3,1,5)(1,3,1,6)(1,3,1,7)(1,3,1,8)(1,3,1,9)(1,3,2,0)(1,3,2,1)(1,3,2,3)(1,3,2,4)(1,3,2,5)(1,3,2,6)(1,3,2,7)(1,3,2,8)(1,3,2,9)(1,3,3,0)(1,3,3,1)(1,3,3,2)(1,3,3,4)(1,3,3,5)(1,3,3,6)(1,3,3,7)(1,3,3,8)(1,3,3,9)(1,3,4,0)(1,3,4,1)(1,3,4,2)(1,3,4,3)(1,3,4,5)(1,3,4,6)(1,3,4,7)(1,3,4,8)(1,3,4,9)(1,3,5,0)(1,3,5,1)(1,3,5,2)(1,3,5,3)(1,3,5,4)(1,3,5,6)(1,3,5,7)(1,3,5,8)(1,3,5,9)(1,3,6,0)(1,3,6,1)(1,3,6,2)(1,3,6,3)(1,3,6,4)(1,3,6,5)(1,3,6,7)(1,3,6,8)(1,3,6,9)(1,3,7,0)(1,3,7,1)(1,3,7,2)(1,3,7,3)(1,3,7,4)(1,3,7,5)(1,3,7,6)(1,3,7,8)(1,3,7,9)(1,3,8,0)(1,3,8,1)(1,3,8,2)(1,3,8,3)(1,3,8,4)(1,3,8,5)(1,3,8,6)(1,3,8,7)(1,3,8,9)(1,3,9,0)(1,3,9,1)(1,3,9,2)(1,3,9,3)(1,3,9,4)(1,3,9,5)(1,3,9,6)(1,3,9,7)(1,3,9,8)(1,4,0,1)(1,4,0,2)(1,4,0,3)(1,4,0,4)(1,4,0,5)(1,4,0,6)(1,4,0,7)(1,4,0,8)(1,4,0,9)(1,4,1,0)(1,4,1,2)(1,4,1,3)(1,4,1,4)(1,4,1,5)(1,4,1,6)(1,4,1,7)(1,4,1,8)(1,4,1,9)(1,4,2,0)(1,4,2,1)(1,4,2,3)(1,4,2,4)(1,4,2,5)(1,4,2,6)(1,4,2,7)(1,4,2,8)(1,4,2,9)(1,4,3,0)(1,4,3,1)(1,4,3,2)(1,4,3,4)(1,4,3,5)(1,4,3,6)(1,4,3,7)(1,4,3,8)(1,4,3,9)(1,4,4,0)(1,4,4,1)(1,4,4,2)(1,4,4,3)(1,4,4,5)(1,4,4,6)(1,4,4,7)(1,4,4,8)(1,4,4,9)(1,4,5,0)(1,4,5,1)(1,4,5,2)(1,4,5,3)(1,4,5,4)(1,4,5,6)(1,4,5,7)(1,4,5,8)(1,4,5,9)(1,4,6,0)(1,4,6,1)(1,4,6,2)(1,4,6,3)(1,4,6,4)(1,4,6,5)(1,4,6,7)(1,4,6,8)(1,4,6,9)(1,4,7,0)(1,4,7,1)(1,4,7,2)(1,4,7,3)(1,4,7,4)(1,4,7,5)(1,4,7,6)(1,4,7,8)(1,4,7,9)(1,4,8,0)(1,4,8,1)(1,4,8,2)(1,4,8,3)(1,4,8,4)(1,4,8,5)(1,4,8,6)(1,4,8,7)(1,4,8,9)(1,4,9,0)(1,4,9,1)(1,4,9,2)(1,4,9,3)(1,4,9,4)(1,4,9,5)(1,4,9,6)(1,4,9,7)(1,4,9,8)(1,5,0,1)(1,5,0,2)(1,5,0,3)(1,5,0,4)(1,5,0,5)(1,5,0,6)(1,5,0,7)(1,5,0,8)(1,5,0,9)(1,5,1,0)(1,5,1,2)(1,5,1,3)(1,5,1,4)(1,5,1,5)(1,5,1,6)(1,5,1,7)(1,5,1,8)(1,5,1,9)(1,5,2,0)(1,5,2,1)(1,5,2,3)(1,5,2,4)(1,5,2,5)(1,5,2,6)(1,5,2,7)(1,5,2,8)(1,5,2,9)(1,5,3,0)(1,5,3,1)(1,5,3,2)(1,5,3,4)(1,5,3,5)(1,5,3,6)(1,5,3,7)(1,5,3,8)(1,5,3,9)(1,5,4,0)(1,5,4,1)(1,5,4,2)(1,5,4,3)(1,5,4,5)(1,5,4,6)(1,5,4,7)(1,5,4,8)(1,5,4,9)(1,5,5,0)(1,5,5,1)(1,5,5,2)(1,5,5,3)(1,5,5,4)(1,5,5,6)(1,5,5,7)(1,5,5,8)(1,5,5,9)(1,5,6,0)(1,5,6,1)(1,5,6,2)(1,5,6,3)(1,5,6,4)(1,5,6,5)(1,5,6,7)(1,5,6,8)(1,5,6,9)(1,5,7,0)(1,5,7,1)(1,5,7,2)(1,5,7,3)(1,5,7,4)(1,5,7,5)(1,5,7,6)(1,5,7,8)(1,5,7,9)(1,5,8,0)(1,5,8,1)(1,5,8,2)(1,5,8,3)(1,5,8,4)(1,5,8,5)(1,5,8,6)(1,5,8,7)(1,5,8,9)(1,5,9,0)(1,5,9,1)(1,5,9,2)(1,5,9,3)(1,5,9,4)(1,5,9,5)(1,5,9,6)(1,5,9,7)(1,5,9,8)(1,6,0,1)(1,6,0,2)(1,6,0,3)(1,6,0,4)(1,6,0,5)(1,6,0,6)(1,6,0,7)(1,6,0,8)(1,6,0,9)(1,6,1,0)(1,6,1,2)(1,6,1,3)(1,6,1,4)(1,6,1,5)(1,6,1,6)(1,6,1,7)(1,6,1,8)(1,6,1,9)(1,6,2,0)(1,6,2,1)(1,6,2,3)(1,6,2,4)(1,6,2,5)(1,6,2,6)(1,6,2,7)(1,6,2,8)(1,6,2,9)(1,6,3,0)(1,6,3,1)(1,6,3,2)(1,6,3,4)(1,6,3,5)(1,6,3,6)(1,6,3,7)(1,6,3,8)(1,6,3,9)(1,6,4,0)(1,6,4,1)(1,6,4,2)(1,6,4,3)(1,6,4,5)(1,6,4,6)(1,6,4,7)(1,6,4,8)(1,6,4,9)(1,6,5,0)(1,6,5,1)(1,6,5,2)(1,6,5,3)(1,6,5,4)(1,6,5,6)(1,6,5,7)(1,6,5,8)(1,6,5,9)(1,6,6,0)(1,6,6,1)(1,6,6,2)(1,6,6,3)(1,6,6,4)(1,6,6,5)(1,6,6,7)(1,6,6,8)(1,6,6,9)(1,6,7,0)(1,6,7,1)(1,6,7,2)(1,6,7,3)(1,6,7,4)(1,6,7,5)(1,6,7,6)(1,6,7,8)(1,6,7,9)(1,6,8,0)(1,6,8,1)(1,6,8,2)(1,6,8,3)(1,6,8,4)(1,6,8,5)(1,6,8,6)(1,6,8,7)(1,6,8,9)(1,6,9,0)(1,6,9,1)(1,6,9,2)(1,6,9,3)(1,6,9,4)(1,6,9,5)(1,6,9,6)(1,6,9,7)(1,6,9,8)(2,1,0,1)(2,1,0,2)(2,1,0,3)(2,1,0,4)(2,1,0,5)(2,1,0,6)(2,1,0,7)(2,1,0,8)(2,1,0,9)(2,1,1,0)(2,1,1,2)(2,1,1,3)(2,1,1,4)(2,1,1,5)(2,1,1,6)(2,1,1,7)(2,1,1,8)(2,1,1,9)(2,1,2,0)(2,1,2,1)(2,1,2,3)(2,1,2,4)(2,1,2,5)(2,1,2,6)(2,1,2,7)(2,1,2,8)(2,1,2,9)(2,1,3,0)(2,1,3,1)(2,1,3,2)(2,1,3,4)(2,1,3,5)(2,1,3,6)(2,1,3,7)(2,1,3,8)(2,1,3,9)(2,1,4,0)(2,1,4,1)(2,1,4,2)(2,1,4,3)(2,1,4,5)(2,1,4,6)(2,1,4,7)(2,1,4,8)(2,1,4,9)(2,1,5,0)(2,1,5,1)(2,1,5,2)(2,1,5,3)(2,1,5,4)(2,1,5,6)(2,1,5,7)(2,1,5,8)(2,1,5,9)(2,1,6,0)(2,1,6,1)(2,1,6,2)(2,1,6,3)(2,1,6,4)(2,1,6,5)(2,1,6,7)(2,1,6,8)(2,1,6,9)(2,1,7,0)(2,1,7,1)(2,1,7,2)(2,1,7,3)(2,1,7,4)(2,1,7,5)(2,1,7,6)(2,1,7,8)(2,1,7,9)(2,1,8,0)(2,1,8,1)(2,1,8,2)(2,1,8,3)(2,1,8,4)(2,1,8,5)(2,1,8,6)(2,1,8,7)(2,1,8,9)(2,1,9,0)(2,1,9,1)(2,1,9,2)(2,1,9,3)(2,1,9,4)(2,1,9,5)(2,1,9,6)(2,1,9,7)(2,1,9,8)(2,2,0,0)(2,2,1,1)(2,2,2,2)(2,2,3,3)(2,2,4,4)(2,2,5,5)(2,2,6,6)(2,2,7,7)(2,2,8,8)(2,2,9,9)(2,3,0,1)(2,3,0,2)(2,3,0,3)(2,3,0,4)(2,3,0,5)(2,3,0,6)(2,3,0,7)(2,3,0,8)(2,3,0,9)(2,3,1,0)(2,3,1,2)(2,3,1,3)(2,3,1,4)(2,3,1,5)(2,3,1,6)(2,3,1,7)(2,3,1,8)(2,3,1,9)(2,3,2,0)(2,3,2,1)(2,3,2,3)(2,3,2,4)(2,3,2,5)(2,3,2,6)(2,3,2,7)(2,3,2,8)(2,3,2,9)(2,3,3,0)(2,3,3,1)(2,3,3,2)(2,3,3,4)(2,3,3,5)(2,3,3,6)(2,3,3,7)(2,3,3,8)(2,3,3,9)(2,3,4,0)(2,3,4,1)(2,3,4,2)(2,3,4,3)(2,3,4,5)(2,3,4,6)(2,3,4,7)(2,3,4,8)(2,3,4,9)(2,3,5,0)(2,3,5,1)(2,3,5,2)(2,3,5,3)(2,3,5,4)(2,3,5,6)(2,3,5,7)(2,3,5,8)(2,3,5,9)(2,3,6,0)(2,3,6,1)(2,3,6,2)(2,3,6,3)(2,3,6,4)(2,3,6,5)(2,3,6,7)(2,3,6,8)(2,3,6,9)(2,3,7,0)(2,3,7,1)(2,3,7,2)(2,3,7,3)(2,3,7,4)(2,3,7,5)(2,3,7,6)(2,3,7,8)(2,3,7,9)(2,3,8,0)(2,3,8,1)(2,3,8,2)(2,3,8,3)(2,3,8,4)(2,3,8,5)(2,3,8,6)(2,3,8,7)(2,3,8,9)(2,3,9,0)(2,3,9,1)(2,3,9,2)(2,3,9,3)(2,3,9,4)(2,3,9,5)(2,3,9,6)(2,3,9,7)(2,3,9,8)(2,4,0,1)(2,4,0,2)(2,4,0,3)(2,4,0,4)(2,4,0,5)(2,4,0,6)(2,4,0,7)(2,4,0,8)(2,4,0,9)(2,4,1,0)(2,4,1,2)(2,4,1,3)(2,4,1,4)(2,4,1,5)(2,4,1,6)(2,4,1,7)(2,4,1,8)(2,4,1,9)(2,4,2,0)(2,4,2,1)(2,4,2,3)(2,4,2,4)(2,4,2,5)(2,4,2,6)(2,4,2,7)(2,4,2,8)(2,4,2,9)(2,4,3,0)(2,4,3,1)(2,4,3,2)(2,4,3,4)(2,4,3,5)(2,4,3,6)(2,4,3,7)(2,4,3,8)(2,4,3,9)(2,4,4,0)(2,4,4,1)(2,4,4,2)(2,4,4,3)(2,4,4,5)(2,4,4,6)(2,4,4,7)(2,4,4,8)(2,4,4,9)(2,4,5,0)(2,4,5,1)(2,4,5,2)(2,4,5,3)(2,4,5,4)(2,4,5,6)(2,4,5,7)(2,4,5,8)(2,4,5,9)(2,4,6,0)(2,4,6,1)(2,4,6,2)(2,4,6,3)(2,4,6,4)(2,4,6,5)(2,4,6,7)(2,4,6,8)(2,4,6,9)(2,4,7,0)(2,4,7,1)(2,4,7,2)(2,4,7,3)(2,4,7,4)(2,4,7,5)(2,4,7,6)(2,4,7,8)(2,4,7,9)(2,4,8,0)(2,4,8,1)(2,4,8,2)(2,4,8,3)(2,4,8,4)(2,4,8,5)(2,4,8,6)(2,4,8,7)(2,4,8,9)(2,4,9,0)(2,4,9,1)(2,4,9,2)(2,4,9,3)(2,4,9,4)(2,4,9,5)(2,4,9,6)(2,4,9,7)(2,4,9,8)(2,5,0,1)(2,5,0,2)(2,5,0,3)(2,5,0,4)(2,5,0,5)(2,5,0,6)(2,5,0,7)(2,5,0,8)(2,5,0,9)(2,5,1,0)(2,5,1,2)(2,5,1,3)(2,5,1,4)(2,5,1,5)(2,5,1,6)(2,5,1,7)(2,5,1,8)(2,5,1,9)(2,5,2,0)(2,5,2,1)(2,5,2,3)(2,5,2,4)(2,5,2,5)(2,5,2,6)(2,5,2,7)(2,5,2,8)(2,5,2,9)(2,5,3,0)(2,5,3,1)(2,5,3,2)(2,5,3,4)(2,5,3,5)(2,5,3,6)(2,5,3,7)(2,5,3,8)(2,5,3,9)(2,5,4,0)(2,5,4,1)(2,5,4,2)(2,5,4,3)(2,5,4,5)(2,5,4,6)(2,5,4,7)(2,5,4,8)(2,5,4,9)(2,5,5,0)(2,5,5,1)(2,5,5,2)(2,5,5,3)(2,5,5,4)(2,5,5,6)(2,5,5,7)(2,5,5,8)(2,5,5,9)(2,5,6,0)(2,5,6,1)(2,5,6,2)(2,5,6,3)(2,5,6,4)(2,5,6,5)(2,5,6,7)(2,5,6,8)(2,5,6,9)(2,5,7,0)(2,5,7,1)(2,5,7,2)(2,5,7,3)(2,5,7,4)(2,5,7,5)(2,5,7,6)(2,5,7,8)(2,5,7,9)(2,5,8,0)(2,5,8,1)(2,5,8,2)(2,5,8,3)(2,5,8,4)(2,5,8,5)(2,5,8,6)(2,5,8,7)(2,5,8,9)(2,5,9,0)(2,5,9,1)(2,5,9,2)(2,5,9,3)(2,5,9,4)(2,5,9,5)(2,5,9,6)(2,5,9,7)(2,5,9,8)(2,6,0,1)(2,6,0,2)(2,6,0,3)(2,6,0,4)(2,6,0,5)(2,6,0,6)(2,6,0,7)(2,6,0,8)(2,6,0,9)(2,6,1,0)(2,6,1,2)(2,6,1,3)(2,6,1,4)(2,6,1,5)(2,6,1,6)(2,6,1,7)(2,6,1,8)(2,6,1,9)(2,6,2,0)(2,6,2,1)(2,6,2,3)(2,6,2,4)(2,6,2,5)(2,6,2,6)(2,6,2,7)(2,6,2,8)(2,6,2,9)(2,6,3,0)(2,6,3,1)(2,6,3,2)(2,6,3,4)(2,6,3,5)(2,6,3,6)(2,6,3,7)(2,6,3,8)(2,6,3,9)(2,6,4,0)(2,6,4,1)(2,6,4,2)(2,6,4,3)(2,6,4,5)(2,6,4,6)(2,6,4,7)(2,6,4,8)(2,6,4,9)(2,6,5,0)(2,6,5,1)(2,6,5,2)(2,6,5,3)(2,6,5,4)(2,6,5,6)(2,6,5,7)(2,6,5,8)(2,6,5,9)(2,6,6,0)(2,6,6,1)(2,6,6,2)(2,6,6,3)(2,6,6,4)(2,6,6,5)(2,6,6,7)(2,6,6,8)(2,6,6,9)(2,6,7,0)(2,6,7,1)(2,6,7,2)(2,6,7,3)(2,6,7,4)(2,6,7,5)(2,6,7,6)(2,6,7,8)(2,6,7,9)(2,6,8,0)(2,6,8,1)(2,6,8,2)(2,6,8,3)(2,6,8,4)(2,6,8,5)(2,6,8,6)(2,6,8,7)(2,6,8,9)(2,6,9,0)(2,6,9,1)(2,6,9,2)(2,6,9,3)(2,6,9,4)(2,6,9,5)(2,6,9,6)(2,6,9,7)(2,6,9,8)(3,1,0,1)(3,1,0,2)(3,1,0,3)(3,1,0,4)(3,1,0,5)(3,1,0,6)(3,1,0,7)(3,1,0,8)(3,1,0,9)(3,1,1,0)(3,1,1,2)(3,1,1,3)(3,1,1,4)(3,1,1,5)(3,1,1,6)(3,1,1,7)(3,1,1,8)(3,1,1,9)(3,1,2,0)(3,1,2,1)(3,1,2,3)(3,1,2,4)(3,1,2,5)(3,1,2,6)(3,1,2,7)(3,1,2,8)(3,1,2,9)(3,1,3,0)(3,1,3,1)(3,1,3,2)(3,1,3,4)(3,1,3,5)(3,1,3,6)(3,1,3,7)(3,1,3,8)(3,1,3,9)(3,1,4,0)(3,1,4,1)(3,1,4,2)(3,1,4,3)(3,1,4,5)(3,1,4,6)(3,1,4,7)(3,1,4,8)(3,1,4,9)(3,1,5,0)(3,1,5,1)(3,1,5,2)(3,1,5,3)(3,1,5,4)(3,1,5,6)(3,1,5,7)(3,1,5,8)(3,1,5,9)(3,1,6,0)(3,1,6,1)(3,1,6,2)(3,1,6,3)(3,1,6,4)(3,1,6,5)(3,1,6,7)(3,1,6,8)(3,1,6,9)(3,1,7,0)(3,1,7,1)(3,1,7,2)(3,1,7,3)(3,1,7,4)(3,1,7,5)(3,1,7,6)(3,1,7,8)(3,1,7,9)(3,1,8,0)(3,1,8,1)(3,1,8,2)(3,1,8,3)(3,1,8,4)(3,1,8,5)(3,1,8,6)(3,1,8,7)(3,1,8,9)(3,1,9,0)(3,1,9,1)(3,1,9,2)(3,1,9,3)(3,1,9,4)(3,1,9,5)(3,1,9,6)(3,1,9,7)(3,1,9,8)(3,2,0,1)(3,2,0,2)(3,2,0,3)(3,2,0,4)(3,2,0,5)(3,2,0,6)(3,2,0,7)(3,2,0,8)(3,2,0,9)(3,2,1,0)(3,2,1,2)(3,2,1,3)(3,2,1,4)(3,2,1,5)(3,2,1,6)(3,2,1,7)(3,2,1,8)(3,2,1,9)(3,2,2,0)(3,2,2,1)(3,2,2,3)(3,2,2,4)(3,2,2,5)(3,2,2,6)(3,2,2,7)(3,2,2,8)(3,2,2,9)(3,2,3,0)(3,2,3,1)(3,2,3,2)(3,2,3,4)(3,2,3,5)(3,2,3,6)(3,2,3,7)(3,2,3,8)(3,2,3,9)(3,2,4,0)(3,2,4,1)(3,2,4,2)(3,2,4,3)(3,2,4,5)(3,2,4,6)(3,2,4,7)(3,2,4,8)(3,2,4,9)(3,2,5,0)(3,2,5,1)(3,2,5,2)(3,2,5,3)(3,2,5,4)(3,2,5,6)(3,2,5,7)(3,2,5,8)(3,2,5,9)(3,2,6,0)(3,2,6,1)(3,2,6,2)(3,2,6,3)(3,2,6,4)(3,2,6,5)(3,2,6,7)(3,2,6,8)(3,2,6,9)(3,2,7,0)(3,2,7,1)(3,2,7,2)(3,2,7,3)(3,2,7,4)(3,2,7,5)(3,2,7,6)(3,2,7,8)(3,2,7,9)(3,2,8,0)(3,2,8,1)(3,2,8,2)(3,2,8,3)(3,2,8,4)(3,2,8,5)(3,2,8,6)(3,2,8,7)(3,2,8,9)(3,2,9,0)(3,2,9,1)(3,2,9,2)(3,2,9,3)(3,2,9,4)(3,2,9,5)(3,2,9,6)(3,2,9,7)(3,2,9,8)(3,3,0,0)(3,3,1,1)(3,3,2,2)(3,3,3,3)(3,3,4,4)(3,3,5,5)(3,3,6,6)(3,3,7,7)(3,3,8,8)(3,3,9,9)(3,4,0,1)(3,4,0,2)(3,4,0,3)(3,4,0,4)(3,4,0,5)(3,4,0,6)(3,4,0,7)(3,4,0,8)(3,4,0,9)(3,4,1,0)(3,4,1,2)(3,4,1,3)(3,4,1,4)(3,4,1,5)(3,4,1,6)(3,4,1,7)(3,4,1,8)(3,4,1,9)(3,4,2,0)(3,4,2,1)(3,4,2,3)(3,4,2,4)(3,4,2,5)(3,4,2,6)(3,4,2,7)(3,4,2,8)(3,4,2,9)(3,4,3,0)(3,4,3,1)(3,4,3,2)(3,4,3,4)(3,4,3,5)(3,4,3,6)(3,4,3,7)(3,4,3,8)(3,4,3,9)(3,4,4,0)(3,4,4,1)(3,4,4,2)(3,4,4,3)(3,4,4,5)(3,4,4,6)(3,4,4,7)(3,4,4,8)(3,4,4,9)(3,4,5,0)(3,4,5,1)(3,4,5,2)(3,4,5,3)(3,4,5,4)(3,4,5,6)(3,4,5,7)(3,4,5,8)(3,4,5,9)(3,4,6,0)(3,4,6,1)(3,4,6,2)(3,4,6,3)(3,4,6,4)(3,4,6,5)(3,4,6,7)(3,4,6,8)(3,4,6,9)(3,4,7,0)(3,4,7,1)(3,4,7,2)(3,4,7,3)(3,4,7,4)(3,4,7,5)(3,4,7,6)(3,4,7,8)(3,4,7,9)(3,4,8,0)(3,4,8,1)(3,4,8,2)(3,4,8,3)(3,4,8,4)(3,4,8,5)(3,4,8,6)(3,4,8,7)(3,4,8,9)(3,4,9,0)(3,4,9,1)(3,4,9,2)(3,4,9,3)(3,4,9,4)(3,4,9,5)(3,4,9,6)(3,4,9,7)(3,4,9,8)(3,5,0,1)(3,5,0,2)(3,5,0,3)(3,5,0,4)(3,5,0,5)(3,5,0,6)(3,5,0,7)(3,5,0,8)(3,5,0,9)(3,5,1,0)(3,5,1,2)(3,5,1,3)(3,5,1,4)(3,5,1,5)(3,5,1,6)(3,5,1,7)(3,5,1,8)(3,5,1,9)(3,5,2,0)(3,5,2,1)(3,5,2,3)(3,5,2,4)(3,5,2,5)(3,5,2,6)(3,5,2,7)(3,5,2,8)(3,5,2,9)(3,5,3,0)(3,5,3,1)(3,5,3,2)(3,5,3,4)(3,5,3,5)(3,5,3,6)(3,5,3,7)(3,5,3,8)(3,5,3,9)(3,5,4,0)(3,5,4,1)(3,5,4,2)(3,5,4,3)(3,5,4,5)(3,5,4,6)(3,5,4,7)(3,5,4,8)(3,5,4,9)(3,5,5,0)(3,5,5,1)(3,5,5,2)(3,5,5,3)(3,5,5,4)(3,5,5,6)(3,5,5,7)(3,5,5,8)(3,5,5,9)(3,5,6,0)(3,5,6,1)(3,5,6,2)(3,5,6,3)(3,5,6,4)(3,5,6,5)(3,5,6,7)(3,5,6,8)(3,5,6,9)(3,5,7,0)(3,5,7,1)(3,5,7,2)(3,5,7,3)(3,5,7,4)(3,5,7,5)(3,5,7,6)(3,5,7,8)(3,5,7,9)(3,5,8,0)(3,5,8,1)(3,5,8,2)(3,5,8,3)(3,5,8,4)(3,5,8,5)(3,5,8,6)(3,5,8,7)(3,5,8,9)(3,5,9,0)(3,5,9,1)(3,5,9,2)(3,5,9,3)(3,5,9,4)(3,5,9,5)(3,5,9,6)(3,5,9,7)(3,5,9,8)(3,6,0,1)(3,6,0,2)(3,6,0,3)(3,6,0,4)(3,6,0,5)(3,6,0,6)(3,6,0,7)(3,6,0,8)(3,6,0,9)(3,6,1,0)(3,6,1,2)(3,6,1,3)(3,6,1,4)(3,6,1,5)(3,6,1,6)(3,6,1,7)(3,6,1,8)(3,6,1,9)(3,6,2,0)(3,6,2,1)(3,6,2,3)(3,6,2,4)(3,6,2,5)(3,6,2,6)(3,6,2,7)(3,6,2,8)(3,6,2,9)(3,6,3,0)(3,6,3,1)(3,6,3,2)(3,6,3,4)(3,6,3,5)(3,6,3,6)(3,6,3,7)(3,6,3,8)(3,6,3,9)(3,6,4,0)(3,6,4,1)(3,6,4,2)(3,6,4,3)(3,6,4,5)(3,6,4,6)(3,6,4,7)(3,6,4,8)(3,6,4,9)(3,6,5,0)(3,6,5,1)(3,6,5,2)(3,6,5,3)(3,6,5,4)(3,6,5,6)(3,6,5,7)(3,6,5,8)(3,6,5,9)(3,6,6,0)(3,6,6,1)(3,6,6,2)(3,6,6,3)(3,6,6,4)(3,6,6,5)(3,6,6,7)(3,6,6,8)(3,6,6,9)(3,6,7,0)(3,6,7,1)(3,6,7,2)(3,6,7,3)(3,6,7,4)(3,6,7,5)(3,6,7,6)(3,6,7,8)(3,6,7,9)(3,6,8,0)(3,6,8,1)(3,6,8,2)(3,6,8,3)(3,6,8,4)(3,6,8,5)(3,6,8,6)(3,6,8,7)(3,6,8,9)(3,6,9,0)(3,6,9,1)(3,6,9,2)(3,6,9,3)(3,6,9,4)(3,6,9,5)(3,6,9,6)(3,6,9,7)(3,6,9,8)(4,1,0,1)(4,1,0,2)(4,1,0,3)(4,1,0,4)(4,1,0,5)(4,1,0,6)(4,1,0,7)(4,1,0,8)(4,1,0,9)(4,1,1,0)(4,1,1,2)(4,1,1,3)(4,1,1,4)(4,1,1,5)(4,1,1,6)(4,1,1,7)(4,1,1,8)(4,1,1,9)(4,1,2,0)(4,1,2,1)(4,1,2,3)(4,1,2,4)(4,1,2,5)(4,1,2,6)(4,1,2,7)(4,1,2,8)(4,1,2,9)(4,1,3,0)(4,1,3,1)(4,1,3,2)(4,1,3,4)(4,1,3,5)(4,1,3,6)(4,1,3,7)(4,1,3,8)(4,1,3,9)(4,1,4,0)(4,1,4,1)(4,1,4,2)(4,1,4,3)(4,1,4,5)(4,1,4,6)(4,1,4,7)(4,1,4,8)(4,1,4,9)(4,1,5,0)(4,1,5,1)(4,1,5,2)(4,1,5,3)(4,1,5,4)(4,1,5,6)(4,1,5,7)(4,1,5,8)(4,1,5,9)(4,1,6,0)(4,1,6,1)(4,1,6,2)(4,1,6,3)(4,1,6,4)(4,1,6,5)(4,1,6,7)(4,1,6,8)(4,1,6,9)(4,1,7,0)(4,1,7,1)(4,1,7,2)(4,1,7,3)(4,1,7,4)(4,1,7,5)(4,1,7,6)(4,1,7,8)(4,1,7,9)(4,1,8,0)(4,1,8,1)(4,1,8,2)(4,1,8,3)(4,1,8,4)(4,1,8,5)(4,1,8,6)(4,1,8,7)(4,1,8,9)(4,1,9,0)(4,1,9,1)(4,1,9,2)(4,1,9,3)(4,1,9,4)(4,1,9,5)(4,1,9,6)(4,1,9,7)(4,1,9,8)(4,2,0,1)(4,2,0,2)(4,2,0,3)(4,2,0,4)(4,2,0,5)(4,2,0,6)(4,2,0,7)(4,2,0,8)(4,2,0,9)(4,2,1,0)(4,2,1,2)(4,2,1,3)(4,2,1,4)(4,2,1,5)(4,2,1,6)(4,2,1,7)(4,2,1,8)(4,2,1,9)(4,2,2,0)(4,2,2,1)(4,2,2,3)(4,2,2,4)(4,2,2,5)(4,2,2,6)(4,2,2,7)(4,2,2,8)(4,2,2,9)(4,2,3,0)(4,2,3,1)(4,2,3,2)(4,2,3,4)(4,2,3,5)(4,2,3,6)(4,2,3,7)(4,2,3,8)(4,2,3,9)(4,2,4,0)(4,2,4,1)(4,2,4,2)(4,2,4,3)(4,2,4,5)(4,2,4,6)(4,2,4,7)(4,2,4,8)(4,2,4,9)(4,2,5,0)(4,2,5,1)(4,2,5,2)(4,2,5,3)(4,2,5,4)(4,2,5,6)(4,2,5,7)(4,2,5,8)(4,2,5,9)(4,2,6,0)(4,2,6,1)(4,2,6,2)(4,2,6,3)(4,2,6,4)(4,2,6,5)(4,2,6,7)(4,2,6,8)(4,2,6,9)(4,2,7,0)(4,2,7,1)(4,2,7,2)(4,2,7,3)(4,2,7,4)(4,2,7,5)(4,2,7,6)(4,2,7,8)(4,2,7,9)(4,2,8,0)(4,2,8,1)(4,2,8,2)(4,2,8,3)(4,2,8,4)(4,2,8,5)(4,2,8,6)(4,2,8,7)(4,2,8,9)(4,2,9,0)(4,2,9,1)(4,2,9,2)(4,2,9,3)(4,2,9,4)(4,2,9,5)(4,2,9,6)(4,2,9,7)(4,2,9,8)(4,3,0,1)(4,3,0,2)(4,3,0,3)(4,3,0,4)(4,3,0,5)(4,3,0,6)(4,3,0,7)(4,3,0,8)(4,3,0,9)(4,3,1,0)(4,3,1,2)(4,3,1,3)(4,3,1,4)(4,3,1,5)(4,3,1,6)(4,3,1,7)(4,3,1,8)(4,3,1,9)(4,3,2,0)(4,3,2,1)(4,3,2,3)(4,3,2,4)(4,3,2,5)(4,3,2,6)(4,3,2,7)(4,3,2,8)(4,3,2,9)(4,3,3,0)(4,3,3,1)(4,3,3,2)(4,3,3,4)(4,3,3,5)(4,3,3,6)(4,3,3,7)(4,3,3,8)(4,3,3,9)(4,3,4,0)(4,3,4,1)(4,3,4,2)(4,3,4,3)(4,3,4,5)(4,3,4,6)(4,3,4,7)(4,3,4,8)(4,3,4,9)(4,3,5,0)(4,3,5,1)(4,3,5,2)(4,3,5,3)(4,3,5,4)(4,3,5,6)(4,3,5,7)(4,3,5,8)(4,3,5,9)(4,3,6,0)(4,3,6,1)(4,3,6,2)(4,3,6,3)(4,3,6,4)(4,3,6,5)(4,3,6,7)(4,3,6,8)(4,3,6,9)(4,3,7,0)(4,3,7,1)(4,3,7,2)(4,3,7,3)(4,3,7,4)(4,3,7,5)(4,3,7,6)(4,3,7,8)(4,3,7,9)(4,3,8,0)(4,3,8,1)(4,3,8,2)(4,3,8,3)(4,3,8,4)(4,3,8,5)(4,3,8,6)(4,3,8,7)(4,3,8,9)(4,3,9,0)(4,3,9,1)(4,3,9,2)(4,3,9,3)(4,3,9,4)(4,3,9,5)(4,3,9,6)(4,3,9,7)(4,3,9,8)(4,4,0,0)(4,4,1,1)(4,4,2,2)(4,4,3,3)(4,4,4,4)(4,4,5,5)(4,4,6,6)(4,4,7,7)(4,4,8,8)(4,4,9,9)(4,5,0,1)(4,5,0,2)(4,5,0,3)(4,5,0,4)(4,5,0,5)(4,5,0,6)(4,5,0,7)(4,5,0,8)(4,5,0,9)(4,5,1,0)(4,5,1,2)(4,5,1,3)(4,5,1,4)(4,5,1,5)(4,5,1,6)(4,5,1,7)(4,5,1,8)(4,5,1,9)(4,5,2,0)(4,5,2,1)(4,5,2,3)(4,5,2,4)(4,5,2,5)(4,5,2,6)(4,5,2,7)(4,5,2,8)(4,5,2,9)(4,5,3,0)(4,5,3,1)(4,5,3,2)(4,5,3,4)(4,5,3,5)(4,5,3,6)(4,5,3,7)(4,5,3,8)(4,5,3,9)(4,5,4,0)(4,5,4,1)(4,5,4,2)(4,5,4,3)(4,5,4,5)(4,5,4,6)(4,5,4,7)(4,5,4,8)(4,5,4,9)(4,5,5,0)(4,5,5,1)(4,5,5,2)(4,5,5,3)(4,5,5,4)(4,5,5,6)(4,5,5,7)(4,5,5,8)(4,5,5,9)(4,5,6,0)(4,5,6,1)(4,5,6,2)(4,5,6,3)(4,5,6,4)(4,5,6,5)(4,5,6,7)(4,5,6,8)(4,5,6,9)(4,5,7,0)(4,5,7,1)(4,5,7,2)(4,5,7,3)(4,5,7,4)(4,5,7,5)(4,5,7,6)(4,5,7,8)(4,5,7,9)(4,5,8,0)(4,5,8,1)(4,5,8,2)(4,5,8,3)(4,5,8,4)(4,5,8,5)(4,5,8,6)(4,5,8,7)(4,5,8,9)(4,5,9,0)(4,5,9,1)(4,5,9,2)(4,5,9,3)(4,5,9,4)(4,5,9,5)(4,5,9,6)(4,5,9,7)(4,5,9,8)(4,6,0,1)(4,6,0,2)(4,6,0,3)(4,6,0,4)(4,6,0,5)(4,6,0,6)(4,6,0,7)(4,6,0,8)(4,6,0,9)(4,6,1,0)(4,6,1,2)(4,6,1,3)(4,6,1,4)(4,6,1,5)(4,6,1,6)(4,6,1,7)(4,6,1,8)(4,6,1,9)(4,6,2,0)(4,6,2,1)(4,6,2,3)(4,6,2,4)(4,6,2,5)(4,6,2,6)(4,6,2,7)(4,6,2,8)(4,6,2,9)(4,6,3,0)(4,6,3,1)(4,6,3,2)(4,6,3,4)(4,6,3,5)(4,6,3,6)(4,6,3,7)(4,6,3,8)(4,6,3,9)(4,6,4,0)(4,6,4,1)(4,6,4,2)(4,6,4,3)(4,6,4,5)(4,6,4,6)(4,6,4,7)(4,6,4,8)(4,6,4,9)(4,6,5,0)(4,6,5,1)(4,6,5,2)(4,6,5,3)(4,6,5,4)(4,6,5,6)(4,6,5,7)(4,6,5,8)(4,6,5,9)(4,6,6,0)(4,6,6,1)(4,6,6,2)(4,6,6,3)(4,6,6,4)(4,6,6,5)(4,6,6,7)(4,6,6,8)(4,6,6,9)(4,6,7,0)(4,6,7,1)(4,6,7,2)(4,6,7,3)(4,6,7,4)(4,6,7,5)(4,6,7,6)(4,6,7,8)(4,6,7,9)(4,6,8,0)(4,6,8,1)(4,6,8,2)(4,6,8,3)(4,6,8,4)(4,6,8,5)(4,6,8,6)(4,6,8,7)(4,6,8,9)(4,6,9,0)(4,6,9,1)(4,6,9,2)(4,6,9,3)(4,6,9,4)(4,6,9,5)(4,6,9,6)(4,6,9,7)(4,6,9,8)(5,1,0,1)(5,1,0,2)(5,1,0,3)(5,1,0,4)(5,1,0,5)(5,1,0,6)(5,1,0,7)(5,1,0,8)(5,1,0,9)(5,1,1,0)(5,1,1,2)(5,1,1,3)(5,1,1,4)(5,1,1,5)(5,1,1,6)(5,1,1,7)(5,1,1,8)(5,1,1,9)(5,1,2,0)(5,1,2,1)(5,1,2,3)(5,1,2,4)(5,1,2,5)(5,1,2,6)(5,1,2,7)(5,1,2,8)(5,1,2,9)(5,1,3,0)(5,1,3,1)(5,1,3,2)(5,1,3,4)(5,1,3,5)(5,1,3,6)(5,1,3,7)(5,1,3,8)(5,1,3,9)(5,1,4,0)(5,1,4,1)(5,1,4,2)(5,1,4,3)(5,1,4,5)(5,1,4,6)(5,1,4,7)(5,1,4,8)(5,1,4,9)(5,1,5,0)(5,1,5,1)(5,1,5,2)(5,1,5,3)(5,1,5,4)(5,1,5,6)(5,1,5,7)(5,1,5,8)(5,1,5,9)(5,1,6,0)(5,1,6,1)(5,1,6,2)(5,1,6,3)(5,1,6,4)(5,1,6,5)(5,1,6,7)(5,1,6,8)(5,1,6,9)(5,1,7,0)(5,1,7,1)(5,1,7,2)(5,1,7,3)(5,1,7,4)(5,1,7,5)(5,1,7,6)(5,1,7,8)(5,1,7,9)(5,1,8,0)(5,1,8,1)(5,1,8,2)(5,1,8,3)(5,1,8,4)(5,1,8,5)(5,1,8,6)(5,1,8,7)(5,1,8,9)(5,1,9,0)(5,1,9,1)(5,1,9,2)(5,1,9,3)(5,1,9,4)(5,1,9,5)(5,1,9,6)(5,1,9,7)(5,1,9,8)(5,2,0,1)(5,2,0,2)(5,2,0,3)(5,2,0,4)(5,2,0,5)(5,2,0,6)(5,2,0,7)(5,2,0,8)(5,2,0,9)(5,2,1,0)(5,2,1,2)(5,2,1,3)(5,2,1,4)(5,2,1,5)(5,2,1,6)(5,2,1,7)(5,2,1,8)(5,2,1,9)(5,2,2,0)(5,2,2,1)(5,2,2,3)(5,2,2,4)(5,2,2,5)(5,2,2,6)(5,2,2,7)(5,2,2,8)(5,2,2,9)(5,2,3,0)(5,2,3,1)(5,2,3,2)(5,2,3,4)(5,2,3,5)(5,2,3,6)(5,2,3,7)(5,2,3,8)(5,2,3,9)(5,2,4,0)(5,2,4,1)(5,2,4,2)(5,2,4,3)(5,2,4,5)(5,2,4,6)(5,2,4,7)(5,2,4,8)(5,2,4,9)(5,2,5,0)(5,2,5,1)(5,2,5,2)(5,2,5,3)(5,2,5,4)(5,2,5,6)(5,2,5,7)(5,2,5,8)(5,2,5,9)(5,2,6,0)(5,2,6,1)(5,2,6,2)(5,2,6,3)(5,2,6,4)(5,2,6,5)(5,2,6,7)(5,2,6,8)(5,2,6,9)(5,2,7,0)(5,2,7,1)(5,2,7,2)(5,2,7,3)(5,2,7,4)(5,2,7,5)(5,2,7,6)(5,2,7,8)(5,2,7,9)(5,2,8,0)(5,2,8,1)(5,2,8,2)(5,2,8,3)(5,2,8,4)(5,2,8,5)(5,2,8,6)(5,2,8,7)(5,2,8,9)(5,2,9,0)(5,2,9,1)(5,2,9,2)(5,2,9,3)(5,2,9,4)(5,2,9,5)(5,2,9,6)(5,2,9,7)(5,2,9,8)(5,3,0,1)(5,3,0,2)(5,3,0,3)(5,3,0,4)(5,3,0,5)(5,3,0,6)(5,3,0,7)(5,3,0,8)(5,3,0,9)(5,3,1,0)(5,3,1,2)(5,3,1,3)(5,3,1,4)(5,3,1,5)(5,3,1,6)(5,3,1,7)(5,3,1,8)(5,3,1,9)(5,3,2,0)(5,3,2,1)(5,3,2,3)(5,3,2,4)(5,3,2,5)(5,3,2,6)(5,3,2,7)(5,3,2,8)(5,3,2,9)(5,3,3,0)(5,3,3,1)(5,3,3,2)(5,3,3,4)(5,3,3,5)(5,3,3,6)(5,3,3,7)(5,3,3,8)(5,3,3,9)(5,3,4,0)(5,3,4,1)(5,3,4,2)(5,3,4,3)(5,3,4,5)(5,3,4,6)(5,3,4,7)(5,3,4,8)(5,3,4,9)(5,3,5,0)(5,3,5,1)(5,3,5,2)(5,3,5,3)(5,3,5,4)(5,3,5,6)(5,3,5,7)(5,3,5,8)(5,3,5,9)(5,3,6,0)(5,3,6,1)(5,3,6,2)(5,3,6,3)(5,3,6,4)(5,3,6,5)(5,3,6,7)(5,3,6,8)(5,3,6,9)(5,3,7,0)(5,3,7,1)(5,3,7,2)(5,3,7,3)(5,3,7,4)(5,3,7,5)(5,3,7,6)(5,3,7,8)(5,3,7,9)(5,3,8,0)(5,3,8,1)(5,3,8,2)(5,3,8,3)(5,3,8,4)(5,3,8,5)(5,3,8,6)(5,3,8,7)(5,3,8,9)(5,3,9,0)(5,3,9,1)(5,3,9,2)(5,3,9,3)(5,3,9,4)(5,3,9,5)(5,3,9,6)(5,3,9,7)(5,3,9,8)(5,4,0,1)(5,4,0,2)(5,4,0,3)(5,4,0,4)(5,4,0,5)(5,4,0,6)(5,4,0,7)(5,4,0,8)(5,4,0,9)(5,4,1,0)(5,4,1,2)(5,4,1,3)(5,4,1,4)(5,4,1,5)(5,4,1,6)(5,4,1,7)(5,4,1,8)(5,4,1,9)(5,4,2,0)(5,4,2,1)(5,4,2,3)(5,4,2,4)(5,4,2,5)(5,4,2,6)(5,4,2,7)(5,4,2,8)(5,4,2,9)(5,4,3,0)(5,4,3,1)(5,4,3,2)(5,4,3,4)(5,4,3,5)(5,4,3,6)(5,4,3,7)(5,4,3,8)(5,4,3,9)(5,4,4,0)(5,4,4,1)(5,4,4,2)(5,4,4,3)(5,4,4,5)(5,4,4,6)(5,4,4,7)(5,4,4,8)(5,4,4,9)(5,4,5,0)(5,4,5,1)(5,4,5,2)(5,4,5,3)(5,4,5,4)(5,4,5,6)(5,4,5,7)(5,4,5,8)(5,4,5,9)(5,4,6,0)(5,4,6,1)(5,4,6,2)(5,4,6,3)(5,4,6,4)(5,4,6,5)(5,4,6,7)(5,4,6,8)(5,4,6,9)(5,4,7,0)(5,4,7,1)(5,4,7,2)(5,4,7,3)(5,4,7,4)(5,4,7,5)(5,4,7,6)(5,4,7,8)(5,4,7,9)(5,4,8,0)(5,4,8,1)(5,4,8,2)(5,4,8,3)(5,4,8,4)(5,4,8,5)(5,4,8,6)(5,4,8,7)(5,4,8,9)(5,4,9,0)(5,4,9,1)(5,4,9,2)(5,4,9,3)(5,4,9,4)(5,4,9,5)(5,4,9,6)(5,4,9,7)(5,4,9,8)(5,5,0,0)(5,5,1,1)(5,5,2,2)(5,5,3,3)(5,5,4,4)(5,5,5,5)(5,5,6,6)(5,5,7,7)(5,5,8,8)(5,5,9,9)(5,6,0,1)(5,6,0,2)(5,6,0,3)(5,6,0,4)(5,6,0,5)(5,6,0,6)(5,6,0,7)(5,6,0,8)(5,6,0,9)(5,6,1,0)(5,6,1,2)(5,6,1,3)(5,6,1,4)(5,6,1,5)(5,6,1,6)(5,6,1,7)(5,6,1,8)(5,6,1,9)(5,6,2,0)(5,6,2,1)(5,6,2,3)(5,6,2,4)(5,6,2,5)(5,6,2,6)(5,6,2,7)(5,6,2,8)(5,6,2,9)(5,6,3,0)(5,6,3,1)(5,6,3,2)(5,6,3,4)(5,6,3,5)(5,6,3,6)(5,6,3,7)(5,6,3,8)(5,6,3,9)(5,6,4,0)(5,6,4,1)(5,6,4,2)(5,6,4,3)(5,6,4,5)(5,6,4,6)(5,6,4,7)(5,6,4,8)(5,6,4,9)(5,6,5,0)(5,6,5,1)(5,6,5,2)(5,6,5,3)(5,6,5,4)(5,6,5,6)(5,6,5,7)(5,6,5,8)(5,6,5,9)(5,6,6,0)(5,6,6,1)(5,6,6,2)(5,6,6,3)(5,6,6,4)(5,6,6,5)(5,6,6,7)(5,6,6,8)(5,6,6,9)(5,6,7,0)(5,6,7,1)(5,6,7,2)(5,6,7,3)(5,6,7,4)(5,6,7,5)(5,6,7,6)(5,6,7,8)(5,6,7,9)(5,6,8,0)(5,6,8,1)(5,6,8,2)(5,6,8,3)(5,6,8,4)(5,6,8,5)(5,6,8,6)(5,6,8,7)(5,6,8,9)(5,6,9,0)(5,6,9,1)(5,6,9,2)(5,6,9,3)(5,6,9,4)(5,6,9,5)(5,6,9,6)(5,6,9,7)(5,6,9,8)(6,1,0,1)(6,1,0,2)(6,1,0,3)(6,1,0,4)(6,1,0,5)(6,1,0,6)(6,1,0,7)(6,1,0,8)(6,1,0,9)(6,1,1,0)(6,1,1,2)(6,1,1,3)(6,1,1,4)(6,1,1,5)(6,1,1,6)(6,1,1,7)(6,1,1,8)(6,1,1,9)(6,1,2,0)(6,1,2,1)(6,1,2,3)(6,1,2,4)(6,1,2,5)(6,1,2,6)(6,1,2,7)(6,1,2,8)(6,1,2,9)(6,1,3,0)(6,1,3,1)(6,1,3,2)(6,1,3,4)(6,1,3,5)(6,1,3,6)(6,1,3,7)(6,1,3,8)(6,1,3,9)(6,1,4,0)(6,1,4,1)(6,1,4,2)(6,1,4,3)(6,1,4,5)(6,1,4,6)(6,1,4,7)(6,1,4,8)(6,1,4,9)(6,1,5,0)(6,1,5,1)(6,1,5,2)(6,1,5,3)(6,1,5,4)(6,1,5,6)(6,1,5,7)(6,1,5,8)(6,1,5,9)(6,1,6,0)(6,1,6,1)(6,1,6,2)(6,1,6,3)(6,1,6,4)(6,1,6,5)(6,1,6,7)(6,1,6,8)(6,1,6,9)(6,1,7,0)(6,1,7,1)(6,1,7,2)(6,1,7,3)(6,1,7,4)(6,1,7,5)(6,1,7,6)(6,1,7,8)(6,1,7,9)(6,1,8,0)(6,1,8,1)(6,1,8,2)(6,1,8,3)(6,1,8,4)(6,1,8,5)(6,1,8,6)(6,1,8,7)(6,1,8,9)(6,1,9,0)(6,1,9,1)(6,1,9,2)(6,1,9,3)(6,1,9,4)(6,1,9,5)(6,1,9,6)(6,1,9,7)(6,1,9,8)(6,2,0,1)(6,2,0,2)(6,2,0,3)(6,2,0,4)(6,2,0,5)(6,2,0,6)(6,2,0,7)(6,2,0,8)(6,2,0,9)(6,2,1,0)(6,2,1,2)(6,2,1,3)(6,2,1,4)(6,2,1,5)(6,2,1,6)(6,2,1,7)(6,2,1,8)(6,2,1,9)(6,2,2,0)(6,2,2,1)(6,2,2,3)(6,2,2,4)(6,2,2,5)(6,2,2,6)(6,2,2,7)(6,2,2,8)(6,2,2,9)(6,2,3,0)(6,2,3,1)(6,2,3,2)(6,2,3,4)(6,2,3,5)(6,2,3,6)(6,2,3,7)(6,2,3,8)(6,2,3,9)(6,2,4,0)(6,2,4,1)(6,2,4,2)(6,2,4,3)(6,2,4,5)(6,2,4,6)(6,2,4,7)(6,2,4,8)(6,2,4,9)(6,2,5,0)(6,2,5,1)(6,2,5,2)(6,2,5,3)(6,2,5,4)(6,2,5,6)(6,2,5,7)(6,2,5,8)(6,2,5,9)(6,2,6,0)(6,2,6,1)(6,2,6,2)(6,2,6,3)(6,2,6,4)(6,2,6,5)(6,2,6,7)(6,2,6,8)(6,2,6,9)(6,2,7,0)(6,2,7,1)(6,2,7,2)(6,2,7,3)(6,2,7,4)(6,2,7,5)(6,2,7,6)(6,2,7,8)(6,2,7,9)(6,2,8,0)(6,2,8,1)(6,2,8,2)(6,2,8,3)(6,2,8,4)(6,2,8,5)(6,2,8,6)(6,2,8,7)(6,2,8,9)(6,2,9,0)(6,2,9,1)(6,2,9,2)(6,2,9,3)(6,2,9,4)(6,2,9,5)(6,2,9,6)(6,2,9,7)(6,2,9,8)(6,3,0,1)(6,3,0,2)(6,3,0,3)(6,3,0,4)(6,3,0,5)(6,3,0,6)(6,3,0,7)(6,3,0,8)(6,3,0,9)(6,3,1,0)(6,3,1,2)(6,3,1,3)(6,3,1,4)(6,3,1,5)(6,3,1,6)(6,3,1,7)(6,3,1,8)(6,3,1,9)(6,3,2,0)(6,3,2,1)(6,3,2,3)(6,3,2,4)(6,3,2,5)(6,3,2,6)(6,3,2,7)(6,3,2,8)(6,3,2,9)(6,3,3,0)(6,3,3,1)(6,3,3,2)(6,3,3,4)(6,3,3,5)(6,3,3,6)(6,3,3,7)(6,3,3,8)(6,3,3,9)(6,3,4,0)(6,3,4,1)(6,3,4,2)(6,3,4,3)(6,3,4,5)(6,3,4,6)(6,3,4,7)(6,3,4,8)(6,3,4,9)(6,3,5,0)(6,3,5,1)(6,3,5,2)(6,3,5,3)(6,3,5,4)(6,3,5,6)(6,3,5,7)(6,3,5,8)(6,3,5,9)(6,3,6,0)(6,3,6,1)(6,3,6,2)(6,3,6,3)(6,3,6,4)(6,3,6,5)(6,3,6,7)(6,3,6,8)(6,3,6,9)(6,3,7,0)(6,3,7,1)(6,3,7,2)(6,3,7,3)(6,3,7,4)(6,3,7,5)(6,3,7,6)(6,3,7,8)(6,3,7,9)(6,3,8,0)(6,3,8,1)(6,3,8,2)(6,3,8,3)(6,3,8,4)(6,3,8,5)(6,3,8,6)(6,3,8,7)(6,3,8,9)(6,3,9,0)(6,3,9,1)(6,3,9,2)(6,3,9,3)(6,3,9,4)(6,3,9,5)(6,3,9,6)(6,3,9,7)(6,3,9,8)(6,4,0,1)(6,4,0,2)(6,4,0,3)(6,4,0,4)(6,4,0,5)(6,4,0,6)(6,4,0,7)(6,4,0,8)(6,4,0,9)(6,4,1,0)(6,4,1,2)(6,4,1,3)(6,4,1,4)(6,4,1,5)(6,4,1,6)(6,4,1,7)(6,4,1,8)(6,4,1,9)(6,4,2,0)(6,4,2,1)(6,4,2,3)(6,4,2,4)(6,4,2,5)(6,4,2,6)(6,4,2,7)(6,4,2,8)(6,4,2,9)(6,4,3,0)(6,4,3,1)(6,4,3,2)(6,4,3,4)(6,4,3,5)(6,4,3,6)(6,4,3,7)(6,4,3,8)(6,4,3,9)(6,4,4,0)(6,4,4,1)(6,4,4,2)(6,4,4,3)(6,4,4,5)(6,4,4,6)(6,4,4,7)(6,4,4,8)(6,4,4,9)(6,4,5,0)(6,4,5,1)(6,4,5,2)(6,4,5,3)(6,4,5,4)(6,4,5,6)(6,4,5,7)(6,4,5,8)(6,4,5,9)(6,4,6,0)(6,4,6,1)(6,4,6,2)(6,4,6,3)(6,4,6,4)(6,4,6,5)(6,4,6,7)(6,4,6,8)(6,4,6,9)(6,4,7,0)(6,4,7,1)(6,4,7,2)(6,4,7,3)(6,4,7,4)(6,4,7,5)(6,4,7,6)(6,4,7,8)(6,4,7,9)(6,4,8,0)(6,4,8,1)(6,4,8,2)(6,4,8,3)(6,4,8,4)(6,4,8,5)(6,4,8,6)(6,4,8,7)(6,4,8,9)(6,4,9,0)(6,4,9,1)(6,4,9,2)(6,4,9,3)(6,4,9,4)(6,4,9,5)(6,4,9,6)(6,4,9,7)(6,4,9,8)(6,5,0,1)(6,5,0,2)(6,5,0,3)(6,5,0,4)(6,5,0,5)(6,5,0,6)(6,5,0,7)(6,5,0,8)(6,5,0,9)(6,5,1,0)(6,5,1,2)(6,5,1,3)(6,5,1,4)(6,5,1,5)(6,5,1,6)(6,5,1,7)(6,5,1,8)(6,5,1,9)(6,5,2,0)(6,5,2,1)(6,5,2,3)(6,5,2,4)(6,5,2,5)(6,5,2,6)(6,5,2,7)(6,5,2,8)(6,5,2,9)(6,5,3,0)(6,5,3,1)(6,5,3,2)(6,5,3,4)(6,5,3,5)(6,5,3,6)(6,5,3,7)(6,5,3,8)(6,5,3,9)(6,5,4,0)(6,5,4,1)(6,5,4,2)(6,5,4,3)(6,5,4,5)(6,5,4,6)(6,5,4,7)(6,5,4,8)(6,5,4,9)(6,5,5,0)(6,5,5,1)(6,5,5,2)(6,5,5,3)(6,5,5,4)(6,5,5,6)(6,5,5,7)(6,5,5,8)(6,5,5,9)(6,5,6,0)(6,5,6,1)(6,5,6,2)(6,5,6,3)(6,5,6,4)(6,5,6,5)(6,5,6,7)(6,5,6,8)(6,5,6,9)(6,5,7,0)(6,5,7,1)(6,5,7,2)(6,5,7,3)(6,5,7,4)(6,5,7,5)(6,5,7,6)(6,5,7,8)(6,5,7,9)(6,5,8,0)(6,5,8,1)(6,5,8,2)(6,5,8,3)(6,5,8,4)(6,5,8,5)(6,5,8,6)(6,5,8,7)(6,5,8,9)(6,5,9,0)(6,5,9,1)(6,5,9,2)(6,5,9,3)(6,5,9,4)(6,5,9,5)(6,5,9,6)(6,5,9,7)(6,5,9,8)(6,6,0,0)(6,6,1,1)(6,6,2,2)(6,6,3,3)(6,6,4,4)(6,6,5,5)(6,6,6,6)(6,6,7,7)(6,6,8,8)(6,6,9,9) + + y[1][1] y[1][2] x[1][1] x[1][2] + y[1][2] y[1][3] x[1][2] x[1][3] + y[1][4] y[1][5] x[1][4] x[1][5] + y[2][3] y[2][4] x[2][3] x[2][4] + y[3][3] y[3][4] x[3][3] x[3][4] + y[4][1] y[4][2] x[4][1] x[4][2] + y[4][2] y[4][3] x[4][2] x[4][3] + y[4][4] y[4][5] x[4][4] x[4][5] + y[5][1] y[5][2] x[5][1] x[5][2] + y[5][2] y[5][3] x[5][2] x[5][3] + y[5][4] y[5][5] x[5][4] x[5][5] + y[1][1] y[2][1] x[1][1] x[2][1] + y[2][1] y[3][1] x[2][1] x[3][1] + y[3][1] y[4][1] x[3][1] x[4][1] + y[3][2] y[4][2] x[3][2] x[4][2] + y[4][2] y[5][2] x[4][2] x[5][2] + y[3][3] y[4][3] x[3][3] x[4][3] + y[4][3] y[5][3] x[4][3] x[5][3] + y[1][4] y[2][4] x[1][4] x[2][4] + y[2][4] y[3][4] x[2][4] x[3][4] + y[3][5] y[4][5] x[3][5] x[4][5] + y[4][5] y[5][5] x[4][5] x[5][5] + + + + %... + (1,1,0,0)(1,1,1,1)(1,1,2,2)(1,1,3,3)(1,1,4,4)(1,1,5,5)(1,1,6,6)(1,1,7,7)(1,1,8,8)(1,1,9,9)(1,3,0,1)(1,3,0,2)(1,3,0,3)(1,3,0,4)(1,3,0,5)(1,3,0,6)(1,3,0,7)(1,3,0,8)(1,3,0,9)(1,3,1,0)(1,3,1,2)(1,3,1,3)(1,3,1,4)(1,3,1,5)(1,3,1,6)(1,3,1,7)(1,3,1,8)(1,3,1,9)(1,3,2,0)(1,3,2,1)(1,3,2,3)(1,3,2,4)(1,3,2,5)(1,3,2,6)(1,3,2,7)(1,3,2,8)(1,3,2,9)(1,3,3,0)(1,3,3,1)(1,3,3,2)(1,3,3,4)(1,3,3,5)(1,3,3,6)(1,3,3,7)(1,3,3,8)(1,3,3,9)(1,3,4,0)(1,3,4,1)(1,3,4,2)(1,3,4,3)(1,3,4,5)(1,3,4,6)(1,3,4,7)(1,3,4,8)(1,3,4,9)(1,3,5,0)(1,3,5,1)(1,3,5,2)(1,3,5,3)(1,3,5,4)(1,3,5,6)(1,3,5,7)(1,3,5,8)(1,3,5,9)(1,3,6,0)(1,3,6,1)(1,3,6,2)(1,3,6,3)(1,3,6,4)(1,3,6,5)(1,3,6,7)(1,3,6,8)(1,3,6,9)(1,3,7,0)(1,3,7,1)(1,3,7,2)(1,3,7,3)(1,3,7,4)(1,3,7,5)(1,3,7,6)(1,3,7,8)(1,3,7,9)(1,3,8,0)(1,3,8,1)(1,3,8,2)(1,3,8,3)(1,3,8,4)(1,3,8,5)(1,3,8,6)(1,3,8,7)(1,3,8,9)(1,3,9,0)(1,3,9,1)(1,3,9,2)(1,3,9,3)(1,3,9,4)(1,3,9,5)(1,3,9,6)(1,3,9,7)(1,3,9,8)(2,2,0,0)(2,2,1,1)(2,2,2,2)(2,2,3,3)(2,2,4,4)(2,2,5,5)(2,2,6,6)(2,2,7,7)(2,2,8,8)(2,2,9,9)(2,3,0,1)(2,3,0,2)(2,3,0,3)(2,3,0,4)(2,3,0,5)(2,3,0,6)(2,3,0,7)(2,3,0,8)(2,3,0,9)(2,3,1,0)(2,3,1,2)(2,3,1,3)(2,3,1,4)(2,3,1,5)(2,3,1,6)(2,3,1,7)(2,3,1,8)(2,3,1,9)(2,3,2,0)(2,3,2,1)(2,3,2,3)(2,3,2,4)(2,3,2,5)(2,3,2,6)(2,3,2,7)(2,3,2,8)(2,3,2,9)(2,3,3,0)(2,3,3,1)(2,3,3,2)(2,3,3,4)(2,3,3,5)(2,3,3,6)(2,3,3,7)(2,3,3,8)(2,3,3,9)(2,3,4,0)(2,3,4,1)(2,3,4,2)(2,3,4,3)(2,3,4,5)(2,3,4,6)(2,3,4,7)(2,3,4,8)(2,3,4,9)(2,3,5,0)(2,3,5,1)(2,3,5,2)(2,3,5,3)(2,3,5,4)(2,3,5,6)(2,3,5,7)(2,3,5,8)(2,3,5,9)(2,3,6,0)(2,3,6,1)(2,3,6,2)(2,3,6,3)(2,3,6,4)(2,3,6,5)(2,3,6,7)(2,3,6,8)(2,3,6,9)(2,3,7,0)(2,3,7,1)(2,3,7,2)(2,3,7,3)(2,3,7,4)(2,3,7,5)(2,3,7,6)(2,3,7,8)(2,3,7,9)(2,3,8,0)(2,3,8,1)(2,3,8,2)(2,3,8,3)(2,3,8,4)(2,3,8,5)(2,3,8,6)(2,3,8,7)(2,3,8,9)(2,3,9,0)(2,3,9,1)(2,3,9,2)(2,3,9,3)(2,3,9,4)(2,3,9,5)(2,3,9,6)(2,3,9,7)(2,3,9,8)(3,3,0,0)(3,3,1,1)(3,3,2,2)(3,3,3,3)(3,3,4,4)(3,3,5,5)(3,3,6,6)(3,3,7,7)(3,3,8,8)(3,3,9,9)(4,3,0,1)(4,3,0,2)(4,3,0,3)(4,3,0,4)(4,3,0,5)(4,3,0,6)(4,3,0,7)(4,3,0,8)(4,3,0,9)(4,3,1,0)(4,3,1,2)(4,3,1,3)(4,3,1,4)(4,3,1,5)(4,3,1,6)(4,3,1,7)(4,3,1,8)(4,3,1,9)(4,3,2,0)(4,3,2,1)(4,3,2,3)(4,3,2,4)(4,3,2,5)(4,3,2,6)(4,3,2,7)(4,3,2,8)(4,3,2,9)(4,3,3,0)(4,3,3,1)(4,3,3,2)(4,3,3,4)(4,3,3,5)(4,3,3,6)(4,3,3,7)(4,3,3,8)(4,3,3,9)(4,3,4,0)(4,3,4,1)(4,3,4,2)(4,3,4,3)(4,3,4,5)(4,3,4,6)(4,3,4,7)(4,3,4,8)(4,3,4,9)(4,3,5,0)(4,3,5,1)(4,3,5,2)(4,3,5,3)(4,3,5,4)(4,3,5,6)(4,3,5,7)(4,3,5,8)(4,3,5,9)(4,3,6,0)(4,3,6,1)(4,3,6,2)(4,3,6,3)(4,3,6,4)(4,3,6,5)(4,3,6,7)(4,3,6,8)(4,3,6,9)(4,3,7,0)(4,3,7,1)(4,3,7,2)(4,3,7,3)(4,3,7,4)(4,3,7,5)(4,3,7,6)(4,3,7,8)(4,3,7,9)(4,3,8,0)(4,3,8,1)(4,3,8,2)(4,3,8,3)(4,3,8,4)(4,3,8,5)(4,3,8,6)(4,3,8,7)(4,3,8,9)(4,3,9,0)(4,3,9,1)(4,3,9,2)(4,3,9,3)(4,3,9,4)(4,3,9,5)(4,3,9,6)(4,3,9,7)(4,3,9,8)(4,4,0,0)(4,4,1,1)(4,4,2,2)(4,4,3,3)(4,4,4,4)(4,4,5,5)(4,4,6,6)(4,4,7,7)(4,4,8,8)(4,4,9,9)(5,3,0,1)(5,3,0,2)(5,3,0,3)(5,3,0,4)(5,3,0,5)(5,3,0,6)(5,3,0,7)(5,3,0,8)(5,3,0,9)(5,3,1,0)(5,3,1,2)(5,3,1,3)(5,3,1,4)(5,3,1,5)(5,3,1,6)(5,3,1,7)(5,3,1,8)(5,3,1,9)(5,3,2,0)(5,3,2,1)(5,3,2,3)(5,3,2,4)(5,3,2,5)(5,3,2,6)(5,3,2,7)(5,3,2,8)(5,3,2,9)(5,3,3,0)(5,3,3,1)(5,3,3,2)(5,3,3,4)(5,3,3,5)(5,3,3,6)(5,3,3,7)(5,3,3,8)(5,3,3,9)(5,3,4,0)(5,3,4,1)(5,3,4,2)(5,3,4,3)(5,3,4,5)(5,3,4,6)(5,3,4,7)(5,3,4,8)(5,3,4,9)(5,3,5,0)(5,3,5,1)(5,3,5,2)(5,3,5,3)(5,3,5,4)(5,3,5,6)(5,3,5,7)(5,3,5,8)(5,3,5,9)(5,3,6,0)(5,3,6,1)(5,3,6,2)(5,3,6,3)(5,3,6,4)(5,3,6,5)(5,3,6,7)(5,3,6,8)(5,3,6,9)(5,3,7,0)(5,3,7,1)(5,3,7,2)(5,3,7,3)(5,3,7,4)(5,3,7,5)(5,3,7,6)(5,3,7,8)(5,3,7,9)(5,3,8,0)(5,3,8,1)(5,3,8,2)(5,3,8,3)(5,3,8,4)(5,3,8,5)(5,3,8,6)(5,3,8,7)(5,3,8,9)(5,3,9,0)(5,3,9,1)(5,3,9,2)(5,3,9,3)(5,3,9,4)(5,3,9,5)(5,3,9,6)(5,3,9,7)(5,3,9,8)(5,5,0,0)(5,5,1,1)(5,5,2,2)(5,5,3,3)(5,5,4,4)(5,5,5,5)(5,5,6,6)(5,5,7,7)(5,5,8,8)(5,5,9,9)(6,3,0,1)(6,3,0,2)(6,3,0,3)(6,3,0,4)(6,3,0,5)(6,3,0,6)(6,3,0,7)(6,3,0,8)(6,3,0,9)(6,3,1,0)(6,3,1,2)(6,3,1,3)(6,3,1,4)(6,3,1,5)(6,3,1,6)(6,3,1,7)(6,3,1,8)(6,3,1,9)(6,3,2,0)(6,3,2,1)(6,3,2,3)(6,3,2,4)(6,3,2,5)(6,3,2,6)(6,3,2,7)(6,3,2,8)(6,3,2,9)(6,3,3,0)(6,3,3,1)(6,3,3,2)(6,3,3,4)(6,3,3,5)(6,3,3,6)(6,3,3,7)(6,3,3,8)(6,3,3,9)(6,3,4,0)(6,3,4,1)(6,3,4,2)(6,3,4,3)(6,3,4,5)(6,3,4,6)(6,3,4,7)(6,3,4,8)(6,3,4,9)(6,3,5,0)(6,3,5,1)(6,3,5,2)(6,3,5,3)(6,3,5,4)(6,3,5,6)(6,3,5,7)(6,3,5,8)(6,3,5,9)(6,3,6,0)(6,3,6,1)(6,3,6,2)(6,3,6,3)(6,3,6,4)(6,3,6,5)(6,3,6,7)(6,3,6,8)(6,3,6,9)(6,3,7,0)(6,3,7,1)(6,3,7,2)(6,3,7,3)(6,3,7,4)(6,3,7,5)(6,3,7,6)(6,3,7,8)(6,3,7,9)(6,3,8,0)(6,3,8,1)(6,3,8,2)(6,3,8,3)(6,3,8,4)(6,3,8,5)(6,3,8,6)(6,3,8,7)(6,3,8,9)(6,3,9,0)(6,3,9,1)(6,3,9,2)(6,3,9,3)(6,3,9,4)(6,3,9,5)(6,3,9,6)(6,3,9,7)(6,3,9,8)(6,6,0,0)(6,6,1,1)(6,6,2,2)(6,6,3,3)(6,6,4,4)(6,6,5,5)(6,6,6,6)(6,6,7,7)(6,6,8,8)(6,6,9,9) + + y[1][3] y[1][4] x[1][3] x[1][4] + y[5][3] y[5][4] x[5][3] x[5][4] + y[4][4] y[5][4] x[4][4] x[5][4] + + + + %... + (1,1,0,0)(1,1,1,1)(1,1,2,2)(1,1,3,3)(1,1,4,4)(1,1,5,5)(1,1,6,6)(1,1,7,7)(1,1,8,8)(1,1,9,9)(1,5,0,1)(1,5,0,2)(1,5,0,3)(1,5,0,4)(1,5,0,5)(1,5,0,6)(1,5,0,7)(1,5,0,8)(1,5,0,9)(1,5,1,0)(1,5,1,2)(1,5,1,3)(1,5,1,4)(1,5,1,5)(1,5,1,6)(1,5,1,7)(1,5,1,8)(1,5,1,9)(1,5,2,0)(1,5,2,1)(1,5,2,3)(1,5,2,4)(1,5,2,5)(1,5,2,6)(1,5,2,7)(1,5,2,8)(1,5,2,9)(1,5,3,0)(1,5,3,1)(1,5,3,2)(1,5,3,4)(1,5,3,5)(1,5,3,6)(1,5,3,7)(1,5,3,8)(1,5,3,9)(1,5,4,0)(1,5,4,1)(1,5,4,2)(1,5,4,3)(1,5,4,5)(1,5,4,6)(1,5,4,7)(1,5,4,8)(1,5,4,9)(1,5,5,0)(1,5,5,1)(1,5,5,2)(1,5,5,3)(1,5,5,4)(1,5,5,6)(1,5,5,7)(1,5,5,8)(1,5,5,9)(1,5,6,0)(1,5,6,1)(1,5,6,2)(1,5,6,3)(1,5,6,4)(1,5,6,5)(1,5,6,7)(1,5,6,8)(1,5,6,9)(1,5,7,0)(1,5,7,1)(1,5,7,2)(1,5,7,3)(1,5,7,4)(1,5,7,5)(1,5,7,6)(1,5,7,8)(1,5,7,9)(1,5,8,0)(1,5,8,1)(1,5,8,2)(1,5,8,3)(1,5,8,4)(1,5,8,5)(1,5,8,6)(1,5,8,7)(1,5,8,9)(1,5,9,0)(1,5,9,1)(1,5,9,2)(1,5,9,3)(1,5,9,4)(1,5,9,5)(1,5,9,6)(1,5,9,7)(1,5,9,8)(2,2,0,0)(2,2,1,1)(2,2,2,2)(2,2,3,3)(2,2,4,4)(2,2,5,5)(2,2,6,6)(2,2,7,7)(2,2,8,8)(2,2,9,9)(2,5,0,1)(2,5,0,2)(2,5,0,3)(2,5,0,4)(2,5,0,5)(2,5,0,6)(2,5,0,7)(2,5,0,8)(2,5,0,9)(2,5,1,0)(2,5,1,2)(2,5,1,3)(2,5,1,4)(2,5,1,5)(2,5,1,6)(2,5,1,7)(2,5,1,8)(2,5,1,9)(2,5,2,0)(2,5,2,1)(2,5,2,3)(2,5,2,4)(2,5,2,5)(2,5,2,6)(2,5,2,7)(2,5,2,8)(2,5,2,9)(2,5,3,0)(2,5,3,1)(2,5,3,2)(2,5,3,4)(2,5,3,5)(2,5,3,6)(2,5,3,7)(2,5,3,8)(2,5,3,9)(2,5,4,0)(2,5,4,1)(2,5,4,2)(2,5,4,3)(2,5,4,5)(2,5,4,6)(2,5,4,7)(2,5,4,8)(2,5,4,9)(2,5,5,0)(2,5,5,1)(2,5,5,2)(2,5,5,3)(2,5,5,4)(2,5,5,6)(2,5,5,7)(2,5,5,8)(2,5,5,9)(2,5,6,0)(2,5,6,1)(2,5,6,2)(2,5,6,3)(2,5,6,4)(2,5,6,5)(2,5,6,7)(2,5,6,8)(2,5,6,9)(2,5,7,0)(2,5,7,1)(2,5,7,2)(2,5,7,3)(2,5,7,4)(2,5,7,5)(2,5,7,6)(2,5,7,8)(2,5,7,9)(2,5,8,0)(2,5,8,1)(2,5,8,2)(2,5,8,3)(2,5,8,4)(2,5,8,5)(2,5,8,6)(2,5,8,7)(2,5,8,9)(2,5,9,0)(2,5,9,1)(2,5,9,2)(2,5,9,3)(2,5,9,4)(2,5,9,5)(2,5,9,6)(2,5,9,7)(2,5,9,8)(3,3,0,0)(3,3,1,1)(3,3,2,2)(3,3,3,3)(3,3,4,4)(3,3,5,5)(3,3,6,6)(3,3,7,7)(3,3,8,8)(3,3,9,9)(3,5,0,1)(3,5,0,2)(3,5,0,3)(3,5,0,4)(3,5,0,5)(3,5,0,6)(3,5,0,7)(3,5,0,8)(3,5,0,9)(3,5,1,0)(3,5,1,2)(3,5,1,3)(3,5,1,4)(3,5,1,5)(3,5,1,6)(3,5,1,7)(3,5,1,8)(3,5,1,9)(3,5,2,0)(3,5,2,1)(3,5,2,3)(3,5,2,4)(3,5,2,5)(3,5,2,6)(3,5,2,7)(3,5,2,8)(3,5,2,9)(3,5,3,0)(3,5,3,1)(3,5,3,2)(3,5,3,4)(3,5,3,5)(3,5,3,6)(3,5,3,7)(3,5,3,8)(3,5,3,9)(3,5,4,0)(3,5,4,1)(3,5,4,2)(3,5,4,3)(3,5,4,5)(3,5,4,6)(3,5,4,7)(3,5,4,8)(3,5,4,9)(3,5,5,0)(3,5,5,1)(3,5,5,2)(3,5,5,3)(3,5,5,4)(3,5,5,6)(3,5,5,7)(3,5,5,8)(3,5,5,9)(3,5,6,0)(3,5,6,1)(3,5,6,2)(3,5,6,3)(3,5,6,4)(3,5,6,5)(3,5,6,7)(3,5,6,8)(3,5,6,9)(3,5,7,0)(3,5,7,1)(3,5,7,2)(3,5,7,3)(3,5,7,4)(3,5,7,5)(3,5,7,6)(3,5,7,8)(3,5,7,9)(3,5,8,0)(3,5,8,1)(3,5,8,2)(3,5,8,3)(3,5,8,4)(3,5,8,5)(3,5,8,6)(3,5,8,7)(3,5,8,9)(3,5,9,0)(3,5,9,1)(3,5,9,2)(3,5,9,3)(3,5,9,4)(3,5,9,5)(3,5,9,6)(3,5,9,7)(3,5,9,8)(4,4,0,0)(4,4,1,1)(4,4,2,2)(4,4,3,3)(4,4,4,4)(4,4,5,5)(4,4,6,6)(4,4,7,7)(4,4,8,8)(4,4,9,9)(4,5,0,1)(4,5,0,2)(4,5,0,3)(4,5,0,4)(4,5,0,5)(4,5,0,6)(4,5,0,7)(4,5,0,8)(4,5,0,9)(4,5,1,0)(4,5,1,2)(4,5,1,3)(4,5,1,4)(4,5,1,5)(4,5,1,6)(4,5,1,7)(4,5,1,8)(4,5,1,9)(4,5,2,0)(4,5,2,1)(4,5,2,3)(4,5,2,4)(4,5,2,5)(4,5,2,6)(4,5,2,7)(4,5,2,8)(4,5,2,9)(4,5,3,0)(4,5,3,1)(4,5,3,2)(4,5,3,4)(4,5,3,5)(4,5,3,6)(4,5,3,7)(4,5,3,8)(4,5,3,9)(4,5,4,0)(4,5,4,1)(4,5,4,2)(4,5,4,3)(4,5,4,5)(4,5,4,6)(4,5,4,7)(4,5,4,8)(4,5,4,9)(4,5,5,0)(4,5,5,1)(4,5,5,2)(4,5,5,3)(4,5,5,4)(4,5,5,6)(4,5,5,7)(4,5,5,8)(4,5,5,9)(4,5,6,0)(4,5,6,1)(4,5,6,2)(4,5,6,3)(4,5,6,4)(4,5,6,5)(4,5,6,7)(4,5,6,8)(4,5,6,9)(4,5,7,0)(4,5,7,1)(4,5,7,2)(4,5,7,3)(4,5,7,4)(4,5,7,5)(4,5,7,6)(4,5,7,8)(4,5,7,9)(4,5,8,0)(4,5,8,1)(4,5,8,2)(4,5,8,3)(4,5,8,4)(4,5,8,5)(4,5,8,6)(4,5,8,7)(4,5,8,9)(4,5,9,0)(4,5,9,1)(4,5,9,2)(4,5,9,3)(4,5,9,4)(4,5,9,5)(4,5,9,6)(4,5,9,7)(4,5,9,8)(5,5,0,0)(5,5,1,1)(5,5,2,2)(5,5,3,3)(5,5,4,4)(5,5,5,5)(5,5,6,6)(5,5,7,7)(5,5,8,8)(5,5,9,9)(6,5,0,1)(6,5,0,2)(6,5,0,3)(6,5,0,4)(6,5,0,5)(6,5,0,6)(6,5,0,7)(6,5,0,8)(6,5,0,9)(6,5,1,0)(6,5,1,2)(6,5,1,3)(6,5,1,4)(6,5,1,5)(6,5,1,6)(6,5,1,7)(6,5,1,8)(6,5,1,9)(6,5,2,0)(6,5,2,1)(6,5,2,3)(6,5,2,4)(6,5,2,5)(6,5,2,6)(6,5,2,7)(6,5,2,8)(6,5,2,9)(6,5,3,0)(6,5,3,1)(6,5,3,2)(6,5,3,4)(6,5,3,5)(6,5,3,6)(6,5,3,7)(6,5,3,8)(6,5,3,9)(6,5,4,0)(6,5,4,1)(6,5,4,2)(6,5,4,3)(6,5,4,5)(6,5,4,6)(6,5,4,7)(6,5,4,8)(6,5,4,9)(6,5,5,0)(6,5,5,1)(6,5,5,2)(6,5,5,3)(6,5,5,4)(6,5,5,6)(6,5,5,7)(6,5,5,8)(6,5,5,9)(6,5,6,0)(6,5,6,1)(6,5,6,2)(6,5,6,3)(6,5,6,4)(6,5,6,5)(6,5,6,7)(6,5,6,8)(6,5,6,9)(6,5,7,0)(6,5,7,1)(6,5,7,2)(6,5,7,3)(6,5,7,4)(6,5,7,5)(6,5,7,6)(6,5,7,8)(6,5,7,9)(6,5,8,0)(6,5,8,1)(6,5,8,2)(6,5,8,3)(6,5,8,4)(6,5,8,5)(6,5,8,6)(6,5,8,7)(6,5,8,9)(6,5,9,0)(6,5,9,1)(6,5,9,2)(6,5,9,3)(6,5,9,4)(6,5,9,5)(6,5,9,6)(6,5,9,7)(6,5,9,8)(6,6,0,0)(6,6,1,1)(6,6,2,2)(6,6,3,3)(6,6,4,4)(6,6,5,5)(6,6,6,6)(6,6,7,7)(6,6,8,8)(6,6,9,9) + + y[2][1] y[2][2] x[2][1] x[2][2] + y[4][1] y[5][1] x[4][1] x[5][1] + y[1][2] y[2][2] x[1][2] x[2][2] + + + + %... + (1,1,0,0)(1,1,1,1)(1,1,2,2)(1,1,3,3)(1,1,4,4)(1,1,5,5)(1,1,6,6)(1,1,7,7)(1,1,8,8)(1,1,9,9)(1,4,0,1)(1,4,0,2)(1,4,0,3)(1,4,0,4)(1,4,0,5)(1,4,0,6)(1,4,0,7)(1,4,0,8)(1,4,0,9)(1,4,1,0)(1,4,1,2)(1,4,1,3)(1,4,1,4)(1,4,1,5)(1,4,1,6)(1,4,1,7)(1,4,1,8)(1,4,1,9)(1,4,2,0)(1,4,2,1)(1,4,2,3)(1,4,2,4)(1,4,2,5)(1,4,2,6)(1,4,2,7)(1,4,2,8)(1,4,2,9)(1,4,3,0)(1,4,3,1)(1,4,3,2)(1,4,3,4)(1,4,3,5)(1,4,3,6)(1,4,3,7)(1,4,3,8)(1,4,3,9)(1,4,4,0)(1,4,4,1)(1,4,4,2)(1,4,4,3)(1,4,4,5)(1,4,4,6)(1,4,4,7)(1,4,4,8)(1,4,4,9)(1,4,5,0)(1,4,5,1)(1,4,5,2)(1,4,5,3)(1,4,5,4)(1,4,5,6)(1,4,5,7)(1,4,5,8)(1,4,5,9)(1,4,6,0)(1,4,6,1)(1,4,6,2)(1,4,6,3)(1,4,6,4)(1,4,6,5)(1,4,6,7)(1,4,6,8)(1,4,6,9)(1,4,7,0)(1,4,7,1)(1,4,7,2)(1,4,7,3)(1,4,7,4)(1,4,7,5)(1,4,7,6)(1,4,7,8)(1,4,7,9)(1,4,8,0)(1,4,8,1)(1,4,8,2)(1,4,8,3)(1,4,8,4)(1,4,8,5)(1,4,8,6)(1,4,8,7)(1,4,8,9)(1,4,9,0)(1,4,9,1)(1,4,9,2)(1,4,9,3)(1,4,9,4)(1,4,9,5)(1,4,9,6)(1,4,9,7)(1,4,9,8)(2,2,0,0)(2,2,1,1)(2,2,2,2)(2,2,3,3)(2,2,4,4)(2,2,5,5)(2,2,6,6)(2,2,7,7)(2,2,8,8)(2,2,9,9)(2,4,0,1)(2,4,0,2)(2,4,0,3)(2,4,0,4)(2,4,0,5)(2,4,0,6)(2,4,0,7)(2,4,0,8)(2,4,0,9)(2,4,1,0)(2,4,1,2)(2,4,1,3)(2,4,1,4)(2,4,1,5)(2,4,1,6)(2,4,1,7)(2,4,1,8)(2,4,1,9)(2,4,2,0)(2,4,2,1)(2,4,2,3)(2,4,2,4)(2,4,2,5)(2,4,2,6)(2,4,2,7)(2,4,2,8)(2,4,2,9)(2,4,3,0)(2,4,3,1)(2,4,3,2)(2,4,3,4)(2,4,3,5)(2,4,3,6)(2,4,3,7)(2,4,3,8)(2,4,3,9)(2,4,4,0)(2,4,4,1)(2,4,4,2)(2,4,4,3)(2,4,4,5)(2,4,4,6)(2,4,4,7)(2,4,4,8)(2,4,4,9)(2,4,5,0)(2,4,5,1)(2,4,5,2)(2,4,5,3)(2,4,5,4)(2,4,5,6)(2,4,5,7)(2,4,5,8)(2,4,5,9)(2,4,6,0)(2,4,6,1)(2,4,6,2)(2,4,6,3)(2,4,6,4)(2,4,6,5)(2,4,6,7)(2,4,6,8)(2,4,6,9)(2,4,7,0)(2,4,7,1)(2,4,7,2)(2,4,7,3)(2,4,7,4)(2,4,7,5)(2,4,7,6)(2,4,7,8)(2,4,7,9)(2,4,8,0)(2,4,8,1)(2,4,8,2)(2,4,8,3)(2,4,8,4)(2,4,8,5)(2,4,8,6)(2,4,8,7)(2,4,8,9)(2,4,9,0)(2,4,9,1)(2,4,9,2)(2,4,9,3)(2,4,9,4)(2,4,9,5)(2,4,9,6)(2,4,9,7)(2,4,9,8)(3,3,0,0)(3,3,1,1)(3,3,2,2)(3,3,3,3)(3,3,4,4)(3,3,5,5)(3,3,6,6)(3,3,7,7)(3,3,8,8)(3,3,9,9)(3,4,0,1)(3,4,0,2)(3,4,0,3)(3,4,0,4)(3,4,0,5)(3,4,0,6)(3,4,0,7)(3,4,0,8)(3,4,0,9)(3,4,1,0)(3,4,1,2)(3,4,1,3)(3,4,1,4)(3,4,1,5)(3,4,1,6)(3,4,1,7)(3,4,1,8)(3,4,1,9)(3,4,2,0)(3,4,2,1)(3,4,2,3)(3,4,2,4)(3,4,2,5)(3,4,2,6)(3,4,2,7)(3,4,2,8)(3,4,2,9)(3,4,3,0)(3,4,3,1)(3,4,3,2)(3,4,3,4)(3,4,3,5)(3,4,3,6)(3,4,3,7)(3,4,3,8)(3,4,3,9)(3,4,4,0)(3,4,4,1)(3,4,4,2)(3,4,4,3)(3,4,4,5)(3,4,4,6)(3,4,4,7)(3,4,4,8)(3,4,4,9)(3,4,5,0)(3,4,5,1)(3,4,5,2)(3,4,5,3)(3,4,5,4)(3,4,5,6)(3,4,5,7)(3,4,5,8)(3,4,5,9)(3,4,6,0)(3,4,6,1)(3,4,6,2)(3,4,6,3)(3,4,6,4)(3,4,6,5)(3,4,6,7)(3,4,6,8)(3,4,6,9)(3,4,7,0)(3,4,7,1)(3,4,7,2)(3,4,7,3)(3,4,7,4)(3,4,7,5)(3,4,7,6)(3,4,7,8)(3,4,7,9)(3,4,8,0)(3,4,8,1)(3,4,8,2)(3,4,8,3)(3,4,8,4)(3,4,8,5)(3,4,8,6)(3,4,8,7)(3,4,8,9)(3,4,9,0)(3,4,9,1)(3,4,9,2)(3,4,9,3)(3,4,9,4)(3,4,9,5)(3,4,9,6)(3,4,9,7)(3,4,9,8)(4,4,0,0)(4,4,1,1)(4,4,2,2)(4,4,3,3)(4,4,4,4)(4,4,5,5)(4,4,6,6)(4,4,7,7)(4,4,8,8)(4,4,9,9)(5,4,0,1)(5,4,0,2)(5,4,0,3)(5,4,0,4)(5,4,0,5)(5,4,0,6)(5,4,0,7)(5,4,0,8)(5,4,0,9)(5,4,1,0)(5,4,1,2)(5,4,1,3)(5,4,1,4)(5,4,1,5)(5,4,1,6)(5,4,1,7)(5,4,1,8)(5,4,1,9)(5,4,2,0)(5,4,2,1)(5,4,2,3)(5,4,2,4)(5,4,2,5)(5,4,2,6)(5,4,2,7)(5,4,2,8)(5,4,2,9)(5,4,3,0)(5,4,3,1)(5,4,3,2)(5,4,3,4)(5,4,3,5)(5,4,3,6)(5,4,3,7)(5,4,3,8)(5,4,3,9)(5,4,4,0)(5,4,4,1)(5,4,4,2)(5,4,4,3)(5,4,4,5)(5,4,4,6)(5,4,4,7)(5,4,4,8)(5,4,4,9)(5,4,5,0)(5,4,5,1)(5,4,5,2)(5,4,5,3)(5,4,5,4)(5,4,5,6)(5,4,5,7)(5,4,5,8)(5,4,5,9)(5,4,6,0)(5,4,6,1)(5,4,6,2)(5,4,6,3)(5,4,6,4)(5,4,6,5)(5,4,6,7)(5,4,6,8)(5,4,6,9)(5,4,7,0)(5,4,7,1)(5,4,7,2)(5,4,7,3)(5,4,7,4)(5,4,7,5)(5,4,7,6)(5,4,7,8)(5,4,7,9)(5,4,8,0)(5,4,8,1)(5,4,8,2)(5,4,8,3)(5,4,8,4)(5,4,8,5)(5,4,8,6)(5,4,8,7)(5,4,8,9)(5,4,9,0)(5,4,9,1)(5,4,9,2)(5,4,9,3)(5,4,9,4)(5,4,9,5)(5,4,9,6)(5,4,9,7)(5,4,9,8)(5,5,0,0)(5,5,1,1)(5,5,2,2)(5,5,3,3)(5,5,4,4)(5,5,5,5)(5,5,6,6)(5,5,7,7)(5,5,8,8)(5,5,9,9)(6,4,0,1)(6,4,0,2)(6,4,0,3)(6,4,0,4)(6,4,0,5)(6,4,0,6)(6,4,0,7)(6,4,0,8)(6,4,0,9)(6,4,1,0)(6,4,1,2)(6,4,1,3)(6,4,1,4)(6,4,1,5)(6,4,1,6)(6,4,1,7)(6,4,1,8)(6,4,1,9)(6,4,2,0)(6,4,2,1)(6,4,2,3)(6,4,2,4)(6,4,2,5)(6,4,2,6)(6,4,2,7)(6,4,2,8)(6,4,2,9)(6,4,3,0)(6,4,3,1)(6,4,3,2)(6,4,3,4)(6,4,3,5)(6,4,3,6)(6,4,3,7)(6,4,3,8)(6,4,3,9)(6,4,4,0)(6,4,4,1)(6,4,4,2)(6,4,4,3)(6,4,4,5)(6,4,4,6)(6,4,4,7)(6,4,4,8)(6,4,4,9)(6,4,5,0)(6,4,5,1)(6,4,5,2)(6,4,5,3)(6,4,5,4)(6,4,5,6)(6,4,5,7)(6,4,5,8)(6,4,5,9)(6,4,6,0)(6,4,6,1)(6,4,6,2)(6,4,6,3)(6,4,6,4)(6,4,6,5)(6,4,6,7)(6,4,6,8)(6,4,6,9)(6,4,7,0)(6,4,7,1)(6,4,7,2)(6,4,7,3)(6,4,7,4)(6,4,7,5)(6,4,7,6)(6,4,7,8)(6,4,7,9)(6,4,8,0)(6,4,8,1)(6,4,8,2)(6,4,8,3)(6,4,8,4)(6,4,8,5)(6,4,8,6)(6,4,8,7)(6,4,8,9)(6,4,9,0)(6,4,9,1)(6,4,9,2)(6,4,9,3)(6,4,9,4)(6,4,9,5)(6,4,9,6)(6,4,9,7)(6,4,9,8)(6,6,0,0)(6,6,1,1)(6,6,2,2)(6,6,3,3)(6,6,4,4)(6,6,5,5)(6,6,6,6)(6,6,7,7)(6,6,8,8)(6,6,9,9) + + y[2][2] y[2][3] x[2][2] x[2][3] + y[1][3] y[2][3] x[1][3] x[2][3] + + + + %... + (1,1,0,0)(1,1,1,1)(1,1,2,2)(1,1,3,3)(1,1,4,4)(1,1,5,5)(1,1,6,6)(1,1,7,7)(1,1,8,8)(1,1,9,9)(1,2,0,1)(1,2,0,2)(1,2,0,3)(1,2,0,4)(1,2,0,5)(1,2,0,6)(1,2,0,7)(1,2,0,8)(1,2,0,9)(1,2,1,0)(1,2,1,2)(1,2,1,3)(1,2,1,4)(1,2,1,5)(1,2,1,6)(1,2,1,7)(1,2,1,8)(1,2,1,9)(1,2,2,0)(1,2,2,1)(1,2,2,3)(1,2,2,4)(1,2,2,5)(1,2,2,6)(1,2,2,7)(1,2,2,8)(1,2,2,9)(1,2,3,0)(1,2,3,1)(1,2,3,2)(1,2,3,4)(1,2,3,5)(1,2,3,6)(1,2,3,7)(1,2,3,8)(1,2,3,9)(1,2,4,0)(1,2,4,1)(1,2,4,2)(1,2,4,3)(1,2,4,5)(1,2,4,6)(1,2,4,7)(1,2,4,8)(1,2,4,9)(1,2,5,0)(1,2,5,1)(1,2,5,2)(1,2,5,3)(1,2,5,4)(1,2,5,6)(1,2,5,7)(1,2,5,8)(1,2,5,9)(1,2,6,0)(1,2,6,1)(1,2,6,2)(1,2,6,3)(1,2,6,4)(1,2,6,5)(1,2,6,7)(1,2,6,8)(1,2,6,9)(1,2,7,0)(1,2,7,1)(1,2,7,2)(1,2,7,3)(1,2,7,4)(1,2,7,5)(1,2,7,6)(1,2,7,8)(1,2,7,9)(1,2,8,0)(1,2,8,1)(1,2,8,2)(1,2,8,3)(1,2,8,4)(1,2,8,5)(1,2,8,6)(1,2,8,7)(1,2,8,9)(1,2,9,0)(1,2,9,1)(1,2,9,2)(1,2,9,3)(1,2,9,4)(1,2,9,5)(1,2,9,6)(1,2,9,7)(1,2,9,8)(2,2,0,0)(2,2,1,1)(2,2,2,2)(2,2,3,3)(2,2,4,4)(2,2,5,5)(2,2,6,6)(2,2,7,7)(2,2,8,8)(2,2,9,9)(3,2,0,1)(3,2,0,2)(3,2,0,3)(3,2,0,4)(3,2,0,5)(3,2,0,6)(3,2,0,7)(3,2,0,8)(3,2,0,9)(3,2,1,0)(3,2,1,2)(3,2,1,3)(3,2,1,4)(3,2,1,5)(3,2,1,6)(3,2,1,7)(3,2,1,8)(3,2,1,9)(3,2,2,0)(3,2,2,1)(3,2,2,3)(3,2,2,4)(3,2,2,5)(3,2,2,6)(3,2,2,7)(3,2,2,8)(3,2,2,9)(3,2,3,0)(3,2,3,1)(3,2,3,2)(3,2,3,4)(3,2,3,5)(3,2,3,6)(3,2,3,7)(3,2,3,8)(3,2,3,9)(3,2,4,0)(3,2,4,1)(3,2,4,2)(3,2,4,3)(3,2,4,5)(3,2,4,6)(3,2,4,7)(3,2,4,8)(3,2,4,9)(3,2,5,0)(3,2,5,1)(3,2,5,2)(3,2,5,3)(3,2,5,4)(3,2,5,6)(3,2,5,7)(3,2,5,8)(3,2,5,9)(3,2,6,0)(3,2,6,1)(3,2,6,2)(3,2,6,3)(3,2,6,4)(3,2,6,5)(3,2,6,7)(3,2,6,8)(3,2,6,9)(3,2,7,0)(3,2,7,1)(3,2,7,2)(3,2,7,3)(3,2,7,4)(3,2,7,5)(3,2,7,6)(3,2,7,8)(3,2,7,9)(3,2,8,0)(3,2,8,1)(3,2,8,2)(3,2,8,3)(3,2,8,4)(3,2,8,5)(3,2,8,6)(3,2,8,7)(3,2,8,9)(3,2,9,0)(3,2,9,1)(3,2,9,2)(3,2,9,3)(3,2,9,4)(3,2,9,5)(3,2,9,6)(3,2,9,7)(3,2,9,8)(3,3,0,0)(3,3,1,1)(3,3,2,2)(3,3,3,3)(3,3,4,4)(3,3,5,5)(3,3,6,6)(3,3,7,7)(3,3,8,8)(3,3,9,9)(4,2,0,1)(4,2,0,2)(4,2,0,3)(4,2,0,4)(4,2,0,5)(4,2,0,6)(4,2,0,7)(4,2,0,8)(4,2,0,9)(4,2,1,0)(4,2,1,2)(4,2,1,3)(4,2,1,4)(4,2,1,5)(4,2,1,6)(4,2,1,7)(4,2,1,8)(4,2,1,9)(4,2,2,0)(4,2,2,1)(4,2,2,3)(4,2,2,4)(4,2,2,5)(4,2,2,6)(4,2,2,7)(4,2,2,8)(4,2,2,9)(4,2,3,0)(4,2,3,1)(4,2,3,2)(4,2,3,4)(4,2,3,5)(4,2,3,6)(4,2,3,7)(4,2,3,8)(4,2,3,9)(4,2,4,0)(4,2,4,1)(4,2,4,2)(4,2,4,3)(4,2,4,5)(4,2,4,6)(4,2,4,7)(4,2,4,8)(4,2,4,9)(4,2,5,0)(4,2,5,1)(4,2,5,2)(4,2,5,3)(4,2,5,4)(4,2,5,6)(4,2,5,7)(4,2,5,8)(4,2,5,9)(4,2,6,0)(4,2,6,1)(4,2,6,2)(4,2,6,3)(4,2,6,4)(4,2,6,5)(4,2,6,7)(4,2,6,8)(4,2,6,9)(4,2,7,0)(4,2,7,1)(4,2,7,2)(4,2,7,3)(4,2,7,4)(4,2,7,5)(4,2,7,6)(4,2,7,8)(4,2,7,9)(4,2,8,0)(4,2,8,1)(4,2,8,2)(4,2,8,3)(4,2,8,4)(4,2,8,5)(4,2,8,6)(4,2,8,7)(4,2,8,9)(4,2,9,0)(4,2,9,1)(4,2,9,2)(4,2,9,3)(4,2,9,4)(4,2,9,5)(4,2,9,6)(4,2,9,7)(4,2,9,8)(4,4,0,0)(4,4,1,1)(4,4,2,2)(4,4,3,3)(4,4,4,4)(4,4,5,5)(4,4,6,6)(4,4,7,7)(4,4,8,8)(4,4,9,9)(5,2,0,1)(5,2,0,2)(5,2,0,3)(5,2,0,4)(5,2,0,5)(5,2,0,6)(5,2,0,7)(5,2,0,8)(5,2,0,9)(5,2,1,0)(5,2,1,2)(5,2,1,3)(5,2,1,4)(5,2,1,5)(5,2,1,6)(5,2,1,7)(5,2,1,8)(5,2,1,9)(5,2,2,0)(5,2,2,1)(5,2,2,3)(5,2,2,4)(5,2,2,5)(5,2,2,6)(5,2,2,7)(5,2,2,8)(5,2,2,9)(5,2,3,0)(5,2,3,1)(5,2,3,2)(5,2,3,4)(5,2,3,5)(5,2,3,6)(5,2,3,7)(5,2,3,8)(5,2,3,9)(5,2,4,0)(5,2,4,1)(5,2,4,2)(5,2,4,3)(5,2,4,5)(5,2,4,6)(5,2,4,7)(5,2,4,8)(5,2,4,9)(5,2,5,0)(5,2,5,1)(5,2,5,2)(5,2,5,3)(5,2,5,4)(5,2,5,6)(5,2,5,7)(5,2,5,8)(5,2,5,9)(5,2,6,0)(5,2,6,1)(5,2,6,2)(5,2,6,3)(5,2,6,4)(5,2,6,5)(5,2,6,7)(5,2,6,8)(5,2,6,9)(5,2,7,0)(5,2,7,1)(5,2,7,2)(5,2,7,3)(5,2,7,4)(5,2,7,5)(5,2,7,6)(5,2,7,8)(5,2,7,9)(5,2,8,0)(5,2,8,1)(5,2,8,2)(5,2,8,3)(5,2,8,4)(5,2,8,5)(5,2,8,6)(5,2,8,7)(5,2,8,9)(5,2,9,0)(5,2,9,1)(5,2,9,2)(5,2,9,3)(5,2,9,4)(5,2,9,5)(5,2,9,6)(5,2,9,7)(5,2,9,8)(5,5,0,0)(5,5,1,1)(5,5,2,2)(5,5,3,3)(5,5,4,4)(5,5,5,5)(5,5,6,6)(5,5,7,7)(5,5,8,8)(5,5,9,9)(6,2,0,1)(6,2,0,2)(6,2,0,3)(6,2,0,4)(6,2,0,5)(6,2,0,6)(6,2,0,7)(6,2,0,8)(6,2,0,9)(6,2,1,0)(6,2,1,2)(6,2,1,3)(6,2,1,4)(6,2,1,5)(6,2,1,6)(6,2,1,7)(6,2,1,8)(6,2,1,9)(6,2,2,0)(6,2,2,1)(6,2,2,3)(6,2,2,4)(6,2,2,5)(6,2,2,6)(6,2,2,7)(6,2,2,8)(6,2,2,9)(6,2,3,0)(6,2,3,1)(6,2,3,2)(6,2,3,4)(6,2,3,5)(6,2,3,6)(6,2,3,7)(6,2,3,8)(6,2,3,9)(6,2,4,0)(6,2,4,1)(6,2,4,2)(6,2,4,3)(6,2,4,5)(6,2,4,6)(6,2,4,7)(6,2,4,8)(6,2,4,9)(6,2,5,0)(6,2,5,1)(6,2,5,2)(6,2,5,3)(6,2,5,4)(6,2,5,6)(6,2,5,7)(6,2,5,8)(6,2,5,9)(6,2,6,0)(6,2,6,1)(6,2,6,2)(6,2,6,3)(6,2,6,4)(6,2,6,5)(6,2,6,7)(6,2,6,8)(6,2,6,9)(6,2,7,0)(6,2,7,1)(6,2,7,2)(6,2,7,3)(6,2,7,4)(6,2,7,5)(6,2,7,6)(6,2,7,8)(6,2,7,9)(6,2,8,0)(6,2,8,1)(6,2,8,2)(6,2,8,3)(6,2,8,4)(6,2,8,5)(6,2,8,6)(6,2,8,7)(6,2,8,9)(6,2,9,0)(6,2,9,1)(6,2,9,2)(6,2,9,3)(6,2,9,4)(6,2,9,5)(6,2,9,6)(6,2,9,7)(6,2,9,8)(6,6,0,0)(6,6,1,1)(6,6,2,2)(6,6,3,3)(6,6,4,4)(6,6,5,5)(6,6,6,6)(6,6,7,7)(6,6,8,8)(6,6,9,9) + + y[2][4] y[2][5] x[2][4] x[2][5] + y[1][5] y[2][5] x[1][5] x[2][5] + + + + %... + (1,1,0,0)(1,1,1,1)(1,1,2,2)(1,1,3,3)(1,1,4,4)(1,1,5,5)(1,1,6,6)(1,1,7,7)(1,1,8,8)(1,1,9,9)(1,6,0,1)(1,6,0,2)(1,6,0,3)(1,6,0,4)(1,6,0,5)(1,6,0,6)(1,6,0,7)(1,6,0,8)(1,6,0,9)(1,6,1,0)(1,6,1,2)(1,6,1,3)(1,6,1,4)(1,6,1,5)(1,6,1,6)(1,6,1,7)(1,6,1,8)(1,6,1,9)(1,6,2,0)(1,6,2,1)(1,6,2,3)(1,6,2,4)(1,6,2,5)(1,6,2,6)(1,6,2,7)(1,6,2,8)(1,6,2,9)(1,6,3,0)(1,6,3,1)(1,6,3,2)(1,6,3,4)(1,6,3,5)(1,6,3,6)(1,6,3,7)(1,6,3,8)(1,6,3,9)(1,6,4,0)(1,6,4,1)(1,6,4,2)(1,6,4,3)(1,6,4,5)(1,6,4,6)(1,6,4,7)(1,6,4,8)(1,6,4,9)(1,6,5,0)(1,6,5,1)(1,6,5,2)(1,6,5,3)(1,6,5,4)(1,6,5,6)(1,6,5,7)(1,6,5,8)(1,6,5,9)(1,6,6,0)(1,6,6,1)(1,6,6,2)(1,6,6,3)(1,6,6,4)(1,6,6,5)(1,6,6,7)(1,6,6,8)(1,6,6,9)(1,6,7,0)(1,6,7,1)(1,6,7,2)(1,6,7,3)(1,6,7,4)(1,6,7,5)(1,6,7,6)(1,6,7,8)(1,6,7,9)(1,6,8,0)(1,6,8,1)(1,6,8,2)(1,6,8,3)(1,6,8,4)(1,6,8,5)(1,6,8,6)(1,6,8,7)(1,6,8,9)(1,6,9,0)(1,6,9,1)(1,6,9,2)(1,6,9,3)(1,6,9,4)(1,6,9,5)(1,6,9,6)(1,6,9,7)(1,6,9,8)(2,2,0,0)(2,2,1,1)(2,2,2,2)(2,2,3,3)(2,2,4,4)(2,2,5,5)(2,2,6,6)(2,2,7,7)(2,2,8,8)(2,2,9,9)(2,6,0,1)(2,6,0,2)(2,6,0,3)(2,6,0,4)(2,6,0,5)(2,6,0,6)(2,6,0,7)(2,6,0,8)(2,6,0,9)(2,6,1,0)(2,6,1,2)(2,6,1,3)(2,6,1,4)(2,6,1,5)(2,6,1,6)(2,6,1,7)(2,6,1,8)(2,6,1,9)(2,6,2,0)(2,6,2,1)(2,6,2,3)(2,6,2,4)(2,6,2,5)(2,6,2,6)(2,6,2,7)(2,6,2,8)(2,6,2,9)(2,6,3,0)(2,6,3,1)(2,6,3,2)(2,6,3,4)(2,6,3,5)(2,6,3,6)(2,6,3,7)(2,6,3,8)(2,6,3,9)(2,6,4,0)(2,6,4,1)(2,6,4,2)(2,6,4,3)(2,6,4,5)(2,6,4,6)(2,6,4,7)(2,6,4,8)(2,6,4,9)(2,6,5,0)(2,6,5,1)(2,6,5,2)(2,6,5,3)(2,6,5,4)(2,6,5,6)(2,6,5,7)(2,6,5,8)(2,6,5,9)(2,6,6,0)(2,6,6,1)(2,6,6,2)(2,6,6,3)(2,6,6,4)(2,6,6,5)(2,6,6,7)(2,6,6,8)(2,6,6,9)(2,6,7,0)(2,6,7,1)(2,6,7,2)(2,6,7,3)(2,6,7,4)(2,6,7,5)(2,6,7,6)(2,6,7,8)(2,6,7,9)(2,6,8,0)(2,6,8,1)(2,6,8,2)(2,6,8,3)(2,6,8,4)(2,6,8,5)(2,6,8,6)(2,6,8,7)(2,6,8,9)(2,6,9,0)(2,6,9,1)(2,6,9,2)(2,6,9,3)(2,6,9,4)(2,6,9,5)(2,6,9,6)(2,6,9,7)(2,6,9,8)(3,3,0,0)(3,3,1,1)(3,3,2,2)(3,3,3,3)(3,3,4,4)(3,3,5,5)(3,3,6,6)(3,3,7,7)(3,3,8,8)(3,3,9,9)(3,6,0,1)(3,6,0,2)(3,6,0,3)(3,6,0,4)(3,6,0,5)(3,6,0,6)(3,6,0,7)(3,6,0,8)(3,6,0,9)(3,6,1,0)(3,6,1,2)(3,6,1,3)(3,6,1,4)(3,6,1,5)(3,6,1,6)(3,6,1,7)(3,6,1,8)(3,6,1,9)(3,6,2,0)(3,6,2,1)(3,6,2,3)(3,6,2,4)(3,6,2,5)(3,6,2,6)(3,6,2,7)(3,6,2,8)(3,6,2,9)(3,6,3,0)(3,6,3,1)(3,6,3,2)(3,6,3,4)(3,6,3,5)(3,6,3,6)(3,6,3,7)(3,6,3,8)(3,6,3,9)(3,6,4,0)(3,6,4,1)(3,6,4,2)(3,6,4,3)(3,6,4,5)(3,6,4,6)(3,6,4,7)(3,6,4,8)(3,6,4,9)(3,6,5,0)(3,6,5,1)(3,6,5,2)(3,6,5,3)(3,6,5,4)(3,6,5,6)(3,6,5,7)(3,6,5,8)(3,6,5,9)(3,6,6,0)(3,6,6,1)(3,6,6,2)(3,6,6,3)(3,6,6,4)(3,6,6,5)(3,6,6,7)(3,6,6,8)(3,6,6,9)(3,6,7,0)(3,6,7,1)(3,6,7,2)(3,6,7,3)(3,6,7,4)(3,6,7,5)(3,6,7,6)(3,6,7,8)(3,6,7,9)(3,6,8,0)(3,6,8,1)(3,6,8,2)(3,6,8,3)(3,6,8,4)(3,6,8,5)(3,6,8,6)(3,6,8,7)(3,6,8,9)(3,6,9,0)(3,6,9,1)(3,6,9,2)(3,6,9,3)(3,6,9,4)(3,6,9,5)(3,6,9,6)(3,6,9,7)(3,6,9,8)(4,4,0,0)(4,4,1,1)(4,4,2,2)(4,4,3,3)(4,4,4,4)(4,4,5,5)(4,4,6,6)(4,4,7,7)(4,4,8,8)(4,4,9,9)(4,6,0,1)(4,6,0,2)(4,6,0,3)(4,6,0,4)(4,6,0,5)(4,6,0,6)(4,6,0,7)(4,6,0,8)(4,6,0,9)(4,6,1,0)(4,6,1,2)(4,6,1,3)(4,6,1,4)(4,6,1,5)(4,6,1,6)(4,6,1,7)(4,6,1,8)(4,6,1,9)(4,6,2,0)(4,6,2,1)(4,6,2,3)(4,6,2,4)(4,6,2,5)(4,6,2,6)(4,6,2,7)(4,6,2,8)(4,6,2,9)(4,6,3,0)(4,6,3,1)(4,6,3,2)(4,6,3,4)(4,6,3,5)(4,6,3,6)(4,6,3,7)(4,6,3,8)(4,6,3,9)(4,6,4,0)(4,6,4,1)(4,6,4,2)(4,6,4,3)(4,6,4,5)(4,6,4,6)(4,6,4,7)(4,6,4,8)(4,6,4,9)(4,6,5,0)(4,6,5,1)(4,6,5,2)(4,6,5,3)(4,6,5,4)(4,6,5,6)(4,6,5,7)(4,6,5,8)(4,6,5,9)(4,6,6,0)(4,6,6,1)(4,6,6,2)(4,6,6,3)(4,6,6,4)(4,6,6,5)(4,6,6,7)(4,6,6,8)(4,6,6,9)(4,6,7,0)(4,6,7,1)(4,6,7,2)(4,6,7,3)(4,6,7,4)(4,6,7,5)(4,6,7,6)(4,6,7,8)(4,6,7,9)(4,6,8,0)(4,6,8,1)(4,6,8,2)(4,6,8,3)(4,6,8,4)(4,6,8,5)(4,6,8,6)(4,6,8,7)(4,6,8,9)(4,6,9,0)(4,6,9,1)(4,6,9,2)(4,6,9,3)(4,6,9,4)(4,6,9,5)(4,6,9,6)(4,6,9,7)(4,6,9,8)(5,5,0,0)(5,5,1,1)(5,5,2,2)(5,5,3,3)(5,5,4,4)(5,5,5,5)(5,5,6,6)(5,5,7,7)(5,5,8,8)(5,5,9,9)(5,6,0,1)(5,6,0,2)(5,6,0,3)(5,6,0,4)(5,6,0,5)(5,6,0,6)(5,6,0,7)(5,6,0,8)(5,6,0,9)(5,6,1,0)(5,6,1,2)(5,6,1,3)(5,6,1,4)(5,6,1,5)(5,6,1,6)(5,6,1,7)(5,6,1,8)(5,6,1,9)(5,6,2,0)(5,6,2,1)(5,6,2,3)(5,6,2,4)(5,6,2,5)(5,6,2,6)(5,6,2,7)(5,6,2,8)(5,6,2,9)(5,6,3,0)(5,6,3,1)(5,6,3,2)(5,6,3,4)(5,6,3,5)(5,6,3,6)(5,6,3,7)(5,6,3,8)(5,6,3,9)(5,6,4,0)(5,6,4,1)(5,6,4,2)(5,6,4,3)(5,6,4,5)(5,6,4,6)(5,6,4,7)(5,6,4,8)(5,6,4,9)(5,6,5,0)(5,6,5,1)(5,6,5,2)(5,6,5,3)(5,6,5,4)(5,6,5,6)(5,6,5,7)(5,6,5,8)(5,6,5,9)(5,6,6,0)(5,6,6,1)(5,6,6,2)(5,6,6,3)(5,6,6,4)(5,6,6,5)(5,6,6,7)(5,6,6,8)(5,6,6,9)(5,6,7,0)(5,6,7,1)(5,6,7,2)(5,6,7,3)(5,6,7,4)(5,6,7,5)(5,6,7,6)(5,6,7,8)(5,6,7,9)(5,6,8,0)(5,6,8,1)(5,6,8,2)(5,6,8,3)(5,6,8,4)(5,6,8,5)(5,6,8,6)(5,6,8,7)(5,6,8,9)(5,6,9,0)(5,6,9,1)(5,6,9,2)(5,6,9,3)(5,6,9,4)(5,6,9,5)(5,6,9,6)(5,6,9,7)(5,6,9,8)(6,6,0,0)(6,6,1,1)(6,6,2,2)(6,6,3,3)(6,6,4,4)(6,6,5,5)(6,6,6,6)(6,6,7,7)(6,6,8,8)(6,6,9,9) + + y[3][1] y[3][2] x[3][1] x[3][2] + y[4][3] y[4][4] x[4][3] x[4][4] + y[2][2] y[3][2] x[2][2] x[3][2] + y[3][4] y[4][4] x[3][4] x[4][4] + + + + %... + (1,1,0,0)(1,1,1,1)(1,1,2,2)(1,1,3,3)(1,1,4,4)(1,1,5,5)(1,1,6,6)(1,1,7,7)(1,1,8,8)(1,1,9,9)(2,1,0,1)(2,1,0,2)(2,1,0,3)(2,1,0,4)(2,1,0,5)(2,1,0,6)(2,1,0,7)(2,1,0,8)(2,1,0,9)(2,1,1,0)(2,1,1,2)(2,1,1,3)(2,1,1,4)(2,1,1,5)(2,1,1,6)(2,1,1,7)(2,1,1,8)(2,1,1,9)(2,1,2,0)(2,1,2,1)(2,1,2,3)(2,1,2,4)(2,1,2,5)(2,1,2,6)(2,1,2,7)(2,1,2,8)(2,1,2,9)(2,1,3,0)(2,1,3,1)(2,1,3,2)(2,1,3,4)(2,1,3,5)(2,1,3,6)(2,1,3,7)(2,1,3,8)(2,1,3,9)(2,1,4,0)(2,1,4,1)(2,1,4,2)(2,1,4,3)(2,1,4,5)(2,1,4,6)(2,1,4,7)(2,1,4,8)(2,1,4,9)(2,1,5,0)(2,1,5,1)(2,1,5,2)(2,1,5,3)(2,1,5,4)(2,1,5,6)(2,1,5,7)(2,1,5,8)(2,1,5,9)(2,1,6,0)(2,1,6,1)(2,1,6,2)(2,1,6,3)(2,1,6,4)(2,1,6,5)(2,1,6,7)(2,1,6,8)(2,1,6,9)(2,1,7,0)(2,1,7,1)(2,1,7,2)(2,1,7,3)(2,1,7,4)(2,1,7,5)(2,1,7,6)(2,1,7,8)(2,1,7,9)(2,1,8,0)(2,1,8,1)(2,1,8,2)(2,1,8,3)(2,1,8,4)(2,1,8,5)(2,1,8,6)(2,1,8,7)(2,1,8,9)(2,1,9,0)(2,1,9,1)(2,1,9,2)(2,1,9,3)(2,1,9,4)(2,1,9,5)(2,1,9,6)(2,1,9,7)(2,1,9,8)(2,2,0,0)(2,2,1,1)(2,2,2,2)(2,2,3,3)(2,2,4,4)(2,2,5,5)(2,2,6,6)(2,2,7,7)(2,2,8,8)(2,2,9,9)(3,1,0,1)(3,1,0,2)(3,1,0,3)(3,1,0,4)(3,1,0,5)(3,1,0,6)(3,1,0,7)(3,1,0,8)(3,1,0,9)(3,1,1,0)(3,1,1,2)(3,1,1,3)(3,1,1,4)(3,1,1,5)(3,1,1,6)(3,1,1,7)(3,1,1,8)(3,1,1,9)(3,1,2,0)(3,1,2,1)(3,1,2,3)(3,1,2,4)(3,1,2,5)(3,1,2,6)(3,1,2,7)(3,1,2,8)(3,1,2,9)(3,1,3,0)(3,1,3,1)(3,1,3,2)(3,1,3,4)(3,1,3,5)(3,1,3,6)(3,1,3,7)(3,1,3,8)(3,1,3,9)(3,1,4,0)(3,1,4,1)(3,1,4,2)(3,1,4,3)(3,1,4,5)(3,1,4,6)(3,1,4,7)(3,1,4,8)(3,1,4,9)(3,1,5,0)(3,1,5,1)(3,1,5,2)(3,1,5,3)(3,1,5,4)(3,1,5,6)(3,1,5,7)(3,1,5,8)(3,1,5,9)(3,1,6,0)(3,1,6,1)(3,1,6,2)(3,1,6,3)(3,1,6,4)(3,1,6,5)(3,1,6,7)(3,1,6,8)(3,1,6,9)(3,1,7,0)(3,1,7,1)(3,1,7,2)(3,1,7,3)(3,1,7,4)(3,1,7,5)(3,1,7,6)(3,1,7,8)(3,1,7,9)(3,1,8,0)(3,1,8,1)(3,1,8,2)(3,1,8,3)(3,1,8,4)(3,1,8,5)(3,1,8,6)(3,1,8,7)(3,1,8,9)(3,1,9,0)(3,1,9,1)(3,1,9,2)(3,1,9,3)(3,1,9,4)(3,1,9,5)(3,1,9,6)(3,1,9,7)(3,1,9,8)(3,3,0,0)(3,3,1,1)(3,3,2,2)(3,3,3,3)(3,3,4,4)(3,3,5,5)(3,3,6,6)(3,3,7,7)(3,3,8,8)(3,3,9,9)(4,1,0,1)(4,1,0,2)(4,1,0,3)(4,1,0,4)(4,1,0,5)(4,1,0,6)(4,1,0,7)(4,1,0,8)(4,1,0,9)(4,1,1,0)(4,1,1,2)(4,1,1,3)(4,1,1,4)(4,1,1,5)(4,1,1,6)(4,1,1,7)(4,1,1,8)(4,1,1,9)(4,1,2,0)(4,1,2,1)(4,1,2,3)(4,1,2,4)(4,1,2,5)(4,1,2,6)(4,1,2,7)(4,1,2,8)(4,1,2,9)(4,1,3,0)(4,1,3,1)(4,1,3,2)(4,1,3,4)(4,1,3,5)(4,1,3,6)(4,1,3,7)(4,1,3,8)(4,1,3,9)(4,1,4,0)(4,1,4,1)(4,1,4,2)(4,1,4,3)(4,1,4,5)(4,1,4,6)(4,1,4,7)(4,1,4,8)(4,1,4,9)(4,1,5,0)(4,1,5,1)(4,1,5,2)(4,1,5,3)(4,1,5,4)(4,1,5,6)(4,1,5,7)(4,1,5,8)(4,1,5,9)(4,1,6,0)(4,1,6,1)(4,1,6,2)(4,1,6,3)(4,1,6,4)(4,1,6,5)(4,1,6,7)(4,1,6,8)(4,1,6,9)(4,1,7,0)(4,1,7,1)(4,1,7,2)(4,1,7,3)(4,1,7,4)(4,1,7,5)(4,1,7,6)(4,1,7,8)(4,1,7,9)(4,1,8,0)(4,1,8,1)(4,1,8,2)(4,1,8,3)(4,1,8,4)(4,1,8,5)(4,1,8,6)(4,1,8,7)(4,1,8,9)(4,1,9,0)(4,1,9,1)(4,1,9,2)(4,1,9,3)(4,1,9,4)(4,1,9,5)(4,1,9,6)(4,1,9,7)(4,1,9,8)(4,4,0,0)(4,4,1,1)(4,4,2,2)(4,4,3,3)(4,4,4,4)(4,4,5,5)(4,4,6,6)(4,4,7,7)(4,4,8,8)(4,4,9,9)(5,1,0,1)(5,1,0,2)(5,1,0,3)(5,1,0,4)(5,1,0,5)(5,1,0,6)(5,1,0,7)(5,1,0,8)(5,1,0,9)(5,1,1,0)(5,1,1,2)(5,1,1,3)(5,1,1,4)(5,1,1,5)(5,1,1,6)(5,1,1,7)(5,1,1,8)(5,1,1,9)(5,1,2,0)(5,1,2,1)(5,1,2,3)(5,1,2,4)(5,1,2,5)(5,1,2,6)(5,1,2,7)(5,1,2,8)(5,1,2,9)(5,1,3,0)(5,1,3,1)(5,1,3,2)(5,1,3,4)(5,1,3,5)(5,1,3,6)(5,1,3,7)(5,1,3,8)(5,1,3,9)(5,1,4,0)(5,1,4,1)(5,1,4,2)(5,1,4,3)(5,1,4,5)(5,1,4,6)(5,1,4,7)(5,1,4,8)(5,1,4,9)(5,1,5,0)(5,1,5,1)(5,1,5,2)(5,1,5,3)(5,1,5,4)(5,1,5,6)(5,1,5,7)(5,1,5,8)(5,1,5,9)(5,1,6,0)(5,1,6,1)(5,1,6,2)(5,1,6,3)(5,1,6,4)(5,1,6,5)(5,1,6,7)(5,1,6,8)(5,1,6,9)(5,1,7,0)(5,1,7,1)(5,1,7,2)(5,1,7,3)(5,1,7,4)(5,1,7,5)(5,1,7,6)(5,1,7,8)(5,1,7,9)(5,1,8,0)(5,1,8,1)(5,1,8,2)(5,1,8,3)(5,1,8,4)(5,1,8,5)(5,1,8,6)(5,1,8,7)(5,1,8,9)(5,1,9,0)(5,1,9,1)(5,1,9,2)(5,1,9,3)(5,1,9,4)(5,1,9,5)(5,1,9,6)(5,1,9,7)(5,1,9,8)(5,5,0,0)(5,5,1,1)(5,5,2,2)(5,5,3,3)(5,5,4,4)(5,5,5,5)(5,5,6,6)(5,5,7,7)(5,5,8,8)(5,5,9,9)(6,1,0,1)(6,1,0,2)(6,1,0,3)(6,1,0,4)(6,1,0,5)(6,1,0,6)(6,1,0,7)(6,1,0,8)(6,1,0,9)(6,1,1,0)(6,1,1,2)(6,1,1,3)(6,1,1,4)(6,1,1,5)(6,1,1,6)(6,1,1,7)(6,1,1,8)(6,1,1,9)(6,1,2,0)(6,1,2,1)(6,1,2,3)(6,1,2,4)(6,1,2,5)(6,1,2,6)(6,1,2,7)(6,1,2,8)(6,1,2,9)(6,1,3,0)(6,1,3,1)(6,1,3,2)(6,1,3,4)(6,1,3,5)(6,1,3,6)(6,1,3,7)(6,1,3,8)(6,1,3,9)(6,1,4,0)(6,1,4,1)(6,1,4,2)(6,1,4,3)(6,1,4,5)(6,1,4,6)(6,1,4,7)(6,1,4,8)(6,1,4,9)(6,1,5,0)(6,1,5,1)(6,1,5,2)(6,1,5,3)(6,1,5,4)(6,1,5,6)(6,1,5,7)(6,1,5,8)(6,1,5,9)(6,1,6,0)(6,1,6,1)(6,1,6,2)(6,1,6,3)(6,1,6,4)(6,1,6,5)(6,1,6,7)(6,1,6,8)(6,1,6,9)(6,1,7,0)(6,1,7,1)(6,1,7,2)(6,1,7,3)(6,1,7,4)(6,1,7,5)(6,1,7,6)(6,1,7,8)(6,1,7,9)(6,1,8,0)(6,1,8,1)(6,1,8,2)(6,1,8,3)(6,1,8,4)(6,1,8,5)(6,1,8,6)(6,1,8,7)(6,1,8,9)(6,1,9,0)(6,1,9,1)(6,1,9,2)(6,1,9,3)(6,1,9,4)(6,1,9,5)(6,1,9,6)(6,1,9,7)(6,1,9,8)(6,6,0,0)(6,6,1,1)(6,6,2,2)(6,6,3,3)(6,6,4,4)(6,6,5,5)(6,6,6,6)(6,6,7,7)(6,6,8,8)(6,6,9,9) + + y[3][2] y[3][3] x[3][2] x[3][3] + y[3][4] y[3][5] x[3][4] x[3][5] + y[2][3] y[3][3] x[2][3] x[3][3] + y[2][5] y[3][5] x[2][5] x[3][5] + + + + diff --git a/cpmpy/tools/xcsp3/parser_callbacks.py b/cpmpy/tools/xcsp3/parser_callbacks.py new file mode 100644 index 000000000..ff1f5dd22 --- /dev/null +++ b/cpmpy/tools/xcsp3/parser_callbacks.py @@ -0,0 +1,855 @@ +""" +Collection of callbacks for the PyCSP3 parser +""" + +from functools import reduce + +from pycsp3.parser.callbacks import Callbacks +from pycsp3.classes.auxiliary.conditions import Condition +from pycsp3.classes.auxiliary.enums import (TypeConditionOperator, TypeArithmeticOperator, TypeUnaryArithmeticOperator, + TypeLogicalOperator, + TypeOrderedOperator, TypeRank, TypeObj) +from pycsp3.classes.main.variables import Variable +from pycsp3.classes.nodes import (Node) + +from pycsp3.parser.xentries import XVar +from pycsp3.tools.utilities import _Star + +import cpmpy as cp +from cpmpy.tools.xcsp3 import xcsp3_globals as xglobals +from cpmpy import cpm_array +from cpmpy.expressions.utils import flatlist, get_bounds, is_boolexpr + + +class CallbacksCPMPy(Callbacks): + """ + A pycsp3-compatible callback for parsing XCSP3 instances into a CPMPy model. + """ + + def __init__(self): + super().__init__() + self.cpm_model = cp.Model() + self.cpm_variables = dict() + self.print_general_methods = False + self.print_specific_methods = False + + def var_integer_range(self, x: Variable, min_value: int, max_value: int): + if min_value == 0 and max_value == 1: + # boolvar + newvar = cp.boolvar(name=x.id) + else: + newvar = cp.intvar(min_value, max_value, name=x.id) + self.cpm_variables[x] = newvar + + def var_integer(self, x: Variable, values: list[int]): + mini = min(values) + maxi = max(values) + if mini == 0 and maxi == 1: + # boolvar + newvar = cp.boolvar(name=x.id) + else: + newvar = cp.intvar(mini, maxi, name=x.id) + self.cpm_variables[x] = newvar + nbvals = maxi - mini + 1 + if len(values) < nbvals: + # only do this if there are holes in the domain + self.cpm_model += cp.InDomain(newvar, values) # faster decomp, only works in positive context + + def load_instance(self, discarded_classes=None): + return self.cpm_model, self.cpm_variables + + def ctr_true(self, scope: list[Variable]): + return cp.BoolVal(True) + + def ctr_false(self, scope: list[Variable]): + assert False, "Problem as a constraint with 0 supports: " + str(scope) + + def ctr_intension(self, scope: list[Variable], tree: Node): + cons = self.intentionfromtree(tree) + self.cpm_model += cons + + funcmap = { + # Arithmetic + "neg": (1, lambda x: -x), + "abs": (1, lambda x: abs(x)), + "add": (0, lambda x: cp.sum(x)), + "sub": (2, lambda x, y: x - y), + "mul": (0, lambda x: reduce((lambda x, y: x * y), x)), + "div": (2, lambda x, y: x // y), + "mod": (2, lambda x, y: x % y), + "sqr": (1, lambda x: x ** 2), + "pow": (2, lambda x, y: x ** y), + "min": (0, lambda x: cp.min(x)), + "max": (0, lambda x: cp.max(x)), + "dist": (2, lambda x, y: abs(x - y)), + # Relational + "lt": (2, lambda x, y: x < y), + "le": (2, lambda x, y: x <= y), + "ge": (2, lambda x, y: x >= y), + "gt": (2, lambda x, y: x > y), + "ne": (2, lambda x, y: x != y), + "eq": (0, lambda x: x[0] == x[1] if len(x) == 2 else cp.AllEqual(x)), + # Set + 'in': (2, lambda x, y: cp.InDomain(x, y)), # could be mixed context here! + 'notin': (2, lambda x, y: xglobals.NotInDomain(x, y)), # could be mixed context here! + 'set': (0, lambda x: list(set(x))), + # TODO 'notin' is the only other set operator (negative indomain) + # Logic + "not": (1, lambda x: ~x), + "and": (0, lambda x: cp.all(x)), + "or": (0, lambda x: cp.any(x)), + "xor": (0, lambda x: cp.Xor(x)), + "iff": (0, lambda x: cp.all(a == b for a, b in zip(x[:-1], x[1:]))), + "imp": (2, lambda x, y: x.implies(y)), + # control + "if": (3, lambda b, x, y: cp.IfThenElse(b, x, y) if is_boolexpr(x) and is_boolexpr(y) + else xglobals.IfThenElseNum(b, x, y)) + } + + def eval_cpm_comp(self, lhs, op: TypeConditionOperator, rhs): + if (op == TypeConditionOperator.IN) or (op == TypeConditionOperator.NOTIN): + assert isinstance(rhs, list), f"Expected list as rhs but got {rhs}" + + arity, cpm_op = self.funcmap[op.name.lower()] + if arity == 2: + return cpm_op(lhs, rhs) + elif arity == 0: + return cpm_op([lhs, rhs]) + else: + raise ValueError(f"Expected operator of arity 0 or 2 but got {cpm_op} which is of arity {arity}") + + def intentionfromtree(self, node): + if isinstance(node, Node): + if node.type.lowercase_name == 'var': + return self.cpm_variables[node.cnt] + if node.type.lowercase_name == 'int': + return node.cnt + arity, cpm_op = self.funcmap[node.type.lowercase_name] + cpm_args = [] + for arg in node.cnt: + cpm_args.append(self.intentionfromtree(arg)) + if arity != 0: + return cpm_op(*cpm_args) + return cpm_op(cpm_args) + else: + return node + + def ctr_primitive1a(self, x: Variable, op: TypeConditionOperator, k: int): + assert op.is_rel() + cpm_x = self.get_cpm_var(x) + self.cpm_model += self.eval_cpm_comp(cpm_x, op, k) + + def ctr_primitive1b(self, x: Variable, op: TypeConditionOperator, term: list[int] | range): + assert op.is_set() + x = self.get_cpm_var(x) + arity, cpm_op = self.funcmap[op.name.lower()] + if isinstance(term, range): + term = [x for x in term] # list from range + if arity == 2: + self.cpm_model += cpm_op(x, term) + elif arity == 0: + self.cpm_model += cpm_op([x, term]) + else: + self._unimplemented(x, op, term) + + def ctr_primitive1c(self, x: Variable, aop: TypeArithmeticOperator, p: int, op: TypeConditionOperator, k: int): + assert op.is_rel() + self.ctr_primitive3(x, aop, p, op, k) # for cpmpy ints and vars are just interchangeable.. + + def ctr_primitive2a(self, x: Variable, aop: TypeUnaryArithmeticOperator, y: Variable): + # TODO this was unimplemented not sure if it should be aop(x) == y or x == aop(y).. + arity, cpm_op = self.funcmap[aop.name.lower()] + assert arity == 1, "unary operator expected" + x = self.get_cpm_var(x) + y = self.get_cpm_var(y) + self.cpm_model += cpm_op(x) == y + + def ctr_primitive2b(self, x: Variable, aop: TypeArithmeticOperator, y: Variable, op: TypeConditionOperator, k: int): + # (x aop y) rel k + self.ctr_primitive3(x, aop, y, op, k) # for cpmpy ints and vars are just interchangeable.. + + def ctr_primitive2c(self, x: Variable, aop: TypeArithmeticOperator, p: int, op: TypeConditionOperator, y: Variable): + # (x aop p) op y + assert op.is_rel() + self.ctr_primitive3(x, aop, p, op, y) # for cpmpy ints and vars are just interchangeable.. + + def ctr_primitive3(self, x: Variable, aop: TypeArithmeticOperator, y: Variable, op: TypeConditionOperator, + z: Variable): + # (x aop y) op z + assert op.is_rel() + arity_op, cpm_op = self.funcmap[(op.name).lower()] + arity, cpm_aop = self.funcmap[aop.name.lower()] + x = self.get_cpm_var(x) + y = self.get_cpm_var(y) + z = self.get_cpm_var(z) + if arity == 2: + if arity_op == 2: + self.cpm_model += cpm_op(cpm_aop(x, y), z) + else: # eq is arity 0, because of allequal global + self.cpm_model += cpm_op([cpm_aop(x, y), z]) + elif arity == 0: + if arity_op == 2: + self.cpm_model += cpm_op(cpm_aop([x, y]), z) + else: # eq is arity 0, because of allequal global + self.cpm_model += cpm_op([cpm_aop([x, y]), z]) + + def ctr_logic(self, lop: TypeLogicalOperator, scope: list[Variable]): # lop(scope) + if lop == TypeLogicalOperator.AND: + self.cpm_model += self.get_cpm_vars(scope) + elif lop == TypeLogicalOperator.OR: + self.cpm_model += cp.any(self.get_cpm_vars(scope)) + elif lop == TypeLogicalOperator.IFF: + assert len(scope) == 2 + a, b = scope + self.cpm_model += self.get_cpm_var(a) == self.get_cpm_var(b) + elif lop == TypeLogicalOperator.IMP: + assert len(scope) == 2 + a, b = scope + self.cpm_model += self.get_cpm_var(a).implies(self.get_cpm_var(b)) + elif lop == TypeLogicalOperator.XOR: + self.cpm_model += cp.Xor(self.get_cpm_vars(scope)) + else: + self._unimplemented(lop, scope) + + def ctr_logic_reif(self, x: Variable, y: Variable, op: TypeConditionOperator, k: int | Variable): # x = y k + assert op.is_rel() + cpm_x, cpm_y, cpm_k = self.get_cpm_vars([x, y, k]) + self.cpm_model += self.eval_cpm_comp(cpm_x == cpm_y, op, cpm_k) + + def ctr_logic_eqne(self, x: Variable, op: TypeConditionOperator, lop: TypeLogicalOperator, + scope: list[Variable]): # x = lop(scope) or x != lop(scope) + cpm_x = self.get_cpm_var(x) + arity, cpm_op = self.funcmap[(lop.name).lower()] + if arity == 0: + rhs = cpm_op(self.get_cpm_vars(scope)) + elif arity == 2: + rhs = cpm_op(*self.get_cpm_vars(scope)) + else: + self._unimplemented(lop, scope) + + self.cpm_model += self.eval_cpm_comp(cpm_x, op, rhs) + + def unroll(self, values): + res = [] + for v in values: + if isinstance(v, range): + res.extend(v) + else: + res.append(v) + return res + + def ctr_extension_unary(self, x: Variable, values: list[int], positive: bool, flags: set[str]): + if len(values) == 1 and isinstance(values[0], range): + values = list(eval(str(values[0]))) + + if positive: + # unary table constraint is just an inDomain + if len(values) == 1: + self.cpm_model += self.get_cpm_var(x) == values[0] + else: + self.cpm_model += cp.InDomain(self.get_cpm_var(x), self.unroll(values)) # faster decomp, only works in positive context + else: + # negative, so not in domain + if len(values) == 1: + self.cpm_model += self.get_cpm_var(x) != values[0] + else: + self.cpm_model += xglobals.NotInDomain(self.get_cpm_var(x), self.unroll(values)) + + def ctr_extension(self, scope: list[Variable], tuples: list, positive: bool, flags: set[str]): + def strwildcard(x): + if isinstance(x, _Star): + return '*' + return x + + if 'starred' in flags: + cpm_vars = self.vars_from_node(scope) + exttuples = [tuple([strwildcard(x) for x in tup]) for tup in tuples] + if positive: + self.cpm_model += xglobals.ShortTable(cpm_vars, exttuples) + else: + self.cpm_model += xglobals.NegativeShortTable(cpm_vars, exttuples) + else: + cpm_vars = self.vars_from_node(scope) + if positive: + self.cpm_model += xglobals.Table(cpm_vars, tuples) + else: + self.cpm_model += cp.NegativeTable(cpm_vars, tuples) + + def ctr_regular(self, scope: list[Variable], transitions: list, start_state: str, final_states: list[str]): + self.cpm_model += xglobals.Regular(self.get_cpm_vars(scope), transitions, start_state, final_states) + + def ctr_mdd(self, scope: list[Variable], transitions: list): + self.cpm_model += xglobals.MDD(self.get_cpm_vars(scope), transitions) + + def ctr_all_different(self, scope: list[Variable] | list[Node], excepting: None | list[int]): + cpm_exprs = self.get_cpm_exprs(scope) + if excepting is None: + self.cpm_model += cp.AllDifferent(cpm_exprs) + elif len(excepting) > 0: + self.cpm_model += cp.AllDifferentExceptN(cpm_exprs, excepting) + elif len(excepting) == 0: + # just in case they get tricky + self.cpm_model += cp.AllDifferent(cpm_exprs) + else: # unsupported for competition + self._unimplemented(scope, excepting) + + def ctr_all_different_lists(self, lists: list[list[Variable]], excepting: None | list[list[int]]): + if excepting is None: + self.cpm_model += xglobals.AllDifferentLists([self.get_cpm_vars(lst) for lst in lists]) + else: + self.cpm_model += xglobals.AllDifferentListsExceptN([self.get_cpm_vars(lst) for lst in lists], excepting) + + def ctr_all_different_matrix(self, matrix: list[list[Variable]], excepting: None | list[int]): + import numpy as np + for row in matrix: + self.ctr_all_different(row, excepting) + for col in np.array(matrix).T: + self.ctr_all_different(col, excepting) + + def ctr_all_equal(self, scope: list[Variable] | list[Node], excepting: None | list[int]): + if excepting is None: + self.cpm_model += cp.AllEqual(self.get_cpm_exprs(scope)) + else: + self.cpm_model += cp.AllEqualExceptN(self.get_cpm_exprs(scope), excepting) + + def ctr_ordered(self, lst: list[Variable], operator: TypeOrderedOperator, + lengths: None | list[int] | list[Variable]): + + cpm_vars = self.get_cpm_vars(lst) + + if lengths is None: + if operator == TypeOrderedOperator.INCREASING: + self.cpm_model += cp.Increasing(cpm_vars) + elif operator == TypeOrderedOperator.STRICTLY_INCREASING: + self.cpm_model += cp.IncreasingStrict(cpm_vars) + elif operator == TypeOrderedOperator.DECREASING: + self.cpm_model += cp.Decreasing(cpm_vars) + elif operator == TypeOrderedOperator.STRICTLY_DECREASING: + self.cpm_model += cp.DecreasingStrict(cpm_vars) + else: + self._unimplemented(lst, operator, lengths) + + # also handle the lengths parameter + if lengths is not None: + lengths = self.get_cpm_vars(lengths) + if operator == TypeOrderedOperator.INCREASING: + for x, l, y in zip(cpm_vars[:-1], lengths, cpm_vars[1:]): + self.cpm_model += x + l <= y + elif operator == TypeOrderedOperator.STRICTLY_INCREASING: + for x, l, y in zip(cpm_vars[:-1], lengths, cpm_vars[1:]): + self.cpm_model += x + l < y + elif operator == TypeOrderedOperator.DECREASING: + for x, l, y in zip(cpm_vars[:-1], lengths, cpm_vars[1:]): + self.cpm_model += x + l >= y + elif operator == TypeOrderedOperator.STRICTLY_DECREASING: + for x, l, y in zip(cpm_vars[:-1], lengths, cpm_vars[1:]): + self.cpm_model += x + l > y + else: + self._unimplemented(lst, operator, lengths) + + def ctr_lex_limit(self, lst: list[Variable], limit: list[int], + operator: TypeOrderedOperator): # should soon enter XCSP3-core + self.ctr_lex([lst, limit], operator) + + def ctr_lex(self, lists: list[list[Variable]], operator: TypeOrderedOperator): + cpm_lists = [self.get_cpm_vars(lst) for lst in lists] + if operator == TypeOrderedOperator.STRICTLY_INCREASING: + self.cpm_model += cp.LexChainLess(cpm_lists) + elif operator == TypeOrderedOperator.INCREASING: + self.cpm_model += cp.LexChainLessEq(cpm_lists) + elif operator == TypeOrderedOperator.STRICTLY_DECREASING: + rev_lsts = list(reversed(cpm_lists)) + self.cpm_model += cp.LexChainLess(rev_lsts) + elif operator == TypeOrderedOperator.DECREASING: + rev_lsts = list(reversed(cpm_lists)) + self.cpm_model += cp.LexChainLessEq(rev_lsts) + else: + self._unimplemented(lists, operator) + + def ctr_lex_matrix(self, matrix: list[list[Variable]], operator: TypeOrderedOperator): + import numpy as np + # lex_chain on rows + self.ctr_lex(matrix, operator) + # lex chain on columns + self.ctr_lex(np.array(matrix).T.tolist(), operator) + + def ctr_precedence(self, lst: list[Variable], values: None | list[int], covered: bool): + if covered is True: # not supported for competition + self._unimplemented(lst, values, covered) + + cpm_vars = self.get_cpm_vars(lst) + if values is None: # assumed to be ordered set of all values collected from domains in lst + lbs, ubs = get_bounds(cpm_vars) + values = set() + for lb, ub in zip(lbs, ubs): + values.update(list(range(lb, ub + 1))) + values = sorted(values) + + self.cpm_model += cp.Precedence(cpm_vars, values) + + def ctr_sum(self, lst: list[Variable] | list[Node], coefficients: None | list[int] | list[Variable], + condition: Condition): + # cpm_vars = [] + # if isinstance(lst[0], XVar): + # for xvar in lst: + # cpm_vars.append(self.cpm_variables[xvar]) + # else: + # cpm_vars = self.exprs_from_node(lst) + # arity, op = self.funcmap[condition.operator.name.lower()] + # if hasattr(condition, "variable"): + # rhs = condition.variable + # elif hasattr(condition, 'value'): + # rhs = condition.value + # elif hasattr(condition, 'max'): + # #operator = in + # rhs = [x for x in range(condition.min, condition.max + 1)] + # else: + # pass + # cpm_rhs = self.get_cpm_var(rhs) + # if coefficients is None: + # cpsum = cp.sum(cpm_vars) + # else: + # cp_coeffs = self.get_cpm_vars(coefficients) + # cpsum = cp.sum(cp.cpm_array(cpm_vars) * cp_coeffs) + # if arity == 0: + # self.cpm_model += op([cpsum, cpm_rhs]) + # else: + # self.cpm_model += op(cpsum, cpm_rhs) + + import numpy as np + if coefficients is None or len(coefficients) == 0: + coefficients = np.ones(len(lst), dtype=int) # TODO I guess, if wsums are preferred over sums + elif isinstance(coefficients[0], Variable): # convert to cpmpy var + coefficients = cp.cpm_array(self.get_cpm_vars(coefficients)) + else: + coefficients = np.array(coefficients) + + lhs = cp.sum(coefficients * self.get_cpm_exprs(lst)) + + if (condition.operator == TypeConditionOperator.IN) or (condition.operator == TypeConditionOperator.NOTIN): + from pycsp3.classes.auxiliary.conditions import ConditionInterval, ConditionSet + assert isinstance(condition, + (ConditionInterval, ConditionSet)), "Competition only supports intervals when operator is `in` or `notin`" # TODO and not in? (notin) + if isinstance(condition, ConditionInterval): + rhs = list(range(condition.min, condition.max + 1)) + else: # ConditionSet + rhs = list(condition.t) + else: + rhs = self.get_cpm_var(condition.right_operand()) + + self.cpm_model += self.eval_cpm_comp(lhs, condition.operator, rhs) + + def ctr_count(self, lst: list[Variable] | list[Node], values: list[int] | list[Variable], condition: Condition): + # General case of count, can accept list of variables for any arg and any operator + cpm_vars = self.get_cpm_exprs(lst) + cpm_vals = self.get_cpm_vars(values) + if condition.operator == TypeConditionOperator.IN or (condition.operator == TypeConditionOperator.NOTIN): + from pycsp3.classes.auxiliary.conditions import ConditionInterval, ConditionSet + assert isinstance(condition, + (ConditionInterval, ConditionSet)), "Competition only supports intervals when operator is `in` or `notin`" # TODO and not in? (notin) + if isinstance(condition, ConditionInterval): + rhs = list(range(condition.min, condition.max + 1)) + else: # ConditionSet + rhs = list(condition.t) + else: + rhs = self.get_cpm_var(condition.right_operand()) + + count_for_each_val = [cp.Count(cpm_vars, val) for val in cpm_vals] + self.cpm_model += self.eval_cpm_comp(cp.sum(count_for_each_val), condition.operator, rhs) + + def ctr_atleast(self, lst: list[Variable] | list[Node], value: int, k: int): + cpm_exprs = self.get_cpm_exprs(lst) + self.cpm_model += (cp.Count(cpm_exprs, value) >= k) + + def ctr_atmost(self, lst: list[Variable], value: int, k: int): + cpm_vars = self.get_cpm_vars(lst) + self.cpm_model += (cp.Count(cpm_vars, value) <= k) + + def ctr_exactly(self, lst: list[Variable], value: int, k: int | Variable): + cpm_vars = self.get_cpm_exprs(lst) + self.cpm_model += (cp.Count(cpm_vars, value) == self.get_cpm_var(k)) + + def ctr_among(self, lst: list[Variable], values: list[int], k: int | Variable): + self.cpm_model += cp.Among(self.get_cpm_vars(lst), values) == self.get_cpm_var(k) + + def ctr_nvalues(self, lst: list[Variable] | list[Node], excepting: None | list[int], condition: Condition): + if excepting is None: + lhs = cp.NValue(self.get_cpm_exprs(lst)) + else: + assert len(excepting) == 1, "Competition only allows 1 integer value in excepting list" + lhs = cp.NValueExcept(self.get_cpm_exprs(lst), excepting[0]) + + if condition.operator == TypeConditionOperator.IN or (condition.operator == TypeConditionOperator.NOTIN): + from pycsp3.classes.auxiliary.conditions import ConditionInterval, ConditionSet + assert isinstance(condition, + (ConditionInterval, ConditionSet)), "Competition only supports intervals when operator is `in` or `notin`" # TODO and not in? (notin) + if isinstance(condition, ConditionInterval): + rhs = list(range(condition.min, condition.max + 1)) + else: # ConditionSet + rhs = list(condition.t) + else: + rhs = self.get_cpm_var(condition.right_operand()) + + self.cpm_model += self.eval_cpm_comp(lhs, condition.operator, rhs) + + def ctr_not_all_qual(self, lst: list[Variable]): + cpm_vars = self.get_cpm_vars(lst) + self.cpm_model += cp.NValue(cpm_vars) > 1 + + def ctr_cardinality(self, lst: list[Variable], values: list[int] | list[Variable], + occurs: list[int] | list[Variable] | list[range], closed: bool): + self.cpm_model += xglobals.GlobalCardinalityCount(self.get_cpm_exprs(lst), + self.get_cpm_exprs(values), + self.get_cpm_exprs(occurs), + closed=closed) + + def ctr_minimum(self, lst: list[Variable] | list[Node], condition: Condition): + cpm_vars = self.get_cpm_exprs(lst) + self.cpm_model += self.eval_cpm_comp(cp.Minimum(cpm_vars), + condition.operator, + self.get_cpm_var(condition.right_operand())) + + def ctr_maximum(self, lst: list[Variable] | list[Node], condition: Condition): + cpm_vars = self.get_cpm_exprs(lst) + self.cpm_model += self.eval_cpm_comp(cp.Maximum(cpm_vars), + condition.operator, + self.get_cpm_var(condition.right_operand())) + + def ctr_minimum_arg(self, lst: list[Variable] | list[Node], condition: Condition, + rank: TypeRank): # should enter XCSP3-core + self._unimplemented(lst, condition, rank) + + def ctr_maximum_arg(self, lst: list[Variable] | list[Node], condition: Condition, + rank: TypeRank): # should enter XCSP3-core + self._unimplemented(lst, condition, rank) + + def ctr_element(self, lst: list[Variable] | list[int], i: Variable, condition: Condition): + cpm_lst = self.get_cpm_vars(lst) + cpm_index = self.get_cpm_var(i) + cpm_rhs = self.get_cpm_var(condition.right_operand()) + self.cpm_model += self.eval_cpm_comp(xglobals.Element(cpm_lst, cpm_index), condition.operator, cpm_rhs) + + def ctr_element_matrix(self, matrix: list[list[Variable]] | list[list[int]], i: Variable, j: Variable, + condition: Condition): + + # this can be optimized by indexing into the matrix directly + mtrx = cp.cpm_array([self.get_cpm_vars(lst) for lst in matrix]) + dim1, dim2 = mtrx.shape + + cpm_i, cpm_j = self.get_cpm_vars([i, j]) + cpm_rhs = self.get_cpm_var(condition.right_operand()) + # ensure i,j are within bounds, we can do this as it is a toplevel constraint + self.cpm_model += [cpm_i >= 0, cpm_i < dim1, cpm_j >= 0, cpm_j < dim2] + + # flatten matrix and lookup with weighed sum + self.cpm_model += self.eval_cpm_comp(xglobals.Element(flatlist(mtrx), dim1 * cpm_i + cpm_j), condition.operator, cpm_rhs) + + + def ctr_channel(self, lst1: list[Variable], lst2: None | list[Variable]): + + if lst2 is None: + self.cpm_model += xglobals.InverseOne(self.get_cpm_vars(lst1)) + else: + cpm_vars1 = self.get_cpm_vars(lst1) + cpm_vars2 = self.get_cpm_vars(lst2) + # Ignace: deprecated, we have InverseAsym now which gets constructed automatically + # # make lists same length, last part is irrelevant if not same length + # if len(cpm_vars2) > len(cpm_vars1): + # cpm_vars2 = cpm_vars2[0:len(cpm_vars1)] + # elif len(cpm_vars1) > len(cpm_vars2): + # cpm_vars1 = cpm_vars1[0:len(cpm_vars2)] + self.cpm_model += xglobals.Inverse(cpm_vars1, cpm_vars2) + + def ctr_channel_value(self, lst: list[Variable], value: Variable): + self.cpm_model += xglobals.Channel(self.get_cpm_vars(lst), self.get_cpm_var(value)) + + def ctr_nooverlap(self, origins: list[Variable], lengths: list[int] | list[Variable], + zero_ignored: bool): # in XCSP3 competitions, no 0 permitted in lengths + cpm_start = self.get_cpm_vars(origins) + cpm_dur = self.get_cpm_vars(lengths) + cpm_end = [cp.intvar(*get_bounds(s + d)) for s, d in zip(cpm_start, cpm_dur)] + self.cpm_model += cp.NoOverlap(cpm_start, cpm_dur, cpm_end) + + def ctr_nooverlap_multi(self, origins: list[list[Variable]], lengths: list[list[int]] | list[list[Variable]], + zero_ignored: bool): + dim = len(origins[0]) + if dim == 2: + + start_x, start_y = self.get_cpm_vars([o[0] for o in origins]), self.get_cpm_vars([o[1] for o in origins]) + dur_x, dur_y = self.get_cpm_vars([l[0] for l in lengths]), self.get_cpm_vars([l[1] for l in lengths]) + + end_x = [cp.intvar(*get_bounds(s + d)) for s, d in zip(start_x, dur_x)] + end_y = [cp.intvar(*get_bounds(s + d)) for s, d in zip(start_y, dur_y)] + + self.cpm_model += xglobals.NoOverlap2d(start_x, dur_x, end_x, + start_y, dur_y, end_y) + + else: # n-dimensional, post decomposition directly + from cpmpy.expressions.utils import all_pairs + from cpmpy import any as cpm_any + starts = cp.cpm_array([self.get_cpm_vars(lst) for lst in origins]) + durs = cp.cpm_array([self.get_cpm_vars(lst) for lst in lengths]) + + for i, j in all_pairs(list(range(len(origins)))): + self.cpm_model += cpm_any([(starts[i, d] + durs[i, d] <= starts[j, d]) | \ + (starts[j, d] + durs[j, d] <= starts[i, d]) for d in range(dim)]) + + def ctr_nooverlap_mixed(self, xs: list[Variable], ys: list[Variable], lx: list[Variable], ly: list[int], + zero_ignored: bool): + start_x = self.get_cpm_vars(xs) + start_y = self.get_cpm_vars(ys) + dur_x = self.get_cpm_vars(lx) + dur_y = ly + + end_x = [cp.intvar(*get_bounds(s + d)) for s, d in zip(start_x, dur_x)] + end_y = [cp.intvar(*get_bounds(s + d)) for s, d in zip(start_y, dur_y)] + + self.cpm_model += xglobals.NoOverlap2d(start_x, dur_x, end_x, + start_y, dur_y, end_y) + + def ctr_cumulative(self, origins: list[Variable], lengths: list[int] | list[Variable], + heights: list[int] | list[Variable], condition: Condition): + cpm_start = self.get_cpm_exprs(origins) + cpm_durations = self.get_cpm_exprs(lengths) + cpm_demands = self.get_cpm_exprs(heights) + cpm_ends = [] + for s, d in zip(cpm_start, cpm_durations): + expr = s + d + cpm_ends.append(cp.intvar(*get_bounds(expr))) + + if condition.operator == TypeConditionOperator.LE: + self.cpm_model += xglobals.Cumulative(cpm_start, cpm_durations, cpm_ends, cpm_demands, + self.get_cpm_var(condition.right_operand())) + else: + # post decomposition directly + # be smart and chose task or time decomposition #TODO you did task decomp in both cases + if max(get_bounds(cpm_ends)[1]) >= 100: + self._cumulative_task_decomp(cpm_start, cpm_durations, cpm_ends, heights, condition) + else: + self._cumulative_time_decomp(cpm_start, cpm_durations, cpm_ends, heights, condition) + + def _cumulative_task_decomp(self, cpm_start, cpm_duration, cpm_ends, cpm_demands, condition: Condition): + cpm_demands = cp.cpm_array(cpm_demands) + cpm_cap = self.get_cpm_var(condition.right_operand()) + # ensure durations are satisfied + for s, d, e in zip(cpm_start, cpm_duration, cpm_ends): + self.cpm_model += s + d == e + + # task decomposition + for s, d, e in zip(cpm_start, cpm_duration, cpm_ends): + # find overlapping tasks + total_running = cp.sum(cpm_demands * ((cpm_start <= s) & (cpm_ends > s))) + self.cpm_model += self.eval_cpm_comp(total_running, condition.operator, cpm_cap) + + def _cumulative_time_decomp(self, cpm_start, cpm_duration, cpm_ends, cpm_demands, condition: Condition): + cpm_demands = cp.cpm_array(cpm_demands) + cpm_cap = self.get_cpm_var(condition.right_operand) + # ensure durations are satisfied + for s, d, e in zip(cpm_start, cpm_duration, cpm_ends): + self.cpm_model += s + d == e + + lb = min(get_bounds(cpm_start)[0]) + ub = max(get_bounds(cpm_ends)[1]) + # time decomposition + for t in range(lb, ub + 1): + total_running = cp.sum(cpm_demands * ((cpm_start <= t) & (cpm_ends > t))) + self.cpm_model += self.eval_cpm_comp(total_running, condition.operator, cpm_cap) + + def ctr_binpacking(self, lst: list[Variable], sizes: list[int], condition: Condition): + cpm_vars = self.get_cpm_vars(lst) + cpm_rhs = self.get_cpm_var(condition.right_operand()) + + for bin in range(0, len(cpm_vars)): # bin labeling starts at 0, contradicting the xcsp3 specification document? + self.cpm_model += self.eval_cpm_comp(cp.sum((cpm_array(cpm_vars) == bin) * sizes), + condition.operator, + cpm_rhs) + + def ctr_binpacking_limits(self, lst: list[Variable], sizes: list[int], limits: list[int] | list[Variable]): + from cpmpy.expressions.utils import eval_comparison + + cpm_vars = self.get_cpm_vars(lst) + + for bin, lim in enumerate(limits): + self.cpm_model += eval_comparison("<=", + cp.sum((cpm_array(cpm_vars) == (bin)) * sizes), + lim) + + def ctr_binpacking_loads(self, lst: list[Variable], sizes: list[int], loads: list[int] | list[Variable]): + from cpmpy.expressions.utils import eval_comparison + + cpm_vars = self.get_cpm_vars(lst) + cpm_loads = self.get_cpm_vars(loads) + + for bin, load in enumerate(cpm_loads): + self.cpm_model += eval_comparison("==", + cp.sum((cpm_array(cpm_vars) == (bin)) * sizes), + load) + + def ctr_binpacking_conditions(self, lst: list[Variable], sizes: list[int], + conditions: list[Condition]): # not in XCSP3-core + self._unimplemented(lst, sizes, conditions) + + def ctr_knapsack(self, lst: list[Variable], weights: list[int], wcondition: Condition, profits: list[int], + pcondition: Condition): + + vars = cpm_array(self.get_cpm_vars(lst)) + cpm_weight = self.get_cpm_var(wcondition.right_operand()) + cpm_profit = self.get_cpm_var(pcondition.right_operand()) + + total_weight = cp.sum(vars * weights) + total_profit = cp.sum(vars * profits) + self.cpm_model += self.eval_cpm_comp(total_weight, wcondition.operator, cpm_weight) + self.cpm_model += self.eval_cpm_comp(total_profit, pcondition.operator, cpm_profit) + + def ctr_flow(self, lst: list[Variable], balance: list[int] | list[Variable], arcs: list, + capacities: None | list[range]): # not in XCSP3-core + self._unimplemented(lst, balance, arcs, capacities) + + def ctr_flow_weighted(self, lst: list[Variable], balance: list[int] | list[Variable], arcs: list, + capacities: None | list[range], + weights: list[int] | list[Variable], + condition: Condition): # not in XCSP3-core + self._unimplemented(lst, balance, arcs, capacities, weights, condition) + + def ctr_instantiation(self, lst: list[Variable], values: list[int]): + self.cpm_model += xglobals.Table(self.get_cpm_vars(lst), [values]) + + def ctr_clause(self, pos: list[Variable], neg: list[Variable]): # not in XCSP3-core + self._unimplemented(pos, neg) + + def ctr_circuit(self, lst: list[Variable], size: None | int | Variable): # size is None in XCSP3 competitions + self.cpm_model += xglobals.SubCircuitWithStart(self.get_cpm_vars(lst), start_index=0) + + # # # # # # # # # # + # All methods about objectives to be implemented + # # # # # # # # # # + + def obj_minimize(self, term: Variable | Node): + if isinstance(term, Node): + cpm_expr = self.exprs_from_node([term]) + assert len(cpm_expr) == 1 + cpm_expr = cpm_expr[0] + else: + cpm_expr = self.get_cpm_var(term) + self.cpm_model.minimize(cpm_expr) + + def obj_maximize(self, term: Variable | Node): + if isinstance(term, Node): + cpm_expr = self.exprs_from_node([term]) + assert len(cpm_expr) == 1 + cpm_expr = cpm_expr[0] + else: + cpm_expr = self.get_cpm_var(term) + self.cpm_model.maximize(cpm_expr) + + def obj_minimize_special(self, obj_type: TypeObj, terms: list[Variable] | list[Node], + coefficients: None | list[int]): + import numpy as np + if coefficients is None: + coefficients = np.ones(len(terms)) + else: + coefficients = np.array(coefficients) + + if obj_type == TypeObj.SUM: + self.cpm_model.minimize(cp.sum(coefficients * self.get_cpm_exprs(terms))) + elif obj_type == TypeObj.MAXIMUM: + self.cpm_model.minimize(cp.max(coefficients * self.get_cpm_exprs(terms))) + elif obj_type == TypeObj.MINIMUM: + self.cpm_model.minimize(cp.min(coefficients * self.get_cpm_exprs(terms))) + elif obj_type == TypeObj.NVALUES: + self.cpm_model.minimize(cp.NValue(coefficients * self.get_cpm_exprs(terms))) + elif obj_type == TypeObj.EXPRESSION: + assert all(coeff == 1 for coeff in coefficients) + assert len(terms) == 1 + cpm_expr = self.get_cpm_exprs(terms)[0] + self.cpm_model.minimize(cpm_expr) + else: + self._unimplemented(obj_type, coefficients, terms) + + # TODO objectives are a bit confusing but i think it's correct + def obj_maximize_special(self, obj_type: TypeObj, terms: list[Variable] | list[Node], + coefficients: None | list[int]): + import numpy as np + if coefficients is None: + coefficients = np.ones(len(terms)) + else: + coefficients = np.array(coefficients) + + if obj_type == TypeObj.SUM: + self.cpm_model.maximize(cp.sum(coefficients * self.get_cpm_exprs(terms))) + elif obj_type == TypeObj.MAXIMUM: + self.cpm_model.maximize(cp.max(coefficients * self.get_cpm_exprs(terms))) + elif obj_type == TypeObj.MINIMUM: + self.cpm_model.maximize(cp.min(coefficients * self.get_cpm_exprs(terms))) + elif obj_type == TypeObj.NVALUES: + self.cpm_model.maximize(cp.NValue(coefficients * self.get_cpm_exprs(terms))) + elif obj_type == TypeObj.EXPRESSION: + assert all(coeff == 1 for coeff in coefficients) + assert len(terms) == 1 + cpm_expr = self.get_cpm_exprs(terms)[0] + self.cpm_model.maximize(cpm_expr) + else: + self._unimplemented(obj_type, coefficients, terms) + + def vars_from_node(self, scope): + cpm_vars = [] + for var in scope: + cpm_var = self.cpm_variables[var] + cpm_vars.append(cpm_var) + return cpm_vars + + def exprs_from_node(self, node): + cpm_exprs = [] + for expr in node: + cpm_expr = self.intentionfromtree(expr) + cpm_exprs.append(cpm_expr) + return cpm_exprs + + def get_cpm_var(self, x): + if isinstance(x, XVar): + return self.cpm_variables[x] + else: + return x # constants + + def get_cpm_vars(self, lst): + if isinstance(lst[0], (XVar, int)): + return [self.get_cpm_var(x) for x in lst] + if isinstance(lst[0], range): + assert len(lst) == 1, "Expected range here, but got list with multiple elements, what's the semantics???" + return list(lst[0]) # this should work without converting to str first + # return list(eval(str(lst[0]))) + else: + return self.vars_from_node(lst) + + def get_cpm_exprs(self, lst): + if isinstance(lst[0], XVar): + return [self.get_cpm_var(x) for x in lst] + if isinstance(lst[0], range): + # assert len(lst) == 1, f"Expected range here, but got list with multiple elements, what's the semantics???{lst}" + + if len(lst) == 1: + return list(lst[0]) # this should work without converting to str first + else: + return [cp.intvar(l.start, l.stop - 1) for l in lst] + + # return list(eval(str(lst[0]))) + else: + return self.exprs_from_node(lst) + + def end_instance(self): + pass + + def load_annotation(self, annotation): + pass + + def load_annotations(self, annotations): + pass + + def load_objectives(self, objectives): + pass + + def ann_decision(self, lst: list[Variable]): + pass + + def ann_val_heuristic_static(self, lst: list[Variable], order: list[int]): + pass diff --git a/cpmpy/tools/xcsp3/requirements.txt b/cpmpy/tools/xcsp3/requirements.txt new file mode 100644 index 000000000..00d30ba74 --- /dev/null +++ b/cpmpy/tools/xcsp3/requirements.txt @@ -0,0 +1,7 @@ +pycsp3 +requests +tqdm +matplotlib +psutil +filelock +readline \ No newline at end of file diff --git a/cpmpy/tools/xcsp3/test_assgn_ilp.py b/cpmpy/tools/xcsp3/test_assgn_ilp.py new file mode 100644 index 000000000..61ed684a7 --- /dev/null +++ b/cpmpy/tools/xcsp3/test_assgn_ilp.py @@ -0,0 +1,46 @@ +import cpmpy as cp +import xcsp3_globals as xglobals +import numpy as np + +# Sample data +n_workers = 3 +n_tasks = 4 + +# Value matrix: value[i,j] represents the value of worker i doing task j +value = np.array([ + [5, 2, 3, 4], + [3, 4, 5, 2], + [2, 5, 4, 3], +]) + +# Create the model +model = cp.Model() + +# Decision variables: task[i] represents which task is assigned to worker i +task = cp.intvar(0, n_tasks-1, shape=n_workers, name="task") + +# Each worker works on a different task +model += cp.AllDifferent(task) +model += cp.sum(cp.Count(task, 0), cp.Count(task, 2)) <= 3 # to test count + +# Objective: maximize sum of values +obj = cp.intvar(np.min(value), np.sum(value), name="obj") +model += (obj == sum(xglobals.Element(value[i], task[i]) for i in range(n_workers))) +model.maximize(obj) + +print("CPMpy model:") +print(model) + +print("---------------------------") +print("Ortools transformed model:") +print(cp.SolverLookup.get("ortools").transform(model.constraints)) +print(model.solve(), model.status(), obj.value()) + +print("---------------------------") +print("Exact transformed model:") +print(cp.SolverLookup.get("exact").transform(model.constraints)) +obj._value = None +m2 = cp.Model(cp.SolverLookup.get("exact").transform(model.constraints), + maximize=obj) +print(m2.solve(), m2.status(), obj.value()) + diff --git a/cpmpy/tools/xcsp3/utils/benchmark.py b/cpmpy/tools/xcsp3/utils/benchmark.py new file mode 100644 index 000000000..b06e5c60b --- /dev/null +++ b/cpmpy/tools/xcsp3/utils/benchmark.py @@ -0,0 +1,47 @@ +""" +Run the benchmark for the targets (year and track) across all solvers. +""" + +import subprocess +import logging + +TIME_LIMIT = 60 +NR_WORKERS = 20 +SOLVERS = ["ortools", "exact", "choco", "z3", "minizinc", "gurobi", "exact", "cpo"] + +TARGETS = { + "2024": ["COP", "CSP", "MiniCSP", "MiniCOP"] +} + + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" +) + +def run_with_solver(solver: str, year:str, track:str, time_limit: int = 60, workers: int=20): + logging.info(f"Running solver: {solver}") + command = [ + "python", + "libraries/cpmpy/cpmpy/tools/xcsp3/xcsp3_benchmark.py", + "--year", year, + "--track", track, + "--solver", solver, + "--time-limit", str(time_limit), + "--workers", str(workers), + "--output-dir", f"results/{year}/{track}" + ] + try: + print(" ".join(command)) + subprocess.run(command)#, check=True) + logging.info(f"Solver {solver} finished successfully.\n") + except subprocess.CalledProcessError as e: + logging.error(f"Solver {solver} failed with error code {e.returncode}.\n") + + +for year, tracks in TARGETS.items(): + for track in tracks: + for solver in SOLVERS: + run_with_solver(solver, year=year, track=track, time_limit=TIME_LIMIT, workers=NR_WORKERS) diff --git a/cpmpy/tools/xcsp3/utils/plot_ecdf.py b/cpmpy/tools/xcsp3/utils/plot_ecdf.py new file mode 100644 index 000000000..09277b314 --- /dev/null +++ b/cpmpy/tools/xcsp3/utils/plot_ecdf.py @@ -0,0 +1,84 @@ +""" +Very basic script to create ECDF plots comparing multiple benchmark runs. +""" + +from pathlib import Path +import re +import pandas as pd +import matplotlib.pyplot as plt +import seaborn as sns +import os + + +# === USER-DEFINED INPUT FILES === +input_files = { + "results/xcsp3_2024_CSP_ortools_20250522_100934.csv": "old", + "results/xcsp3_2024_CSP_ortools_20250522_101358.csv": "new", +} + +# Time columns to compare +time_columns = ['time_total', 'time_parse', 'time_model', 'time_post', 'time_solve'] +output_folder = "ecdf_plots" # Folder to save images + + +# Set style for seaborn +sns.set_theme(style="whitegrid") + +os.makedirs(output_folder, exist_ok=True) + +# Read CSVs and extract metadata +dataframes = {} +file_metadata = {} # label -> (year, track) +for path, label in input_files.items(): + df = pd.read_csv(path) + dataframes[label] = df + + # Extract year and track from filename + filename = os.path.basename(path) + match = re.match(r'xcsp3_(\d{4})_(\w+)_', filename) + if match: + year, track = match.groups() + else: + year, track = "UnknownYear", "UnknownTrack" + file_metadata[label] = (year, track) + +# Generate ECDF plots for each time metric +# Generate ECDF plots for each time metric +for time_col in time_columns: + plt.figure(figsize=(10, 6)) + + max_instances = 0 # Track overall max for the dotted line + + for label, df in dataframes.items(): + if time_col in df.columns: + df_clean = df[df[time_col].notna()] + n = len(df_clean) + if n == 0: + continue + max_instances = max(max_instances, n) + + sorted_values = df_clean[time_col].sort_values() + y_values = range(1, n + 1) + plt.step(sorted_values, y_values, where="post", label=label) + + # Get year/track from one file (assume same for all) + example_label = next(iter(file_metadata)) + year, track = file_metadata[example_label] + + # Plot the max line + plt.axhline(y=max_instances, color="gray", linestyle="dotted", linewidth=1) + + # Labels and layout + plt.title(f"ECDF of {time_col} - Track: {track}, Year: {year}") + plt.xlabel(f"{time_col} (seconds)") + plt.ylabel("Number of Instances") + plt.legend(title="Version") + plt.grid(True) + plt.tight_layout() + + # Save plot + file_path = os.path.join(output_folder, f"ecdf_{time_col}.png") + plt.savefig(file_path, dpi=300) + plt.close() + +print(f"Saved ECDF plots to: {output_folder}") \ No newline at end of file diff --git a/cpmpy/tools/xcsp3/utils/result_copier.py b/cpmpy/tools/xcsp3/utils/result_copier.py new file mode 100644 index 000000000..9ff9c0083 --- /dev/null +++ b/cpmpy/tools/xcsp3/utils/result_copier.py @@ -0,0 +1,70 @@ +""" +CLI tool to extract the latest run from each solver. + +E.g. + + python cpmpy/tools/xcsp3/utils/result_copier.py results/2024/COP/ + +Will place the latest runs in results/2024/COP/best + +Usefull when analysing runs with xcsp3_analyzer.py +""" + +import os +import re +import shutil +from pathlib import Path +from collections import defaultdict +import argparse + +def copy_latest_csvs(source_dir, destination_dir): + source = Path(source_dir) + destination = Path(destination_dir) + + # If destination exists, remove all files inside it + if destination.exists(): + for file in destination.glob('*'): + if file.is_file(): + file.unlink() + else: + destination.mkdir(parents=True, exist_ok=True) + + # Regex pattern to match: prefix_timestamp (e.g. xcsp3_2023_CSP23_choco_20250515_134044.csv) + pattern = re.compile(r'(.+?)_(\d{8}_\d{6})\.csv$') + + latest_files = {} + + for file in source.glob('*.csv'): + match = pattern.match(file.name) + if not match: + continue + prefix, timestamp = match.groups() + if prefix not in latest_files or timestamp > latest_files[prefix][0]: + latest_files[prefix] = (timestamp, file) + + # Copy files + for prefix, (timestamp, file) in latest_files.items(): + target = destination / file.name + shutil.copy2(file, target) + print(f"Copied: {file.name} -> {target}") + +def main(): + parser = argparse.ArgumentParser(description="Copy the latest CSVs from source to destination.") + parser.add_argument( + "source", + type=str, + help="Path to the source directory containing CSV files" + ) + + args = parser.parse_args() + + source_dir = Path(args.source) + if not source_dir.exists() or not source_dir.is_dir(): + print(f"Error: {source_dir} is not a valid directory.") + return + + destination_dir = source_dir / "best" + copy_latest_csvs(source_dir, destination_dir) + +if __name__ == "__main__": + main() diff --git a/cpmpy/tools/xcsp3/utils/yappi/combine_profiles.py b/cpmpy/tools/xcsp3/utils/yappi/combine_profiles.py new file mode 100644 index 000000000..579baadf0 --- /dev/null +++ b/cpmpy/tools/xcsp3/utils/yappi/combine_profiles.py @@ -0,0 +1,75 @@ +import pstats +from pathlib import Path +import argparse +import os + +def combine_pstats(profile_dir): + combined_stats = None + profile_dir = Path(profile_dir) + + for profile_file in sorted(profile_dir.glob("*.pstat")): + stats = pstats.Stats(str(profile_file)) + if combined_stats is None: + combined_stats = stats + else: + combined_stats.add(stats) + + return combined_stats + + +def relativize_path(path, base): + """Return path relative to base, or original if not under base.""" + try: + return str(Path(path).relative_to(base)) + except ValueError: + return str(path) + + +def print_pretty_stats(stats, limit=50, fullpath=False): + cwd = Path.cwd() + if not fullpath: + stats.strip_dirs() # remove directory info if not full path + + entries = [] + for func, data in stats.stats.items(): + filename, lineno, funcname = func + cc, nc, tt, ct, callers = data + + if fullpath: + display_file = relativize_path(filename, cwd) + else: + display_file = os.path.basename(filename) + + entries.append({ + "ncalls": nc, + "tottime": tt, + "percall": tt / nc if nc else 0, + "cumtime": ct, + "percall_cum": ct / nc if nc else 0, + "func": f"{display_file}:{lineno}({funcname})" + }) + + entries.sort(key=lambda e: e["tottime"], reverse=True) + + header_fmt = "{:>10} {:>8} {:>8} {:>8} {:>8} {}" + print(header_fmt.format("ncalls", "tottime", "percall", "cumtime", "percall", "function")) + + line_fmt = "{ncalls:10} {tottime:8.3f} {percall:8.3f} {cumtime:8.3f} {percall_cum:8.3f} {func}" + for e in entries[:limit]: + print(line_fmt.format(**e)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Combine and print pstats files with aligned columns.") + parser.add_argument("profile_dir", nargs="?", default="profiles", + help="Directory containing .pstat files (default: profiles)") + parser.add_argument("--limit", type=int, default=50, help="Max number of entries to print") + parser.add_argument("--fullpath", action="store_true", help="Show full file paths (relative to cwd)") + + args = parser.parse_args() + + combined = combine_pstats(args.profile_dir) + if combined: + print_pretty_stats(combined, limit=args.limit, fullpath=args.fullpath) + else: + print("No profiles found.") diff --git a/cpmpy/tools/xcsp3/utils/yappi/print_profiles.py b/cpmpy/tools/xcsp3/utils/yappi/print_profiles.py new file mode 100644 index 000000000..1e32b33be --- /dev/null +++ b/cpmpy/tools/xcsp3/utils/yappi/print_profiles.py @@ -0,0 +1,65 @@ +import argparse +import pstats +from pathlib import Path + +def print_pretty_profile(pstat_file, sort_by="tottime", output_file=None, limit=50): + stats = pstats.Stats(str(pstat_file)) + stats.sort_stats(sort_by) + + entries = [] + for func, stat in stats.stats.items(): + filename, lineno, name = func + cc, nc, tt, ct, callers = stat + entries.append(( + nc, # number of calls + f"{tt:.3f}", # total time + f"{tt/nc if nc else 0:.3f}", # per-call total time + f"{ct:.3f}", # cumulative time + f"{ct/nc if nc else 0:.3f}", # per-call cumulative time + f"{filename}:{lineno}({name})" + )) + + entries.sort(key=lambda x: float(x[1]), reverse=True) + + header = f"{'ncalls':>10} {'tottime':>8} {'percall':>8} {'cumtime':>8} {'percall':>8} function" + output_lines = [header] + output_lines += [f"{nc:>10} {tt:>8} {tt_pc:>8} {ct:>8} {ct_pc:>8} {func}" for nc, tt, tt_pc, ct, ct_pc, func in entries[:limit]] + + output_text = "\n".join(output_lines) + + if output_file: + with open(output_file, "w") as f: + f.write(output_text) + print(f"Wrote formatted output to {output_file}") + else: + print(f"\n=== {pstat_file.name} ===") + print(output_text) + + +def main(): + parser = argparse.ArgumentParser(description="Print .pstat files in pretty columns.") + parser.add_argument("profile", type=str, nargs="?", default=None, + help="Path to a .pstat file or directory containing them.") + parser.add_argument("--sort", type=str, default="tottime", + help="Sort key (default: tottime).") + parser.add_argument("--out", type=str, default=None, + help="Write output to file.") + parser.add_argument("--limit", type=int, default=50, + help="Maximum number of entries to print.") + args = parser.parse_args() + + path = Path(args.profile) if args.profile else Path("profiles") + if path.is_file(): + print_pretty_profile(path, sort_by=args.sort, output_file=args.out, limit=args.limit) + elif path.is_dir(): + pstat_files = sorted(path.glob("*.pstat")) + if not pstat_files: + print("No .pstat files found.") + return + for pstat_file in pstat_files: + print_pretty_profile(pstat_file, sort_by=args.sort, limit=args.limit) + else: + print(f"Invalid path: {path}") + +if __name__ == "__main__": + main() diff --git a/cpmpy/tools/xcsp3/utils/yappi/profile_worker.py b/cpmpy/tools/xcsp3/utils/yappi/profile_worker.py new file mode 100644 index 000000000..d1dbca089 --- /dev/null +++ b/cpmpy/tools/xcsp3/utils/yappi/profile_worker.py @@ -0,0 +1,48 @@ +# profile_worker.py + +import sys +import yappi +import io +import lzma +from cpmpy.tools.xcsp3 import _parse_xcsp3, _load_xcsp3 +from pathlib import Path +import timeit +import logging + +# Basic logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def run_profile(xcsp3_file_path: str, output_profile_path: str): + try: + with lzma.open(xcsp3_file_path, 'rt', encoding='utf-8') as f: + xml_file = io.StringIO(f.read()) + + start = timeit.default_timer() + parser = _parse_xcsp3(xml_file) + + yappi.set_clock_type("wall") + yappi.clear_stats() + yappi.start() + model = _load_xcsp3(parser) + # Optional: model.solve("choco", time_limit=10) + end = timeit.default_timer() + + yappi.stop() + stats = yappi.get_func_stats() + stats.save(output_profile_path, type="pstat") + + logger.info(f"Profiling completed: {xcsp3_file_path} in {end - start:.2f}s") + + except Exception as e: + logger.error(f"Error profiling {xcsp3_file_path}: {e}") + sys.exit(1) + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python profile_worker.py ") + sys.exit(1) + + input_file = sys.argv[1] + output_profile = sys.argv[2] + run_profile(input_file, output_profile) diff --git a/cpmpy/tools/xcsp3/utils/yappi/yappi_parallel.py b/cpmpy/tools/xcsp3/utils/yappi/yappi_parallel.py new file mode 100644 index 000000000..6e612598d --- /dev/null +++ b/cpmpy/tools/xcsp3/utils/yappi/yappi_parallel.py @@ -0,0 +1,42 @@ +# run_all_profiles.py + +import subprocess +from pathlib import Path +from concurrent.futures import ThreadPoolExecutor, as_completed +from cpmpy.tools.xcsp3.xcsp3_dataset import XCSP3Dataset +from tqdm import tqdm + +PROFILE_DIR = Path("profiles") +PROFILE_DIR.mkdir(exist_ok=True) + +MAX_PARALLEL_JOBS = 4 # Adjust to your machine + +def profile_instance(idx, filename): + profile_file = PROFILE_DIR / f"profile_{idx}_{Path(filename).stem}.pstat" + return subprocess.run( + ["python", "yappi/profile_worker.py", filename, str(profile_file)], + capture_output=True + ) + +def main(): + dataset = XCSP3Dataset(year=2024, track="MiniCOP", download=True) + tasks = [(i, str(file)) for i, (file, _) in enumerate(dataset)] + + results = [] + with ThreadPoolExecutor(max_workers=MAX_PARALLEL_JOBS) as executor: + future_to_idx = { + executor.submit(profile_instance, i, fname): (i, fname) for i, fname in tasks + } + + with tqdm(total=len(future_to_idx), desc="Profiling Benchmarks") as pbar: + for future in as_completed(future_to_idx): + i, fname = future_to_idx[future] + result = future.result() + if result.returncode != 0: + print(f"\n[ERROR] {fname}:\n{result.stderr.decode()}") + else: + pass # silently record success + pbar.update(1) + +if __name__ == "__main__": + main() diff --git a/cpmpy/tools/xcsp3/utils/yappi/yappi_sequential.py b/cpmpy/tools/xcsp3/utils/yappi/yappi_sequential.py new file mode 100644 index 000000000..99d61a2ea --- /dev/null +++ b/cpmpy/tools/xcsp3/utils/yappi/yappi_sequential.py @@ -0,0 +1,73 @@ +import cpmpy +import lzma, tqdm +import io +import os +import yappi +import logging +import timeit +from pathlib import Path + +from cpmpy.tools.xcsp3.xcsp3_dataset import XCSP3Dataset +from cpmpy.tools.xcsp3 import _load_xcsp3, _parse_xcsp3 + +# Setup logger +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) +fh = logging.FileHandler('benchmark_log.txt') +fh.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +fh.setFormatter(formatter) +logger.addHandler(fh) +ch = logging.StreamHandler() +ch.setLevel(logging.INFO) +ch.setFormatter(formatter) +logger.addHandler(ch) + +# Output directory for profiling results +profile_output_dir = Path("profiles") +profile_output_dir.mkdir(exist_ok=True) + +def run_with_solver(solver: str): + dataset = XCSP3Dataset(year=2024, track="MiniCOP", download=True) + + for i, (filename, metadata) in tqdm.tqdm(enumerate(dataset), total=len(dataset)): + profile_name = profile_output_dir / f"profile_{i}_{Path(filename).stem}.pstat" + + try: + f = lzma.open(filename, 'rt', encoding='utf-8') + try: + xml_file = io.StringIO(f.read()) + + # Start profiler + yappi.set_clock_type("wall") + yappi.clear_stats() + yappi.start() + + start = timeit.default_timer() + parser = _parse_xcsp3(xml_file) + model = _load_xcsp3(parser) + # model.solve(solver, time_limit=10) + end = timeit.default_timer() + + # Stop profiler + yappi.stop() + + # Save profiling stats + func_stats = yappi.get_func_stats() + func_stats.save(str(profile_name), type='pstat') + + logger.info(f"Benchmark completed for {filename} in {end - start:.2f}s") + + finally: + f.close() + except Exception as e: + logger.error(f"Failed benchmark for {filename}: {e}") + raise + + # if i >= 20: + # break + +if __name__ == "__main__": + solvers = ["choco"] + for solver in solvers: + run_with_solver(solver) diff --git a/cpmpy/tools/xcsp3/xcsp3_analyze.py b/cpmpy/tools/xcsp3/xcsp3_analyze.py new file mode 100644 index 000000000..90bbfa521 --- /dev/null +++ b/cpmpy/tools/xcsp3/xcsp3_analyze.py @@ -0,0 +1,250 @@ +""" +Collection of visualisation tools for processing the result of a `xcsp3_benchmark` run. + +Best used though its CLI, a command-line tool to visualize and analyze solver performance +based on CSV output files. + +E.g. to compare the results of multiple solvers: + +.. code-block:: console + + python xcsp3_analyze.py + +Positional Arguments +-------------------- +files : str + One or more CSV files (or a single directory) containing performance data to analyze. + +Optional Arguments +------------------ +--time_limit : float, optional + Maximum time limit (in seconds) to display on the x-axis of the plot. + +--output, -o : str, optional + Path to save the generated plot image (e.g., "output.png"). If not provided, the plot will be displayed interactively. +""" + +import argparse +import ast +from pathlib import Path +import re +import matplotlib +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.cm as cm + +def _extract_cost(solution_str): + """ + Extract numeric cost from solution string like '' + """ + if isinstance(solution_str, str): + match = re.search(r'cost="(\d+)"', solution_str) + if match: + return float(match.group(1)) + return np.nan + + +def xcsp3_plot(df, time_limit=None): + # Get unique solvers + solvers = df['solver'].unique() + + total_instances = df['instance'].nunique() + + # Determine the status to plot (Opt if at least one opt, otherwise sat) + statuses = df['status'].unique() + if 'OPTIMUM FOUND' in statuses: + status_filter = 'OPTIMUM FOUND' + else: + status_filter = 'SATISFIABLE' + df = df[(df['status'] == status_filter) | (df['status'] == 'UNSATISFIABLE')] # only those that reached the desired status + + # Count how many instances each solver solved (with correct status) + solver_counts = df['solver'].value_counts() + solvers_sorted_by_perf = solver_counts.sort_values(ascending=False).index.tolist() + + # Assign colors by alphabetical order of solvers + solvers_sorted_alpha = sorted(solvers) + cmap = cm.get_cmap('tab10', len(solvers_sorted_alpha)) + solver_to_color = {solver: cmap(i) for i, solver in enumerate(solvers_sorted_alpha)} + + # Create figure + fig = plt.figure(figsize=(10, 6)) + + for solver in solvers_sorted_alpha: # Use sorted solvers by count + # Get data for this solver + solver_data = df[df['solver'] == solver] + + # Sort by time_total + solver_data = solver_data.sort_values('time_total') + + # If time_limit is set, truncate data + if time_limit is not None: + solver_data = solver_data[solver_data['time_total'] <= time_limit] + + # Build x and y values + x = [0.0] + solver_data['time_total'].tolist() + y = [0] + list(range(1, len(solver_data) + 1)) + + # Plot the performance curve + plt.plot(x, y, label=f"{solver} ({len(solver_data)})", linewidth=2.5, color=solver_to_color[solver]) + + # Add horizontal dotted line for total number of instances + plt.axhline(y=total_instances, color='gray', linestyle='--', linewidth=1.5, label=f'Total ({total_instances})') + + # Set plot properties + plt.xlabel('Time (seconds)') + plt.ylabel(f'Number of instances returning \'{status_filter}\'') + year_track_pairs = df[['year', 'track']].drop_duplicates() + datasets = ', '.join([f'{row.year}:{row.track}' for _, row in year_track_pairs.iterrows()]) + plt.title(f'Performance Plot ({datasets})') + plt.grid(True) + plt.legend() + + # Move 'Total' to the top of the legend + handles, labels = plt.gca().get_legend_handles_labels() + # Sort so that 'Total' is first, others remain in order + sorted_handles_labels = sorted(zip(handles, labels), key=lambda x: 0 if x[1].startswith("Total") else 1) + handles, labels = zip(*sorted_handles_labels) + plt.legend(handles, labels) + + # Set x-axis limit if specified + if time_limit is not None: + plt.xlim(0, time_limit) + + return fig + +def get_cost(row): + """ + Get the achieved objective value from the provided row. + If intermediate solutions are available, get the best found (not neccesarily proven optimal). + """ + intermediate = row['intermediate'] + + # Try to parse string representations safely + if isinstance(intermediate, str): + try: + intermediate = ast.literal_eval(intermediate) + except (ValueError, SyntaxError): + intermediate = None + + # If it's a valid list of tuples, return the last objective + if isinstance(intermediate, list) and len(intermediate) > 0: + try: + return intermediate[-1][1] + except (IndexError, TypeError): + pass + + # Fallback to extracting from solution + return _extract_cost(row['solution']) + +def xcsp3_objective_performance_profile(df): + # Parse cost from the solution string + df = df.copy() + # print(df["intermediate"]) + df['cost'] = df.apply(get_cost, axis=1) + # df['cost'] = df['solution'].apply(extract_cost) + + # Pivot to get costs per instance per solver + pivot = df.pivot_table(index='instance', columns='solver', values='cost') + + # Drop instances not solved by all solvers (for fair comparison) + pivot = pivot.dropna(how='all') + + # Compute the best (minimum) cost per instance + best_costs = pivot.min(axis=1) + + # Compute performance ratios: solver_cost / best_cost + perf_ratios = pivot.divide(best_costs, axis=0) + + # Replace inf or NaN with a large number for safe plotting + perf_ratios = perf_ratios.replace([np.inf, np.nan], np.max(perf_ratios.values) * 10) + + # Compute a score for sorting: fraction of instances with ratio ≤ 1.1 (or similar) + score_threshold = 1.1 + solver_scores = (perf_ratios <= score_threshold).mean().sort_values(ascending=False) + sorted_solvers = solver_scores.index.tolist() + + # τ range for plotting + tau_vals = np.linspace(1, perf_ratios.max().max(), 500) + + # Plotting + fig = plt.figure(figsize=(10, 6)) + + for solver in sorted_solvers: + y_vals = [(perf_ratios[solver] <= tau).mean() for tau in tau_vals] + plt.plot(tau_vals, y_vals, label=solver, linewidth=2.5) + + plt.xlabel(r'Objective ratio $\tau$') + plt.ylabel('Fraction of instances') + year_track_pairs = df[['year', 'track']].drop_duplicates() + datasets = ', '.join([f'{row.year}:{row.track}' for _, row in year_track_pairs.iterrows()]) + plt.title(f'Objective Performance Profile ({datasets})') + plt.grid(True) + plt.legend() + plt.xlim(left=1) + + return fig + +def xcsp3_stats(df): + + for phase in ['parse', 'model', 'post']: + slowest_idx = df[f'time_{phase}'].idxmax() + if slowest_idx is not None and not pd.isna(slowest_idx): + print(f"Slowest {phase}: {df.loc[slowest_idx, f'time_{phase}']}s ({df.loc[slowest_idx, 'instance']}, {df.loc[slowest_idx, 'solver']})") + + for solver in df['solver'].unique(): + solver_total = df[df['solver'] == solver]['time_total'].sum() + print(f"Grand total for {solver}: {solver_total/60:.2f} minutes") + + +def main(): + # Set up argument parser + parser = argparse.ArgumentParser(description='Analyze XCSP3 solver performance data') + parser.add_argument('files', nargs='+', help='List of CSV files or directories to analyze') + parser.add_argument('--time_limit', type=float, default=None, + help='Maximum time limit in seconds to show on x-axis') + parser.add_argument('--output', '-o', type=str, default=None, + help='Path to save the plot image (e.g., output.png)') + args = parser.parse_args() + + # Gather all CSV files + csv_files = [] + for path_str in args.files: + path = Path(path_str) + if path.is_file() and path.suffix == '.csv': + csv_files.append(path) + elif path.is_dir(): + csv_files.extend(path.rglob('*.csv')) + else: + print(f"Warning: {path} is not a valid CSV file or directory") + + if not csv_files: + print("No CSV files found.") + return + + # Read and merge all CSV files + dfs = [] + for file in csv_files: + df = pd.read_csv(file) + dfs.append(df) + + merged_df = pd.concat(dfs, ignore_index=True) + + # Print some stats + xcsp3_stats(merged_df) + + # Create performance plot + fig = xcsp3_plot(merged_df, args.time_limit) + # fig = xcsp3_objective_performance_profile(merged_df) + + # Save or show plot + if args.output: + fig.savefig(args.output, bbox_inches='tight') + print(f"Plot saved to {args.output}") + else: + plt.show() + + +if __name__ == '__main__': + main() diff --git a/cpmpy/tools/xcsp3/xcsp3_benchmark.py b/cpmpy/tools/xcsp3/xcsp3_benchmark.py new file mode 100644 index 000000000..64183093e --- /dev/null +++ b/cpmpy/tools/xcsp3/xcsp3_benchmark.py @@ -0,0 +1,399 @@ +""" +Benchmark Solvers on XCSP3 Instances. + +A command-line tool for benchmarking constraint solvers on XCSP3 competition instances. +Supports parallel execution, time/memory limits, and solver configuration. + +Required Arguments +------------------ +--year : int + The competition year (e.g., 2023). + +--track : str + The competition track (e.g., "CSP", "COP", "MiniCOP"). + +--solver : str + The name of the solver to benchmark (e.g., "ortools", "exact", "choco"). + +Optional Arguments +------------------ +--workers : int, default=4 + Number of parallel workers to use. + +--time-limit : int, default=300 + Time limit in seconds per instance. + +--mem-limit : int, default=8192 + Memory limit in megabytes per instance. + +--output-dir : str, default='results' + Directory where result CSV files will be saved. + +--verbose + If set, display full xcsp3 output during execution. + +--intermediate + If set, report intermediate solutions (if supported by the solver). +""" + +import csv +import os +import signal +import subprocess +import time +import lzma +import sys +import argparse +import warnings +import traceback +import multiprocessing +from tqdm import tqdm +from pathlib import Path +from typing import Optional, Tuple +from io import StringIO +from datetime import datetime +from filelock import FileLock +from concurrent.futures import ThreadPoolExecutor + +from cpmpy.tools.xcsp3.xcsp3_dataset import XCSP3Dataset +from cpmpy.tools.xcsp3.xcsp3_cpmpy import xcsp3_cpmpy, init_signal_handlers, ExitStatus + +class Tee: + """ + A stream-like object that duplicates writes to multiple underlying streams. + """ + def __init__(self, *streams): + """ + Arguments: + *streams: Any number of file-like objects that implement a write() method, + such as sys.stdout, sys.stderr, or StringIO. + """ + self.streams = streams + + def write(self, data): + """ + Write data to all underlying streams. + + Args: + data (str): The string to write. + """ + for s in self.streams: + s.write(data) + + def flush(self): + """ + Flush all underlying streams to ensure all data is written out. + """ + for s in self.streams: + s.flush() + +class PipeWriter: + """ + Stdout wrapper for a multiprocessing pipe. + """ + def __init__(self, conn): + self.conn = conn + def write(self, data): + if data: # avoid empty writes + try: + self.conn.send(data) + except: + pass + def flush(self): + pass # no buffering + + +def xcsp3_wrapper(conn, kwargs, verbose): + """ + Wraps a call to xcsp3_cpmpy as to correctly + forward stdout to the multiprocessing pipe (conn). + Also sends a last status report though the pipe. + + Status report can be missing when process has been terminated by a SIGTERM. + """ + warnings.filterwarnings("ignore") + + original_stdout = sys.stdout + + pipe_writer = PipeWriter(conn) + + if not verbose: + sys.stdout = pipe_writer # only forward to pipe + else: + sys.stdout = Tee(original_stdout, pipe_writer) # forward to pipe and console + + try: + init_signal_handlers() # configure OS signal handlers + xcsp3_cpmpy(**kwargs) + conn.send({"status": "ok"}) + except Exception as e: # capture exceptions and report in state + tb_str = traceback.format_exc() + conn.send({"status": "error", "exception": e, "traceback": tb_str}) + finally: + sys.stdout = original_stdout + conn.close() + +# exec_args = (filename, metadata, solver, time_limit, mem_limit, output_file, verbose) +def execute_instance(args: Tuple[str, dict, str, int, int, int, str, bool, bool, str]) -> None: + """ + Solve a single XCSP3 instance and write results to file immediately. + + Args is a list of: + filename: Path to the XCSP3 instance file + metadata: Dictionary containing instance metadata (year, track, name) + solver: Name of the solver to use + time_limit: Time limit in seconds + mem_limit: Memory limit in MB + output_file: Path to the output CSV file + verbose: Whether to show solver output + """ + warnings.filterwarnings("ignore") + + filename, metadata, solver, time_limit, mem_limit, cores, output_file, verbose, intermediate, checker_path = args + + # Fieldnames for the CSV file + fieldnames = ['year', 'track', 'instance', 'solver', + 'time_total', 'time_parse', 'time_model', 'time_post', 'time_solve', + 'status', 'objective_value', 'solution', 'intermediate', 'checker_result'] + result = dict.fromkeys(fieldnames) # init all fields to None + result['year'] = metadata['year'] + result['track'] = metadata['track'] + result['instance'] = metadata['name'] + result['solver'] = solver + + # Decompress before timers start + file_path = filename + if str(filename).endswith(".lzma"): + # Decompress the XZ file + with lzma.open(filename, 'rt', encoding='utf-8') as f: + xml_file = StringIO(f.read()) # read to memory-mapped file + filename = xml_file + + # Start total timing + total_start = time.time() + + # Call xcsp3 in separate process + ctx = multiprocessing.get_context("spawn") + parent_conn, child_conn = multiprocessing.Pipe() # communication pipe between processes + process = ctx.Process(target=xcsp3_wrapper, args=( + child_conn, + { + "benchname": filename, + "solver": solver, + "time_limit": time_limit, + "mem_limit": mem_limit, + "intermediate": intermediate, + "force_mem_limit": True, + "time_buffer": 1, + "cores": cores, + }, + verbose)) + process.start() + process.join(timeout=time_limit) + + # Replicate competition convention on how jobs get terminated + if process.is_alive(): + # Send sigterm to let process know it reached its time limit + os.kill(process.pid, signal.SIGTERM) + # 1 second grace period + process.join(timeout=1) + # Kill if still alive + if process.is_alive(): + os.kill(process.pid, signal.SIGKILL) + process.join() + + result['time_total'] = time.time() - total_start + + sol_time = None # For annotation intermediate solutions (when they were received) + + # Default status if nothing returned by subprocess + # -> process exited prematurely due to sigterm + status = {"status": "error", "exception": "sigterm"} + + # Parse the output to get status, solution and timings + complete_solution = None + while parent_conn.poll(timeout=1): + line = parent_conn.recv() + + # Received a print statement from the subprocess + if isinstance(line, str): + if line.startswith('s '): + result['status'] = line[2:].strip() + elif line.startswith('v ') and result['solution'] is None: + # only record first line, contains 'type' and 'cost' + solution = line.split("\n")[0][2:].strip() + result['solution'] = str(solution) + complete_solution = line + if "cost" in solution: + result['objective_value'] = solution.split('cost="')[-1][:-2] + elif line.startswith('o '): + obj = int(line[2:].strip()) + if result['intermediate'] is None: + result['intermediate'] = [] + result['intermediate'] += [(sol_time, obj)] + result['objective_value'] = obj + obj = None + elif line.startswith('c Solution'): + parts = line.split(', time = ') + # Get solution time from comment for intermediate solution -> used for annotating 'o ...' lines + sol_time = float(parts[-1].replace('s', '').rstrip()) + elif line.startswith('c took '): + # Parse timing information + parts = line.split(' seconds to ') + if len(parts) == 2: + time_val = float(parts[0].replace('c took ', '')) + action = parts[1].strip() + if action.startswith('parse'): + result['time_parse'] = time_val + elif action.startswith('convert'): + result['time_model'] = time_val + elif action.startswith('post'): + result['time_post'] = time_val + elif action.startswith('solve'): + result['time_solve'] = time_val + + # Received a new status from the subprocess + elif isinstance(line, dict): + status = line + + else: + raise() + + # Parse the exit status + if status["status"] == "error": + # Ignore timeouts + if "TimeoutError" in repr(status["exception"]): + pass + # All other exceptions, put in solution field + elif result['solution'] is None: + result['status'] = ExitStatus.unknown.value + result["solution"] = status["exception"] + + if checker_path is not None and complete_solution is not None: + checker_output, checker_time = run_solution_checker( + JAR=checker_path, + instance_location=file_path, + out_file="'" + complete_solution.replace("\n\r", " ").replace("\n", " ").replace("v ", "").replace("v ", "")+ "'", + verbose=verbose, + cpm_time=result.get('time_solve', 0) # or total solve time you have + ) + + if checker_output is not None: + result['checker_result'] = checker_output + else: + result['checker_result'] = None + + # Use a lock file to prevent concurrent writes + lock_file = f"{output_file}.lock" + lock = FileLock(lock_file) + try: + with lock: + # Pre-check if file exists to determine if we need to write header + write_header = not os.path.exists(output_file) + + with open(output_file, 'a', newline='') as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + if write_header: + writer.writeheader() + writer.writerow(result) + finally: + # Optional: cleanup if the lock file somehow persists + if os.path.exists(lock_file): + try: + os.remove(lock_file) + except Exception: + pass # avoid crashing on cleanup + + +def run_solution_checker(JAR, instance_location, out_file, verbose, cpm_time): + + start = time.time() + command = ["java", "-jar", JAR, "'" + str(instance_location) + "'" + " " + str(out_file)] + command = " ".join(command) + test_res_str = subprocess.run(command, capture_output=True, text=True, shell=True) + checker_time = time.time() - start + + if verbose: + for line in test_res_str.stdout.split("\n"): + print("c " + line) + print(f"c cpmpy time: {cpm_time}") + print(f"c validation time: {checker_time}") + print(f"c elapsed time: {cpm_time + checker_time}") + + return test_res_str.stdout.split("\n")[-2], checker_time + + +def xcsp3_benchmark(year: int, track: str, solver: str, workers: int = 1, + time_limit: int = 300, mem_limit: Optional[int] = 4096, cores: int=1, + output_dir: str = 'results', + verbose: bool = False, intermediate: bool = False, + checker_path: Optional[str] = None) -> str: + """ + Benchmark a solver on XCSP3 instances. + + Args: + year (int): Competition year (e.g., 2023) + track (str): Track type (e.g., COP, CSP, MiniCOP) + solver (str): Solver name (e.g., ortools, exact, choco, ...) + workers (int): Number of parallel workers + time_limit (int): Time limit in seconds per instance + mem_limit (int): Memory limit in MB per instance + output_dir (str): Output directory for CSV files + verbose (bool): Whether to show solver output + + Returns: + str: Path to the output CSV file + """ + # Create output directory + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + # Get current timestamp in a filename-safe format + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Define output file path with timestamp + output_file = str(output_dir / f"xcsp3_{year}_{track}_{solver}_{timestamp}.csv") + + # Initialize dataset + dataset = XCSP3Dataset(year=year, track=track, download=True) + + # Process instances in parallel + with ThreadPoolExecutor(max_workers=workers) as executor: + # Submit all tasks and track their futures + futures = [executor.submit(execute_instance, # below: args + (filename, metadata, solver, time_limit, mem_limit, cores, output_file, verbose, intermediate, checker_path)) + for filename, metadata in dataset] + # Process results as they complete + for i,future in enumerate(tqdm(futures, total=len(futures), desc=f"Running {solver}")): + try: + _ = future.result(timeout=time_limit+60) # for cleanliness sake, result is empty + except TimeoutError: + pass + except Exception as e: + print(f"Job {i}: {dataset[i][1]['name']}, ProcessPoolExecutor caught: {e}") + + raise() + + return output_file + +if __name__ == "__main__": + warnings.filterwarnings("ignore") + + parser = argparse.ArgumentParser(description='Benchmark solvers on XCSP3 instances') + parser.add_argument('--year', type=int, required=True, help='Competition year (e.g., 2023)') + parser.add_argument('--track', type=str, required=True, help='Track type (e.g., COP, CSP, MiniCOP)') + parser.add_argument('--solver', type=str, required=True, help='Solver name (e.g., ortools, exact, choco, ...)') + parser.add_argument('--workers', type=int, default=4, help='Number of parallel workers') + parser.add_argument('--time-limit', type=int, default=300, help='Time limit in seconds per instance') + parser.add_argument('--mem-limit', type=int, default=8192, help='Memory limit in MB per instance') + parser.add_argument('--cores', type=int, default=1, help='Number of cores to assign tp a single instance') + parser.add_argument('--output-dir', type=str, default='results', help='Output directory for CSV files') + parser.add_argument('--verbose', action='store_true', help='Show solver output') + parser.add_argument('--intermediate', action='store_true', help='Report on intermediate solutions') + parser.add_argument('--checker-path', type=str, default=None, + help='Path to the XCSP3 solution checker JAR file') + + args = parser.parse_args() + + output_file = xcsp3_benchmark(**vars(args)) + print(f"Results added to {output_file}") diff --git a/cpmpy/tools/xcsp3/xcsp3_cpmpy.py b/cpmpy/tools/xcsp3/xcsp3_cpmpy.py new file mode 100644 index 000000000..6d19a202c --- /dev/null +++ b/cpmpy/tools/xcsp3/xcsp3_cpmpy.py @@ -0,0 +1,732 @@ +""" + CLI script for the XCSP3 competition. + + A command-line interface for solving XCSP3 constraint satisfaction problems using CPMpy. + + Example usage: + + .. code-block:: console + + python xcsp3_cpmpy + + + Required Arguments + ------------------ + benchname : str + Path to the XCSP3 XML file to solve. + + Optional XCSP3 Arguments + ------------------------ + -s, --seed : int, optional + Random seed (between 0 and 4294967295) for reproducibility. + -l, --time-limit : int, optional + CPU time limit in seconds. The process will be killed if this limit is exceeded. + -m, --mem-limit : int, optional + Memory usage limit in MiB (1 MiB = 1024 * 1024 bytes). + -t, --tmpdir : str, optional + Directory where temporary read/write operations are allowed. + -c, --cores : int, optional + Number of processing units to use (logical cores or processors). + + Required CPMpy Argument + ----------------------- + --solver : str + The name of the CPMpy solver to use. Can be in the format "solver" or "solver:subsolver". + + Optional CPMpy Arguments + ------------------------ + --time-buffer : int, optional + Time in seconds reserved before hitting the time limit, used for solver cleanup and solution printing. + --intermediate : bool, optional + If specified, intermediate solutions will be printed (if supported by the solver). +""" + +from __future__ import annotations + +import argparse +import lzma +import warnings +import psutil +import signal +import time +import sys, os +import random +import resource +import pathlib +from pathlib import Path +from enum import Enum +from typing import Optional +from io import StringIO + +from contextlib import contextmanager + + +# CPMpy +import cpmpy as cp +from cpmpy.solvers.ortools import CPM_ortools +from cpmpy.solvers.solver_interface import ExitStatus as CPMStatus +from cpmpy.tools.xcsp3 import _parse_xcsp3, _load_xcsp3 +from cpmpy.tools.xcsp3 import xcsp3_natives + +# PyCSP3 +from xml.etree.ElementTree import ParseError + +# Utils +import os, pathlib +sys.path.append(os.path.join(pathlib.Path(__file__).parent.resolve())) +from cpmpy.tools.xcsp3.xcsp3_solution import solution_xml + + +# Configuration +SUPPORTED_SOLVERS = ["choco", "ortools", "exact", "z3", "minizinc", "gurobi"] +SUPPORTED_SUBSOLVERS = { + "minizinc": ["gecode", "chuffed"] +} +DEFAULT_SOLVER = "ortools" +TIME_BUFFER = 5 # seconds +# TODO : see if good value +MEMORY_BUFFER_SOFT = 2 # MiB +MEMORY_BUFFER_HARD = 0 # MiB +MEMORY_BUFFER_SOLVER = 20 # MB + +original_stdout = sys.stdout + +def sigterm_handler(_signo, _stack_frame): + """ + Handles a SIGTERM. Gives us 1 second to finish the current job before we get killed. + """ + # Report that we haven't found a solution in time + print_status(ExitStatus.unknown) + print_comment("SIGTERM raised.") + sys.exit(0) + +def rlimit_cpu_handler(_signo, _stack_frame): + """ + Handles a SIGXCPU. + """ + # Report that we haven't found a solution in time + print_status(ExitStatus.unknown) + print_comment("SIGXCPU raised.") + print(flush=True) + sys.exit(0) + +def init_signal_handlers(): + signal.signal(signal.SIGINT, sigterm_handler) + signal.signal(signal.SIGTERM, sigterm_handler) + signal.signal(signal.SIGINT, sigterm_handler) + signal.signal(signal.SIGABRT, sigterm_handler) + signal.signal(signal.SIGXCPU, rlimit_cpu_handler) + +def set_memory_limit(mem_limit): + if mem_limit is not None: + soft = max(mib_as_bytes(mem_limit) - mib_as_bytes(MEMORY_BUFFER_SOFT), mib_as_bytes(MEMORY_BUFFER_SOFT)) + hard = max(mib_as_bytes(mem_limit) - mib_as_bytes(MEMORY_BUFFER_HARD), mib_as_bytes(MEMORY_BUFFER_HARD)) + print_comment(f"Setting memory limit: {soft} -- {hard}") + resource.setrlimit(resource.RLIMIT_AS, (soft, hard)) # limit memory in number of bytes + +def mib_as_bytes(mib: int) -> int: + return mib * 1024 * 1024 + +def mb_as_bytes(mb: int) -> int: + return mb * 1000 * 1000 + +def bytes_as_mb(bytes: int) -> int: + return bytes // (1000 * 1000) + +def bytes_as_gb(bytes: int) -> int: + return bytes // (1000 * 1000 * 1000) + +def bytes_as_mb_float(bytes: int) -> float: + return bytes / (1000 * 1000) + +def bytes_as_gb_float(bytes: int) -> float: + return bytes / (1000 * 1000 * 1000) + +# def current_memory_usage() -> int: # returns bytes +# # not really sure which memory measurement to use: https://stackoverflow.com/questions/7880784/what-is-rss-and-vsz-in-linux-memory-management/21049737#21049737 +# return psutil.Process(os.getpid()).memory_info().rss + +def remaining_memory(limit:int) -> int: # bytes + return limit # - current_memory_usage() + +def get_subsolver(solver: str, model: cp.Model, subsolver: Optional[str] = None) -> Optional[str]: + # Update subsolver + if solver == "z3": + if model.objective_ is not None: + return "opt" + else: + return "sat" + elif subsolver is None: + if solver in SUPPORTED_SUBSOLVERS: + return SUPPORTED_SUBSOLVERS[solver][0] + return subsolver + + +class Capturing(list): + def __enter__(self): + self._stdout = sys.stdout + sys.stdout = self._stringio = StringIO() + return self + def __exit__(self, *args): + self.extend(self._stringio.getvalue().splitlines()) + del self._stringio # free up some memory + sys.stdout = self._stdout + +class Callback: + + def __init__(self, model:cp.Model): + self.__start_time = time.time() + self.__solution_count = 0 + self.model = model + + def callback(self, *args, **kwargs): + current_time = time.time() + model, state = args + + # Callback codes: https://www.gurobi.com/documentation/current/refman/cb_codes.html#sec:CallbackCodes + + from gurobipy import GurobiError, GRB + # if state == GRB.Callback.MESSAGE: # verbose logging + # print_comment("log message: " + str(model.cbGet(GRB.Callback.MSG_STRING))) + if state == GRB.Callback.MIP: # callback from the MIP solver + if model.cbGet(GRB.Callback.MIP_SOLCNT) > self.__solution_count: # do we have a new solution? + + obj = int(model.cbGet(GRB.Callback.MIP_OBJBST)) + print_comment('Solution %i, time = %0.2fs' % + (self.__solution_count, current_time - self.__start_time)) + print_objective(obj) + self.__solution_count = model.cbGet(GRB.Callback.MIP_SOLCNT) + + +# ---------------------------------------------------------------------------- # +# XCSP3 Output formatting # +# ---------------------------------------------------------------------------- # + +def status_line_start() -> str: + return 's' + chr(32) + +def value_line_start() -> str: + return 'v' + chr(32) + +def objective_line_start() -> str: + return 'o' + chr(32) + +def comment_line_start() -> str: + return 'c' + chr(32) + +class ExitStatus(Enum): + unsupported:str = "UNSUPPORTED" # instance contains a unsupported feature (e.g. a unsupported global constraint) + sat:str = "SATISFIABLE" # CSP : found a solution | COP : found a solution but couldn't prove optimality + optimal:str = "OPTIMUM" + chr(32) + "FOUND" # optimal COP solution found + unsat:str = "UNSATISFIABLE" # instance is unsatisfiable + unknown:str = "UNKNOWN" # any other case + +def print_status(status: ExitStatus) -> None: + print(status_line_start() + status.value, end="\n", flush=True) + +def print_value(value: str) -> None: + value = value[:-2].replace("\n", "\nv" + chr(32)) + value[-2:] + print(value_line_start() + value, end="\n", flush=True) + +def print_objective(objective: int) -> None: + print(objective_line_start() + str(objective), end="\n", flush=True) + +def print_comment(comment: str) -> None: + print(comment_line_start() + comment.rstrip('\n'), end="\r\n", flush=True) + +# ---------------------------------------------------------------------------- # +# CLI argument type checkers # +# ---------------------------------------------------------------------------- # + +def dir_path(path): + if os.path.isfile(path): + return Path(path) + else: + raise argparse.ArgumentTypeError(f"readable_dir:{path} is not a valid path") + + +def is_supported_solver(solver:Optional[str]): + if (solver is not None) and (solver not in SUPPORTED_SOLVERS): + return False + else: + return True + +def is_supported_subsolver(solver, subsolver:Optional[str]): + if (subsolver is not None) and (subsolver not in SUPPORTED_SUBSOLVERS.get(solver, [None])): + return False + else: + return True + +def supported_solver(solver:Optional[str]): + if not is_supported_solver(solver): + argparse.ArgumentTypeError(f"solver:{solver} is not a supported solver. Options are: {str(SUPPORTED_SOLVERS)}") + else: + return solver + + +# ---------------------------------------------------------------------------- # +# Executable & Solver arguments # +# ---------------------------------------------------------------------------- # + +def ortools_arguments(model: cp.Model, + cores: Optional[int] = None, + seed: Optional[int] = None, + intermediate: bool = False, + **kwargs): + # https://github.com/google/or-tools/blob/stable/ortools/sat/sat_parameters.proto + res = dict() + + # https://github.com/google/or-tools/blob/1c5daab55dd84bca7149236e4b4fa009e5fd95ca/ortools/flatzinc/cp_model_fz_solver.cc#L1688 + res |= { + "interleave_search": True, + "use_rins_lns": False, + } + if not model.has_objective(): + res |= { "num_violation_ls": 1 } + + if cores is not None: + res |= { "num_search_workers": cores } + if seed is not None: + res |= { "random_seed": seed } + + if intermediate and model.has_objective(): + # Define custom ORT solution callback, then register it + from ortools.sat.python import cp_model as ort + class OrtSolutionCallback(ort.CpSolverSolutionCallback): + """ + For intermediate objective printing. + """ + + def __init__(self): + super().__init__() + self.__start_time = time.time() + self.__solution_count = 1 + + def on_solution_callback(self): + """Called on each new solution.""" + + current_time = time.time() + obj = int(self.ObjectiveValue()) + print_comment('Solution %i, time = %0.2fs' % + (self.__solution_count, current_time - self.__start_time)) + print_objective(obj) + self.__solution_count += 1 + + + def solution_count(self): + """Returns the number of solutions found.""" + return self.__solution_count + + # Register the callback + res |= { "solution_callback": OrtSolutionCallback() } + + def internal_options(solver: CPM_ortools): + # https://github.com/google/or-tools/blob/1c5daab55dd84bca7149236e4b4fa009e5fd95ca/ortools/flatzinc/cp_model_fz_solver.cc#L1688 + solver.ort_solver.parameters.subsolvers.extend(["default_lp", "max_lp", "quick_restart"]) + if not model.has_objective(): + solver.ort_solver.parameters.subsolvers.append("core_or_no_lp") + if len(solver.ort_model.proto.search_strategy) != 0: + solver.ort_solver.parameters.subsolvers.append("fixed") + + return res, internal_options + +def exact_arguments(seed: Optional[int] = None, **kwargs): + # Documentation: https://gitlab.com/JoD/exact/-/blob/main/src/Options.hpp?ref_type=heads + res = dict() + if seed is not None: + res |= { "seed": seed } + + return res, None + +def choco_arguments(): + # Documentation: https://github.com/chocoteam/pychoco/blob/master/pychoco/solver.py + return {}, None + +def z3_arguments(model: cp.Model, + cores: int = 1, + seed: Optional[int] = None, + mem_limit: Optional[int] = None, + **kwargs): + # Documentation: https://microsoft.github.io/z3guide/programming/Parameters/ + # -> is outdated, just let it crash and z3 will report the available options + + res = dict() + + if model.has_objective(): + # Opt does not seem to support setting random seed or max memory + pass + else: + # Sat parameters + if cores is not None: + res |= { "threads": cores } # TODO what with hyperthreadding, when more threads than cores + if seed is not None: + res |= { "random_seed": seed } + if mem_limit is not None: + res |= { "max_memory": bytes_as_mb(mem_limit) } + + return res, None + +def minizinc_arguments(solver: str, + cores: Optional[int] = None, + seed: Optional[int] = None, + **kwargs): + # Documentation: https://minizinc-python.readthedocs.io/en/latest/api.html#minizinc.instance.Instance.solve + res = dict() + if cores is not None: + res |= { "processes": cores } + if seed is not None: + res |= { "random_seed": seed } + + #if solver.endswith("gecode"): + # Documentation: https://www.minizinc.org/doc-2.4.3/en/lib-gecode.html + #elif solver.endswith("chuffed"): + # Documentation: + # - https://www.minizinc.org/doc-2.5.5/en/lib-chuffed.html + # - https://github.com/chuffed/chuffed/blob/develop/chuffed/core/options.h + + return res, None + +def gurobi_arguments(model: cp.Model, + cores: Optional[int] = None, + seed: Optional[int] = None, + mem_limit: Optional[int] = None, + intermediate: bool = False, + **kwargs): + # Documentation: https://www.gurobi.com/documentation/9.5/refman/parameters.html#sec:Parameters + res = dict() + if cores is not None: + res |= { "Threads": cores } + if seed is not None: + res |= { "Seed": seed } + if mem_limit is not None: + res |= { "MemLimit": bytes_as_gb(remaining_memory(mem_limit)) } + + if intermediate and model.has_objective(): + res |= { "solution_callback": Callback(model).callback } + + return res, None + +def cpo_arguments(model: cp.Model, + cores: Optional[int] = None, + seed: Optional[int] = None, + intermediate: bool = False, + **kwargs): + # Documentation: https://ibmdecisionoptimization.github.io/docplex-doc/cp/docplex.cp.parameters.py.html#docplex.cp.parameters.CpoParameters + res = dict() + if cores is not None: + res |= { "Workers": cores } + if seed is not None: + res |= { "RandomSeed": seed } + + if intermediate and model.has_objective(): + from docplex.cp.solver.solver_listener import CpoSolverListener + + class CpoSolutionCallback(CpoSolverListener): + + def __init__(self): + super().__init__() + self.__start_time = time.time() + self.__solution_count = 1 + + def result_found(self, solver, sres): + current_time = time.time() + obj = sres.get_objective_value() + if obj is not None: + print_comment('Solution %i, time = %0.2fs' % + (self.__solution_count, current_time - self.__start_time)) + print_objective(obj) + self.__solution_count += 1 + + def solution_count(self): + """Returns the number of solutions found.""" + return self.__solution_count + + # Register the callback + res |= { "solution_callback": CpoSolutionCallback } + + return res, None + + +def solver_arguments(solver: str, + model: cp.Model, + seed: Optional[int] = None, + intermediate: bool = False, + cores: int = 1, + mem_limit: Optional[int] = None, + **kwargs): + opt = model.has_objective() + sat = not opt + + if solver == "ortools": + return ortools_arguments(model, cores=cores, seed=seed, intermediate=intermediate, **kwargs) + elif solver == "exact": + return exact_arguments(seed=seed, **kwargs) + elif solver == "choco": + return choco_arguments() + elif solver == "z3": + return z3_arguments(model, cores=cores, seed=seed, mem_limit=mem_limit, **kwargs) + elif solver.startswith("minizinc"): # also can have a subsolver + return minizinc_arguments(solver, cores=cores, seed=seed, **kwargs) + elif solver == "gurobi": + return gurobi_arguments(model, cores=cores, seed=seed, mem_limit=mem_limit, intermediate=intermediate, opt=opt, **kwargs) + elif solver == "cpo": + return cpo_arguments(model=model, cores=cores, seed=seed, intermediate=intermediate, **kwargs) + else: + print_comment(f"setting parameters of {solver} is not (yet) supported") + return dict() + +@contextmanager +def prepend_print(): + # Save the original stdout + original_stdout = sys.stdout + + class PrependStream: + def __init__(self, stream): + self.stream = stream + + def write(self, message): + # Prepend 'c' to each message before writing it + if message.strip(): # Avoid prepending 'c' to empty messages (like newlines) + self.stream.write('c ' + message) + else: + self.stream.write(message) + + def flush(self): + self.stream.flush() + + # Override stdout with our custom stream + sys.stdout = PrependStream(original_stdout) + + try: + yield + finally: + # Restore the original stdout + sys.stdout = original_stdout + + +# Run the instance; exceptions are caught and printed but also re-raised +def xcsp3_cpmpy(benchname: str, + seed: Optional[int] = None, + time_limit: Optional[int] = None, + mem_limit: Optional[int] = None, # MiB: 1024 * 1024 bytes + cores: int = 1, + solver: str = None, + time_buffer: int = 0, + intermediate: bool = False, + force_mem_limit: bool = False, + **kwargs, +): + + # Configure signal handlers + signal.signal(signal.SIGTERM, sigterm_handler) + signal.signal(signal.SIGINT, sigterm_handler) + signal.signal(signal.SIGABRT, sigterm_handler) + signal.signal(signal.SIGXCPU, rlimit_cpu_handler) + + try: + + # --------------------------- Global Configuration --------------------------- # + + if solver == "choco" and mem_limit is not None: + warnings.warn("'mem_limit' is currently not supported with choco, issues with GraalVM") + mem_limit = None + + if seed is not None: + random.seed(seed) + if mem_limit is not None and force_mem_limit: + set_memory_limit(mem_limit) + # TODO should the executable even interrupt itself -> just wait for external signal + # let the executable use all the time it can get + # if time_limit is not None: + # soft = time_limit + 1 + # hard = time_limit + 2 + # resource.setrlimit(resource.RLIMIT_CPU, (soft, hard)) + + sys.argv = ["-nocompile"] # Stop pyxcsp3 from complaining on exit + + # Get the current process + p = psutil.Process() + + # Get the start time as a timestamp + time_start = p.create_time() + + # ------------------------------ Parse instance ------------------------------ # + + time_parse = time.time() + parser = _parse_xcsp3(benchname) + time_parse = time.time() - time_parse + print_comment(f"took {time_parse:.4f} seconds to parse XCSP3 model [{benchname}]") + + if time_limit and time_limit < (time.time() - time_start): + raise TimeoutError("Time's up after parse") + + # ---------------- Convert XCSP3 to CPMpy model with callbacks --------------- # + + time_callback = time.time() + model = _load_xcsp3(parser) + time_callback = time.time() - time_callback + print_comment(f"took {time_callback:.4f} seconds to convert to CPMpy model") + + if time_limit and time_limit < (time.time() - time_start): + raise TimeoutError("Time's up after callback") + + # ------------ Replace solver supported constraints with natives ------------- # + + # Additional XCSP3-specific native constraints + added_natives = { + "ortools": { + "no_overlap2d": xcsp3_natives.OrtNoOverlap2D, + "subcircuit": xcsp3_natives.OrtNoOverlap2D, + "subcircuitwithstart": lambda args: xcsp3_natives.OrtSubcircuitWithStart(args[:-1], args[-1]), + }, + "choco": { + "subcircuit": xcsp3_natives.ChocoSubcircuit, + }, + "minizinc": { + "subcircuit": xcsp3_natives.MinizincSubcircuit, + "subcircuitwithstart": xcsp3_natives.MinizincSubcircuitWithStart, + }, + } + + # Loop through all constraints and replace with native if supported + if solver in added_natives: + for i, constraint in enumerate(model.constraints): + if constraint.name in added_natives[solver]: + model.constraints[i] = added_natives[solver][constraint.name](constraint.args) + + + # ------------------------ Post CPMpy model to solver ------------------------ # + + solver_args, internal_options = solver_arguments(solver, model=model, seed=seed, + intermediate=intermediate, + cores=cores, mem_limit=mib_as_bytes(mem_limit) if mem_limit is not None else None, + **kwargs) + # time_limit is generic for all, done later + + + + # Post model to solver + time_post = time.time() + # with prepend_print(): # catch prints and prepend 'c' to each line (still needed?) + if solver == "exact": # Exact2 takes its options at creation time + s = cp.SolverLookup.get(solver, model, **solver_args) + solver_args = dict() # no more solver args needed + else: + s = cp.SolverLookup.get(solver, model) + time_post = time.time() - time_post + print_comment(f"took {time_post:.4f} seconds to post model to {solver}") + + if time_limit and time_limit < (time.time() - time_start): + raise TimeoutError("Time's up after post") + + + # ------------------------------- Solve model ------------------------------- # + + if time_limit: + # give solver only the remaining time + time_limit = time_limit - (time.time() - time_start) - time_buffer + print_comment(f"{time_limit}s left to solve") + + time_solve = time.time() + try: + if internal_options is not None: + internal_options(s) # Set more internal solver options (need access to native solver object) + is_sat = s.solve(time_limit=time_limit, **solver_args) + except RuntimeError as e: + if "Program interrupted by user." in str(e): # Special handling for Exact + raise TimeoutError("Exact interrupted due to timeout") + else: + raise e + + time_solve = time.time() - time_solve + print_comment(f"took {time_solve:.4f} seconds to solve") + + # ------------------------------- Print result ------------------------------- # + + if s.status().exitstatus == CPMStatus.OPTIMAL: + print_value(solution_xml(s)) + print_status(ExitStatus.optimal) + elif s.status().exitstatus == CPMStatus.FEASIBLE: + print_value(solution_xml(s)) + print_status(ExitStatus.sat) + elif s.status().exitstatus == CPMStatus.UNSATISFIABLE: + print_status(ExitStatus.unsat) + else: + print_comment("Solver did not find any solution within the time/memory limit") + print_status(ExitStatus.unknown) + + except MemoryError as e: + print_comment(f"MemoryError raised. Reached limit of {mem_limit} MiB") + print_status(ExitStatus.unknown) + raise e + except ParseError as e: + if "out of memory" in e.msg: + print_comment(f"MemoryError raised by parser. Reached limit of {mem_limit} MiB") + print_status(ExitStatus.unknown) + else: + print_comment(f"An {type(e)} got raised: {e}") + print_status(ExitStatus.unknown) + raise e + except NotImplementedError as e: + print_comment(str(e)) + print_status(ExitStatus.unsupported) + raise e + except Exception as e: + print_comment(f"An {type(e)} got raised: {e}") + import traceback + print_comment("Stack trace:") + for line in traceback.format_exc().split('\n'): + if line.strip(): + print_comment(line) + print_status(ExitStatus.unknown) + raise e + + +if __name__ == "__main__": + warnings.filterwarnings("ignore") + + # Configure signal handles + init_signal_handlers() + + # ------------------------------ Argument parsing ------------------------------ # + parser = argparse.ArgumentParser("CPMpy XCSP3 executable") + + ## XCSP3 required arguments: + # BENCHNAME: Name of the XCSP3 XML file with path and extension + parser.add_argument("benchname", type=dir_path) + # RANDOMSEED: Seed between 0 and 4294967295 + parser.add_argument("-s", "--seed", required=False, type=int, default=None) + # TIMELIMIT: Total CPU time in seconds (before it gets killed) + parser.add_argument("-l", "--time-limit", required=False, type=int, default=None) # TIMELIMIT + # MEMLIMIT: Total amount of memory in MiB (mebibyte = 1024 * 1024 bytes) + parser.add_argument("-m", "--mem-limit", required=False, type=int, default=None) + # TMPDIR: Only location where temporary read/write is allowed + parser.add_argument("-t","--tmpdir", required=False, type=dir_path) + # NBCORE: Number of processing units (can by any of the following: a processor / a processor core / logical processor (hyper-threading)) + parser.add_argument("-c", "--cores", required=False, type=int, default=None) + # DIR: not needed, e.g. we just import files + + ## CPMpy optional arguments: + # The underlying solver which should be used (can also be "solver:subsolver") + parser.add_argument("--solver", required=True, type=str) + # How much time before SIGTERM should we halt solver (for the final post-processing steps and solution printing) + parser.add_argument("--time-buffer", required=False, type=int, default=1) + # If intermediate solutions should be printed (if the solver supports it) + parser.add_argument("--intermediate", action=argparse.BooleanOptionalAction) + # If memory limit gets exceeded, force quit the instance + parser.add_argument("--force-mem-limit", action=argparse.BooleanOptionalAction) + + # Process cli arguments + args = parser.parse_args() + print_comment(f"Arguments: {args}") + + try: + if str(args.benchname).endswith(".lzma"): + # Decompress the XZ file + with lzma.open(args.benchname, 'rt', encoding='utf-8') as f: + xml_file = StringIO(f.read()) # read to memory-mapped file + args.benchname = xml_file + + xcsp3_cpmpy(**vars(args)) + except Exception as e: + print_comment(f"{type(e).__name__} -- {e}") \ No newline at end of file diff --git a/cpmpy/tools/xcsp3/xcsp3_dataset.py b/cpmpy/tools/xcsp3/xcsp3_dataset.py new file mode 100644 index 000000000..f01a2cf13 --- /dev/null +++ b/cpmpy/tools/xcsp3/xcsp3_dataset.py @@ -0,0 +1,173 @@ +""" +PyTorch-style Dataset for XCSP3 competition instances. + +Simply create a dataset instance (configured for the targeted competition year/track) and start iterating over its contents: + +.. code-block:: python + + from cpmpy.tools.xcsp3 import XCSP3Dataset, read_xcsp3 + + for filename, metadata in XCSP3Dataset(year=2024, track="COP", download=True): # auto download dataset and iterate over its instances + # Do whatever you want here, e.g. reading to a CPMpy model and solving it: + model = read_xcsp3(filename) + model.solve() + print(model.status()) + +The `metadata` contains usefull information about the current problem instance. + +Since the dataset is PyTorch compatible, it can be used with a DataLoader: + +.. code-block:: python + + from cpmpy.tools.xcsp3 import XCSP3Dataset, read_xcsp3 + + # Initialize the dataset + dataset = XCSP3Dataset(year=2024, track="COP", download=True) + + from torch.utils.data import DataLoader + + # Wrap dataset in a DataLoader + data_loader = DataLoader(dataset, batch_size=10, shuffle=False) + + # Iterate over the dataset + for batch in data_loader: + # Your code here +""" + +import pathlib +from typing import Tuple, Any +import xml.etree.ElementTree as ET +from urllib.request import urlretrieve +from urllib.error import HTTPError, URLError +import zipfile + +class XCSP3Dataset(object): # torch.utils.data.Dataset compatible + + """ + XCSP3 Dataset in a PyTorch compatible format. + + Arguments: + root (str): Root directory containing the XCSP3 instances (if 'download', instances will be downloaded to this location) + year (int): Competition year (2022, 2023 or 2024) + track (str, optional): Filter instances by track type (e.g., "COP", "CSP", "MiniCOP") + transform (callable, optional): Optional transform to be applied on the instance data + target_transform (callable, optional): Optional transform to be applied on the file path + download (bool): If True, downloads the dataset from the internet and puts it in `root` directory + """ + + def __init__(self, root: str = ".", year: int = 2023, track: str = None, transform=None, target_transform=None, download: bool = False): + """ + Initialize the XCSP3 Dataset. + """ + self.root = pathlib.Path(root) + self.year = year + self.transform = transform + self.target_transform = target_transform + self.track = track + self.track_dir = self.root / str(year) / track + + if not str(year).startswith('20'): + raise ValueError("Year must start with '20'") + if not track: + raise ValueError("Track must be specified, e.g. COP, CSP, MiniCOP, ...") + # Create root directory if it doesn't exist + self.root.mkdir(parents=True, exist_ok=True) + + if not self.track_dir.exists(): + if not download: + raise ValueError(f"Dataset for year {year} and track {track} not found. Please set download=True to download the dataset.") + else: + print(f"Downloading XCSP3 {year} instances...") + url = f"https://www.cril.univ-artois.fr/~lecoutre/compets/" + year_suffix = str(year)[2:] # Drop the starting '20' + url_path = url + f"instancesXCSP{year_suffix}.zip" + zip_path = self.root / f"instancesXCSP{year_suffix}.zip" + + try: + urlretrieve(url_path, str(zip_path)) + except (HTTPError, URLError) as e: + raise ValueError(f"No dataset available for year {year}. Error: {str(e)}") + + # Extract only the specific track folder from the zip + with zipfile.ZipFile(zip_path, 'r') as zip_ref: + # Get the main folder name (e.g., "024_V3") + main_folder = None + for name in zip_ref.namelist(): + if '/' in name: + main_folder = name.split('/')[0] + break + + if main_folder is None: + raise ValueError(f"Could not find main folder in zip file") + + # Extract only files from the specified track + # Get all unique track names from zip + tracks = set() + for file_info in zip_ref.infolist(): + parts = file_info.filename.split('/') + if len(parts) > 2 and parts[0] == main_folder: + tracks.add(parts[1]) + + # Check if requested track exists + if track not in tracks: + raise ValueError(f"Track '{track}' not found in dataset. Available tracks: {sorted(tracks)}") + + # Create track folder in root directory, parents=True ensures recursive creation + self.track_dir.mkdir(parents=True, exist_ok=True) + + # Extract files for the specified track + prefix = f"{main_folder}/{track}/" + for file_info in zip_ref.infolist(): + if file_info.filename.startswith(prefix): + # Extract file to track_dir, removing main_folder/track prefix + filename = pathlib.Path(file_info.filename).name + with zip_ref.open(file_info) as source, open(self.track_dir / filename, 'wb') as target: + target.write(source.read()) + # Clean up the zip file + zip_path.unlink() + + + def __len__(self) -> int: + """Return the total number of instances.""" + return len(list(self.track_dir.glob("*.xml.lzma"))) + + def __getitem__(self, index: int) -> Tuple[Any, Any]: + """ + Get a single XCSP3 instance filename and metadata. + + Args: + index (int): Index of the instance to retrieve + + Returns: + Tuple[Any, Any]: A tuple containing: + - The filename of the instance + - Metadata dictionary with file name, track, year etc. + """ + if index < 0 or index >= len(self): + raise IndexError("Index out of range") + + # Get all compressed XML files and sort for deterministic behavior + files = sorted(list(self.track_dir.glob("*.xml.lzma"))) + file_path = files[index] + + filename = str(file_path) + if self.transform: + # does not need to remain a filename... + filename = self.transform(filename) + + # Basic metadata about the instance + metadata = { + 'year': self.year, + 'track': self.track, + 'name': file_path.stem.replace('.xml.lzma', ''), + 'path': filename, + } + if self.target_transform: + metadata = self.target_transform(metadata) + + return filename, metadata + +if __name__ == "__main__": + dataset = XCSP3Dataset(year=2024, track="MiniCOP", download=True) + print("Dataset size:", len(dataset)) + print("Instance 0:", dataset[0]) diff --git a/cpmpy/tools/xcsp3/xcsp3_globals.py b/cpmpy/tools/xcsp3/xcsp3_globals.py new file mode 100644 index 000000000..7e0f8efb4 --- /dev/null +++ b/cpmpy/tools/xcsp3/xcsp3_globals.py @@ -0,0 +1,1137 @@ +""" +Additional global constraints which are not (yet) part of the standard CPMpy collection. + +This file contains all the missing global constraints in order to support XCSP3-core, +which is a restricted scope of the complete XCSP3 specification (as used for the competitions). + +Currently, version 3.2 is supported. +""" + +import numpy as np +import cpmpy as cp +from cpmpy import cpm_array, intvar, boolvar +from cpmpy.exceptions import CPMpyException, IncompleteFunctionError +from cpmpy.expressions.core import Expression, Operator +from cpmpy.expressions.globalconstraints import GlobalConstraint, GlobalFunction, AllDifferent, InDomain, MapDomain +from cpmpy.expressions.utils import STAR, is_any_list, is_num, all_pairs, argvals, flatlist, is_boolexpr, argval, is_int, \ + get_bounds, eval_comparison +from cpmpy.expressions.variables import _IntVarImpl + +class AllDifferentLists(GlobalConstraint): + """ + Ensures none of the lists given are exactly the same. + Called 'lex_alldifferent' in the global constraint catalog: + https://sofdem.github.io/gccat/gccat/Clex_alldifferent.html#uid24923 + """ + def __init__(self, lists): + if any(not is_any_list(lst) for lst in lists): + raise TypeError(f"AllDifferentLists expects a list of lists, but got {lists}") + if any(len(lst) != len(lists[0]) for lst in lists): + raise ValueError("Lists should have equal length, but got these lengths:", list(map(len, lists))) + super().__init__("alldifferent_lists", [flatlist(lst) for lst in lists]) + + def decompose(self): + """Returns the decomposition + """ + from cpmpy.expressions.python_builtins import any as cpm_any + constraints = [] + for lst1, lst2 in all_pairs(self.args): + constraints += [cpm_any(var1 != var2 for var1, var2 in zip(lst1, lst2))] + return constraints, [] + + def value(self): + lst_vals = [tuple(argvals(a)) for a in self.args] + return len(set(lst_vals)) == len(self.args) +class AllDifferentListsExceptN(GlobalConstraint): + """ + Ensures none of the lists given are exactly the same. Excluding the tuples given in N + Called 'lex_alldifferent' in the global constraint catalog: + https://sofdem.github.io/gccat/gccat/Clex_alldifferent.html#uid24923 + """ + def __init__(self, lists, n): + if not is_any_list(n): + raise TypeError(f"AllDifferentListsExceptN expects a (list of) lists to exclude but got {n}") + if any(not is_any_list(x) for x in n): #only one list given, not a list of lists + n = [n] + for lst in n: + if not all(is_num(x) for x in lst): + raise TypeError("Can only use constants as excepting argument") + if any(not is_any_list(lst) for lst in lists): + raise TypeError(f"AllDifferentListsExceptN expects a list of lists, but got {lists}") + if any(len(lst) != len(lists[0]) for lst in lists + n): + raise ValueError("Lists should have equal length, but got these lengths:", list(map(len, lists))) + super().__init__("alldifferent_lists_except_n", [[flatlist(lst) for lst in lists], [flatlist(x) for x in n]]) + + def decompose(self): + """Returns the decomposition + """ + from cpmpy.expressions.python_builtins import all as cpm_all + constraints = [] + for lst1, lst2 in all_pairs(self.args[0]): + constraints += [cpm_all(var1 == var2 for var1, var2 in zip(lst1, lst2)).implies(Table(lst1, self.args[1]))] + return constraints, [] + + def value(self): + lst_vals = [tuple(argvals(a)) for a in self.args[0]] + except_vals = [tuple(argvals(a)) for a in self.args[1]] + return len(set(lst_vals) - set(except_vals)) == len([x for x in lst_vals if x not in except_vals]) + +class SubCircuit(GlobalConstraint): + """ + The sequence of variables form a subcircuit, where x[i] = j means that j is the successor of i. + Contrary to Circuit, there is no requirement on all nodes needing to be part of the circuit. + Nodes which aren't part of the subcircuit, should self loop i.e. x[i] = i. + The subcircuit can be empty (all stops self-loop). + A length 1 subcircuit is treated as an empty subcircuit. + Global Constraint Catalog: + https://sofdem.github.io/gccat/gccat/Cproper_circuit.html + """ + + def __init__(self, *args): + flatargs = flatlist(args) + + # Ensure all args are integer successor values + if any(is_boolexpr(arg) for arg in flatargs): + raise TypeError("SubCircuit global constraint only takes arithmetic arguments: {}".format(flatargs)) + # Ensure there are at least two stops to create a circuit with + if len(flatargs) < 2: + raise CPMpyException("SubCircuitWithStart constraint must be given a minimum of 2 variables for field 'args' as stops to route between.") + + # Create the object + super().__init__("subcircuit", flatargs) + + def decompose(self): + """ + Decomposition for SubCircuit + A mix of the above Circuit decomposition, with elements from the Minizinc implementation for the support of optional visits: + https://github.com/MiniZinc/minizinc-old/blob/master/lib/minizinc/std/subcircuit.mzn + """ + from cpmpy.expressions.python_builtins import min as cpm_min + from cpmpy.expressions.python_builtins import all as cpm_all + + # Input arguments + succ = cpm_array(self.args) # Successor variables + n = len(succ) + + # Decision variables + start_node = intvar(0, n-1) # The first stop in the subcircuit. + end_node = intvar(0, n-1) # The last stop in the subcircuit, before looping back to the "start_node". + index_within_subcircuit = intvar(0, n-1, shape=n) # The position each stop takes within the subcircuit, with the assumption that the stop "start_node" gets index 0. + is_part_of_circuit = boolvar(shape=n) # Whether a stop is part of the subcircuit. + empty = boolvar() # To detect when a subcircuit is completely empty + + # Constraining + constraining = [] + constraining += [AllDifferent(succ)] # All stops should have a unique successor. + constraining += list( is_part_of_circuit.implies(succ < len(succ)) ) # Successor values should remain within domain. + for i in range(0, n): + # If a stop is on the subcircuit and it is not the last one, than its successor should have +1 as index. + constraining += [(is_part_of_circuit[i] & (i != end_node)).implies( + index_within_subcircuit[succ[i]] == (index_within_subcircuit[i] + 1) + )] + constraining += list( is_part_of_circuit == (succ != np.arange(n)) ) # When a node is part of the subcircuit it should not self loop, if it is not part it should self loop. + + # Defining + defining = [] + defining += [ empty == cpm_all(succ == np.arange(n)) ] # Definition of empty subcircuit (all nodes self-loop) + defining += [ empty.implies(cpm_all(index_within_subcircuit == cpm_array([0]*n))) ] # If the subcircuit is empty, default all index values to 0 + defining += [ empty.implies(start_node == 0) ] # If the subcircuit is empty, any node could be a start of a 0-length circuit. Default to node 0 as symmetry breaking. + defining += [succ[end_node] == start_node] # Definition of the last node. As the successor we should cycle back to the start. + defining += [ index_within_subcircuit[start_node] == 0 ] # The ordering starts at the start_node. + defining += [ ( empty | (is_part_of_circuit[start_node] == True) ) ] # The start node can only NOT belong to the subcircuit when the subcircuit is empty. + # Nodes which are not part of the subcircuit get an index fixed to +1 the index of "end_node", which equals the length of the subcircuit. + # Nodes part of the subcircuit must have an index <= index_within_subcircuit[end_node]. + # The case of an empty subcircuit is an exception, since "end_node" itself is not part of the subcircuit + defining += [ (is_part_of_circuit[i] == ((~empty) & (index_within_subcircuit[end_node] + 1 != index_within_subcircuit[i]))) for i in range(n)] + # In a subcircuit any of the visited nodes can be the "start node", resulting in symmetrical solutions -> Symmetry breaking + # Part of the formulation from the following is used: https://sofdem.github.io/gccat/gccat/Ccycle.html#uid18336 + subcircuit_visits = intvar(0, n-1, shape=n) # The visited nodes in sequence of length n, with possible repeated stops. e.g. subcircuit [0, 2, 1] -> [0, 2, 1, 0, 2, 1] + defining += [subcircuit_visits[0] == start_node] # The start nodes is the first stop + defining += [subcircuit_visits[i+1] == succ[subcircuit_visits[i]] for i in range(n-1)] # We follow the successor values + # The free "start_node" could be any of the values of aux_subcircuit_visits (the actually visited nodes), resulting in degenerate solutions. + # By enforcing "start_node" to take the smallest value, symmetry breaking is ensured. + defining += [start_node == cpm_min(subcircuit_visits)] + + return constraining, defining + + def value(self): + + succ = [argval(a) for a in self.args] + n = len(succ) + + # Find a start_index + start_index = None + for i,s in enumerate(succ): + if i != s: + # first non self-loop found is taken as start + start_index = i + break + # No valid start found, thus empty subcircuit + if start_index is None: + return True # Change to False if empty subcircuits not allowed + + # Check AllDiff + if not AllDifferent(s).value(): + return False + + # Collect subcircuit + visited = set([start_index]) + idx = succ[start_index] + for i in range(len(succ)): + if idx ==start_index: + break + else: + if idx in visited: + return False + # Check bounds on successor value + if not (0 <= idx < n): return False + # Collect + visited.add(idx) + idx = succ[idx] + + # Check subcircuit + for i in range(n): + # A stop is either visited or self-loops + if not ( (i in visited) or (succ[i] == i) ): + return False + + # Check that subcircuit has length of at least 1. + return succ[start_index] != start_index + +class SubCircuitWithStart(GlobalConstraint): + + """ + The sequence of variables form a subcircuit, where x[i] = j means that j is the successor of i. + Contrary to Circuit, there is no requirement on all nodes needing to be part of the circuit. + Nodes which aren't part of the subcircuit, should self loop i.e. x[i] = i. + The size of the subcircuit should be strictly greater than 1, so not all stops can self loop + (as otherwise the start_index will never get visited). + start_index will be treated as the start of the subcircuit. + The only impact of start_index is that it will be guaranteed to be inside the subcircuit. + Global Constraint Catalog: + https://sofdem.github.io/gccat/gccat/Cproper_circuit.html + """ + + def __init__(self, *args, start_index:int=0): + flatargs = flatlist(args) + + # Ensure all args are integer successor values + if any(is_boolexpr(arg) for arg in flatargs): + raise TypeError("SubCircuitWithStart global constraint only takes arithmetic arguments: {}".format(flatargs)) + # Ensure start_index is an integer + if not isinstance(start_index, int): + raise TypeError("SubCircuitWithStart global constraint's start_index argument must be an integer: {}".format(start_index)) + # Ensure that the start_index is within range + if not ((start_index >= 0) and (start_index < len(flatargs))): + raise ValueError("SubCircuitWithStart's start_index must be within the range [0, #stops-1] and thus refer to an actual stop as provided through 'args'.") + # Ensure there are at least two stops to create a circuit with + if len(flatargs) < 2: + raise CPMpyException("SubCircuitWithStart constraint must be given a minimum of 2 variables for field 'args' as stops to route between.") + + # Create the object + super().__init__("subcircuitwithstart", flatargs + [start_index]) + + def decompose(self): + """ + Decomposition for SubCircuitWithStart. + SubCircuitWithStart simply gets decomposed into SubCircuit and a constraint + enforcing the start_index to be part of the subcircuit. + """ + # Get the arguments + start_index = self.args[-1] + succ = cpm_array(self.args[:-1]) # Successor variables + + constraining = [] + constraining += [SubCircuit(succ)] # The successor variables should form a subcircuit. + constraining += [succ[start_index] != start_index] # The start_index should be inside the subcircuit. + + defining = [] + + return constraining, defining + + def value(self): + start_index = self.args[-1] + succ = [argval(a) for a in self.args[:-1]] # Successor variables + + # Check if we have a valid subcircuit and that the start_index is part of it. + return SubCircuit(succ).value() and (succ[start_index] != start_index) + +class Inverse(GlobalConstraint): + """ + Inverse (aka channeling / assignment) constraint. 'fwd' and + 'rev' represent inverse functions; that is, + + The symmetric version (where len(fwd) == len(rev)) is defined as: + fwd[i] == x <==> rev[x] == i + The asymmetric version (where len(fwd) < len(rev)) is defined as: + fwd[i] == x => rev[x] == i + + """ + def __init__(self, fwd, rev): + flatargs = flatlist([fwd, rev]) + if any(is_boolexpr(arg) for arg in flatargs): + raise TypeError("Only integer arguments allowed for global constraint Inverse: {}".format(flatargs)) + if len(fwd) > len(rev): + raise TypeError("len(fwd) should be equal to len(rev) for the symmetric inverse, or smaller than len(rev) for the asymmetric inverse") + if len(fwd) == len(rev): + name = "inverse" + else: + name = "inverseAsym" + super().__init__(name, [fwd, rev]) + + def decompose(self): + fwd, rev = self.args + rev = cpm_array(rev) + return [cp.all(rev[x] == i for i, x in enumerate(fwd))], [] + + def value(self): + fwd = argvals(self.args[0]) + rev = argvals(self.args[1]) + # args are fine, now evaluate actual inverse cons + try: + return all(rev[x] == i for i, x in enumerate(fwd)) + except IndexError: # partiality of Element constraint + return False + +class InverseOne(GlobalConstraint): + """ + Inverse (aka channeling / assignment) constraint but with only one array. + Equivalent to Inverse(x,x) + arr[i] == j <==> arr[j] == i + """ + def __init__(self, arr): + flatargs = flatlist([arr]) + if any(is_boolexpr(arg) for arg in flatargs): + raise TypeError("Only integer arguments allowed for global constraint Inverse: {}".format(flatargs)) + super().__init__("inverseOne", [arr]) + + def decompose(self): + arr = self.args[0] + arr = cpm_array(arr) + return [MapDomain(x) for x in arr] +\ + [cp.all(arr[x] == i for i, x in enumerate(arr))], [] + + def value(self): + valsx = argvals(self.args[0]) + try: + return all(valsx[x] == i for i, x in enumerate(valsx)) + except IndexError: # partiality of Element constraint + return False + + +class Channel(GlobalConstraint): + """ + Channeling constraint. Channeling integer representation of a variable into a representation with boolean + indicators + for all 0<=i value = i + exists 0<=i= 0 and x.ub <= 1 for x in flatargs): + raise TypeError( + "the first argument of a Channel constraint should only contain 0-1 variables/expressions (i.e., " + + "intvars/intexprs with domain {0,1} or boolvars/boolexprs)") + super().__init__("channelValue", [arr, v]) + + def decompose(self): + arr, v = self.args + # TODO... this is not ILP friendly!, but we have MapDomain now... + return [MapDomain(v)] +\ + [(arr[i] == 1) == (v == i) for i in range(len(arr))] + [v >= 0, v < len(arr)], [] + + def value(self): + arr, v = self.args + return sum(argvals(x) for x in arr) == 1 and 0 <= argval(v) < len(arr) and arr[argval(v)] == 1 + + +class Table(GlobalConstraint): + """The values of the variables in 'array' correspond to a row in 'table' + """ + + def __init__(self, array, table): + array = flatlist(array) + if not all(isinstance(x, Expression) for x in array): + raise TypeError("the first argument of a Table constraint should only contain variables/expressions") + super().__init__("table", [array, table]) + + def decompose(self): + """ + This decomposition is only valid in a non-reified setting. + """ + arr, tab = self.args + if len(tab) == 1: + return [x == v for x,v in zip(arr, tab[0])], [] + + row_selected = boolvar(shape=len(tab)) + + classic = False + if classic: + cons = [Operator("or", row_selected)] + cons.extend(MapDomain(x) for x in arr) + for i, row in enumerate(tab): + # lets already flatten it a bit + cons += [Operator("->", [row_selected[i], x == v]) for x,v in zip(arr, row)] + else: + # ILP friendly decomposition, from Gleb's paper + cons = [] + nptab = np.array(tab) + cons += [x == cp.sum(row_selected*nptab[:,i]) for i,x in enumerate(arr)] + cons += [ cp.sum(row_selected) == 1 ] + + return cons,[] + + def value(self): + arr, tab = self.args + arrval = argvals(arr) + return arrval in tab + + @property + def vars(self): + return self._args[0] + + # specialisation to avoid recursing over big tables + def has_subexpr(self): + if not hasattr(self, '_has_subexpr'): # if _has_subexpr has not been computed before or has been reset + arr, tab = self.args # the table 'tab' can only hold constants, never a nested expression + self._has_subexpr = any(a.has_subexpr() for a in arr) + return self._has_subexpr + +class ShortTable(GlobalConstraint): + """ + Extension of the `Table` constraint where the `table` matrix may contain wildcards (STAR), meaning there are + no restrictions for the corresponding variable in that tuple. + """ + def __init__(self, array, table): + array = flatlist(array) + if not all(isinstance(x, Expression) for x in array): + raise TypeError("The first argument of a Table constraint should only contain variables/expressions") + if isinstance(table, np.ndarray): # Ensure it is a list + table = table.tolist() + super().__init__("short_table", [array, table]) + + def decompose(self): + arr, tab = self.args + if len(tab) == 1: + return [x == v for (x, v) in zip(arr, tab[0]) if v != STAR], [] + + row_selected = boolvar(shape=(len(tab),)) + cons = [cp.any(row_selected)] + cons.extend(MapDomain(x) for x in arr) + for i, row in enumerate(tab): + # lets already flatten it a bit + cons += [Operator("->", [row_selected[i], x == v]) for x,v in zip(arr, row) if v != STAR] + + return cons,[] + + def value(self): + arr, tab = self.args + tab = np.array(tab) + arrval = np.array(argvals(arr)) + for row in tab: + num_row = row[row != STAR].astype(int) + num_vals = arrval[row != STAR].astype(int) + if (num_row == num_vals).all(): + return True + return False + +class NegativeShortTable(GlobalConstraint): + """The values of the variables in 'array' do not correspond to any row in 'table' + """ + def __init__(self, array, table): + array = flatlist(array) + if not all(isinstance(x, Expression) for x in array): + raise TypeError("the first argument of a Table constraint should only contain variables/expressions") + super().__init__("negative_shorttable", [array, table]) + + def decompose(self): + arr, tab = self.args + return [MapDomain(x) for x in arr] +\ + [Operator("or", [x != v for x,v in zip(arr, row) if v != "*"]) for row in tab], [] + + def value(self): + arr, tab = self.args + arrval = [argval(a) for a in arr] + for tup in tab: + thistup = True + for aval, tval in zip(arrval, tup): + if tval != '*': + if aval != tval: + thistup = False + break + if thistup: + # found tuple that matches + return False + # didn't find tuple that matches + return True + + +class MDD(GlobalConstraint): + """ + MDD-constraint: an MDD (Multi-valued Decision Diagram) is an acyclic layerd graph starting from a single node and + ending in one. Each edge layer corresponds to a variables and each path corresponds to a solution + The values of the variables in 'array' correspond to a path in the mdd formed by the transitions in 'transitions'. + Root node is the first node used as a start in the first transition (i.e. transitions[0][0]) + spec: + - array: an array of CPMpy expressions (integer variable, global functions,...) + - transitions: an array of tuples (nodeID, int, nodeID) where nodeID is some unique identifiers for the nodes + (int or str are fine) + Example: + The following transitions depict a 3 layer MDD, starting at 'r' and ending in 't' + ("r", 0, "n1"), ("r", 1, "n2"), ("r", 2, "n3"), ("n1", 2, "n4"), ("n2", 2, "n4"), ("n3", 0, "n5"), + ("n4", 0, "t"), ("n5", 1, "t") + Its graphical representation is: + r + 0/ |1 \2 X + n1 n2 n3 + 2| /2 /O Y + n4 n5 + 0\ /1 Z + t + It has 3 paths, corresponding to 3 solution for (X,Y,Z): (0,2,0), (1,2,0) and (2,0,1) + """ + + def __init__(self, array, transitions): + array = flatlist(array) + if not all(isinstance(x, Expression) for x in array): + raise TypeError("The first argument of an MDD constraint should only contain variables/expressions") + if not all(is_transition(transition) for transition in transitions): + raise TypeError("The second argument of an MDD constraint should be collection of transitions") + super().__init__("mdd", [array, transitions]) + self.root_node = transitions[0][0] + self.mapping = {} + for s, v, e in transitions: + self.mapping[(s, v)] = e + + def _transition_to_layer_representation(self): + """ auxiliary function to compute which nodes belongs to which node-layer and which transition belongs to which + edge-layer of the MDD, needed to compute decomposition + """ + arr, transitions = self.args + nodes_by_level = [[self.root_node]] + transitions_by_level = [] + tran = transitions + for i in range(len(arr)): # go through each layer + nodes_by_level.append([]) + transitions_by_level.append([]) + remaining_tran = [] + for t in tran: # test each transition + ns, _, ne = t + if ns in nodes_by_level[i]: # add to the current layer if start node belongs to the node-layer + if ne not in nodes_by_level[i + 1]: + nodes_by_level[i + 1].append(ne) + transitions_by_level[i].append(t) + else: + remaining_tran.append(t) + tran = remaining_tran + return nodes_by_level, transitions_by_level + + # auxillary method to transform into layered representation (gather all the node by node-layers) + def _normalize_layer_representation(self, nodes_by_level, transitions_by_level): + """ auxiliary function to normalize the names of the nodes in layer by layer representation. Node ID in + normalized representation goes from 0 to n-1 for each layer. Used by the decomposition of the constraint. + """ + nb_nodes_by_level = [len(x) for x in nodes_by_level] + num_mapping = {} + for lvl in nodes_by_level: + for i in range(len(lvl)): + num_mapping[lvl[i]] = i + transitions_by_level_normalized = [[[num_mapping[n_in], v, num_mapping[n_out]] + for n_in, v, n_out in lvl] + for lvl in transitions_by_level] + return nb_nodes_by_level, num_mapping, transitions_by_level_normalized + + + def decompose(self): + # Table decomposition (not by decomposition of the mdd into one big table, but by having transitions tables for + # each layer and auxiliary variables for the nodes. Similar to decomposition of regular into table, + # but with one table for each layer + arr, _ = self.args + lb = [x.lb for x in arr] + ub = [x.ub for x in arr] + # transform to layer representation + nbl, tbl = self._transition_to_layer_representation() + # normalize the naming of the nodes so it can be use as value for aux variables + nb_nodes_by_level, num_mapping, transitions_by_level_normalized = self._normalize_layer_representation(nbl, tbl) + # choose the best decomposition depending on number of levels + if len(transitions_by_level_normalized) > 2: + # decomposition with multiple transitions table and aux variables for the nodes + aux = [intvar(0, nb_nodes) for nb_nodes in nb_nodes_by_level[1:]] + # complete the MDD with additional dummy transitions to get the false end node also represented, + # needed so the negation works. + # I.E., now any assignment have a path in the MDD, some, the solutions, ending in an accepting state + # (end node of the initial MDD), other, the non-solutions, ending in a rejecting state (dummy end node) + for i in range(len(arr)): + # add for each state the missing transition to a dummy node on the next level + transition_dummy = [[num_mapping[n], v, nb_nodes_by_level[i+1]] for n in nbl[i] for v in range(lb[i], ub[i] + 1) if + (n, v) not in self.mapping] + if i != 0: + # add transition from one dummy node to the other (not needed for initial layer as no dummy there) + transition_dummy += [[nb_nodes_by_level[i], v, nb_nodes_by_level[i+1]] for v in range(lb[i], ub[i] + 1)] + # add the new transitions + transitions_by_level_normalized[i] = transitions_by_level_normalized[i] + transition_dummy + # optimization for first level (only one node, allows to deal with smaller table on first layer) + tab_first = [x[1:] for x in transitions_by_level_normalized[0]] + # defining constraints: aux and arr variables define a path in the augmented-with-negative-path-MDD + defining = [Table([arr[0], aux[0]], tab_first)] \ + + [Table([aux[i - 1], arr[i], aux[i]], transitions_by_level_normalized[i]) for i in + range(1, len(arr))] + # constraining constraint: end of the path in accepting node + constraining = [aux[-1] == 0] + return constraining, defining + elif len(transitions_by_level_normalized) == 2: + # decomposition by unfolding into a table (i.e., extract all paths and list them as table entries), + # avoid auxiliary variables + tab = [[t_a[1], t_b[1]] for t_a in transitions_by_level_normalized[0] for t_b in + transitions_by_level_normalized[1] if t_a[2] == t_b[0]] + return [Table(arr, tab)], [] + + elif len(transitions_by_level_normalized) == 1: + # decomposition to inDomain, avoid auxiliary variables and tables + return [InDomain(arr[0], [t[1] for t in transitions_by_level_normalized[0]])], [] + + def value(self): + arr, transitions = self.args + arrval = [argval(a) for a in arr] + curr_node = self.root_node + for v in arrval: + if (curr_node, v) in self.mapping: + curr_node = self.mapping[curr_node, v] + else: + return False + return True # can only have reached end node + +class Regular(GlobalConstraint): + """ + Regular-constraint (or Automaton-constraint) + Takes as input a sequence of variables and a automaton representation using a transition table. + The constraint is satisfied if the sequence of variables corresponds to an accepting path in the automaton. + + The automaton is defined by a list of transitions, a starting node and a list of accepting nodes. + The transitions are represented as a list of tuples, where each tuple is of the form (id1, value, id2). + An id is an integer or string representing a state in the automaton, and value is an integer representing the value of the variable in the sequence. + The starting node is an integer or string representing the starting state of the automaton. + The accepting nodes are a list of integers or strings representing the accepting states of the automaton. + + Example: an automaton that accepts the language 0*10* (exactly 1 variable taking value 1) is defined as: + cp.Regular(array = cp.intvar(0,1, shape=4), + transitions = [("A",0,"A"), ("A",1,"B"), ("B",0,"C"), ("C",0,"C")], + start = "A", + accepting = ["C"]) + """ + def __init__(self, array, transitions, start, accepting): + array = flatlist(array) + # skip all typechecks for comp + # if not all(isinstance(x, Expression) for x in array): + # raise TypeError("The first argument of a regular constraint should only contain variables/expressions") + + # if not is_any_list(transitions): + # raise TypeError("The second argument of a regular constraint should be a list of transitions") + # _node_type = type(transitions[0][0]) + # for s,v,e in transitions: + # if not isinstance(s, _node_type) or not isinstance(e, _node_type) or not isinstance(v, int): + # raise TypeError(f"The second argument of a regular constraint should be a list of transitions ({_node_type}, int, {_node_type})") + # if not isinstance(start, _node_type): + # raise TypeError("The third argument of a regular constraint should be a node id") + # if not (is_any_list(accepting) and all(isinstance(e, _node_type) for e in accepting)): + # raise TypeError("The fourth argument of a regular constraint should be a list of node ids") + super().__init__("regular", [array, transitions, start, list(accepting)]) + + self.nodes = set() + self.trans_dict = {} + for s, v, e in transitions: + self.nodes.update([s,e]) + self.trans_dict[(s, v)] = e + self.nodes = sorted(self.nodes) + # normalize node_ids to be 0..n-1, allows for smaller domains + self.node_map = {n: i for i, n in enumerate(self.nodes)} + + self.mapping = {} # for classic decomp + + + def decompose(self): + + version = "mip" + + if version == "classic": + arr, transitions, start, ends = self.args + # get the range of possible transition value + lb = min([x.lb for x in arr]) + ub = max([x.ub for x in arr]) + # Table decomposition with aux variables for the states + nodes = list(set([t[0] for t in transitions] + [t[-1] for t in transitions])) # get all nodes used + # normalization of the id of the node (from 0 to n-1) + num_mapping = dict(zip(nodes, range(len(nodes)))) # map node to integer ids for the nodes + num_transitions = [[num_mapping[n_in], v, num_mapping[n_out]] for n_in, v, n_out in + transitions] # apply mapping to transition + # compute missing transition with an additionnal never-accepting sink node (dummy default node) + id_dummy = len(nodes) # default node id + transition_dummy = [[num_mapping[n], v, id_dummy] for n in nodes for v in range(lb, ub + 1) if + (n, v) not in self.mapping] + [[id_dummy, v, id_dummy] for v in range(lb, ub + 1)] + num_transitions = num_transitions + transition_dummy + # auxiliary variable representing the sequence of state node in the path + aux_vars = intvar(0, id_dummy, shape=len(arr)) + id_start = num_mapping[start] + # optimization for first level (only one node, allows to deal with smaller table on first layer) + tab_first = [t[1:] for t in num_transitions if t[0] == id_start] + id_ends = [num_mapping[e] for e in ends] + # defining constraints: aux and arr variables define a path in the augmented-with-negative-path-Automaton + defining = [Table([arr[0], aux_vars[0]], tab_first)] + \ + [Table([aux_vars[i - 1], arr[i], aux_vars[i]], num_transitions) for i + in range(1, len(arr))] + # constraining constraint: end of the path in accepting node + constraining = [InDomain(aux_vars[-1], id_ends)] + return constraining, defining + + elif version == "ignace": + # Decompose to transition table using Table constraints + arr, transitions, start, accepting = self.args + lbs, ubs = get_bounds(arr) + lb, ub = min(lbs), max(ubs) + + transitions = [[self.node_map[n_in], v, self.node_map[n_out]] for n_in, v, n_out in transitions] + + # add a sink node for transitions that are not defined + # --> not necessary for comp, because positive context + # sink = len(self.nodes) + # transitions += [[self.node_map[n], v, sink] for n in self.nodes for v in range(lb, ub + 1) if (n, v) not in self.trans_dict] + # transitions += [[sink, v, sink] for v in range(lb, ub + 1)] + + # keep track of current state when traversing the array + state_vars = intvar(0, len(self.nodes)-1, shape=len(arr)) + id_start = self.node_map[start] + # optimization: we know the entry node of the automaton, results in smaller table + defining = [Table([arr[0], state_vars[0]], [[v,e] for s,v,e in transitions if s == id_start])] + # define the rest of the automaton using transition table + defining += [Table([state_vars[i - 1], arr[i], state_vars[i]], transitions) for i in range(1, len(arr))] + + # constraint is satisfied iff last state is accepting + return [InDomain(state_vars[-1], [self.node_map[e] for e in accepting])], defining + + elif version == "mip": + """ + Deterministic Finite Automata (DFA) MIP decomposition based on Côté et al. (2007): + "Modeling the Regular Constraint with Integer Programming" + """ + arr, transitions, start, ends = self.args + + # get number of possible states + nodes = list(set([t[0] for t in transitions] + [t[-1] for t in transitions])) # get all nodes used + Q = len(nodes) + # get number of layers + I = len(arr) + # get possible transition values + D = list(set([t[1] for t in transitions])) + J = len(D) + + # collect possible transitions + Tin = [list() for _ in range(Q)] + Tout = [list() for _ in range(Q)] + for trans in transitions: + Tin[nodes.index(trans[2])].append( (D.index(trans[1]), nodes.index(trans[0])) ) + Tout[nodes.index(trans[0])].append( (D.index(trans[1]), nodes.index(trans[2])) ) + + # get special start / end states + E = [nodes.index(e) for e in ends] + S = nodes.index(start) + + defining = [] + constraining = [] + + # auxiliary decision variables + s = cp.boolvar(shape=(I, J, Q)) # flow variable + sf = cp.boolvar(shape=(len(ends),)) # flow leaving states of last layer + + # 1 unit of flow entering the graph + for q in range(Q): + if q == S: + defining.append( cp.sum(s[0, j, S] for j,_ in Tout[S]) == 1 ) + else: + defining.append( cp.sum(s[0, j, q] for j in range(J)) == 0 ) + # incoming = outgoing + for i in range(1, I): + for q in range(Q): + defining.append( cp.sum([s[i-1, j, q_] for j,q_ in Tin[q]]) == cp.sum([s[i, j, q] for j,_ in Tout[q]]) ) + # collect total flow exiting graph + for q in range(Q): + if q in E: + defining.append( cp.sum([s[-1, j, q_] for j,q_ in Tin[q]]) == sf[E.index(q)] ) + else: + defining.append( cp.sum([s[-1, j, q_] for j,q_ in Tin[q]]) == 0) + # 1 unit of flow exiting the graph + defining.append( cp.sum(sf) == 1 ) + + # channeling with 'arr' + defining.extend([MapDomain(a) for a in arr]) + for i in range(I): + for j in range(J): + constraining.append( (arr[i] == D[j]) == ( cp.sum(s[i,j,:]) ) ) + + return constraining + defining, [] + + + def value(self): + arr, transitions, start, accepting = self.args + arrval = [argval(a) for a in arr] + curr_node = start + for v in arrval: + if (curr_node, v) in self.trans_dict: + curr_node = self.trans_dict[curr_node, v] + else: + return False + return curr_node in accepting + + + +class NotInDomain(GlobalConstraint): + """ + The "NotInDomain" constraint, defining non-interval domains for an expression + """ + + def __init__(self, expr, arr): + super().__init__("NotInDomain", [expr, arr]) + + def decompose(self): + """ + This decomp only works in positive context + """ + from cpmpy.expressions.python_builtins import any, all + expr, arr = self.args + lb, ub = expr.get_bounds() + + defining = [] + #if expr is not a var + if not isinstance(expr, _IntVarImpl): + aux = intvar(lb, ub) + defining.append(aux == expr) + expr = aux + + # Decomposition is wrong, forces value to be within gaps, but does not allow outside of [lb, ub] + # if not any(isinstance(a, Expression) for a in arr): + # given = len(set(arr)) + # missing = ub + 1 - lb - given + # if missing < 2 * given: # != leads to double the amount of constraints + # # use == if there is less than twice as many gaps in the domain. + # row_selected = boolvar(shape=missing) + # return [any(row_selected)] + [rs.implies(expr == val) for val,rs in zip(range(lb, ub + 1), row_selected) if val not in arr], defining + a = [(expr != a) for a in arr] + return a, defining + + + def value(self): + return argval(self.args[0]) not in argvals(self.args[1]) + + def __repr__(self): + return "{} not in {}".format(self.args[0], self.args[1]) + + +class NoOverlap2d(GlobalConstraint): + """ + 2D-version of the NoOverlap constraint. + Ensures a set of rectangles is placed on a grid such that they do not overlap. + """ + def __init__(self, start_x, dur_x, end_x, start_y, dur_y, end_y): + assert len(start_x) == len(dur_x) == len(end_x) == len(start_y) == len(dur_y) == len(end_y) + super().__init__("no_overlap2d", [start_x, dur_x, end_x, start_y, dur_y, end_y]) + + def decompose(self): + from cpmpy.expressions.python_builtins import any as cpm_any + + start_x, dur_x, end_x, start_y, dur_y, end_y = self.args + n = len(start_x) + cons = [s + d == e for s,d,e in zip(start_x, dur_x, end_x)] + cons += [s + d == e for s,d,e in zip(start_y, dur_y, end_y)] + + for i,j in all_pairs(list(range(n))): + cons += [cpm_any([end_x[i] <= start_x[j], end_x[j] <= start_x[i], + end_y[i] <= start_y[j], end_y[j] <= start_y[i]])] + return cons,[] + def value(self): + start_x, dur_x, end_x, start_y, dur_y, end_y = argvals(self.args) + n = len(start_x) + if any(s + d != e for s, d, e in zip(start_x, dur_x, end_x)): + return False + if any(s + d != e for s, d, e in zip(start_y, dur_y, end_y)): + return False + for i,j in all_pairs(list(range(n))): + if end_x[i] > start_x[j] and end_x[j] > start_x[i] and \ + end_y[i] > start_y[j] and end_y[j] > start_y[i]: + return False + return True + + +class IfThenElseNum(GlobalFunction): + """ + Function returning x if b is True and otherwise y + """ + def __init__(self, b, x,y): + super().__init__("IfThenElseNum",[b,x,y]) + + def decompose_comparison(self, cmp_op, cpm_rhs): + b,x,y = self.args + + lbx,ubx = get_bounds(x) + lby,uby = get_bounds(y) + iv = intvar(min(lbx,lby), max(ubx,uby)) + defining = [b.implies(x == iv), (~b).implies(y == iv)] + + return [eval_comparison(cmp_op, iv, cpm_rhs)], defining + + def get_bounds(self): + b,x,y = self.args + lbs,ubs = get_bounds([x,y]) + return min(lbs), max(ubs) + def value(self): + b,x,y = self.args + if argval(b): + return argval(x) + else: + return argval(y) + +class Element(GlobalFunction): + """ + XCSP3 copy for doing Gleb-style ILP friendly decomposition + """ + + def __init__(self, arr, idx): + if is_boolexpr(idx): + raise TypeError("index cannot be a boolean expression: {}".format(idx)) + if is_any_list(idx): + raise TypeError("For using multiple dimensions in the Element constraint, use comma-separated indices") + super().__init__("element", [arr, idx]) + + def __getitem__(self, index): + raise CPMpyException("For using multiple dimensions in the Element constraint use comma-separated indices") + + def value(self): + arr, idx = self.args + idxval = argval(idx) + if idxval is not None: + if idxval >= 0 and idxval < len(arr): + return argval(arr[idxval]) + raise IncompleteFunctionError(f"Index {idxval} out of range for array of length {len(arr)} while calculating value for expression {self}" + + "\n Use argval(expr) to get the value of expr with relational semantics.") + return None # default + + def decompose_comparison(self, cpm_op, cpm_rhs): + """ + `Element(arr,ix)` represents the array lookup itself (a numeric variable) + When used in a comparison relation: Element(arr,idx) CMP_RHS + it is a constraint, and that one can be decomposed. + + Returns two lists of constraints: + + 1) constraints representing the comparison + 2) constraints that (totally) define new auxiliary variables needed in the decomposition, + they should be enforced toplevel. + + """ + arr, idx = self.args + + # Find where the array indices and the bounds of `idx` intersect + lb, ub = get_bounds(idx) + new_lb, new_ub = max(lb, 0), min(ub, len(arr) - 1) + cons, defn = [],[] + + classic = False + if classic: + # For every `i` in that intersection, post `(idx = i) -> idx=i -> arr[i] cpm_rhs`. + for i in range(new_lb, new_ub+1): + cons.append((idx == i).implies(eval_comparison(cpm_op, arr[i], cpm_rhs))) + cons+=[idx >= new_lb, idx <= new_ub] # also enforce the new bounds + else: + # ILP friendly decomposition, from Gleb's paper + defn += [MapDomain(idx)] + expr = cp.sum(arr[i]*(idx == i) for i in range(lb, ub+1)) + cons += [eval_comparison(cpm_op, expr, cpm_rhs)] + + return cons, defn # no auxiliary variables + + + def decompose_numerical(self): + """ + Return a numerical expression to replace the array loopup with in the expression tree + """ + arr, idx = self.args + lb, ub = get_bounds(idx) + expr = cp.sum(arr[i]*(idx == i) for i in range(lb, ub+1)) # <- not missing in CSE since MapDomain done later? + return expr, [MapDomain(idx)] + + + def __repr__(self): + return "{}[{}]".format(self.args[0], self.args[1]) + + def get_bounds(self): + """ + Returns the bounds of the (numerical) global constraint + """ + arr, idx = self.args + bnds = [get_bounds(x) for x in arr] + return min(lb for lb,ub in bnds), max(ub for lb,ub in bnds) + + +class Cumulative(GlobalConstraint): + """ + Global cumulative constraint. Used for resource aware scheduling. + Ensures that the capacity of the resource is never exceeded. + Equivalent to :class:`~cpmpy.expressions.globalconstraints.NoOverlap` when demand and capacity are equal to 1. + Supports both varying demand across tasks or equal demand for all jobs. + """ + def __init__(self, start, duration, end, demand, capacity): + assert is_any_list(start), "start should be a list" + assert is_any_list(duration), "duration should be a list" + assert is_any_list(end), "end should be a list" + + start = flatlist(start) + duration = flatlist(duration) + end = flatlist(end) + assert len(start) == len(duration) == len(end), "Start, duration and end should have equal length" + n_jobs = len(start) + + for lb in get_bounds(duration)[0]: + if lb < 0: + raise TypeError("Durations should be non-negative") + + if is_any_list(demand): + demand = flatlist(demand) + assert len(demand) == n_jobs, "Demand should be supplied for each task or be single constant" + else: # constant demand + demand = [demand] * n_jobs + + super(Cumulative, self).__init__("cumulative", [start, duration, end, demand, capacity]) + + def decompose(self): + """ + Decomposition from: + Schutt, Andreas, et al. "Why cumulative decomposition is not as bad as it sounds." + International Conference on Principles and Practice of Constraint Programming. Springer, Berlin, Heidelberg, 2009. + + Heuristically switches between time-resource and task-resource decomposition depending on the relative size of the time horizon and the number of tasks. + If + n = number of tasks + t = size of time horizon + then + time-resource decomposition scales with n*t + task-resource decomposition scales with 3n(n-1) + thus + switch when t > 3*n + """ + + arr_args = (cpm_array(arg) if is_any_list(arg) else arg for arg in self.args) + start, duration, end, demand, capacity = arr_args + + num_tasks = len(demand) # number of tasks + lb, ub = min(get_bounds(start)[0]), max(get_bounds(end)[1]) + time_horizon = ub - lb + + cons = [] + + if time_horizon > 3 * num_tasks: + version = "task" + else: + version = "time" + + if version == "time": + # set duration of tasks + for t in range(len(start)): + cons += [start[t] + duration[t] == end[t]] + + # demand doesn't exceed capacity + for t in range(lb,ub+1): + demand_at_t = 0 + for job in range(len(start)): + if is_num(demand): + demand_at_t += demand * ((start[job] <= t) & (t < end[job])) + else: + demand_at_t += demand[job] * ((start[job] <= t) & (t < end[job])) + + cons += [demand_at_t <= capacity] + + elif version == "task": + + # set duration of tasks + for t in range(num_tasks): + cons += [start[t] + duration[t] == end[t]] + + for j in range(num_tasks): + cons += [capacity >= demand[j] + cp.sum([(start[i] <= start[j]) & (start[j] < start[i] + duration[i]) for i in range(num_tasks) if i != j])] + + return cons, [] + + def value(self): + arg_vals = [np.array(argvals(arg)) if is_any_list(arg) + else argval(arg) for arg in self.args] + + if any(a is None for a in arg_vals): + return None + + # start, dur, end are np arrays + start, dur, end, demand, capacity = arg_vals + # start and end seperated by duration + if not (start + dur == end).all(): + return False + + # demand doesn't exceed capacity + lb, ub = min(start), max(end) + for t in range(lb, ub+1): + if capacity < sum(demand * ((start <= t) & (t < end))): + return False + + return True + + +class GlobalCardinalityCount(GlobalConstraint): + """ + The number of occurrences of each value `vals[i]` in the list of variables `vars` + must be equal to `occ[i]`. + """ + + def __init__(self, vars, vals, occ, closed=False): + flatargs = flatlist([vars, vals, occ]) + if any(is_boolexpr(arg) for arg in flatargs): + raise TypeError("Only numerical arguments allowed for gcc global constraint: {}".format(flatargs)) + super().__init__("gcc", [vars,vals,occ]) + self.closed = closed + + def decompose(self): + vars, vals, occ = self.args + + variant = "boolean" + + if variant == "classic": + constraints = [cp.Count(vars, i) == v for i, v in zip(vals, occ)] + if self.closed: + constraints += [InDomain(v, vals) for v in vars] + + elif variant == "boolean": + vlb, vub = min(vals), max(vals) + constraints = [] + for var in vars: + constraints += [MapDomain(var)] + X = [[] for _ in range(len((vals)))] + for var in vars: + lb, ub = get_bounds(var) + a = [] + for i in range(max((lb, vlb)), min((ub, vub))+1): + if i in vals: + index = vals.index(i) + aux = (var == i) #cp.boolvar() + a.append(aux) + X[index].append(aux) + if self.closed: + constraints += [ cp.sum(a) == 1 ] + for x, val, oc in zip(X, vals, occ): + constraints += [cp.sum(x) == oc] + + return constraints, [] + + def value(self): + decomposed, _ = self.decompose() + return cp.all(decomposed).value() + +# helper function +def is_transition(arg): + """ test if the argument is a transition, i.e. a 3-elements-tuple specifying a starting state, + a transition value and an ending node""" + return len(arg) == 3 and \ + isinstance(arg[0], (int, str)) and is_int(arg[1]) and isinstance(arg[2], (int, str)) \ No newline at end of file diff --git a/cpmpy/tools/xcsp3/xcsp3_natives.py b/cpmpy/tools/xcsp3/xcsp3_natives.py new file mode 100644 index 000000000..299593e7a --- /dev/null +++ b/cpmpy/tools/xcsp3/xcsp3_natives.py @@ -0,0 +1,90 @@ +""" +A collection of XCSP3 solver-native global constraints. +""" + +import numpy as np +import cpmpy as cp +from cpmpy import cpm_array +from cpmpy.expressions.globalconstraints import DirectConstraint + + +# --------------------------------- OR-Tools --------------------------------- # + +class OrtNoOverlap2D(DirectConstraint): + def __init__(self, arguments): + super().__init__("ortnooverlap2d", arguments) + + def callSolver(self, CPMpy_solver, Native_solver): + start_x, dur_x, end_x, start_y, dur_y, end_y = CPMpy_solver.solver_vars(self.args[0]) + intervals_x = [Native_solver.NewIntervalVar(s,d,e, f"xinterval_{s}-{d}-{d}") for s,d,e in zip(start_x,dur_x,end_x)] + intervals_y = [Native_solver.NewIntervalVar(s,d,e, f"yinterval_{s}-{d}-{d}") for s,d,e in zip(start_y,dur_y,end_y)] + return Native_solver.add_no_overlap_2d(intervals_x, intervals_y) + +class OrtSubcircuit(DirectConstraint): + def __init__(self, arguments): + super().__init__("ortsubcircuit", arguments) + + def callSolver(self, CPMpy_solver, Native_solver): + N = len(self.args[0]) + arcvars = cp.boolvar(shape=(N,N)) + # post channeling constraints from int to bool + CPMpy_solver.add([b == (self.args[0][i] == j) for (i,j),b in np.ndenumerate(arcvars)], internal=True) + # post the global constraint + # posting arcs on diagonal (i==j) allows for subcircuits + ort_arcs = [(i,j, CPMpy_solver.solver_var(b)) for (i,j),b in np.ndenumerate(arcvars)] # Allows for empty subcircuits + + return Native_solver.AddCircuit(ort_arcs) + +class OrtSubcircuitWithStart(DirectConstraint): + def __init__(self, arguments, start_index:int=0): + super().__init__("ortsubcircuitwithstart", (arguments, start_index)) + + def callSolver(self, CPMpy_solver, Native_solver): + N = len(self.args[0]) + arcvars = cp.boolvar(shape=(N,N)) + # post channeling constraints from int to bool + CPMpy_solver.add([b == (self.args[0][i] == j) for (i,j),b in np.ndenumerate(arcvars)], internal=True) + # post the global constraint + # posting arcs on diagonal (i==j) allows for subcircuits + ort_arcs = [(i,j,CPMpy_solver.solver_var(b)) for (i,j),b in np.ndenumerate(arcvars) if not ((i == j) and (i == self.args[1]))] # The start index cannot self loop and thus must be part of the subcircuit. + + return Native_solver.AddCircuit(ort_arcs) + + +# ----------------------------------- Choco ---------------------------------- # + +class ChocoSubcircuit(DirectConstraint): + def __init__(self, arguments): + super().__init__("chocosubcircuit", arguments) + + def callSolver(self, CPMpy_solver, Native_solver): + # Successor variables + succ = CPMpy_solver.solver_vars(self.args[0]) + # Add an unused variable for the subcircuit length. + subcircuit_length = CPMpy_solver.solver_var(cp.intvar(0, len(succ))) + return Native_solver.sub_circuit(succ, 0, subcircuit_length) + +# --------------------------------- Minizinc --------------------------------- # + +class MinizincSubcircuit(DirectConstraint): + def __init__(self, arguments): + super().__init__("minizincsubcircuit", arguments) + + def callSolver(self, CPMpy_solver, Native_solver): + # minizinc is offset 1, which can be problematic here... + args_str = ["{}+1".format(CPMpy_solver._convert_expression(e)) for e in self.args[0]] + return "{}([{}])".format("subcircuit", ",".join(args_str)) + +class MinizincSubcircuitWithStart(DirectConstraint): + def __init__(self, arguments): + super().__init__("minizincsubcircuitwithstart", arguments) + + def callSolver(self, CPMpy_solver, Native_solver): + # minizinc is offset 1, which can be problematic here... + start_index = self.args[0][-1] + succ = cpm_array(self.args[0][:-1]) # Successor variables + + CPMpy_solver += (succ[start_index] != start_index) + args_str = ["{}+1".format(CPMpy_solver._convert_expression(e)) for e in succ] + return "{}([{}])".format("subcircuit", ",".join(args_str)) + \ No newline at end of file diff --git a/cpmpy/tools/xcsp3/xcsp3_solution.py b/cpmpy/tools/xcsp3/xcsp3_solution.py new file mode 100644 index 000000000..ca7ad0f57 --- /dev/null +++ b/cpmpy/tools/xcsp3/xcsp3_solution.py @@ -0,0 +1,51 @@ +""" +Collection of tools for handeling solutions in XCSP3 format. +""" + +import xml.etree.cElementTree as ET + +def solution_xml(model, useless_style="*", boolean_style="int"): + """ + Formats a solution according to the XCSP3 specification. + + Arguments: + model: CPMpy model for which to format its solution (should be solved first) + useless_style: How to process unused decision variables (with value `None`). + If "*", variable is included in reporting with value "*". + If "drop", variable is excluded from reporting. + boolean_style: Print style for boolean constants. + "int" results in 0/1, "bool" results in False/True. + + Returns: + XML-formatted model solution according to XCSP3 specification. + """ + + # CSP + if not model.has_objective(): + root = ET.Element("instantiation", type="solution") + # COP + else: + root = ET.Element("instantiation", type="optimum", cost=str(int(model.objective_value()))) + + # How useless variables should be handled + # (variables which have value `None` in the solution) + variables = {var.name: var for var in model.user_vars if var.name[:2] not in ["IV", "BV", "B#"]} # dirty workaround for all missed aux vars in user vars + if useless_style == "*": + variables = {k:(v.value() if v.value() is not None else "*") for k,v in variables.items()} + elif useless_style == "drop": + variables = {k:v.value() for k,v in variables.items() if v.value() is not None} + + # Convert booleans + if boolean_style == "bool": + pass + elif boolean_style == "int": + variables = {k:(v if (not isinstance(v, bool)) else (1 if v else 0)) for k,v in variables.items()} + + # Build XCSP3 XML tree + ET.SubElement(root, "list").text=" " + " ".join([str(v) for v in variables.keys()]) + " " + ET.SubElement(root, "values").text=" " + " ".join([str(v) for v in variables.values()]) + " " + tree = ET.ElementTree(root) + ET.indent(tree, space=" ", level=0) + res = ET.tostring(root).decode("utf-8") + + return str(res) \ No newline at end of file diff --git a/cpmpy/transformations/decompose_global.py b/cpmpy/transformations/decompose_global.py index 0b752080f..648824e32 100644 --- a/cpmpy/transformations/decompose_global.py +++ b/cpmpy/transformations/decompose_global.py @@ -10,7 +10,7 @@ from ..expressions.globalfunctions import GlobalFunction from ..expressions.core import Expression, Comparison, Operator from ..expressions.variables import intvar, cpm_array, NDVarArray -from ..expressions.utils import is_any_list, eval_comparison +from ..expressions.utils import is_any_list, eval_comparison, is_num from ..expressions.python_builtins import all from .flatten_model import flatten_constraint, normalized_numexpr @@ -64,8 +64,32 @@ def decompose_in_tree(lst_of_expr, supported=set(), supported_reified=set(), _to continue if any(isinstance(a,GlobalFunction) for a in expr.args): + # XXX This is where GlobalFunctions get turned into Comparisons... + newargs = [a for a in expr.args] # copy + modified = False + for i, a in enumerate(expr.args): + # we can do something special for Element + if hasattr(a, "name") and ( + (a.name == "element" and "element" not in supported and all(is_num(t) for t in a.args[0])) or \ + (a.name == "count" and "count" not in supported) + ): + # it's an element with constants or a count + encoding, otherdef = a.decompose_numerical() + assert encoding.is_bool() is False, "we should get a numerical expression here (wsum over bools)" + newargs[i] = encoding + # call decompose here so that the MapDomain is populated in the csemap + xtratoplevel = [] + newotherdef = decompose_in_tree(otherdef, supported, supported_reified, xtratoplevel, nested=False, csemap=csemap) + _toplevel.extend(xtratoplevel) + _toplevel.extend(newotherdef) + modified = True + if modified: + # copy of entire expr with the new args + expr = Operator(expr.name, newargs) + expr, base_con = normalized_numexpr(expr, csemap=csemap) _toplevel.extend(base_con) # should be added toplevel + # recurse into arguments, recreate through constructor (we know it stores no other state) args = decompose_in_tree(expr.args, supported, supported_reified, _toplevel, nested=True, csemap=csemap) newlist.append(Operator(expr.name, args)) @@ -77,6 +101,18 @@ def decompose_in_tree(lst_of_expr, supported=set(), supported_reified=set(), _to is_supported = (expr.name in supported_reified) else: is_supported = (expr.name in supported) + + # special case: MapDomain + if expr.name == "mapdomain": + assert nested is False, "'mapdomain' cannot be nested" + # populate csemap, return decomposition if not supported + decomposed, all_in_csemap = expr.decompose(is_supported=is_supported, csemap=csemap) + if not all_in_csemap: + if is_supported: + newlist.append(expr) + else: + newlist.extend(decomposed) + continue if is_supported: # If no nested expressions, don't recurse the arguments @@ -109,6 +145,17 @@ def decompose_in_tree(lst_of_expr, supported=set(), supported_reified=set(), _to else: # global function, replace by a fresh variable and decompose the equality to this assert isinstance(expr, GlobalFunction) + + # we can do something special for Element + if (expr.name == "element" and all(is_num(a) for a in expr.args[0])) or (expr.name == "count"): + # it's an array with constants + encoding, otherdef = expr.decompose_numerical() + assert encoding.is_bool() is False, "we should get a numerical expression here (wsum over bools)" + newlist.append(encoding) + _toplevel.extend(otherdef) + continue + + # else, do the usual thing lb,ub = expr.get_bounds() if csemap is not None and expr in csemap: diff --git a/cpmpy/transformations/flatten_model.py b/cpmpy/transformations/flatten_model.py index bed4c5a9e..49149f52a 100644 --- a/cpmpy/transformations/flatten_model.py +++ b/cpmpy/transformations/flatten_model.py @@ -119,12 +119,18 @@ def flatten_model(orig_model): else: return cp.Model(*basecons, maximize=newobj) +# constants to determine Boolean context +POSITIVE = 1 +NEGATIVE = 2 +MIXED = 3 -def flatten_constraint(expr, csemap=None): + +def flatten_constraint(expr, context=MIXED, csemap=None): """ input is any expression; except is_num(), pure _NumVarImpl, or Operator/GlobalConstraint with not is_bool() - + context: the context of the given expression, can be POSITIVE, NEGATIVE or MIXED + default is MIXED, just to be safe if we forget to specify output: see definition of 'flat normal form' above. it will return 'Exception' if something is not supported @@ -164,7 +170,7 @@ def flatten_constraint(expr, csemap=None): if isinstance(a, Operator) and a.name == '->': newargs[i:i+1] = [~a.args[0],a.args[1]] # there could be nested implications - newlist.extend(flatten_constraint(Operator('or', newargs), csemap=csemap)) + newlist.extend(flatten_constraint(Operator('or', newargs), csemap=csemap, context=context)) continue # conjunctions in disjunctions could be split out by applying distributivity, # but this would explode the number of constraints in favour of having less auxiliary variables. @@ -175,30 +181,30 @@ def flatten_constraint(expr, csemap=None): if expr.args[1].name == 'and': a1s = expr.args[1].args a0 = expr.args[0] - newlist.extend(flatten_constraint([a0.implies(a1) for a1 in a1s], csemap=csemap)) + newlist.extend(flatten_constraint([a0.implies(a1) for a1 in a1s], csemap=csemap, context=context)) continue # 2) if lhs is 'or' then or([a01..a0n])->a1 :: ~a1->and([~a01..~a0n] and split elif expr.args[0].name == 'or': a0s = expr.args[0].args a1 = expr.args[1] - newlist.extend(flatten_constraint([(~a1).implies(~a0) for a0 in a0s], csemap=csemap)) + newlist.extend(flatten_constraint([(~a1).implies(~a0) for a0 in a0s], csemap=csemap, context=context)) continue # 2b) if lhs is ->, like 'or': a01->a02->a1 :: (~a01|a02)->a1 :: ~a1->a01,~a1->~a02 elif expr.args[0].name == '->': a01,a02 = expr.args[0].args a1 = expr.args[1] - newlist.extend(flatten_constraint([(~a1).implies(a01), (~a1).implies(~a02)], csemap=csemap)) + newlist.extend(flatten_constraint([(~a1).implies(a01), (~a1).implies(~a02)], csemap=csemap, context=context)) continue # ->, allows a boolexpr on one side elif isinstance(expr.args[0], _BoolVarImpl): # LHS is var, ensure RHS is normalized 'Boolexpr' lhs,lcons = expr.args[0], () - rhs,rcons = normalized_boolexpr(expr.args[1], csemap=csemap) + rhs,rcons = normalized_boolexpr(expr.args[1], csemap=csemap, context=max(POSITIVE, context)) else: # make LHS normalized 'Boolexpr', RHS must be a var - lhs,lcons = normalized_boolexpr(expr.args[0], csemap=csemap) - rhs,rcons = get_or_make_var(expr.args[1], csemap=csemap) + lhs,lcons = normalized_boolexpr(expr.args[0], csemap=csemap, context=max(NEGATIVE, context)) + rhs,rcons = get_or_make_var(expr.args[1], csemap=csemap, context=max(POSITIVE, context)) newlist.append(Operator(expr.name, (lhs,rhs))) newlist.extend(lcons) @@ -209,7 +215,7 @@ def flatten_constraint(expr, csemap=None): # if none of the above cases + continue matched: # a normalizable boolexpr - (con, flatcons) = normalized_boolexpr(expr, csemap=csemap) + (con, flatcons) = normalized_boolexpr(expr, csemap=csemap, context=context) newlist.append(con) newlist.extend(flatcons) @@ -259,10 +265,10 @@ def flatten_constraint(expr, csemap=None): if exprname == '==' and lexpr.is_bool(): if rvar.is_bool(): # this is a reification - (lhs, lcons) = normalized_boolexpr(lexpr, csemap=csemap) + (lhs, lcons) = normalized_boolexpr(lexpr, csemap=csemap, context=MIXED) else: # integer comparison - (lhs, lcons) = get_or_make_var(lexpr, csemap=csemap) + (lhs, lcons) = get_or_make_var(lexpr, csemap=csemap, context=MIXED) else: (lhs, lcons) = normalized_numexpr(lexpr, csemap=csemap) @@ -274,7 +280,7 @@ def flatten_constraint(expr, csemap=None): """ - Global constraint: global([Var]*) (CPMpy class 'GlobalConstraint') """ - (con, flatcons) = normalized_boolexpr(expr, csemap=csemap) + (con, flatcons) = normalized_boolexpr(expr, csemap=csemap, context=context) newlist.append(con) newlist.extend(flatcons) @@ -322,7 +328,7 @@ def __is_flat_var_or_list(arg): is_any_list(arg) and all(__is_flat_var_or_list(el) for el in arg) or \ is_star(arg) -def get_or_make_var(expr, csemap=None): +def get_or_make_var(expr, csemap=None, context=MIXED): """ Must return a variable, and list of flat normal constraints Determines whether this is a Boolean or Integer variable and returns @@ -335,12 +341,17 @@ def get_or_make_var(expr, csemap=None): if is_any_list(expr): raise Exception(f"Expected single variable, not a list for: {expr}") - if csemap is not None and expr in csemap: - return csemap[expr], [] - if expr.is_bool(): + + if csemap is not None and (expr, context) in csemap: + return csemap[(expr, context)], [] + if csemap is not None and (expr, MIXED) in csemap: # if we have it already in mixed context anyway, we can also use that + return csemap[(expr, MIXED)], [] + if csemap is not None and expr in csemap: # maybe it was put in by decompos_in_tree already? (unlikely) + return csemap[expr], [] + # normalize expr into a boolexpr LHS, reify LHS == bvar - (flatexpr, flatcons) = normalized_boolexpr(expr, csemap=csemap) + (flatexpr, flatcons) = normalized_boolexpr(expr, csemap=csemap, context=context) if isinstance(flatexpr,_BoolVarImpl): # avoids unnecessary bv == bv or bv == ~bv assignments @@ -349,10 +360,18 @@ def get_or_make_var(expr, csemap=None): # save expr in dict if csemap is not None: - csemap[expr] = bvar - return bvar, [flatexpr == bvar] + flatcons + csemap[(expr, context)] = bvar + + if context == POSITIVE: + return bvar, [bvar.implies(flatexpr)] + flatcons + else: + return bvar, [flatexpr == bvar] + flatcons else: + + if csemap is not None and expr in csemap: # numexpr is saved without context + return csemap[expr], [] + # normalize expr into a numexpr LHS, # then compute bounds and return (newintvar, LHS == newintvar) (flatexpr, flatcons) = normalized_numexpr(expr, csemap=csemap) @@ -366,7 +385,7 @@ def get_or_make_var(expr, csemap=None): # save expr in dict if csemap is not None: - csemap[expr] = ivar + csemap[(expr, context)] = ivar return ivar, [flatexpr == ivar] + flatcons def get_or_make_var_or_list(expr, csemap=None): @@ -383,7 +402,7 @@ def get_or_make_var_or_list(expr, csemap=None): return get_or_make_var(expr, csemap=csemap) -def normalized_boolexpr(expr, csemap=None): +def normalized_boolexpr(expr, csemap=None, context=MIXED): """ input is any Boolean (is_bool()) expression output are all 'flat normal form' Boolean expressions that can be 'reified', meaning that @@ -421,7 +440,7 @@ def normalized_boolexpr(expr, csemap=None): # TODO, optimisation if args0 is an 'and'? (lhs,lcons) = get_or_make_var(expr.args[0], csemap=csemap) # TODO, optimisation if args1 is an 'or'? - (rhs,rcons) = get_or_make_var(expr.args[1], csemap=csemap) + (rhs,rcons) = get_or_make_var(expr.args[1], csemap=csemap, context=max(POSITIVE, context)) return ((~lhs | rhs), lcons+rcons) if expr.name == 'not': flatvar, flatcons = get_or_make_var(expr.args[0], csemap=csemap) @@ -429,8 +448,9 @@ def normalized_boolexpr(expr, csemap=None): if not expr.has_subexpr(): return (expr, []) else: + assert expr.name in {"and", "or"}, f"Unexpected `Operator`: {expr.name}" # one of the arguments is not flat, flatten all - flatvars, flatcons = zip(*[get_or_make_var(arg, csemap=csemap) for arg in expr.args]) + flatvars, flatcons = zip(*[get_or_make_var(arg, csemap=csemap, context=max(POSITIVE,context)) for arg in expr.args]) newexpr = Operator(expr.name, flatvars) return (newexpr, [c for con in flatcons for c in con]) diff --git a/cpmpy/transformations/linearize.py b/cpmpy/transformations/linearize.py index 8735fff1a..95d9d9900 100644 --- a/cpmpy/transformations/linearize.py +++ b/cpmpy/transformations/linearize.py @@ -52,7 +52,6 @@ import numpy as np import cpmpy as cp from cpmpy.transformations.get_variables import get_variables - from cpmpy.transformations.reification import only_implies, only_bv_reifies @@ -64,7 +63,7 @@ from ..exceptions import TransformationNotImplementedError from ..expressions.core import Comparison, Expression, Operator, BoolVal -from ..expressions.globalconstraints import GlobalConstraint, DirectConstraint +from ..expressions.globalconstraints import GlobalConstraint, DirectConstraint, MapDomain from ..expressions.globalfunctions import GlobalFunction from ..expressions.utils import is_bool, is_num, eval_comparison, get_bounds, is_true_cst, is_false_cst @@ -361,22 +360,41 @@ def linearize_constraint(lst_of_expr, supported={"sum","wsum"}, reified=False, c if reified is True: raise ValueError("Linear decomposition of AllDifferent does not work reified. " "Ensure 'alldifferent' is not in the 'supported_nested' set of 'decompose_in_tree'") + + # Create Boolvars and seed CSE map + exprs = decompose_in_tree([MapDomain(a) for a in cpm_expr.args], csemap=csemap) + newlist.extend(linearize_constraint(exprs, supported=supported, reified=reified, csemap=csemap)) lbs, ubs = get_bounds(cpm_expr.args) lb, ub = min(lbs), max(ubs) - n_vals = (ub-lb) + 1 - - x = boolvar(shape=(len(cpm_expr.args), n_vals)) - - newlist += [sum(row) == 1 for row in x] # each var has exactly one value - newlist += [sum(col) <= 1 for col in x.T] # each value can be taken at most once - # link Boolean matrix and integer variable - for arg, row in zip(cpm_expr.args, x): - if is_num(arg): # constant, fix directly - newlist.append(Operator("sum", [row[arg-lb]]) == 1) # ensure it is linear - else: # ensure result is canonical - newlist.append(sum(np.arange(lb, ub + 1) * row) + -1 * arg == 0) + for val in range(lb, ub+1): + bvs = [] + cons = [] + for a in cpm_expr.args: + bv, con = get_or_make_var(a == val, csemap=csemap) + bvs.append(bv) + if len(con) > 0: + cons.append(con) + # each value can be taken at most once (not necessarily exactly once) + newlist.append(cp.sum(bvs) <= 1) + if len(cons) > 0: + newlist += linearize_constraint(cons, supported=supported, reified=reified, csemap=csemap) + # XXX TODO It can be very tricky to get the get/make var and linearize correct... + # I think we should have AllDiff just use this decomp by default... or have + # another way to overwrite the decomp before the 'decompose_in_tree' call... + + # x = boolvar(shape=(len(cpm_expr.args), n_vals)) + + # newlist += [sum(row) == 1 for row in x] # each var has exactly one value + # newlist += [sum(col) <= 1 for col in x.T] # each value can be taken at most once + + # # link Boolean matrix and integer variable + # for arg, row in zip(cpm_expr.args, x): + # if is_num(arg): # constant, fix directly + # newlist.append(Operator("sum", [row[arg-lb]]) == 1) # ensure it is linear + # else: # ensure result is canonical + # newlist.append(sum(np.arange(lb, ub + 1) * row) + -1 * arg == 0) elif isinstance(cpm_expr, (DirectConstraint, BoolVal)): newlist.append(cpm_expr) diff --git a/cpmpy/transformations/reification.py b/cpmpy/transformations/reification.py index b3d682fad..8501a611c 100644 --- a/cpmpy/transformations/reification.py +++ b/cpmpy/transformations/reification.py @@ -52,7 +52,7 @@ def only_bv_reifies(constraints, csemap=None): newcons.append(cpm_expr) return newcons -def only_implies(constraints, csemap=None): +def only_implies(constraints, csemap=None, rewrite_bool_eq=True): """ Transforms all reifications to ``BV -> BE`` form @@ -75,7 +75,7 @@ def only_implies(constraints, csemap=None): # Operators: check BE -> BV if cpm_expr.name == '->' and cpm_expr.args[1].name == '==': a0,a1 = cpm_expr.args - if a1.args[0].is_bool() and a1.args[1].is_bool(): + if rewrite_bool_eq and a1.args[0].is_bool() and a1.args[1].is_bool(): # BV0 -> BV2 == BV3 :: BV0 -> (BV2->BV3 & BV3->BV2) # :: BV0 -> (BV2->BV3) & BV0 -> (BV3->BV2) # :: BV0 -> (~BV2|BV3) & BV0 -> (~BV3|BV2) diff --git a/docs/api/tools/xcsp3.rst b/docs/api/tools/xcsp3.rst new file mode 100644 index 000000000..ad3bd0868 --- /dev/null +++ b/docs/api/tools/xcsp3.rst @@ -0,0 +1,15 @@ +XCSP3 (:mod:`cpmpy.tools.xcsp3`) +===================================================== + +.. automodule:: cpmpy.tools.xcsp3 + :members: + :undoc-members: + :inherited-members: + +.. include:: ./xcsp3/analyze.rst +.. include:: ./xcsp3/dataset.rst +.. include:: ./xcsp3/globals.rst +.. include:: ./xcsp3/solution.rst +.. include:: ./xcsp3/cli.rst +.. include:: ./xcsp3/benchmark.rst + diff --git a/docs/api/tools/xcsp3/analyze.rst b/docs/api/tools/xcsp3/analyze.rst new file mode 100644 index 000000000..6ce3702d7 --- /dev/null +++ b/docs/api/tools/xcsp3/analyze.rst @@ -0,0 +1,4 @@ +Analyze (:mod:`cpmpy.tools.xcsp3.xcsp3_analyze`) +===================================================== + +.. automodule:: cpmpy.tools.xcsp3.xcsp3_analyze diff --git a/docs/api/tools/xcsp3/benchmark.rst b/docs/api/tools/xcsp3/benchmark.rst new file mode 100644 index 000000000..ac770d07c --- /dev/null +++ b/docs/api/tools/xcsp3/benchmark.rst @@ -0,0 +1,4 @@ +Benchmark (:mod:`cpmpy.tools.xcsp3.xcsp3_benchmark`) +===================================================== + +.. automodule:: cpmpy.tools.xcsp3.xcsp3_benchmark diff --git a/docs/api/tools/xcsp3/cli.rst b/docs/api/tools/xcsp3/cli.rst new file mode 100644 index 000000000..efb0f44e0 --- /dev/null +++ b/docs/api/tools/xcsp3/cli.rst @@ -0,0 +1,5 @@ +CLI (:mod:`cpmpy.tools.xcsp3.xcsp3_cpmpy`) +===================================================== + +.. automodule:: cpmpy.tools.xcsp3.xcsp3_cpmpy + \ No newline at end of file diff --git a/docs/api/tools/xcsp3/dataset.rst b/docs/api/tools/xcsp3/dataset.rst new file mode 100644 index 000000000..de9d24edc --- /dev/null +++ b/docs/api/tools/xcsp3/dataset.rst @@ -0,0 +1,7 @@ +Dataset (:mod:`cpmpy.tools.xcsp3.xcsp3_dataset`) +===================================================== + +.. automodule:: cpmpy.tools.xcsp3.xcsp3_dataset + :members: + :undoc-members: + :inherited-members: \ No newline at end of file diff --git a/docs/api/tools/xcsp3/globals.rst b/docs/api/tools/xcsp3/globals.rst new file mode 100644 index 000000000..7858836f1 --- /dev/null +++ b/docs/api/tools/xcsp3/globals.rst @@ -0,0 +1,7 @@ +Globals (:mod:`cpmpy.tools.xcsp3.xcsp3_globals`) +===================================================== + +.. automodule:: cpmpy.tools.xcsp3.xcsp3_globals + :members: + :undoc-members: + :inherited-members: \ No newline at end of file diff --git a/docs/api/tools/xcsp3/solution.rst b/docs/api/tools/xcsp3/solution.rst new file mode 100644 index 000000000..536450a76 --- /dev/null +++ b/docs/api/tools/xcsp3/solution.rst @@ -0,0 +1,7 @@ +Solution (:mod:`cpmpy.tools.xcsp3.xcsp3_solution`) +===================================================== + +.. automodule:: cpmpy.tools.xcsp3.xcsp3_solution + :members: + :undoc-members: + :inherited-members: \ No newline at end of file diff --git a/setup.py b/setup.py index 583160942..cf557f286 100644 --- a/setup.py +++ b/setup.py @@ -55,7 +55,7 @@ def get_version(rel_path): # Solvers **solver_dependencies, # Tools - # "xcsp3": ["pycsp3"], <- for when xcsp3 is merged + "xcsp3": ["pycsp3", "requests"], # didn't add CLI-specific req since some are not cross-platform # Other "test": ["pytest"], "docs": ["sphinx>=5.3.0", "sphinx_rtd_theme>=2.0.0", "myst_parser", "sphinx-automodapi", "readthedocs-sphinx-search>=0.3.2"],