************************************************************************** * * * The SubCircuit C++11 library * * * * An implementation of a modified Ullmann Subgraph Isomorphism Algorithm * * for coarse grain logic networks. by Clifford Wolf * * * ************************************************************************** ============ Introduction ============ This is a library that implements a modified Ullmann Subgraph Isomorphism Algorithm with additional features aimed at working with coarse grain logic networks. It also contains a simple frequent subcircuit mining algorithm. A simple command line tool that exposes the features of the library is also included. C++11 Warning ------------- This project is written in C++11. Use appropriate compiler switches to compile it. Tested with clang version 3.0 and option -std=c++11. Also tested with gcc version 4.6.3 and option -std=c++0x. ======== Features ======== The input is two graphs (needle and haystack) that represent coarse grain logic networks. The algorithm identifies all subgraphs of haystack that are isomorphic to needle. The following additional features over the regular Ullmann Subgraph Isomorphism Algorithm are provided by the library. * The graphs are attributed hypergraphs capable of representing netlists: - Nodes represent the logic cells: - Nodes have types and only match compatible types - Nodes have ports with variable bit-width - Hyperedges represent the signals: - Each hyperedge connects one to many bits on ports on nodes - Callback functions for advanced attributes and compatibility rules: Any set of node-node compatibility rules and edge-edge compatibility rules can be implemented by providing the necessary callback functions. * The algorithm is very efficient when all or many bits of one port are connected to bits of the same other port. This is usually the case in coarse grain logic networks. But the algorithm does not add any restrictions in this area; it is just optimized for this scenario. * The algorithm can be configured to allow larger ports in needle cells to match smaller ports in haystack cells in certain situations. This way it is possible to e.g. have a 32-bit adder cell in the needle match a 16-bit adder cell in the haystack. * The algorithm can be configured to perform port-swapping on certain ports on certain cell types to match commutative operations properly. This is, however, not implemented very efficiently when a larger number of permutations is possible on a cell type. Therefore it is recommended to only use swap groups with only a few members and a few such groups on one cell type type. Also note, that the algorithm can not resolve complex dependencies between the port swappings of different cells. Therefore it is recommended to only use port swapping on input pins of commutative operations, where such complex dependencies can not emerge. * The algorithm can be configured to distinguish between internal signals of the needle and externally visible signals. The needle will only match a subgraph of the haystack if that subgraph does not expose the internal signal to nodes in the haystack outside the matching subgraph. * The algorithm can recognize a subcircuit even if some or all of its inputs and/or outputs are shorted together. * Explicit fast support for constant signals without extra nodes for constant drivers. * Support for finding only non-overlapping matches. * A simple miner for frequent subcircuts that operates on the same circuit description format. * The public API of the library is using std::string identifiers for nodes, node types and ports. Internally the costly part of the algorithm is only using integer values, thus speeding up the algorithm without exposing complex internal encodings to the caller. ================= API Documentation ================= This section gives a brief overview of the API. For a working example, have a look at the demo.cc example program in this directory. Setting up graphs ----------------- Instanciate the SubCircuit::Graph class and use the methods of this class to set up the circuit. SubCircuit::Graph myGraph; For each node in the circuit call the createNode() method. Specify the identifier for the node and also the type of function implemented by the node. Then call createPort() for each port of this node. E.g. the following code adds a node "myAdder" of type "add" with three 32 bit wide ports "A", "B" and "Y". Note that SubCircuit does not care which port is an input and which port is an output. The last (and optional) argument to createPort() specifies the minimum number of bits required for this port in the haystack (this field is only used in the needle graph). So in this example the node would e.g. also match any adder with a bit width smaller 32. myGraph.createNode("myAdder", "add"); myGraph.createPort("myAdder", "A", 32, 1); myGraph.createPort("myAdder", "B", 32, 1); myGraph.createPort("myAdder", "Y", 32, 1); The createConnection() method can be used to connect the nodes. It internally creates a hypergraph. So the following code does not only connect cell1.Y with cell2.A and cell3.A but also implicitly cell2.A with cell3.A. myGraph.createConnection("cell1", "Y", "cell2", "A"); myGraph.createConnection("cell1", "Y", "cell3", "A"); Redundent calls to createConnection() are ignored. As long as the method is called after the relevant nodes and ports are created, the order in which the createConnection() calls are performed is irrelevant. The createConnection() method can also be used to connect single bit signals. In this case the start bit for both ports must be provided as well as an optional width (which defaults to 1). E.g. the following calls can be used to connect the 32 bit port cell4.Y to the 32 bit port cell5.A with a one bit left rotate shift, myGraph.createConnection("cell4", "Y", 0, "cell5", "A", 1, 31); myGraph.createConnection("cell4", "Y", 31, "cell5", "A", 0); The method createConstant() can be used to add a constant driver to a signal. The signal value is encoded as one char by bit, allowing for multi-valued logic matching. The follwoing command sets the lowest bit of cell6.A to a logic 1: myGraph.createConnection("cell6", "A", 0, '1'); It is also possible to set an entire port to a integer value, using the encodings '0' and '1' for the binary digits: myGraph.createConnection("cell6", "A", 42); The method markExtern() can be used to mark a signal as externally visible. In a needle graph this means, this signal may match a signal in the haystack that is used outside the matching subgraph. In a haystack graph this means, this signal is used outside the haystack graph. I.e. an internal signal of the needle won't match an external signal of the haystack regardless where the signal is used in the haystack. In some application one may disable this extern/intern checks. This can easily be achieved by marking all signals in the needle as extern. This can be done using the Graph::markAllExtern() method. Setting up and running solvers ------------------------------ To actually run the subgraph isomorphism algorithm, an instance of SubCircuit::Solver must be created. SubCircuit::Solver mySolver; The addGraph() method can be used to register graphs with the solver: mySolver.addGraph("graph1", myGraph); mySolver.addGraph("graph2", myOtherGraph); Usually nodes in the needle and the haystack must have the same type identifier to match each other. Additionally pairs of compatible needle and haystack node pairs can be registered using the addCompatibleTypes() method: mySolver.addCompatibleTypes("alu", "add"); mySolver.addCompatibleTypes("alu", "sub"); mySolver.addCompatibleTypes("alu", "and"); mySolver.addCompatibleTypes("alu", "or"); mySolver.addCompatibleTypes("alu", "xor"); Note that nodes in needle and haystack must also use the same naming convention for their ports in order to be considered compatible by the algorithm. Similarly the method addCompatibleConstants() can be used the specify which constant values in the needle should match which constant value in the haystack. Equal values always do match. mySolver.addCompatibleConstants('x', '0'); mySolver.addCompatibleConstants('x', '1'); Some cells implement commutative operations that don't care if their input operands are swapped. For this cell types it is possible to register groups of swappable ports. Let's consider a cell "macc23" that implements the function Y = (A * B) + (C * D * E): mySolver.addSwappablePorts("macc23", "A", "B"); mySolver.addSwappablePorts("macc23", "C", "D", "E"); Sometimes the rules for port swapping are a more complicated and the swapping of one port is related to the swapping of another port. Let's consider a cell "macc22" that implements the function Y = (A * B) + (C * D): mySolver.addSwappablePorts("macc22", "A", "B"); mySolver.addSwappablePorts("macc22", "C", "D"); std::map portMapping; portMapping["A"] = "C"; portMapping["B"] = "D"; portMapping["C"] = "A"; portMapping["D"] = "B"; mySolver.addSwappablePortsPermutation("macc22", portMapping); I.e. the method mySolver.addSwappablePortsPermutation() can be used to register additional permutations for a node type of which one or none is applied on top of the permutations yielded by the permutations generated by the swap groups. Note that two solutions that differ only in the applied port swapping are not reported as separate solutions. Instead only one of them is selected (in most cases the one with less port swapping as it is usually identified first). Once everything has been set up, the solve() method can be used to actually search for isomorphic subgraphs. The first argument to solve() is an std::vector objects to which all found solutions are appended. The second argument is the identifier under which the needle graph has been registered and the third argument is the identifier under which the haystack graph has been registered: std::vector results; mySolver.solve(results, "graph1", "graph2"); The SubCircuit::Solver::Result object is a simple data structure that contains the mappings between needle and haystack nodes, port mappings after the port swapping and some additional metadata. See "subcircuit.h" and "demo.cc" for details. The solve() method has a third optional boolean argument. If it is set to false, solve will not return any solutions that contain haystack nodes that have been part of a previously found solution. This way it is e.g. easy to implement a greedy macro cell matching algorithm: std::vector results; mySolver.solve(results, "macroCell1", "circuit", false); mySolver.solve(results, "macroCell2", "circuit", false); mySolver.solve(results, "macroCell3", "circuit", false); After this code has been executed, the results vector contains all non-overlapping matches of the three macrocells. The method clearOverlapHistory() can be used to reset the internal state used for this feature. The default value for the third argument to solve() is true (allow overlapping). The optional boolean fourth argument to the Graph::createNode() method can be used to mark a node as shareable even in non-overlapping solver mode. The solve() method also has a fourth optional integer argument. If it is set to a positive integer, this integer specifies the maximum number of solutions to be appended to the results vector, i.e. to terminate the algorithm early when the set number of matches is found. When this fourth argument is negative or omitted all matches are found and appended. An alternative version of the solve() method supports an additional argument after they haystack graph identifier that specifies initial mappings for the algorithm. In the following example only the haystack nodes cell_1 and cell_2 are considered as mappings for the needle node cell_A: std::map> initialMappings; initialMappings["cell_A"].insert("cell_1"); initialMappings["cell_A"].insert("cell_2"); std::vector results; mySolver.solve(results, "graph1", "graph2", initialMappings); The clearConfig() method can be used to clear all data registered using addCompatibleTypes(), addCompatibleConstants(), addSwappablePorts() and addSwappablePortsPermutation() but retaining the graphs and the overlap state. Using user callback function ---------------------------- For more complex tasks it is possible to derive a class from SubCircuit::Solver that overloads one or more of the following virtual methods. The userData arguments to the following methods are void pointers that can be passed as third argument to Graph::createNode() and are simly passed thru to the user callback functions together with the node id whenever a node is referenced. bool userCompareNodes(needleGraphId, needleNodeId, needleUserData, haystackGraphId, haystackNodeId, haystackUserData): Perform additional checks on a pair of nodes (one from the needle, one from the haystack) to determine if the nodes are compatible. The default implementation always returns true. bool userCompareEdge(needleGraphId, needleFromNodeId, needleFromUserData, needleToNodeId, needleToUserData, haystackGraphId, haystackFromNodeId, haystackFromUserData, haystackToNodeId, haystackToUserData): Perform additional checks on a pair of a pair of adjacent nodes (one adjacent pair from the needle and one adjacent pair from the haystack) to determine wheter this edge from the needle is compatible with that edge from the haystack. The default implementation always returns true. bool userCheckSolution(result): Perform additional checks on a solution before appending it to the results vector. When this function returns false, the solution is ignored. The default implementation always returns true. Mining for frequent SubCircuits ------------------------------- The solver also contains a miner for frequent subcircuits. The following code fragment will find all frequent subcircuits with at least minNodes nodes and at most maxNodes nodes that occurs at least minMatches times: std::vector results; mySolver.mine(results, minNodes, maxNodes, minMatches); The mine() method has an optional fifth parameter that limits the number of matches counted in one graph. This can be useful when mining for circuits that are found in at least a number of graphs. E.g. the following call would find all subcircuits with 5 nodes that are found in at least 7 of the registered graphs: mySolver.mine(results, 5, 5, 7, 1); Note that this miner is not very efficient and therefore its use is not recommended for large circuits. Also note that the miner is working under the assumption that subgraph isomorphism is bidirectional. This is not the case in circuits with gates with shorted pins. This can result in undetected frequent subcircuits in some corner cases. Debugging --------- For debugging purposes the SubCircuit::Solver class implements a setVerbose() method. When called once, all further calls to the solve() method cause the algorithm to dump out a lot of debug information to stdout. In conjunction with setVerbose() one can also overload the userAnnotateEdge() method in order to add additional information about the edges to the debug output. =================== Shell Documentation =================== This package also contains a small command-line tool called "scshell" that can be used for experimentation with the algorithm. This program reads a series of commands from stdin and reports its findings to stdout on exit. $ ./scshell < test_macc22.txt ... Match #3: (macc22 in macc4x2) add_1 -> add_2 A:B B:A Y:Y mul_1 -> mul_4 A:A B:B Y:Y mul_2 -> mul_3 A:A B:B Y:Y The following commands can be used in scshell to specify graphs: graph ... endgraph Used to specify a graph with the given name. Only the commands "node", "connect" and "extern" may be used within the graph ... endgraph block. node [ [ []]]+ Used to create a node and ports. This command is a direct frontend to the Graph::createNode() and Graph::createPort() methods. connect connect connect Used to connect the nodes in the graph via Graph::createConnection(). constant [] Call Graph::createConstant(). extern [ []]+ Mark signals as extern via Graph::markExtern(). allextern Mark all signals as extern via Graph::markAllExtern(). The following commands can be used in scshell outside a graph ... endgraph block: compatible Call Solver::addCompatibleTypes(). constcompat Call Solver::addCompatibleConstants(). swapgroup + Call Solver::addSwappablePorts(). swapperm + : + Call Solver::addSwappablePortsPermutation(). Both port lists must have the same length and the second one must be a permutation of the first one. initmap + Add an entry to the initial mappings for the next solve command. This mappings are automatically reset after the solve command. solve [ []] Call Solver::solve(). The must be "1" or "true" for true and "0" or "false" for false. mine [] Call Solver::mine(). expect Print all results so far since the last call to expect. Expect results and exit with error code 1 if a different number of results have been found. clearoverlap Call Solver::clearOverlapHistory(). clearconfig Call Solver::clearConfig(). verbose Call Solver::setVerbose().