示例#1
0
    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)
示例#2
0
    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
示例#3
0
    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
示例#4
0
    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)
示例#5
0
    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)
示例#6
0
    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)
示例#7
0
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