# csp.py # From Classic Computer Science Problems in Python Chapter 3 # Copyright 2018 David Kopec # # Modified by Ariel Ortiz, 2022. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Generic, TypeVar, Optional from abc import ABC, abstractmethod V = TypeVar('V') # variable type D = TypeVar('D') # domain type # Base class for all constraints class Constraint(Generic[V, D], ABC): # The variables that the constraint is between def __init__(self, variables: list[V]) -> None: self.variables = variables # Must be overridden by subclasses @abstractmethod def satisfied(self, assignment: dict[V, D]) -> bool: ... # A constraint satisfaction problem consists of variables of type V # that have ranges of values known as domains of type D and constraints # that determine whether a particular variable's domain selection is valid class CSP(Generic[V, D]): def __init__(self, variables: list[V], domains: dict[V, list[D]]) -> None: self.variables: list[V] = variables # variables to be constrained self.domains: dict[V, list[D]] = domains # domain of each variable self.constraints: dict[V, list[Constraint[V, D]]] = {} for variable in self.variables: self.constraints[variable] = [] if variable not in self.domains: raise LookupError("Every variable should have a domain " "assigned to it.") def add_constraint(self, constraint: Constraint[V, D]) -> None: for variable in constraint.variables: if variable not in self.variables: raise LookupError("Variable in constraint not in CSP") else: self.constraints[variable].append(constraint) # Check if the value assignment is consistent by checking all constraints # for the given variable against it def consistent(self, variable: V, assignment: dict[V, D]) -> bool: for constraint in self.constraints[variable]: if not constraint.satisfied(assignment): return False return True def backtracking_search( self, assignment: dict[V, D] = {}) -> Optional[dict[V, D]]: # assignment is complete if every variable is assigned (our base case) if len(assignment) == len(self.variables): return assignment # get all variables in the CSP but not in the assignment unassigned: list[V] = [ v for v in self.variables if v not in assignment] # get the every possible domain value of the first unassigned variable first: V = unassigned[0] for value in self.domains[first]: local_assignment = assignment.copy() local_assignment[first] = value # if we're still consistent, we recurse (continue) if self.consistent(first, local_assignment): result: Optional[dict[V, D]] = self.backtracking_search( local_assignment) # if we didn't find the result, we will end up backtracking if result is not None: return result return None