def __attrs_post_init__(self): entities = list(self.graph.iter_entities()) for inp in self.inputs: for ent in entities: if inp is ent.value: self._input_entities.append(ent) break else: raise AssertionError(f"No entity found for input {inp}.") try: self._output_entity = next(ent for ent in entities if ent.value is self.output) except StopIteration: raise AssertionError(f"No entity found for output {self.output}.") # Add placeholder nodes for every input and output entity. This facilitates graph algorithms. # TODO : Is there a better way? Should we introduce the concept of wildcard nodes in subgraph isomorphism? # TODO : Seems like that would entail significant engineering, not to mention rethinking the API. # TODO : Keeping this as low priority. for ent in itertools.chain(self._input_entities, [self._output_entity]): self.graph.add_node(PlaceholderNode(entity=ent)) self.transformation = Transformation.build_from_graph( self.graph, input_entities=self._input_entities, output_entity=self._output_entity)
def _adapt_query_plan(self, plan: QueryPlan, query: Query): # The given plan is assumed to be a canonical query plan. # Also, the transformation in plan should be the same as the transformation in query. This should be # guaranteed by construction of the query plan. # Create a fresh copy of the plan where the transformation is the actual subgraph. adapted_plan = plan.deepcopy().adapt(new_transformation=query.subgraph, mapping_old_to_new=query.mapping) equality_label = self._domain.get_equality_edge_label() if equality_label is None: return None # Propagate known values amongst the nodes with the equality edge influence: Dict[Node, Set[Node]] = collections.defaultdict(set) for k, v in adapted_plan.all_connections.m_node.items(): influence[v].add(k) seen = set() worklist = collections.deque(adapted_plan.transformation.iter_nodes()) while len(worklist) > 0: node = worklist.popleft() if node in seen: continue seen.add(node) if node.value is SYMBOLIC_VALUE: continue # Connected nodes inherit the value for n in influence[node]: if n.value is SYMBOLIC_VALUE: n.value = node.value worklist.append(n) # Equality edges with src and dst as node also propagate the values for unit in adapted_plan.units: for e in unit.iter_edges(src=node, label=equality_label): if e.dst.value is SYMBOLIC_VALUE: e.dst.value = node.value worklist.append(e.dst) for e in unit.iter_edges(dst=node, label=equality_label): if e.src.value is SYMBOLIC_VALUE: e.src.value = node.value worklist.append(e.src) # We may have wrecked the internal data-structures of the unit transformations by changing values directly. # Create shallow copies which force a rebuild adapted_plan.units = [Transformation.build_from_graph(Graph.from_nodes_and_edges(unit.get_all_nodes(), unit.get_all_edges()), unit.get_input_entities(), unit.get_output_entity()) for unit in adapted_plan.units] return adapted_plan
def _get_canonical_query_plans(self, sequence: List[str], transformation: Transformation) -> Dict[Skeleton, Set[QueryPlan]]: meta_plan = self._meta_plans[transformation] blueprint_item_lists = self._get_blueprint_item_lists(sequence, meta_plan, _d=len(sequence)) canonical_transformation = meta_plan.canonical_transformations[len(sequence)] mapping = next(canonical_transformation.get_subgraph_mappings(transformation)) skeletons_to_plans: Dict[Skeleton, Set[QueryPlan]] = collections.defaultdict(set) for blueprint_item_list in blueprint_item_lists: # Breakdown the overall transformation in terms of the unit plans contained in the blueprint items. # Store the connections between them as a graph mapping. connections = GraphMapping() connections.update(mapping) graph = Graph() for item in blueprint_item_list: graph.merge(item.unit.transformation) connections = connections.apply_mapping(item.canonical_mapping, only_keys=True) if item.border_mapping: connections.update(item.border_mapping) connections = connections.apply_mapping(connections, only_values=True) # Assemble the query plan query_plan = QueryPlan(transformation, units=[item.unit.transformation for item in blueprint_item_list], all_connections=connections, strengthenings=[item.unit.strengthenings[component_name] for component_name, item in zip(sequence, blueprint_item_list)]) # Obtain the skeletons for which this query plan would work. # External inputs are negative integers. See gauss.synthesis.skeleton for details. ent_to_idx = {ent: -idx for idx, ent in enumerate(transformation.get_input_entities(), 1)} possible_arg_ints_lists = [] for component_name, (idx, item) in zip(sequence, enumerate(blueprint_item_list, 1)): # Get the mapped entities to the inputs of this unit's transformation, and look up their idx values. arg_ints = [ent_to_idx[connections.m_ent[ent]] for ent in item.unit.transformation.get_input_entities()] # Get all the permutations as well. arg_ints_list = [arg_num_mapping.apply_list(arg_ints) for arg_num_mapping in item.unit.component_entries[component_name].argument_mappings] possible_arg_ints_lists.append(arg_ints_list) ent_to_idx[item.unit.transformation.get_output_entity()] = idx # The skeletons are then simply the all the combinations for arg_ints_list in itertools.product(*possible_arg_ints_lists): skeleton = Skeleton(list(zip(sequence, arg_ints_list))) skeletons_to_plans[skeleton].add(query_plan) return skeletons_to_plans
def _record_unit_meta_query_plan(self, component_name: str, subgraph: Graph, input_entities: List[Entity], output_entity: Entity, empty: bool = False): # First extract the symbolic transformation symbolic_copy, mapping = create_symbolic_copy(subgraph) mapped_input_entities = [mapping.m_ent[i] for i in input_entities] mapped_output_entity = mapping.m_ent[output_entity] transformation = Transformation.build_from_graph(symbolic_copy, input_entities=mapped_input_entities, output_entity=mapped_output_entity) if transformation not in self._unit_plans: # Transformation never seen before. Create a fresh unit plan. arg_number_mapping = ArgumentNumberMapping({i: i for i in range(len(transformation.get_input_entities()))}) unit_plan = UnitMetaPlan(transformation=transformation, empty=empty) unit_plan.component_entries[component_name] = UnitMetaPlan.ComponentEntry(component_name, {arg_number_mapping}) self._unit_plans[transformation] = unit_plan else: # Update the existing unit plan with the current component name and argument mapping(s). unit_plan: UnitMetaPlan = self._unit_plans[transformation] canonical_transform = unit_plan.transformation idx_input_entities = {ent: idx for idx, ent in enumerate(transformation.get_input_entities())} canonical_inp_entities = canonical_transform.get_input_entities() if component_name not in unit_plan.component_entries: entry = unit_plan.component_entries[component_name] = UnitMetaPlan.ComponentEntry(component_name) else: entry = unit_plan.component_entries[component_name] for m in canonical_transform.get_subgraph_mappings(transformation): arg_num_mapping = ArgumentNumberMapping({i: idx_input_entities[m.m_ent[ent]] for i, ent in enumerate(canonical_inp_entities)}) entry.argument_mappings.add(arg_num_mapping)
def _solve_for_skeleton_recursive( self, problem: SynthesisProblem, skeleton: Skeleton, query_plans: QueryPlans, context: SolverContext, _depth: int = 0) -> Iterator[Tuple[Any, Graph]]: domain = self._domain component_name, arg_ints = skeleton[_depth] inputs, g_inputs = context.get_arguments(depth=_depth) inp_entities = [ next(iter(g_inp.iter_entities())) for g_inp in g_inputs ] inp_graph = Graph() for g_inp in g_inputs: inp_graph.merge(g_inp) # Get the strengthening constraint for this depth. # Specifically, for every query, get the intersection of the strengthenings of all the query plans for that # query at this particular depth. Then take the union of all of these. # In other words, this strengthening constraint is a graph containing the nodes, edges, tags and tagged edges # that must be satisfied by the graph containing the inputs, that is `inp_graph` in this context. # This constraint can then be used by the `enumerate` procedure to speed up the search. strengthening_constraint: Graph = context.waypoints[ _depth].get_strengthening_constraint(inp_graph) enumeration_item: EnumerationItem for enumeration_item in domain.enumerate( component_name=component_name, inputs=inputs, g_inputs=g_inputs, constants=problem.constants, strengthening_constraint=strengthening_constraint): output = enumeration_item.output c_graph = enumeration_item.graph o_graph = enumeration_item.o_graph # for g in g_inputs: # assert set(g.iter_nodes()).issubset(set(c_graph.iter_nodes())) if problem.timeout is not None and time.time( ) - self._time_start > problem.timeout: raise TimeoutError("Exceeded time limit.") out_entity = next(iter(o_graph.iter_entities())) c_graph.add_node(PlaceholderNode(entity=out_entity)) c_graph = Transformation.build_from_graph( c_graph, input_entities=inp_entities, output_entity=out_entity) # Check if the returned graph is consistent with the query plans. if not context.check_validity(c_graph, depth=_depth): continue # Prepare for the next round. context.step(output=output, graph=c_graph, output_graph=o_graph, enumeration_item=enumeration_item, depth=_depth) if _depth == skeleton.length - 1: # This was the last component, prepare the program and return it along with the final output and graph. yield output, o_graph else: # Move on to the next component. yield from self._solve_for_skeleton_recursive(problem, skeleton, query_plans, context, _depth=_depth + 1)
def _evolve_meta_plan(self, plan: MetaQueryPlan, depth: int, nlabel_to_unit_plan: Dict[int, Set[UnitMetaPlan]]): # Replace an input node of plan with the output of a unit meta-plan (contained in nlabel_to_meta_plan). # Thus we extend an existing plan with the output of exactly one component, thus increasing the program # depth by exactly one. plan_transformation: Transformation = plan.canonical_transformations[depth - 1] all_input_nodes: Set[Node] = set(plan_transformation.get_input_nodes()) for inp_node in all_input_nodes: # Even if it is a placeholder node, it can only be extended via the empty transform. This makes sense # as no matter what the extension is, it will never "influence" the final output, as there is no path # between a placeholder node and the output node. This helps reduce the size of the collection of # meta query-plans by a large margin. remaining_inputs = all_input_nodes - {inp_node} for extender in nlabel_to_unit_plan[int(inp_node.label)]: # We can map the other inputs to the inputs of the extender plan. They can also be distinct # inputs of their own. The total number of inputs should, however, be less than max_inputs. # For the remaining inputs, if they are a placeholder node, they can be mapped to *any* of the # input nodes of the extender plan, regardless of the label. nlabel_to_inp_node: Dict[int, Set[Node]] = collections.defaultdict(set) extender_inputs: Set[Node] = set(extender.transformation.get_input_nodes()) # Guaranteed to be a single output node by construction. extender_output: Node = next(extender.transformation.get_output_nodes()) for inp in extender_inputs: nlabel_to_inp_node[inp.label].add(inp) nlabel_to_inp_node[extender_output.label].add(extender_output) mapping_possibilities: Dict[Node, Set[Node]] = {inp_node: {extender_output}} for inp in remaining_inputs: if inp.label == PLACEHOLDER_LABEL: mapping_possibilities[inp] = extender_inputs | {extender_output, inp} else: mapping_possibilities[inp] = nlabel_to_inp_node[inp.label] | {inp} node_list = list(all_input_nodes) for border_node_mapping in itertools.product(*[mapping_possibilities[n] for n in node_list]): border_node_mapping: Dict[Node, Node] = dict(zip(node_list, border_node_mapping)) border_mapping = GraphMapping(m_ent={k.entity: v.entity for k, v in border_node_mapping.items()}, m_node=border_node_mapping.copy()) # Create a deepcopy of the extender for safety copied_extender, copy_mapping = extender.deepcopy() copied_extender_inputs: Set[Node] = set(copied_extender.transformation.get_input_nodes()) copied_extender_output: Node = next(copied_extender.transformation.get_output_nodes()) border_mapping = border_mapping.apply_mapping(copy_mapping, only_values=True) assert border_mapping.m_node != border_node_mapping # The new inputs are the inputs of extender, plus the nodes of the current plan # which were not bound to any of the inputs of extender. # We also decide the order of the nodes/entities right now. new_input_nodes: List[Node] = [] for inp in plan_transformation.get_input_nodes(): mapped = border_mapping.m_node[inp] if mapped is inp: new_input_nodes.append(inp) elif mapped is copied_extender_output: new_input_nodes.extend(i for i in copied_extender.transformation.get_input_nodes() if i not in new_input_nodes) elif mapped not in new_input_nodes: assert mapped in copied_extender_inputs new_input_nodes.append(mapped) new_input_entities = [n.entity for n in new_input_nodes] # Every entity is associated with one node so the following should hold true. assert len(new_input_entities) == len(set(new_input_entities)) if len(new_input_entities) > self._config.max_inputs: continue new_output_entity = plan_transformation.get_output_entity() # Obtain the transformation by establishing common edges between the node pairs in # border_node_mapping, taking the transitive closure w.r.t equality, and finally the # induced subgraph by removing the input nodes of the current plan. joint_graph = Graph.from_graph(copied_extender.transformation) joint_graph.merge(plan_transformation) final_border_mapping = GraphMapping() for k, v in border_mapping.m_node.items(): if k is not v: final_border_mapping.m_node[k] = v final_border_mapping.m_ent[k.entity] = v.entity for edge in plan_transformation.iter_edges(src=k): joint_graph.add_edge(Edge(v, edge.dst, edge.label)) for edge in plan_transformation.iter_edges(dst=k): joint_graph.add_edge(Edge(edge.src, v, edge.label)) join_nodes: Set[Node] = set(all_input_nodes) join_nodes.difference_update(new_input_nodes) join_nodes.add(copied_extender_output) self._domain.perform_transitive_closure(joint_graph, join_nodes=join_nodes) keep_nodes = set(joint_graph.iter_nodes()) keep_nodes.difference_update(join_nodes) new_transformation_subgraph = joint_graph.induced_subgraph(keep_nodes=keep_nodes) new_transformation = Transformation.build_from_graph(new_transformation_subgraph, input_entities=new_input_entities, output_entity=new_output_entity) # Record the transformation and how it was obtained. if new_transformation not in self._meta_plans: # The transformation was never seen before. blueprint = collections.defaultdict(lambda: collections.defaultdict(list)) meta_plan = MetaQueryPlan(transformation=new_transformation.deepcopy()[0], blueprint=blueprint) self._meta_plans[new_transformation] = meta_plan else: meta_plan = self._meta_plans[new_transformation] if depth not in meta_plan.canonical_transformations: copy, mapping = new_transformation.deepcopy() meta_plan.canonical_transformations[depth] = copy # mapping = mapping.slice(nodes=set(copied_extender.transformation.iter_nodes())) mapping = mapping.reverse() else: canonical = meta_plan.canonical_transformations[depth] mapping = next(new_transformation.get_subgraph_mappings(canonical)) # mapping = mapping.slice(nodes=set(copied_extender.transformation.iter_nodes())) mapping = mapping.reverse() bp_item = MetaQueryPlan.BlueprintItem(depth=depth, unit=copied_extender, canonical_mapping=mapping, sub_plan=plan, border_mapping=final_border_mapping) for c in copied_extender.component_entries: meta_plan.blueprint[depth][c].append(bp_item)
def extract_queries( problem: SynthesisProblem) -> Dict[Transformation, List[Query]]: # Stage 1 : Get all paths from one of the input nodes to an output node without any input/output node in between. graph = problem.graph inputs = problem.inputs output = problem.output entities = list(graph.iter_entities()) input_entities = [ next(ent for ent in entities if ent.value is inp) for inp in inputs ] output_entity = next(ent for ent in entities if ent.value is output) arg_numbering = {ent: idx for idx, ent in enumerate(input_entities)} placeholder_dict = {} # A placeholder node can represent any node belonging to an entity. # This helps coalesce equivalent query plans. for ent in itertools.chain(input_entities, [output_entity]): placeholder_dict[ent] = PlaceholderNode(entity=ent) path_dict: Dict[Entity, List[Path]] = extract_paths(graph, input_entities, output_entity) canonical_transformations: Dict[Transformation, Transformation] = {} # Only keep one transformation for a set of nodes as satisfying that query means satisfying all the others. seen: Set[Tuple[Transformation, FrozenSet[Node]]] = set() queries: Dict[Transformation, List[Query]] = collections.defaultdict(list) set_input_entities = set(input_entities) # Find queries by taking exactly one path, and placeholder nodes for the # input entities not present in the path. for path_ent, paths in path_dict.items(): remaining_entities = [ ent for ent in set_input_entities if ent is not path_ent ] for path in paths: path_nodes, path_edges = path nodes = list(path_nodes) + [ placeholder_dict[ent] for ent in remaining_entities ] edges = path_edges # Get the corresponding subgraph. subgraph = Graph.from_nodes_and_edges(nodes=set(nodes), edges=set(edges)) subgraph_transformation = Transformation.build_from_graph( subgraph, input_entities=input_entities, output_entity=output_entity) # Compute the symbolic counter-part to group subgraphs together by the underlying transformation. symbolic_copy, mapping = create_symbolic_copy(subgraph) mapped_input_entities = [mapping.m_ent[i] for i in input_entities] mapped_output_entity = mapping.m_ent[output_entity] transformation = Transformation.build_from_graph( symbolic_copy, input_entities=mapped_input_entities, output_entity=mapped_output_entity) # Check if the transformation was seen before if transformation not in canonical_transformations: canonical_transformations[transformation] = transformation seen.add((transformation, frozenset(n for n in subgraph.iter_nodes() if n.entity in set_input_entities))) mapping = mapping.reverse() arg_number_mapping = ArgumentNumberMapping({ idx: arg_numbering[mapping.m_ent[ent]] for idx, ent in enumerate(mapped_input_entities) }) # We need a mapping from transformation to the subgraph. queries[transformation].append( Query(transformation=transformation, subgraph=subgraph_transformation, mapping=mapping, arg_number_mapping=arg_number_mapping)) else: canonical = canonical_transformations[transformation] key = (transformation, frozenset(n for n in subgraph.iter_nodes() if n.entity in set_input_entities)) # Check if the transformation was seen before with the same input nodes. If yes, continue. This is # because if a graph satisfies the already seen transformation for these input nodes, it will satisfy # this one as well. So no point in checking it. if key in seen: continue seen.add(key) # We need a mapping from the canonical transformation to the subgraph. mapping = next(canonical.get_subgraph_mappings( transformation)).apply_mapping(mapping.reverse()) arg_number_mapping = ArgumentNumberMapping({ idx: arg_numbering[mapping.m_ent[ent]] for idx, ent in enumerate(canonical.get_input_entities()) }) queries[canonical].append( Query(transformation=canonical, subgraph=subgraph_transformation, mapping=mapping, arg_number_mapping=arg_number_mapping)) return queries