diff --git a/csp/csp.rkt b/csp/csp.rkt new file mode 100644 index 00000000..c6c0cbde --- /dev/null +++ b/csp/csp.rkt @@ -0,0 +1,164 @@ +#lang racket/base + +;; Adapted from work by Peter Norvig +;; http://aima-python.googlecode.com/svn/trunk/csp.py + +(require racket/list racket/bool racket/contract) +(require "csp-utils.rkt" "csp-search.rkt") + +#| + +class CSP(search.Problem): + """This class describes finite-domain Constraint Satisfaction Problems. + A CSP is specified by the following inputs: + vars A list of variables; each is atomic (e.g. int or string). + domains A dict of {var:[possible_value, ...]} entries. + neighbors A dict of {var:[var,...]} that for each variable lists + the other variables that participate in constraints. + constraints A function f(A, a, B, b) that returns true if neighbors + A, B satisfy the constraint when they have values A=a, B=b + In the textbook and in most mathematical definitions, the + constraints are specified as explicit pairs of allowable values, + but the formulation here is easier to express and more compact for + most cases. (For example, the n-Queens problem can be represented + in O(n) space using this notation, instead of O(N^4) for the + explicit representation.) In terms of describing the CSP as a + problem, that's all there is. + + However, the class also supports data structures and methods that help you + solve CSPs by calling a search function on the CSP. Methods and slots are + as follows, where the argument 'a' represents an assignment, which is a + dict of {var:val} entries: + assign(var, val, a) Assign a[var] = val; do other bookkeeping + unassign(var, a) Do del a[var], plus other bookkeeping + nconflicts(var, val, a) Return the number of other variables that + conflict with var=val + curr_domains[var] Slot: remaining consistent values for var + Used by constraint propagation routines. + The following methods are used only by graph_search and tree_search: + actions(state) Return a list of actions + result(state, action) Return a successor of state + goal_test(state) Return true if all constraints satisfied + The following are just for debugging purposes: + nassigns Slot: tracks the number of assignments made + display(a) Print a human-readable representation + + >>> search.depth_first_graph_search(australia) + + """ + +|# + + +(define (init csp vars domains neighbors constraints) + ;; Construct a CSP problem. If vars is empty, it becomes domains.keys(). + (define vars (if (null? vars) (hash-keys domains) vars)) + (hash-set*! csp 'vars vars 'domains domains + 'neighbors neighbors 'constraints constraints + 'initial null 'curr_domains null 'nassigns 0)) + +(define (assign csp var val assignment) + ;; Add {var: val} to assignment; Discard the old value if any. + (hash-set! assignment var val) + (hash-update! csp 'nassigns add1)) + + +(define (unassign csp var assignment) + ;; Remove {var: val} from assignment. + ;; DO NOT call this if you are changing a variable to a new value; + ;; just call assign for that. + (hash-remove! csp var)) + + +(define (nconflicts csp var val assignment) + ;; Return the number of conflicts var=val has with other variables. + (define (conflict var2) + (and (hash-has-key? assignment var2) + (not ((hash-ref csp 'constraints) var val var2 (hash-ref assignment var2))))) + (length (filter-not false? (map conflict (hash-ref (hash-ref csp 'neighbors) var))))) + +(define (display csp assignment) + ;; Show a human-readable representation of the CSP. + (displayln (format "CSP: ~a with assignment: ~a" csp (hash-ref csp assignment)))) + +(define (actions csp state) + ;; Return a list of applicable actions: nonconflicting + ;; assignments to an unassigned variable. + (if (= (length state) (length (hash-ref csp 'vars))) + null + (let () + (define assignment (make-hash state)) + (define var (findf (λ(v) (not (hash-has-key? assignment v))) (hash-ref csp 'vars))) + (map (λ(val) (list var val)) + (filter (λ(val) (= 0 (nconflicts csp var val assignment))) (hash-ref (hash-ref csp 'domains) var)))))) + + +#| + + def actions(self, state): + """Return a list of applicable actions: nonconflicting + assignments to an unassigned variable.""" + if len(state) == len(self.vars): + return [] + else: + assignment = dict(state) + var = find_if(lambda v: v not in assignment, self.vars) + return [(var, val) for val in self.domains[var] + if self.nconflicts(var, val, assignment) == 0] + + def result(self, state, (var, val)): + "Perform an action and return the new state." + return state + ((var, val),) + + def goal_test(self, state): + "The goal is to assign all vars, with all constraints satisfied." + assignment = dict(state) + return (len(assignment) == len(self.vars) and + every(lambda var: self.nconflicts(var, assignment[var], + assignment) == 0, + self.vars)) + + ## These are for constraint propagation + + def support_pruning(self): + """Make sure we can prune values from domains. (We want to pay + for this only if we use it.)""" + if self.curr_domains is None: + self.curr_domains = dict((v, list(self.domains[v])) + for v in self.vars) + + def suppose(self, var, value): + "Start accumulating inferences from assuming var=value." + self.support_pruning() + removals = [(var, a) for a in self.curr_domains[var] if a != value] + self.curr_domains[var] = [value] + return removals + + def prune(self, var, value, removals): + "Rule out var=value." + self.curr_domains[var].remove(value) + if removals is not None: removals.append((var, value)) + + def choices(self, var): + "Return all values for var that aren't currently ruled out." + return (self.curr_domains or self.domains)[var] + + def infer_assignment(self): + "Return the partial assignment implied by the current inferences." + self.support_pruning() + return dict((v, self.curr_domains[v][0]) + for v in self.vars if 1 == len(self.curr_domains[v])) + + def restore(self, removals): + "Undo a supposition and all inferences from it." + for B, b in removals: + self.curr_domains[B].append(b) + + ## This is for min_conflicts search + + def conflicted_vars(self, current): + "Return a list of variables in current assignment that are in conflict" + return [var for var in self.vars + if self.nconflicts(var, current[var], current) > 0] + +|#