def _at_most(pvars, k, var_prefix=""): """ Implement LTseq, as described in: Towards an Optimal CNF Encoding of Boolean Cardinality Constraints by Carsten Sinz (2005). Note that there may be truth assignments that satisfy the expression returned, which differ only in the internal variables. As such duplicate solutions may be returned. """ s = {(i, j): wff.Var("{} s[{},{}]".format(var_prefix, i, j)) for i in range(1, len(pvars)) for j in range(1, k + 1)} n = len(pvars) expr = ((pvars[0] >> s[1, 1]) & wff.for_all(~s[1, j] for j in range(2, k + 1))) for i in range(2, n): expr &= (pvars[i - 1] >> s[i, 1]) expr &= (s[i - 1, 1] >> s[i, 1]) for j in range(2, k + 1): expr &= ~pvars[i - 1] | ~s[i - 1, j - 1] | s[i, j] expr &= s[i - 1, j] >> s[i, j] expr &= pvars[i - 1] >> ~s[i - 1, k] expr &= pvars[-1] >> ~s[n - 1, k] cnf = wff.to_cnf(expr) assert len(cnf) == 2 * n * k + n - 3 * k - 1 return cnf
def physical_constraints(): """ Produce a CNF expression to enforce physical constraints. Ie. There must not be multiple components that occupy a given space. """ # Make internal variables to determine whether a given component is in # a particular space. occ = {(c, s): wff.Var("{} occ {}".format(c, s)) for s in board.spaces for c in components} # Generate constraints to enforce the definition of `occ`. occ[s, c] is # true iff there is a position `p` for `c` which covers `s` such that # comp_pos[c, p] is true. The first line handles the forward # implication, and the second the converse. positions_which_occupy = {(c, s): [p for p in positions[c] if s in p.occupies] for c in components for s in board.spaces} occ_constraints = wff.to_cnf( wff.for_all(occ[c, s].iff(wff.exists(comp_pos[c, p] for p in positions_which_occupy[c, s])) for c in components for s in board.spaces)) # Enforce that at most one component/jumper can occupy a space. jumpers_that_occupy_space = {s: [j for j in jumpers if s in j.occupies] for s in board.spaces} one_component_per_space = cnf.Expr.all( cnf.at_most_one( {occ[c, s] for c in components} | {j.pres_var for j in jumpers_that_occupy_space[s]}) for s in board.spaces) # Return all of the above. return occ_constraints | one_component_per_space
def continuity_constraints(): """ Produce a CNF expression to enforce electrical continuity constraints. Ie. continuity between terminals that are in a common net, and discontinuity between terminals that are in different nets. """ # Produce a dict which maps a terminal `t` and a hole `h` to a list of # positions of t.component which have `t` in `h`. Used a couple of # times in this function. positions_which_have_term_in = { (t, h): [p for p in positions[c] if p.terminal_positions[t] == h] for c in components for t in c.terminals for h in board.holes} # Produce an adjacency dict for the electrical continuity graph implied # by links. Include the variable that must be true for said neighbour # to be present. neighbours = {h: [(l.get_other(h), l.pres_var) for l in links if h in l.holes] for h in board.holes} # Make internal variables to indicate whether a hole is connected to a # particular terminal. Defined for all holes, and the first terminal in # each net. (This is sufficient for validating (dis)continuity # constraints. term_conn = {(n[0], h): wff.Var("{} conn {}".format(n[0], h)) for n in nets for h in board.holes} # Also make internal variables to indicate the minimum distance of each # hole to the nearest terminal. term_dist[h, i] is true iff there is no # path of length `i` or less from hole `h` to a head terminal. (A head # terminal is a terminal that is at the start of its net.) # # In other words, term_dist[h, *] is a unary encoding of the distance # to the nearest head terminal. Holes which are not connected to a # terminal will take the maximum value len(board.holes). Conversely, # holes which are connected will take a value < len(board.holes). term_dist = {(h, i): wff.Var("{} dist {}".format(h, i)) for h in board.holes for i in range(len(board.holes))} # Generate constraints to enforce the definition of `term_conn`. A hole # is connected to a particular terminal iff one of its neighbours is # connected to the terminal or the terminal is in this hole. The first # expression handles the forward implication, whereas the second # expression handles the converse. term_conn_constraints = wff.to_cnf( wff.for_all( term_conn[net[0], h].iff( wff.exists( wff.add_var(term_conn[net[0], n] & link_pres) for n, link_pres in neighbours[h]) | wff.exists(comp_pos[net[0].component, p] for p in positions_which_have_term_in[net[0], h])) for net in nets for h in board.holes)) if _DEBUG: print("Term conn constraints: {}".format( term_conn_constraints.stats)) # Add constraints to enforce the definition of `term_dist[h, 0]`, for # all holes `h`. term_hist[h, 0] is false iff a component is positioned # such that a head terminal is in hole `h`. The first statement # expresses the forward implication, and the second statement expresses # the converse. zero_term_dist_constraints = wff.to_cnf( wff.for_all( (~term_dist[h, 0]).iff( wff.exists(comp_pos[net[0].component, p] for net in nets for p in positions_which_have_term_in[net[0], h])) for h in board.holes)) if _DEBUG: print("Zero term dist constraints: {}".format( zero_term_dist_constraints.stats)) # Add constraints to enforce the definition of `term_dist[h, i]`, for # 0 < 1 < |holes|. term_dist[h, i] is true iff for each neighbour `n` # term_dist[n, i - 1] is true. The first statement expresses the # forward implication, and the second statement expresses the converse. non_zero_term_dist_constraints = wff.to_cnf( wff.for_all( term_dist[h, i].iff( wff.for_all( wff.add_var(term_dist[n, i - 1] | ~link_pres) for n, link_pres in neighbours[h]) & term_dist[h, i - 1]) for h in board.holes for i in range(1, len(board.holes)))) if _DEBUG: print("Non-zero term dist contraints: {}".format( non_zero_term_dist_constraints.stats)) # Add constraints which ensure any terminals are connected to the # terminal that's at the head of its net. def term_to_net(t): l = [net for net in nets if t in net] assert len(l) == 1, "Terminal is not in exactly one net" return l[0] head_term = {t: term_to_net(t)[0] for c in components for t in c.terminals} net_continuity_constraints = wff.to_cnf( wff.for_all(comp_pos[c, p] >> term_conn[head_term[t], h] for h in board.holes for c in components for t in c.terminals for p in positions_which_have_term_in[t, h])) if _DEBUG: print("Net continuity constraints: {}".format( net_continuity_constraints.stats)) # Add constraints which ensure that no hole is part of more than one # net, and if its disconnected from all nets, then it can be part of no # net. net_discontinuity_constraints = cnf.Expr.all( cnf.at_most_one( {term_conn[net[0], h] for net in nets} | {term_dist[h, len(board.holes) - 1]}) for h in board.holes) if _DEBUG: print("Net discontinuity constraints: {}".format( net_discontinuity_constraints.stats)) # Return all of the above. return (term_conn_constraints | zero_term_dist_constraints | non_zero_term_dist_constraints | net_discontinuity_constraints | net_continuity_constraints)
def place(board, components, nets, *, allow_drilled=False, max_jumper_length=0, max_drilled=None, max_jumpers=None, slvr=None): """ Place components on a board, according to a net list. board: The board to place components on. A subclas of `component.Board`. components: Iterable of components to place on the board. Each component is subclass of `component.Component`. nets: Iterable of nets. Each net is a set of terminals that are to be condutively connected. allow_drilled: If set, solutions may contain drilled out holes. Traces that are connected to drilled out holes are considered to not conduct. max_jumper_length: Maximum length of conductive jumper links. max_drilled: Maximum number of drilled holes in the solution. None implies unbounded. max_jumpers: Maximum number of jumpers in the solution. None implies unbounded. slvr: Solver to use to solve the placement. Yields: Placements which satify the input constraints. """ # Unpack arguments in case the caller provided a generator (or other # one-time iterable), so they can be re-iterated and subscripted in this # function. nets = [list(net) for net in nets] components = list(components) # Position objects that represent the same position may have different # hashes (their hash function is the default id based implementation). # # Allow the positions to be hashed correctly by using only one Position for # each component position within this function. positions = {c: list(c.get_positions(board)) for c in components} def physical_constraints(): """ Produce a CNF expression to enforce physical constraints. Ie. There must not be multiple components that occupy a given space. """ # Make internal variables to determine whether a given component is in # a particular space. occ = {(c, s): wff.Var("{} occ {}".format(c, s)) for s in board.spaces for c in components} # Generate constraints to enforce the definition of `occ`. occ[s, c] is # true iff there is a position `p` for `c` which covers `s` such that # comp_pos[c, p] is true. The first line handles the forward # implication, and the second the converse. positions_which_occupy = {(c, s): [p for p in positions[c] if s in p.occupies] for c in components for s in board.spaces} occ_constraints = wff.to_cnf( wff.for_all(occ[c, s].iff(wff.exists(comp_pos[c, p] for p in positions_which_occupy[c, s])) for c in components for s in board.spaces)) # Enforce that at most one component/jumper can occupy a space. jumpers_that_occupy_space = {s: [j for j in jumpers if s in j.occupies] for s in board.spaces} one_component_per_space = cnf.Expr.all( cnf.at_most_one( {occ[c, s] for c in components} | {j.pres_var for j in jumpers_that_occupy_space[s]}) for s in board.spaces) # Return all of the above. return occ_constraints | one_component_per_space def continuity_constraints(): """ Produce a CNF expression to enforce electrical continuity constraints. Ie. continuity between terminals that are in a common net, and discontinuity between terminals that are in different nets. """ # Produce a dict which maps a terminal `t` and a hole `h` to a list of # positions of t.component which have `t` in `h`. Used a couple of # times in this function. positions_which_have_term_in = { (t, h): [p for p in positions[c] if p.terminal_positions[t] == h] for c in components for t in c.terminals for h in board.holes} # Produce an adjacency dict for the electrical continuity graph implied # by links. Include the variable that must be true for said neighbour # to be present. neighbours = {h: [(l.get_other(h), l.pres_var) for l in links if h in l.holes] for h in board.holes} # Make internal variables to indicate whether a hole is connected to a # particular terminal. Defined for all holes, and the first terminal in # each net. (This is sufficient for validating (dis)continuity # constraints. term_conn = {(n[0], h): wff.Var("{} conn {}".format(n[0], h)) for n in nets for h in board.holes} # Also make internal variables to indicate the minimum distance of each # hole to the nearest terminal. term_dist[h, i] is true iff there is no # path of length `i` or less from hole `h` to a head terminal. (A head # terminal is a terminal that is at the start of its net.) # # In other words, term_dist[h, *] is a unary encoding of the distance # to the nearest head terminal. Holes which are not connected to a # terminal will take the maximum value len(board.holes). Conversely, # holes which are connected will take a value < len(board.holes). term_dist = {(h, i): wff.Var("{} dist {}".format(h, i)) for h in board.holes for i in range(len(board.holes))} # Generate constraints to enforce the definition of `term_conn`. A hole # is connected to a particular terminal iff one of its neighbours is # connected to the terminal or the terminal is in this hole. The first # expression handles the forward implication, whereas the second # expression handles the converse. term_conn_constraints = wff.to_cnf( wff.for_all( term_conn[net[0], h].iff( wff.exists( wff.add_var(term_conn[net[0], n] & link_pres) for n, link_pres in neighbours[h]) | wff.exists(comp_pos[net[0].component, p] for p in positions_which_have_term_in[net[0], h])) for net in nets for h in board.holes)) if _DEBUG: print("Term conn constraints: {}".format( term_conn_constraints.stats)) # Add constraints to enforce the definition of `term_dist[h, 0]`, for # all holes `h`. term_hist[h, 0] is false iff a component is positioned # such that a head terminal is in hole `h`. The first statement # expresses the forward implication, and the second statement expresses # the converse. zero_term_dist_constraints = wff.to_cnf( wff.for_all( (~term_dist[h, 0]).iff( wff.exists(comp_pos[net[0].component, p] for net in nets for p in positions_which_have_term_in[net[0], h])) for h in board.holes)) if _DEBUG: print("Zero term dist constraints: {}".format( zero_term_dist_constraints.stats)) # Add constraints to enforce the definition of `term_dist[h, i]`, for # 0 < 1 < |holes|. term_dist[h, i] is true iff for each neighbour `n` # term_dist[n, i - 1] is true. The first statement expresses the # forward implication, and the second statement expresses the converse. non_zero_term_dist_constraints = wff.to_cnf( wff.for_all( term_dist[h, i].iff( wff.for_all( wff.add_var(term_dist[n, i - 1] | ~link_pres) for n, link_pres in neighbours[h]) & term_dist[h, i - 1]) for h in board.holes for i in range(1, len(board.holes)))) if _DEBUG: print("Non-zero term dist contraints: {}".format( non_zero_term_dist_constraints.stats)) # Add constraints which ensure any terminals are connected to the # terminal that's at the head of its net. def term_to_net(t): l = [net for net in nets if t in net] assert len(l) == 1, "Terminal is not in exactly one net" return l[0] head_term = {t: term_to_net(t)[0] for c in components for t in c.terminals} net_continuity_constraints = wff.to_cnf( wff.for_all(comp_pos[c, p] >> term_conn[head_term[t], h] for h in board.holes for c in components for t in c.terminals for p in positions_which_have_term_in[t, h])) if _DEBUG: print("Net continuity constraints: {}".format( net_continuity_constraints.stats)) # Add constraints which ensure that no hole is part of more than one # net, and if its disconnected from all nets, then it can be part of no # net. net_discontinuity_constraints = cnf.Expr.all( cnf.at_most_one( {term_conn[net[0], h] for net in nets} | {term_dist[h, len(board.holes) - 1]}) for h in board.holes) if _DEBUG: print("Net discontinuity constraints: {}".format( net_discontinuity_constraints.stats)) # Return all of the above. return (term_conn_constraints | zero_term_dist_constraints | non_zero_term_dist_constraints | net_discontinuity_constraints | net_continuity_constraints) # Make variables to indicate whether a component is in a particular # position. Assignments for these variables will be used to produce # placements. comp_pos = {(comp, pos): wff.Var("comp {} in pos {}".format(comp, pos)) for comp in components for pos in positions[comp]} # Constrain the `comp_pos` variables such that a component must be in # exactly one position. one_pos_per_comp = cnf.Expr.all(cnf.exactly_one(comp_pos[comp, pos] for pos in positions[comp]) for comp in components) # Make jumpers, and their associated links. if max_jumpers == 0: max_jumper_length = 0 jumpers = [j for j in _Jumper.gen_jumpers(board, max_jumper_length)] jumper_links = [_Link(j.h1, j.h2, j.pres_var) for j in jumpers] # Make links for each trace. trace_links = [_Link(h1, h2, wff.Var("trace {} link".format((h1, h2)))) for h1, h2 in board.traces] # Make variables to indicate holes which have been drilled out. drilled = {h: wff.Var("{} drilled".format(h)) for h in board.holes} # Add a constraint to enforce the following: A trace link is present iff # neither of the holes it is connected to are drilled. drilled_link_constraints = wff.to_cnf( wff.for_all(l.pres_var.iff(~drilled[l.h1] & ~drilled[l.h2]) for l in trace_links)) links = jumper_links + trace_links # Enforce cardinality constraints on drilled holes. if max_drilled == 0: drilled_link_constraints |= wff.to_cnf( wff.for_all(~drilled[h] for h in board.holes)) elif max_drilled is not None: drilled_link_constraints |= _at_most( [drilled[h] for h in board.holes], max_drilled, var_prefix="max drilled") # Enforce cardinality constraints on jumpers. if max_jumpers is not None and max_jumpers > 0 and len(jumpers) > 1: drilled_link_constraints |= _at_most([j.pres_var for j in jumpers], max_jumpers, var_prefix="max jumper") # Combine all the constraints into a single expression. expr = (one_pos_per_comp | drilled_link_constraints | physical_constraints() | continuity_constraints()) if _DEBUG: print(expr.stats) print("Solving!") # Find solutions and map each one back to a Placement. for sol in cnf.solve(expr, slvr=slvr): if _DEBUG: print("Done") for var, val in sol.items(): print("{} {}".format("~" if not val else " ", var)) mapping = {comp: pos for (comp, pos), var in comp_pos.items() if sol[var]} drilled_holes = {h for h in board.holes if sol[drilled[h]]} jumpers = {(l.h1, l.h2) for l in jumper_links if sol[l.pres_var]} # If this fails the "exactly one position" constraint has been # violated. assert len(mapping) == len(components) yield Placement(board, mapping, drilled_holes, jumpers)