From 7e936f117a2d0acb6d800a25d4e5d9d53f9936c4 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Tue, 24 Jun 2025 18:49:50 +0200 Subject: [PATCH 01/20] abstract vars not bool --- pycona/problem_instance/language.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycona/problem_instance/language.py b/pycona/problem_instance/language.py index 4d4cc79..1032a2c 100644 --- a/pycona/problem_instance/language.py +++ b/pycona/problem_instance/language.py @@ -38,7 +38,7 @@ def __init__(self, name): def is_bool(self): """ is it a Boolean (return type) Operator? """ - return NotImplementedError("Abstract variable is not supposed to be used") + return False def value(self): """ the value obtained in the last solve call From 660ed4ea8f36c0e537f23d375bd65ca8cdfcf4b3 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Tue, 24 Jun 2025 18:49:54 +0200 Subject: [PATCH 02/20] Create zebra.py --- pycona/benchmarks/zebra.py | 63 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 pycona/benchmarks/zebra.py diff --git a/pycona/benchmarks/zebra.py b/pycona/benchmarks/zebra.py new file mode 100644 index 0000000..952ec86 --- /dev/null +++ b/pycona/benchmarks/zebra.py @@ -0,0 +1,63 @@ +import cpmpy as cp +from cpmpy.transformations.normalize import toplevel_list +from ..answering_queries.constraint_oracle import ConstraintOracle +from ..problem_instance import ProblemInstance, absvar + + +def construct_zebra_problem(): + """ + :Description: The zebra puzzle is a well-known logic puzzle. Five houses, each of a different color, are occupied by men of + different nationalities, with different pets, drinks and cigarettes. The puzzle is to find out who owns the zebra. + The puzzle has 15 clues that help determine the solution. + :return: a ProblemInstance object, along with a constraint-based oracle + """ + # Create a dictionary with the parameters + parameters = {"grid_size": 5, "num_categories": 5} + + # Variables + # Flattened array with 25 elements, representing 5 elements for each of the 5 categories + grid = cp.intvar(1, 5, shape=(5, 5), name="grid") + + C_T = list() + + # Extract variables for readability + ukr, norge, eng, spain, jap = grid[0, :] # Nationalities + red, blue, yellow, green, ivory = grid[1,:] # Colors + oldGold, parly, kools, lucky, chest = grid[2,:] # Cigarettes + zebra, dog, horse, fox, snails = grid[3,:] # Pets + coffee, tea, h2o, milk, oj = grid[4,:] # Drinks + + # Add all constraints + C_T += [(eng == red)] # Englishman lives in the red house + C_T += [(spain == dog)] # Spaniard owns the dog + C_T += [(coffee == green)] # Coffee is drunk in the green house + C_T += [(ukr == tea)] # Ukrainian drinks tea + C_T += [(green == ivory + 1)] # Green house is immediately right of the ivory house + C_T += [(oldGold == snails)] # OldGold smoker owns snails + C_T += [(kools == yellow)] # Kools are smoked in the yellow house + C_T += [(milk == 3)] # Milk is drunk in the middle house + C_T += [(norge == 1)] # Norwegian lives in the first house + C_T += [(abs(chest - fox) == 1)] # Chesterfield smoker lives next to the man with the fox + C_T += [(abs(kools - horse) == 1)] # Kools are smoked in the house next to the house with the horse + C_T += [(lucky == oj)] # Lucky smoker drinks orange juice + C_T += [(jap == parly)] # Japanese smokes Parliaments + C_T += [(abs(norge - blue) == 1)] # Norwegian lives next to the blue house + + # Each row must have different values + for row in grid: + C_T += list(cp.AllDifferent(row).decompose()) + + # Create the language: + AV = absvar(2) # create abstract vars - as many as maximum arity + + # create abstract relations using the abstract vars + lang = [AV[0] == AV[1], AV[0] != AV[1], AV[0] < AV[1], AV[0] > AV[1], AV[0] >= AV[1], AV[0] <= AV[1], + abs(AV[0] - AV[1]) == 1, abs(AV[0] - AV[1]) != 1, AV[0] - AV[1] == 1, AV[1] - AV[0] == 1] + [AV[0] == constant for constant in range(1, 6)] + [AV[0] != constant for constant in range(1, 6)] + + instance = ProblemInstance(variables=grid, params=parameters, language=lang, name="zebra") + + oracle = ConstraintOracle(list(set(toplevel_list(C_T)))) + + + + return instance, oracle From d03b283d2562447606dec06dfa4f1506bedb78af Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Tue, 24 Jun 2025 18:50:08 +0200 Subject: [PATCH 03/20] zebra in init --- pycona/benchmarks/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pycona/benchmarks/__init__.py b/pycona/benchmarks/__init__.py index 2cfc4cd..e2963ea 100644 --- a/pycona/benchmarks/__init__.py +++ b/pycona/benchmarks/__init__.py @@ -4,4 +4,5 @@ from .exam_timetabling import construct_examtt_simple from .job_shop_scheduling import construct_job_shop_scheduling_problem from .nurse_rostering import construct_nurse_rostering +from .zebra import construct_zebra_problem From 80341a291e6f92d712712cb4e96184e8d736f891 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Tue, 24 Jun 2025 18:50:16 +0200 Subject: [PATCH 04/20] correct names in feature repr --- pycona/predictor/feature_representation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycona/predictor/feature_representation.py b/pycona/predictor/feature_representation.py index 1e23e44..fb60ac9 100644 --- a/pycona/predictor/feature_representation.py +++ b/pycona/predictor/feature_representation.py @@ -112,11 +112,11 @@ def _init_features(self): self._features['Var_name_same'] = 'Bool' for i in range(self._max_ndims): + self._features[f"Dim{i}_same"] = 'Bool' self._features[f"Dim{i}_max"] = 'Int' self._features[f"Dim{i}_min"] = 'Int' self._features[f"Dim{i}_avg"] = 'Real' self._features[f"Dim{i}_diff"] = 'Real' - self._features[f"Dim{i}_avg_diff"] = 'Real' self._features[f"Relation"] = self._lang self._features[f"Arity"] = 'Real' From 0a1bc9b324d76649413554ff07224eeadf24abb0 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Tue, 24 Jun 2025 19:09:24 +0200 Subject: [PATCH 05/20] nqueens --- pycona/benchmarks/__init__.py | 2 +- pycona/benchmarks/nqueens.py | 47 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 pycona/benchmarks/nqueens.py diff --git a/pycona/benchmarks/__init__.py b/pycona/benchmarks/__init__.py index e2963ea..04436e5 100644 --- a/pycona/benchmarks/__init__.py +++ b/pycona/benchmarks/__init__.py @@ -5,4 +5,4 @@ from .job_shop_scheduling import construct_job_shop_scheduling_problem from .nurse_rostering import construct_nurse_rostering from .zebra import construct_zebra_problem - +from .nqueens import construct_nqueens_problem diff --git a/pycona/benchmarks/nqueens.py b/pycona/benchmarks/nqueens.py new file mode 100644 index 0000000..05c5e79 --- /dev/null +++ b/pycona/benchmarks/nqueens.py @@ -0,0 +1,47 @@ +import cpmpy as cp +from cpmpy.transformations.normalize import toplevel_list +from ..answering_queries.constraint_oracle import ConstraintOracle +from ..problem_instance import ProblemInstance, absvar + + + +def construct_nqueens_problem(n): + + parameters = {"n": n} + + queens = cp.intvar(1, n, shape=n, name="queens") + + # Model + model = cp.Model() + + # Constraints list + CT = [] + + CT += list(cp.AllDifferent(queens).decompose()) + + for i in range(n): + for j in range(i + 1, n): # Compare each queen with every other queen once + CT += [(queens[i] - i != queens[j] - j)] # Different major diagonals + CT += [(queens[i] + i != queens[j] + j)] # Different minor diagonals + + + # Add all collected constraints to the model + model += CT + + C_T = toplevel_list(CT) + + AV = absvar(2) + #lang = [AV[0] == AV[1], AV[0] != AV[1], AV[0] < AV[1], AV[0] > AV[1], AV[0] >= AV[1], AV[0] <= AV[1]] + + lang = [AV[0] - AV[1] == constant for constant in range(-n, 2*n)] + [AV[0] - AV[1] != constant for constant in range(-n, 2*n)] + + instance = ProblemInstance(variables=queens, params=parameters, language=lang, name="nqueens") + + oracle = ConstraintOracle(list(set(toplevel_list(C_T)))) + + print("oracle constraints: ", len(oracle.constraints)) + for c in oracle.constraints: + print(c) + + input("Press Enter to continue...") + + return instance, oracle From 078047cdd0de0a12d6bf96c7b4db31089e204ef4 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Wed, 25 Jun 2025 19:37:56 +0200 Subject: [PATCH 06/20] correct remove redundant conj --- pycona/find_constraint/utils.py | 108 +++++++++++++++++++------------- 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/pycona/find_constraint/utils.py b/pycona/find_constraint/utils.py index 899825b..ab8b30c 100644 --- a/pycona/find_constraint/utils.py +++ b/pycona/find_constraint/utils.py @@ -1,4 +1,5 @@ from itertools import chain +import cpmpy as cp def get_max_conjunction_size(C1): @@ -70,53 +71,72 @@ def join_con_net(C1, C2): return C3 -def remove_redundant_conj(C1): +def get_conjunction_args(constraint): """ - Remove redundant conjunctions from the given list of constraints. - - :param C1: A list of constraints. - :return: A list of constraints with redundant conjunctions removed. + Break down a constraint into its constituent conjunctive arguments. + + Args: + constraint: A CPMpy constraint that may contain conjunctions + + Returns: + list: A list of atomic constraints that make up the conjunction """ - C2 = list() + stack = [constraint] + conj_args = [] - for c in C1: - C = [c] - conj_args = [] + while stack: + current = stack.pop() + if current.name == 'and': + stack.extend(current.args) + else: + conj_args.append(current) + + return conj_args - while len(C) > 0: - c1 = C.pop() - if c1.name == 'and': - [C.append(c2) for c2 in c1.args] - else: - conj_args.append(c1) - - flag_eq = False - flag_neq = False - flag_geq = False - flag_leq = False - flag_ge = False - flag_le = False - - for c1 in conj_args: - print(c1.name) - # Tias is on 3.9, no 'match' please! - if c1.name == "==": - flag_eq = True - elif c1.name == "!=": - flag_neq = True - elif c1.name == "<=": - flag_leq = True - elif c1.name == ">=": - flag_geq = True - elif c1.name == "<": - flag_le = True - elif c1.name == ">": - flag_ge = True - else: - raise Exception("constraint name is not recognised") +def remove_redundant_conj(constraints: list) -> list: + """ + Remove redundant conjunctions from the given list of constraints. + A conjunction is considered redundant if: + 1. It contains the same set of atomic constraints as another conjunction, or + 2. It is unsatisfiable + + Args: + constraints: A list of CPMpy constraints, potentially containing conjunctions + + Returns: + list: A filtered list of constraints with redundant conjunctions removed + + Example: + >>> x = cp.intvar(0, 10, "x") + >>> constraints = [x >= 0, x >= 0 & x <= 5, x >= 2 & x <= 5] + >>> result = remove_redundant_conj(constraints) + >>> len(result) < len(constraints) # Some redundant constraints removed + True + """ + unique_constraints = [] + unique_atomic_sets = [] + + for constraint in constraints: + # Break down the constraint into atomic parts + atomic_constraints = get_conjunction_args(constraint) + + # Check if this set of atomic constraints is unique + is_redundant = any( + len(atomic_constraints) == len(existing_set) and + set(atomic_constraints) == set(existing_set) + for existing_set in unique_atomic_sets + ) + + if not is_redundant: + # Verify the constraint is satisfiable + try: + if cp.Model(constraint).solve(): + unique_constraints.append(constraint) + unique_atomic_sets.append(atomic_constraints) + except cp.exceptions.UnsatisfiableError: + # Skip unsatisfiable constraints + continue + + return unique_constraints - if not ((flag_eq and (flag_neq or flag_le or flag_ge)) or ( - (flag_leq or flag_le) and (flag_geq or flag_ge))): - C2.append(c) - return C2 From dbdb17fd61aba0cd0cee8930bcc845c453ab138b Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Wed, 25 Jun 2025 19:39:17 +0200 Subject: [PATCH 07/20] correct findc2 query generation --- pycona/find_constraint/findc2.py | 76 +++++++++++++++++++------------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/pycona/find_constraint/findc2.py b/pycona/find_constraint/findc2.py index 0afd37b..4b80b02 100644 --- a/pycona/find_constraint/findc2.py +++ b/pycona/find_constraint/findc2.py @@ -1,10 +1,11 @@ import cpmpy as cp +import copy from ..ca_environment.active_ca import ActiveCAEnv from .utils import get_max_conjunction_size, get_delta_p from .findc_core import FindCBase from .utils import join_con_net -from ..utils import restore_scope_values, get_con_subset, check_value +from ..utils import restore_scope_values, get_con_subset, check_value, get_scope class FindC2(FindCBase): @@ -14,7 +15,6 @@ class FindC2(FindCBase): This function works also for non-normalised target networks! """ - # TODO optimize to work better (probably only needs to make better the generate_find_query2) def __init__(self, ca_env: ActiveCAEnv = None, time_limit=0.2, findscope=None): """ @@ -54,15 +54,16 @@ def run(self, scope): """ assert self.ca is not None + scope_values = [x.value() for x in scope] + # Initialize delta delta = get_con_subset(self.ca.instance.bias, scope) - delta = join_con_net(delta, [c for c in delta if check_value(c) is False]) + kappaD = [c for c in delta if check_value(c) is False] + delta = join_con_net(delta, kappaD) # We need to take into account only the constraints in the scope we search on sub_cl = get_con_subset(self.ca.instance.cl, scope) - scope_values = [x.value() for x in scope] - while True: # Try to generate a counter example to reduce the candidates @@ -76,6 +77,8 @@ def run(self, scope): restore_scope_values(scope, scope_values) # Return random c in delta otherwise (if more than one, they are equivalent w.r.t. C_l) + # Choose the constraint with the smallest number of conjunctions + delta = sorted(delta, key=lambda x: len(x.args)) return delta[0] self.ca.metrics.increase_findc_queries() @@ -90,15 +93,14 @@ def run(self, scope): kappaD = [c for c in delta if check_value(c) is False] - scope2 = self.ca.run_find_scope(list(scope), kappaD) # TODO: replace with real findscope arguments when done! + #scope2 = self.ca.run_find_scope(list(scope), kappaD) # TODO: replace with real findscope arguments when done! - if len(scope2) < len(scope): - self.run(scope2) - else: - delta = join_con_net(delta, kappaD) + #if len(scope2) < len(scope): + # self.run(scope2) + #else: + delta = join_con_net(delta, kappaD) def generate_findc_query(self, L, delta): - # TODO: optimize to work better """ Changes directly the values of the variables @@ -107,35 +109,47 @@ def generate_findc_query(self, L, delta): :return: Boolean value representing a success or failure on the generation """ - tmp = cp.Model(L) + tmp = cp.Model(L) + + satisfied_delta = sum([c for c in delta]) # get the amount of satisfied constraints from B + + scope = get_scope(delta[0]) + # at least 1 violated and at least 1 satisfied + # we want this to assure that each answer of the user will reduce + # the set of candidates + tmp += satisfied_delta < len(delta) + tmp += satisfied_delta > 0 + max_conj_size = get_max_conjunction_size(delta) delta_p = get_delta_p(delta) - p = cp.intvar(0, max_conj_size) - kappa_delta_p = cp.intvar(0, len(delta), shape=(max_conj_size,)) - p_soft_con = cp.boolvar(shape=(max_conj_size,)) + for p in range(max_conj_size): + s = cp.SolverLookup.get("ortools", tmp) - for i in range(max_conj_size): - tmp += kappa_delta_p[i] == sum([c for c in delta_p[i]]) - p_soft_con[i] = (kappa_delta_p[i] > 0) + kappa_delta_p = sum([c for c in delta_p[p]]) + s += kappa_delta_p < len(delta_p[p]) + - tmp += p == min([i for i in range(max_conj_size) if (kappa_delta_p[i] < len(delta_p[i]))]) + if not s.solve(): # if a solution is found + continue - objective = sum([c for c in delta]) # get the amount of satisfied constraints from B + # Next solve will change the values of the variables in lY + # so we need to return them to the original ones to continue if we don't find a solution next + values = [x.value() for x in scope] - # at least 1 violated and at least 1 satisfied - # we want this to assure that each answer of the user will reduce - # the set of candidates - tmp += objective < len(delta) - tmp += objective > 0 - # Try first without objective - s = cp.SolverLookup.get("ortools", tmp) + p_soft_con = (kappa_delta_p > 0) + + # run with the objective + s.maximize(p_soft_con) - # run with the objective - s.minimize(100 * p - p_soft_con[p]) + # So a solution was found, try to find a better one now + s.solution_hint(scope, values) - flag = s.solve(time_limit=self.time_limit) + flag = s.solve(time_limit=self.time_limit, num_workers=8) + if not flag: + restore_scope_values(scope, values) + return True - return flag + return False \ No newline at end of file From b0072d194ecf9c37e5d6611a979a3c78039c1365 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Wed, 25 Jun 2025 19:40:00 +0200 Subject: [PATCH 08/20] nqueens (handcrafted B for now) --- pycona/benchmarks/nqueens.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pycona/benchmarks/nqueens.py b/pycona/benchmarks/nqueens.py index 05c5e79..3da9f54 100644 --- a/pycona/benchmarks/nqueens.py +++ b/pycona/benchmarks/nqueens.py @@ -16,26 +16,29 @@ def construct_nqueens_problem(n): # Constraints list CT = [] + diag = [] CT += list(cp.AllDifferent(queens).decompose()) for i in range(n): for j in range(i + 1, n): # Compare each queen with every other queen once - CT += [(queens[i] - i != queens[j] - j)] # Different major diagonals - CT += [(queens[i] + i != queens[j] + j)] # Different minor diagonals - + diag += [(queens[i] - i != queens[j] - j)] # Different major diagonals + diag += [(queens[i] + i != queens[j] + j)] # Different minor diagonals # Add all collected constraints to the model - model += CT + model += CT + diag - C_T = toplevel_list(CT) + C_T = toplevel_list(CT + diag) AV = absvar(2) - #lang = [AV[0] == AV[1], AV[0] != AV[1], AV[0] < AV[1], AV[0] > AV[1], AV[0] >= AV[1], AV[0] <= AV[1]] + - lang = [AV[0] - AV[1] == constant for constant in range(-n, 2*n)] + [AV[0] - AV[1] != constant for constant in range(-n, 2*n)] + lang = [AV[0] == AV[1], AV[0] != AV[1], AV[0] < AV[1], AV[0] > AV[1], AV[0] >= AV[1], AV[0] <= AV[1]] + #lang = [AV[0] - AV[1] == constant for constant in range(-n, 2*n)] + [AV[0] - AV[1] != constant for constant in range(-n, 2*n)] instance = ProblemInstance(variables=queens, params=parameters, language=lang, name="nqueens") + instance.construct_bias() + instance.bias = instance.bias + diag + oracle = ConstraintOracle(list(set(toplevel_list(C_T)))) print("oracle constraints: ", len(oracle.constraints)) From 3c6bbd3d5129550e84ddc7db99d394082977b4e8 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Thu, 26 Jun 2025 13:25:17 +0200 Subject: [PATCH 09/20] golomb --- pycona/benchmarks/__init__.py | 1 + pycona/benchmarks/golomb8.py | 90 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 pycona/benchmarks/golomb8.py diff --git a/pycona/benchmarks/__init__.py b/pycona/benchmarks/__init__.py index 04436e5..72d933e 100644 --- a/pycona/benchmarks/__init__.py +++ b/pycona/benchmarks/__init__.py @@ -6,3 +6,4 @@ from .nurse_rostering import construct_nurse_rostering from .zebra import construct_zebra_problem from .nqueens import construct_nqueens_problem +from .golomb8 import construct_golomb8 \ No newline at end of file diff --git a/pycona/benchmarks/golomb8.py b/pycona/benchmarks/golomb8.py new file mode 100644 index 0000000..7b19e66 --- /dev/null +++ b/pycona/benchmarks/golomb8.py @@ -0,0 +1,90 @@ +import cpmpy as cp +from cpmpy.transformations.normalize import toplevel_list +from ..answering_queries.constraint_oracle import ConstraintOracle +from ..problem_instance import ProblemInstance, absvar +from itertools import combinations +from ..utils import get_scope, replace_variables, combine_sets_distinct + +class GolombInstance(ProblemInstance): + + def construct_bias(self, X=None): + """ + Construct the bias (candidate constraints) for the golomb instance. + We need a different bias construction for the golomb instance because + it needs to include all permutations of scopes for the quaternary relations. + """ + if X is None: + X = self.X + + all_cons = [] + + for relation in self.language: + + abs_vars = get_scope(relation) + + combs = list(combinations(X, 2)) + + if len(abs_vars) == 2: + for comb in combs: + replace_dict = dict() + for i, v in enumerate(comb): + replace_dict[abs_vars[i]] = v + constraint = replace_variables(relation, replace_dict) + all_cons.append(constraint) + elif len(abs_vars) == 4: + result_combinations = combine_sets_distinct(combs, combs) + for ((v1, v2), (v3, v4)) in result_combinations: + replace_dict = dict() + replace_dict[abs_vars[0]] = v1 + replace_dict[abs_vars[1]] = v2 + replace_dict[abs_vars[2]] = v3 + replace_dict[abs_vars[3]] = v4 + constraint = replace_variables(relation, replace_dict) + all_cons.append(constraint) + + self.bias = list(set(all_cons) - set(self.cl) - set(self.excluded_cons)) + + + +def construct_golomb8(): + """ + :Description: The Golomb ruler problem is to place n marks on a ruler such that the distances between any two marks are all different. + A Golomb ruler with 8 marks is sought in this instance. + :return: a ProblemInstance object, along with a constraint-based oracle + """ + # Parameters + parameters = {"n_marks": 8} + + # Variables + grid = cp.intvar(1, 35, shape=(1, 8), name="grid") + + C_T = [] + + all_mark_pairs = [] + for a in range(8): + for b in range(a + 1, 8): + all_mark_pairs.append((a, b)) + + for outer_idx in range(len(all_mark_pairs)): + i, j = all_mark_pairs[outer_idx] # Get the first pair of marks (i, j) + + for inner_idx in range(outer_idx + 1, len(all_mark_pairs)): + x, y = all_mark_pairs[inner_idx] # Get the second pair of marks (x, y) + + C_T += [cp.abs(grid[0, j] - grid[0, i]) != cp.abs(grid[0, y] - grid[0, x])] + + for i in range(8): + for j in range(i + 1, 8): + C_T += [grid[0, i] < grid[0, j]] + + # Create the language: + AV = absvar(4) # create abstract vars - as many as maximum arity + + # create abstract relations using the abstract vars + lang = [AV[0] == AV[1], AV[0] != AV[1], AV[0] < AV[1], AV[0] > AV[1], AV[0] >= AV[1], AV[0] <= AV[1], cp.abs(AV[0] - AV[1]) != cp.abs(AV[2] - AV[3])] + + instance = GolombInstance(variables=grid, params=parameters, language=lang, name="golomb8") + + oracle = ConstraintOracle(list(set(toplevel_list(C_T)))) + + return instance, oracle \ No newline at end of file From 496dfbbce9d1c93e8c9a160f445f874f47f7d789 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Thu, 26 Jun 2025 15:59:25 +0200 Subject: [PATCH 10/20] findscope in findc2 --- pycona/find_constraint/findc2.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/pycona/find_constraint/findc2.py b/pycona/find_constraint/findc2.py index 4b80b02..67b1999 100644 --- a/pycona/find_constraint/findc2.py +++ b/pycona/find_constraint/findc2.py @@ -93,12 +93,14 @@ def run(self, scope): kappaD = [c for c in delta if check_value(c) is False] - #scope2 = self.ca.run_find_scope(list(scope), kappaD) # TODO: replace with real findscope arguments when done! - - #if len(scope2) < len(scope): - # self.run(scope2) - #else: - delta = join_con_net(delta, kappaD) + scope2 = self.ca.run_find_scope(list(scope)) + + if len(scope2) < len(scope): + c = self.run(scope2) + self.ca.add_to_cl(c) + sub_cl.append(c) + else: + delta = join_con_net(delta, kappaD) def generate_findc_query(self, L, delta): """ From ea2d69c2794419630c034b2be92e0410d607d0ca Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Thu, 26 Jun 2025 16:00:35 +0200 Subject: [PATCH 11/20] Update nqueens.py --- pycona/benchmarks/nqueens.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pycona/benchmarks/nqueens.py b/pycona/benchmarks/nqueens.py index 3da9f54..cc3bfd9 100644 --- a/pycona/benchmarks/nqueens.py +++ b/pycona/benchmarks/nqueens.py @@ -3,8 +3,6 @@ from ..answering_queries.constraint_oracle import ConstraintOracle from ..problem_instance import ProblemInstance, absvar - - def construct_nqueens_problem(n): parameters = {"n": n} From c75b38dba4f5bfc20555352b57529993cba3fac0 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Thu, 26 Jun 2025 17:11:08 +0200 Subject: [PATCH 12/20] unravel delta before decided which to return --- pycona/find_constraint/findc2.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pycona/find_constraint/findc2.py b/pycona/find_constraint/findc2.py index 67b1999..0cd5cf0 100644 --- a/pycona/find_constraint/findc2.py +++ b/pycona/find_constraint/findc2.py @@ -76,10 +76,21 @@ def run(self, scope): restore_scope_values(scope, scope_values) + # Unravel delta nested ands + delta_unraveled = [] + for c in delta: + if c.name == 'and': + sub_list = [] + for sub_c in c.args: + sub_list.append(sub_c) + delta_unraveled.append(sub_list) + else: + delta_unraveled.append([c]) + # Return random c in delta otherwise (if more than one, they are equivalent w.r.t. C_l) # Choose the constraint with the smallest number of conjunctions - delta = sorted(delta, key=lambda x: len(x.args)) - return delta[0] + delta_unraveled = sorted(delta_unraveled, key=lambda x: len(x)) + return delta_unraveled[0] self.ca.metrics.increase_findc_queries() From 7d74ac0d8f2ae4d7960bbc8f71b707caaf55dbd4 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Thu, 26 Jun 2025 17:11:22 +0200 Subject: [PATCH 13/20] correct counter increase in cl metric --- pycona/ca_environment/ca_env_core.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pycona/ca_environment/ca_env_core.py b/pycona/ca_environment/ca_env_core.py index a2e1fb6..e3f9e7a 100644 --- a/pycona/ca_environment/ca_env_core.py +++ b/pycona/ca_environment/ca_env_core.py @@ -92,6 +92,7 @@ def add_to_cl(self, C): self.instance.cl.extend(C) self.instance.bias = list(set(self.instance.bias) - set(C)) - self.metrics.cl += 1 + self.metrics.cl += len(C) if self.verbose == 1: - print("L", end="") + for c in C: + print("L", end="") From e2094705e81a3e1435f717d05ddad849e2fc2a39 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Fri, 27 Jun 2025 17:42:18 +0200 Subject: [PATCH 14/20] document finc2 --- pycona/find_constraint/findc2.py | 66 +++++++++++++++++++------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/pycona/find_constraint/findc2.py b/pycona/find_constraint/findc2.py index 0cd5cf0..6ec35ce 100644 --- a/pycona/find_constraint/findc2.py +++ b/pycona/find_constraint/findc2.py @@ -10,9 +10,8 @@ class FindC2(FindCBase): """ - This is the version of the FindC function that was presented in - Bessiere, Christian, et al., "Learning constraints through partial queries", AIJ 2023 - + Implementation of the FindC algorithm from Bessiere et al., "Learning constraints through partial queries" (AIJ 2023). + This function works also for non-normalised target networks! """ @@ -32,7 +31,8 @@ def findscope(self): """ Get the findscope function to be used. - :return: The findscope function. + Returns: + callable: The function used to determine constraint scopes """ return self._findscope @@ -41,32 +41,39 @@ def findscope(self, findscope): """ Set the findscope function to be used. - :param findscope: The findscope function. + Args: + findscope (callable): The function to be used for determining constraint scopes """ self._findscope = findscope def run(self, scope): """ - Run the FindC2 algorithm. + Execute the FindC2 algorithm to learn constraints within a given scope. + + Args: + scope (list): Variables defining the scope in which to search for constraints - :param scope: The scope in which we search for a constraint. - :return: The constraint found. + Returns: + list: The constraint(s) found in the given scope. + + Raises: + Exception: If the target constraint is not in the bias (search space). """ assert self.ca is not None scope_values = [x.value() for x in scope] - # Initialize delta + # Initialize delta with constraints from bias that match the scope delta = get_con_subset(self.ca.instance.bias, scope) kappaD = [c for c in delta if check_value(c) is False] + # Join the constraints in delta with the violated constraints in kappaD delta = join_con_net(delta, kappaD) - # We need to take into account only the constraints in the scope we search on + # Get subset of learned constraints in the current scope sub_cl = get_con_subset(self.ca.instance.cl, scope) while True: - - # Try to generate a counter example to reduce the candidates + # Generate a query to distinguish between candidate constraints if self.generate_findc_query(sub_cl, delta) is False: # If no example could be generated @@ -76,7 +83,7 @@ def run(self, scope): restore_scope_values(scope, scope_values) - # Unravel delta nested ands + # Unravel nested AND constraints delta_unraveled = [] for c in delta: if c.name == 'and': @@ -87,8 +94,7 @@ def run(self, scope): else: delta_unraveled.append([c]) - # Return random c in delta otherwise (if more than one, they are equivalent w.r.t. C_l) - # Choose the constraint with the smallest number of conjunctions + # Return the smallest equivalent conjunction (if more than one, they are equivalent w.r.t. C_l) delta_unraveled = sorted(delta_unraveled, key=lambda x: len(x)) return delta_unraveled[0] @@ -103,10 +109,10 @@ def run(self, scope): # delta <- joint(delta,K_{delta}(e)) kappaD = [c for c in delta if check_value(c) is False] - scope2 = self.ca.run_find_scope(list(scope)) if len(scope2) < len(scope): + # Recursively learn constraint in sub-scope c = self.run(scope2) self.ca.add_to_cl(c) sub_cl.append(c) @@ -115,25 +121,30 @@ def run(self, scope): def generate_findc_query(self, L, delta): """ - Changes directly the values of the variables + Generate a query that helps distinguish between candidate constraints. - :param L: learned network in the given scope - :param delta: candidate constraints in the given scope - :return: Boolean value representing a success or failure on the generation - """ + Args: + L (list): Currently learned constraints in the scope + delta (list): Candidate constraints to distinguish between + + Returns: + bool: True if a query was generated successfully, False otherwise + Note: + The method directly modifies variable values in the constraint network + """ tmp = cp.Model(L) satisfied_delta = sum([c for c in delta]) # get the amount of satisfied constraints from B scope = get_scope(delta[0]) + # at least 1 violated and at least 1 satisfied # we want this to assure that each answer of the user will reduce # the set of candidates tmp += satisfied_delta < len(delta) tmp += satisfied_delta > 0 - max_conj_size = get_max_conjunction_size(delta) delta_p = get_delta_p(delta) @@ -143,23 +154,24 @@ def generate_findc_query(self, L, delta): kappa_delta_p = sum([c for c in delta_p[p]]) s += kappa_delta_p < len(delta_p[p]) - - if not s.solve(): # if a solution is found + # Solve without objective for start + if not s.solve(): # if a solution is not found continue # Next solve will change the values of the variables in lY # so we need to return them to the original ones to continue if we don't find a solution next values = [x.value() for x in scope] - p_soft_con = (kappa_delta_p > 0) - # run with the objective + # So a solution was found, try to find a better one now + # set the objective s.maximize(p_soft_con) - # So a solution was found, try to find a better one now + # Give hint with previous solution to the solver s.solution_hint(scope, values) + # Solve with objective flag = s.solve(time_limit=self.time_limit, num_workers=8) if not flag: restore_scope_values(scope, values) From e7479572c33549b3b321fd738f885f62be59ad4e Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Fri, 27 Jun 2025 19:04:45 +0200 Subject: [PATCH 15/20] general golomb --- pycona/benchmarks/__init__.py | 2 +- pycona/benchmarks/{golomb8.py => golomb.py} | 63 ++++++++++++++++++--- 2 files changed, 56 insertions(+), 9 deletions(-) rename pycona/benchmarks/{golomb8.py => golomb.py} (56%) diff --git a/pycona/benchmarks/__init__.py b/pycona/benchmarks/__init__.py index 72d933e..7506ce0 100644 --- a/pycona/benchmarks/__init__.py +++ b/pycona/benchmarks/__init__.py @@ -6,4 +6,4 @@ from .nurse_rostering import construct_nurse_rostering from .zebra import construct_zebra_problem from .nqueens import construct_nqueens_problem -from .golomb8 import construct_golomb8 \ No newline at end of file +from .golomb import construct_golomb \ No newline at end of file diff --git a/pycona/benchmarks/golomb8.py b/pycona/benchmarks/golomb.py similarity index 56% rename from pycona/benchmarks/golomb8.py rename to pycona/benchmarks/golomb.py index 7b19e66..070bd5b 100644 --- a/pycona/benchmarks/golomb8.py +++ b/pycona/benchmarks/golomb.py @@ -44,25 +44,71 @@ def construct_bias(self, X=None): self.bias = list(set(all_cons) - set(self.cl) - set(self.excluded_cons)) + def construct_bias_for_vars(self, v1, X=None): + """ + Construct the bias (candidate constraints) for specific variables in the golomb instance. + Overrides the parent class method to handle the special case of quaternary relations in Golomb. + + Args: + v1: The variable(s) for which to construct the bias. Can be a single variable or list of variables. + X: The set of variables to consider, default is None (uses self.X). + """ + if not isinstance(v1, list): + v1 = [v1] + + if X is None: + X = self.X + + # Sort X based on variable names for consistency + X = sorted(X, key=lambda var: var.name) + + all_cons = [] + + for relation in self.language: + abs_vars = get_scope(relation) + + combs = list(combinations(X, 2)) + + if len(abs_vars) == 2: + for comb in combs: + replace_dict = dict() + for i, v in enumerate(comb): + replace_dict[abs_vars[i]] = v + constraint = replace_variables(relation, replace_dict) + all_cons.append(constraint) + elif len(abs_vars) == 4: + result_combinations = combine_sets_distinct(combs, combs) + for ((v1_, v2), (v3, v4)) in result_combinations: + replace_dict = dict() + replace_dict[abs_vars[0]] = v1_ + replace_dict[abs_vars[1]] = v2 + replace_dict[abs_vars[2]] = v3 + replace_dict[abs_vars[3]] = v4 + constraint = replace_variables(relation, replace_dict) + all_cons.append(constraint) + + # Filter constraints to only include those containing at least one of the specified variables + filtered_cons = [c for c in all_cons if any(v in set(get_scope(c)) for v in v1)] + self.bias = list(set(filtered_cons) - set(self.cl) - set(self.excluded_cons)) -def construct_golomb8(): +def construct_golomb(n_marks=8): """ :Description: The Golomb ruler problem is to place n marks on a ruler such that the distances between any two marks are all different. A Golomb ruler with 8 marks is sought in this instance. :return: a ProblemInstance object, along with a constraint-based oracle """ # Parameters - parameters = {"n_marks": 8} + parameters = {"n_marks": n_marks} # Variables - grid = cp.intvar(1, 35, shape=(1, 8), name="grid") + grid = cp.intvar(1, n_marks*4, shape=(1, n_marks), name="grid") C_T = [] all_mark_pairs = [] - for a in range(8): - for b in range(a + 1, 8): + for a in range(n_marks): + for b in range(a + 1, n_marks): all_mark_pairs.append((a, b)) for outer_idx in range(len(all_mark_pairs)): @@ -73,8 +119,8 @@ def construct_golomb8(): C_T += [cp.abs(grid[0, j] - grid[0, i]) != cp.abs(grid[0, y] - grid[0, x])] - for i in range(8): - for j in range(i + 1, 8): + for i in range(n_marks): + for j in range(i + 1, n_marks): C_T += [grid[0, i] < grid[0, j]] # Create the language: @@ -83,8 +129,9 @@ def construct_golomb8(): # create abstract relations using the abstract vars lang = [AV[0] == AV[1], AV[0] != AV[1], AV[0] < AV[1], AV[0] > AV[1], AV[0] >= AV[1], AV[0] <= AV[1], cp.abs(AV[0] - AV[1]) != cp.abs(AV[2] - AV[3])] - instance = GolombInstance(variables=grid, params=parameters, language=lang, name="golomb8") + instance = GolombInstance(variables=grid, params=parameters, language=lang, name="golomb") oracle = ConstraintOracle(list(set(toplevel_list(C_T)))) + print("Target constraints: ", len(oracle.constraints)) return instance, oracle \ No newline at end of file From 99a22a351205eb3866723b10d18b07dcca4cc002 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Fri, 27 Jun 2025 19:43:42 +0200 Subject: [PATCH 16/20] fix golomb domains - satisfiable --- pycona/benchmarks/golomb.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pycona/benchmarks/golomb.py b/pycona/benchmarks/golomb.py index 070bd5b..b3c9923 100644 --- a/pycona/benchmarks/golomb.py +++ b/pycona/benchmarks/golomb.py @@ -102,7 +102,9 @@ def construct_golomb(n_marks=8): parameters = {"n_marks": n_marks} # Variables - grid = cp.intvar(1, n_marks*4, shape=(1, n_marks), name="grid") + grid = cp.intvar(1, n_marks*8, shape=(1, n_marks), name="grid") + # adaptive domain: the larger the domain the slower query generation is + # current domain makes it satisfiable up to 12 marks, more marks make it too slow either way C_T = [] @@ -133,5 +135,4 @@ def construct_golomb(n_marks=8): oracle = ConstraintOracle(list(set(toplevel_list(C_T)))) - print("Target constraints: ", len(oracle.constraints)) return instance, oracle \ No newline at end of file From 5a5937c0304b0c663e8fb02ae742480a2e5f426c Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Fri, 27 Jun 2025 19:44:01 +0200 Subject: [PATCH 17/20] improved conjunction making --- pycona/find_constraint/findc2.py | 20 +++++--------------- pycona/find_constraint/utils.py | 21 ++++++++++++++++++++- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/pycona/find_constraint/findc2.py b/pycona/find_constraint/findc2.py index 6ec35ce..4ac6675 100644 --- a/pycona/find_constraint/findc2.py +++ b/pycona/find_constraint/findc2.py @@ -2,9 +2,8 @@ import copy from ..ca_environment.active_ca import ActiveCAEnv -from .utils import get_max_conjunction_size, get_delta_p +from .utils import get_max_conjunction_size, get_delta_p, join_con_net, unravel_conjunctions from .findc_core import FindCBase -from .utils import join_con_net from ..utils import restore_scope_values, get_con_subset, check_value, get_scope @@ -60,13 +59,14 @@ def run(self, scope): Exception: If the target constraint is not in the bias (search space). """ assert self.ca is not None - scope_values = [x.value() for x in scope] # Initialize delta with constraints from bias that match the scope delta = get_con_subset(self.ca.instance.bias, scope) - kappaD = [c for c in delta if check_value(c) is False] + delta = [c for c in delta if len(get_scope(c)) == len(scope)] + # Join the constraints in delta with the violated constraints in kappaD + kappaD = [c for c in delta if check_value(c) is False] delta = join_con_net(delta, kappaD) # Get subset of learned constraints in the current scope @@ -75,7 +75,6 @@ def run(self, scope): while True: # Generate a query to distinguish between candidate constraints if self.generate_findc_query(sub_cl, delta) is False: - # If no example could be generated # Check if delta is the empty set, and if yes then collapse if len(delta) == 0: @@ -84,15 +83,7 @@ def run(self, scope): restore_scope_values(scope, scope_values) # Unravel nested AND constraints - delta_unraveled = [] - for c in delta: - if c.name == 'and': - sub_list = [] - for sub_c in c.args: - sub_list.append(sub_c) - delta_unraveled.append(sub_list) - else: - delta_unraveled.append([c]) + delta_unraveled = unravel_conjunctions(delta) # Return the smallest equivalent conjunction (if more than one, they are equivalent w.r.t. C_l) delta_unraveled = sorted(delta_unraveled, key=lambda x: len(x)) @@ -110,7 +101,6 @@ def run(self, scope): kappaD = [c for c in delta if check_value(c) is False] scope2 = self.ca.run_find_scope(list(scope)) - if len(scope2) < len(scope): # Recursively learn constraint in sub-scope c = self.run(scope2) diff --git a/pycona/find_constraint/utils.py b/pycona/find_constraint/utils.py index ab8b30c..e78e25f 100644 --- a/pycona/find_constraint/utils.py +++ b/pycona/find_constraint/utils.py @@ -65,8 +65,9 @@ def join_con_net(C1, C2): :param C2: The second list of constraints. :return: A list of constraints resulting from the conjunction of C1 and C2. """ - C3 = [[c1 & c2 if c1 is not c2 else c1 for c2 in C2] for c1 in C1] + C3 = [[set(c1 + c2) for c2 in unravel_conjunctions(C2)] for c1 in unravel_conjunctions(C1)] C3 = list(chain.from_iterable(C3)) + C3 = [cp.all(c) for c in C3] C3 = remove_redundant_conj(C3) return C3 @@ -140,3 +141,21 @@ def remove_redundant_conj(constraints: list) -> list: return unique_constraints +def unravel_conjunctions(constraints: list) -> list: + """ + Unravel conjunctions in the given list of constraints. + """ + if not isinstance(constraints, list): + constraints = [constraints] + + unraveled = [] + for c in constraints: + if c.name == 'and': + sub_list = [] + for sub_c in c.args: + sub_list.append(sub_c) + unraveled.append(sub_list) + else: + unraveled.append([c]) + + return unraveled \ No newline at end of file From 6ea8aacf8d37585e983598141634314fe660b521 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Fri, 27 Jun 2025 21:05:48 +0200 Subject: [PATCH 18/20] fast findscope tests --- tests/test_findscope.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_findscope.py b/tests/test_findscope.py index 2810322..974ecec 100644 --- a/tests/test_findscope.py +++ b/tests/test_findscope.py @@ -6,11 +6,19 @@ import cpmpy as cp algorithms = [ca.FindScope(), ca.FindScope2()] +fast_algorithms = [ca.FindScope2()] # Use only FindScope for fast tests class TestFindScope: - def test_findscope(self): + @pytest.mark.parametrize( + "algorithm", + [ + *[pytest.param(alg, marks=pytest.mark.fast) for alg in fast_algorithms], + *[pytest.param(alg) for alg in algorithms if alg not in fast_algorithms] + ] + ) + def test_findscope(self, algorithm): a, b, c, d = cp.intvar(0, 9, shape=4) # variables vars_array = cp.cpm_array([a, b, c, d]) @@ -21,7 +29,7 @@ def test_findscope(self): constraints = toplevel_list(oracle_model.constraints) instance = ca.ProblemInstance(variables=cp.cpm_array(vars_array)) - ca_env = ca.ActiveCAEnv(find_scope=ca.FindScope()) + ca_env = ca.ActiveCAEnv(find_scope=algorithm) for con in range(len(constraints)): model = cp.Model(constraints[:con] + constraints[con + 1:]) # all constraints except this @@ -33,5 +41,4 @@ def test_findscope(self): ca_env.init_state(oracle=ca.ConstraintOracle(oracle_model.constraints), instance=instance, verbose=1) Y = ca_env.run_find_scope(vars_array) - assert ca.utils.compare_scopes(Y, - get_variables(constraints[con])), f"{Y}, {get_variables(constraints[con])}" + assert ca.utils.compare_scopes(Y, get_variables(constraints[con])), f"{Y}, {get_variables(constraints[con])}" From 1656584cb1a32de47b7eb136446383cc9147ba65 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Fri, 27 Jun 2025 21:05:52 +0200 Subject: [PATCH 19/20] Create test_finc.py --- tests/test_finc.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/test_finc.py diff --git a/tests/test_finc.py b/tests/test_finc.py new file mode 100644 index 0000000..3ac5455 --- /dev/null +++ b/tests/test_finc.py @@ -0,0 +1,81 @@ +import pytest +import cpmpy as cp +from pycona.find_constraint import FindC, FindC2 +from pycona.ca_environment.active_ca import ActiveCAEnv +from pycona.find_constraint.findc_obj import findc_obj_splithalf, findc_obj_proba +from pycona.benchmarks.golomb import construct_golomb +import pycona as ca + +algorithms = [FindC(), FindC2()] +fast_algorithms = [FindC()] # Use only FindC for fast tests + +class TestFinC: + @pytest.mark.fast + def test_findc_query_generation(self): + """Test query generation in FindC""" + ca_env = ActiveCAEnv() + findc = FindC(ca_env=ca_env) + + # Create test variables + x = cp.intvar(1, 10, name="x") + y = cp.intvar(1, 10, name="y") + + # Create constraints + L = [x <= y] # learned constraints + delta = [x < y, x >= y, x == y] # candidate constraints + + # Test query generation + assert findc.generate_findc_query(L, delta) + + @pytest.mark.fast + def test_findc2_query_generation(self): + """Test query generation in FindC2""" + ca_env = ActiveCAEnv() + findc2 = FindC2(ca_env=ca_env) + + # Create test variables + x = cp.intvar(1, 10, name="x") + y = cp.intvar(1, 10, name="y") + + # Create constraints + L = [x <= y] # learned constraints + delta = [x < y, x >= y, x == y] # candidate constraints + + # Test query generation + assert findc2.generate_findc_query(L, delta) + + @pytest.mark.fast + def test_findc_objective_functions(self): + """Test objective function changes in FindC""" + findc = FindC() + + # Test probability-based objective + findc.obj = findc_obj_proba + assert findc.obj == findc_obj_proba + + # Test split-half objective + findc.obj = findc_obj_splithalf + assert findc.obj == findc_obj_splithalf + + def test_findc2_with_golomb4(self): + """Test FindC with a Golomb ruler of order 4""" + ca_env = ActiveCAEnv(findc=FindC2()) + alg = ca.QuAcq(ca_env) + + # Create Golomb ruler instance of order 4 + instance, oracle = construct_golomb(n_marks=4) + + li = alg.learn(instance, oracle) + + # oracle model imply learned? + oracle_not_learned = cp.Model(oracle.constraints) + oracle_not_learned += cp.any([~c for c in li._cl]) + assert not oracle_not_learned.solve() + + # learned model imply oracle? + learned_not_oracle = cp.Model(li._cl) + learned_not_oracle += cp.any([~c for c in oracle.constraints]) + assert not learned_not_oracle.solve() + + + From 0deb0752b66746ef97eb994945f7f700c8cd7651 Mon Sep 17 00:00:00 2001 From: Dimos Tsouros Date: Sat, 28 Jun 2025 01:05:37 +0200 Subject: [PATCH 20/20] fix findscope tests --- tests/test_findscope.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_findscope.py b/tests/test_findscope.py index 974ecec..9245b7b 100644 --- a/tests/test_findscope.py +++ b/tests/test_findscope.py @@ -29,6 +29,7 @@ def test_findscope(self, algorithm): constraints = toplevel_list(oracle_model.constraints) instance = ca.ProblemInstance(variables=cp.cpm_array(vars_array)) + instance.bias = [10 * c + d == 3 * (10 * a + b), 10 * d + a == 2 * (10 * b + c)] ca_env = ca.ActiveCAEnv(find_scope=algorithm) for con in range(len(constraints)):