VIAplanner/via-timetable

View on GitHub
src/timetable-planner/csp/index.js

Summary

Maintainability
A
2 hrs
Test Coverage
/* Enums */
export const FAILURE = 'FAILURE';


/* Helper functions */

const partialAssignment = (assigned, unassigned) => {
  // Combine unassigned and assigned for use in enforceConsistency.
  const partial = {};
  Object.keys(unassigned).forEach((key) => { partial[key] = unassigned[key].slice();})
  Object.keys(assigned).forEach((key) => { partial[key] = assigned[key].slice(); })
  return partial;
}

const enforceConsistency = (assigned, unassigned, csp) => {
  // Enforces arc consistency by removing inconsistent values from
  // every constraint's tail node.

  const removeInconsistentValues = (head, tail, constraint, variables) => {
    // Removes inconsistent values from the tail node. A value is
    // inconsistent when if the `tail` is assigned that value, there are
    // no values in `head`'s domain that satisfies the constraint.
    const headVal = variables[head];
    const tailVal = variables[tail];
    const validTailValues = tailVal.filter((t) => 
      headVal.some((h) =>
        constraint(h, t)
      )
    );
    const removed = tailVal.length !== validTailValues.length;
    variables[tail] = validTailValues;
    return removed;
  }

  // Returns all the constraints where `node` is the head node.
  const incomingConstraints = (node) => csp.constraints.filter((c) => c[0] === node);
  
  
  let queue = csp.constraints.slice();
  const variables = partialAssignment(assigned, unassigned);
  while (queue.length) { // While there are more constraints to test.
    const c = queue.shift()
    const [head, tail, constraint] = c;
    if (removeInconsistentValues(head, tail, constraint, variables)) {
      // If values from the tail have been removed, incoming constraints
      // to the tail must be rechecked.
      queue = queue.concat(incomingConstraints(tail));
    }
  }
  return variables;
}

const orderValues = (nextKey, assigned, unassigned, csp) => {
  // Orders the values of an unassigned variable according to the
  // Least Constraining Values heuristic. Perform arc consistency
  // on each possible value, and order variables according to the
  // how many values were eliminated from all the domains (fewest
  // eliminated in the front). This helps makes success more likely
  // by keeping future options open.
  
  const countValues = (vars) => 
    // Returns total length of all keys
    Object.keys(vars).reduce((prevSum, key) => prevSum + vars[key].length, 0)

  const valuesEliminated = (val) => {
    assigned[nextKey] = [val];
    const newLength = countValues(enforceConsistency(assigned, unassigned, csp));
    delete assigned[nextKey];
    return newLength;
  }

  // Cache valuesEliminated to be used in sort.
  const cache = {}, values = unassigned[nextKey];
  values.forEach((val) => {
    cache[val] = valuesEliminated(val);
  });
  // Descending order based on the number of domain values remaining.
  values.sort((a, b) => cache[b] - cache[a]);
  return values;
}

const selectUnassignedVariable = (unassigned)  => {
  // Picks the next variable to assign according to the Minimum
  // Remaining Values heuristic. Pick the variable with the fewest
  // values remaining in its domain. This helps identify domain
  // failures earlier.
  let minKey = null;
  let minLen = Number.POSITIVE_INFINITY;
  for (const key in unassigned) {
    const len = unassigned[key].length;
    if (len < minLen) { 
      minKey = key;
      minLen = len;
    }
  }
  return minKey;
}


const anyEmpty = (consistent) => {
  // Checks if any variable's domain is empty.
  for (const key in consistent) {
    if (consistent[key].length === 0) { return true; }
  }
  return false;
}

// Checks if there are no more variables to assign.
const finished = (unassigned) => Object.keys(unassigned).length === 0;


const backtrack = (_assigned, unassigned, csp) => {
  // Backtracking search.
  
  // Copying assigned in necessary because we modify it. Without copying
  // the object over, modifying assigned would also change values for old
  // assigned objects (which are used in callbacks).
  const assigned = { ..._assigned};

  if (finished(unassigned)) { return assigned; } // Base case.
  const nextKey = selectUnassignedVariable(unassigned);
  const values = orderValues(nextKey, assigned, unassigned, csp);
  delete unassigned[nextKey];

  for (const value of values) {
    assigned[nextKey] = [value]; // Assign a value to a variable.
    const consistent = enforceConsistency(assigned, unassigned, csp);
    const newUnassigned = {}, newAssigned = {};
    for (const key in consistent) {
      if (assigned[key]) { newAssigned[key] = assigned[key].slice(); }
      else { newUnassigned[key] = consistent[key].slice(); }
    }
    // eslint-disable-next-line no-continue
    if (anyEmpty(consistent)) { continue; } // Empty domains means failure.
    const result = backtrack(newAssigned, newUnassigned, csp);
    if (result !== FAILURE) { return result; }
  }

  return FAILURE;
}


// Solves a constraint satisfaction problem.
// `csp` is an object that should have the properties:
//    `variables`  : object that holds variable names and their domain.
//    `constraints`: list of constraints where each element is an 
//                   array of [head node, tail node, constraint function]
//    `cb`: callback function for visualizing assignments. It is passed in
//          an "assigned" object, an "unassigned" object, and `csp`.
export default function solve(csp) {
  const result = backtrack({}, csp.variables, csp);
  if (result === FAILURE) { return result; }
  // Unwrap values from array containers.
  for (const key in result) {
    // eslint-disable-next-line prefer-destructuring
    result[key] = result[key][0];
  }
  return result;
};