예제 #1
0
def get_graph(matrix, labels):
    G = nx.from_numpy_array(matrix.T, create_using=nx.DiGraph)
    g = nx.transitive_reduction(G)
    mapping = dict(zip(g, labels))
    # ncenter = list(nx.topological_sort(g))
    g = nx.relabel_nodes(g, mapping)
    return g
예제 #2
0
def graph_from_document(rows: Dict[str, List[Tuple[int, int]]]) -> nx.DiGraph:
    res = get_dense_matrix(rows)
    items = get_elementary_nodes(rows)
    df = matrix_to_dataframe(res, items)
    edges = edges_from_dataframe(df, items)
    graph = nx.DiGraph()
    graph.add_nodes_from(items)
    graph.add_edges_from(edges)

    create_entry_and_exit_nodes(graph, items)
    #remove_shortcuts(graph)
    graph = nx.transitive_reduction(graph)
    add_blanks(graph)
    #remove_shortcuts(graph)
    graph = nx.transitive_reduction(graph)
    return graph
예제 #3
0
def create_lattice(concept_class, *, parallel=True):
    child_map = adj_list(concept_class)
    clses = equiv_classes.find_equiv_cls(concept_class,
                                         child_map=child_map,
                                         parallel=parallel)
    g = nx.DiGraph()
    for cls1, cls2 in possible_edges(clses):
        if cls1 <= cls2:
            g.add_edge(cls1.rep, cls2.rep)
        if cls2 <= cls1:
            g.add_edge(cls2.rep, cls1.rep)

    return nx.transitive_reduction(g)
예제 #4
0
 def getOptimizedGraph(self, headers_tuples):
     Gc = self.getClosedGraph()
     GMin = nx.DiGraph()
     for n, i in enumerate(headers_tuples):
         if n % 1000 == 999:
             print("Optimized Graph Header tuple {}".format(n))
         i.append(self.initState)
         i.append(self.lastState)
         tmp = Gc.subgraph(i)
         tmp = nx.transitive_reduction(tmp)
         GMin = nx.compose(GMin, tmp)
     GMin = Gc.edge_subgraph(GMin.edges)
     return GMin
예제 #5
0
def check_relationships(branches):

    ancestors = {b: set() for b in branches}
    length = len(branches) * (len(branches) - 1)
    for b1, b2 in ub.ProgIter(it.combinations(branches, 2), length=length):
        ret = ub.cmd('git merge-base --is-ancestor {} {}'.format(b1, b2))['ret']
        if ret == 0:
            ancestors[b1].add(b2)
        ret = ub.cmd('git merge-base --is-ancestor {} {}'.format(b2, b1))['ret']
        if ret == 0:
            ancestors[b2].add(b1)
    print('<key> is an ancestor of <value>')
    print(ub.repr2(ancestors))

    descendants = {b: set() for b in branches}
    for key, others in ancestors.items():
        for o in others:
            descendants[o].add(key)
    print('<key> descends from <value>')
    print(ub.repr2(descendants))

    import plottool as pt
    import networkx as nx
    G = nx.DiGraph()
    G.add_nodes_from(branches)
    for key, others in ancestors.items():
        for o in others:
            # G.add_edge(key, o)
            G.add_edge(o, key)

    from networkx.algorithms.connectivity.edge_augmentation import collapse
    flag = True
    G2 = G
    while flag:
        flag = False
        for u, v in list(G2.edges()):
            if G2.has_edge(v, u):
                G2 = collapse(G2, [[u, v]])

                node_relabel = ub.ddict(list)
                for old, new in G2.graph['mapping'].items():
                    node_relabel[new].append(old)
                G2 = nx.relabel_nodes(G2, {k: '\n'.join(v) for k, v in node_relabel.items()})
                flag = True
                break

    G3 = nx.transitive_reduction(G2)
    pt.show_nx(G3, arrow_width=1.5, prog='dot', layoutkw=dict(prog='dot'))
    pt.zoom_factory()
    pt.pan_factory()
    pt.plt.show()
예제 #6
0
 def dag(self):
     try:
         from networkx import DiGraph, transitive_reduction
     except ImportError:
         raise ImportError("the dag method requires networkx>=2.0")
     dag = DiGraph()
     for order, node in enumerate(self.funcs):
         dag.add_node(node, order=order)
     for sig1, sig2 in combinations(self.funcs, 2):
         if refines(sig1, sig2):
             dag.add_edge(sig1, sig2)
         elif refines(sig2, sig1):
             dag.add_edge(sig2, sig1)
     return transitive_reduction(dag)
예제 #7
0
 def validate_graph(cls, value, values, **kwargs):
     """
     Build the job.graph from the job.tasks, and ensure that the graph is
     acyclic (a directed acyclic graph).
     """
     graph = nx.DiGraph()
     for task_name, task in (values.get('tasks') or {}).items():
         graph.add_node(task_name)  # make sure every task is added
         for depend_name in task.depends:
             graph.add_edge(depend_name, task_name)
     if not nx.is_directed_acyclic_graph(graph):
         raise ValueError(
             'The tasks graph must be acyclic, but it currently includes cycles.'
         )
     # the transitive reduction ensures the shortest version of the workflow.
     return nx.transitive_reduction(graph)
예제 #8
0
def buildDAGadjacencyMatrix(GEM, met_orders):
    # Build full graph adjacency matrix
    total_mets = np.unique(
        np.array([[met_pairs[0], met_pairs[1]]
                  for met_pairs in met_orders]).flatten()).tolist()
    N_nodes = len(total_mets)

    A = np.zeros((N_nodes, N_nodes))
    for met_pair in met_orders:
        i, j = total_mets.index(met_pair[0]), total_mets.index(met_pair[1])
        A[i, j] = met_pair[2]

    # Get transitive reduction of the graph
    G = nx.from_numpy_matrix(A, create_using=nx.DiGraph())
    G_plus = nx.transitive_reduction(G)

    # Get adjacency matrix of the transitive reduction
    A_plus = nx.to_numpy_matrix(G)
    return (A_plus, total_mets)
예제 #9
0
    def getGraph(self):
        """
        Construct a the transitive reduction of the graph representing flux
        order relations. Only after calling either of both
        buildFluxOrderDataFrame or findFluxOrderRelations methods.

        Returns
        -------
        A networkx graph object
        """
        if not hasattr(self, 'A'):
            raise ValueError("""Adjacency matrix missing! Call
                             .buildFluxOrderDataFrame or
                              .findFluxOrderRelations first""")

        collapsed = self.__collapseFullyCoupledWithEqualFlux()
        collapsed_Adjacency_Matrix = collapsed['A']
        collapsed_GEM = collapsed['GEM']
        A = collapsed_Adjacency_Matrix.values

        G = nx.from_numpy_matrix(A, create_using=nx.DiGraph())
        G_plus = nx.transitive_reduction(G)

        nodeMap = {}
        nodeAttributes = {}
        nodes = list(G_plus.nodes)
        reactions = collapsed_GEM.reactions
        for i in range(len(nodes)):
            nodeMap[nodes[i]] = reactions[nodes[i]].id
            nodeAttributes[nodes[i]] = {
                'name': reactions[nodes[i]].name,
                'subsystem': reactions[nodes[i]].subsystem,
                'macrosystem': reactions[nodes[i]].macrosystem
            }

        nx.set_node_attributes(G_plus, nodeAttributes)
        G_out = nx.relabel_nodes(G_plus, nodeMap, copy=False)
        isolated_nodes = list(nx.isolates(G_out))
        G_out.remove_nodes_from(isolated_nodes)
        self.G = G_out
        return OrderGraph(G_out)
예제 #10
0
def induced_connected_subgraph(G, nodes):
    _nodes = set(nodes)
    g = nx.DiGraph()
    g.add_nodes_from(G)
    g.add_edges_from(G.edges)
    roots = list(filter(lambda n: g.in_degree(n)==0, g))
    W = set(roots)
    while len(W)>0:
        n = W.pop()
        successors = list(g.successors(n))
        if  n in _nodes:
            for successor in successors:
                W.add(successor)
        else:
            for successor in successors:
                W.add(successor)
                for predecessor in filter(lambda p: p in _nodes, g.predecessors(n)):
                    g.add_edge(predecessor, successor)
    g.remove_nodes_from(list(filter(lambda n: n not in _nodes, g)))
    g = nx.transitive_reduction(g)
    return g
def circuit_to_dag(c: qiskit.circuit.QuantumCircuit) -> nx.DiGraph:

    dag = nx.DiGraph()

    qubit_last_use = {}

    null_op = LabeledOp([None, []], -1)

    for index, instruction in enumerate(c):
        op = LabeledOp(instruction, index)

        for qubit in op.qubits:
            if qubit not in qubit_last_use:
                dag.add_edge(null_op, op)
            else:
                dag.add_edge(qubit_last_use[qubit], op)

            qubit_last_use[qubit] = op

    final_op = LabeledOp([None, []], float('inf'))
    for q in qubit_last_use:
        dag.add_edge(qubit_last_use[qubit], final_op)

    return nx.transitive_reduction(dag)
예제 #12
0
def group_substructures(mols, patterns=None,
                        mol_instantiator=unsanitized_mol_from_smiles,
                        pattern_instantiator=mol_from_smarts,
                        matcher=has_query_query_match,
                        reduce=True):

    import networkx as nx

    # Instantiate mols and their "pattern" representation
    # Must document that, when already provided Chem.Mol objects, instantiators usually are no-ops
    if pattern_instantiator is not None:
        patterns = list(to_rdkit_mols(mols, pattern_instantiator))
    if mol_instantiator is not None:
        mols = list(to_rdkit_mols(mols, mol_instantiator))

    if patterns is None:
        patterns = mols

    # Sort substructures by decreasing number of atoms
    num_atoms = [mol.GetNumAtoms() for mol in mols]
    descending_number_of_atoms_order = np.argsort(num_atoms)[::-1]

    representative = [None] * len(mols)  # For duplicates
    graph = nx.DiGraph()                 # Directed graph, if (p1, p2) on it,

    # Nasty stuff that would not happen if cheminformatics were logical
    # noinspection PyUnusedLocal
    has_equal_nonequal = has_cycles = False

    for p1, p2 in combinations(descending_number_of_atoms_order, 2):
        p2_in_p1, p1_in_p2 = matcher(mols[p1], patterns[p2]), matcher(mols[p2], patterns[p1])
        representative[p1] = representative[p1] or p1
        representative[p2] = representative[p2] or p2
        if p2_in_p1 and p1_in_p2:
            representative[p2] = representative[p1]
        elif p2_in_p1:
            if num_atoms[p1] == num_atoms[p2] and not has_equal_nonequal:
                has_equal_nonequal = True
                info('mindblowingly, with equal number of atoms, one contains the other but not viceversa')
            graph.add_edge(representative[p1], representative[p2])
        elif p1_in_p2:
            if num_atoms[p1] == num_atoms[p2] and not has_equal_nonequal:
                has_equal_nonequal = True
                info('mindblowingly, with equal number of atoms, one contains the other but not viceversa')
            graph.add_edge(representative[p2], representative[p1])
        else:
            graph.add_node(representative[p1])
            graph.add_node(representative[p2])

    # Cycles?
    try:
        nx.find_cycle(graph)
        has_cycles = True
        info('containment graph has cycles')
    except nx.NetworkXNoCycle:
        has_cycles = False

    if reduce:
        graph = nx.transitive_reduction(graph)

    groups = list(nx.weakly_connected_components(graph))
    # noinspection PyCallingNonCallable
    roots = [node for node, degree in graph.in_degree() if 0 == degree]
    # noinspection PyCallingNonCallable
    leaves = [node for node, degree in graph.out_degree() if 0 == degree]

    return graph, groups, representative, roots, leaves, num_atoms, has_cycles, has_equal_nonequal
예제 #13
0
graph = nx.DiGraph()

with open(filename) as f:
    for line in f:
        left, right = line.strip().split(')')
        if left not in graph:
            graph.add_node(left)
        if right not in graph:
            graph.add_node(right)
        graph.add_edge(left, right)

n_nodes = len(graph)

if debug:
    print(len(graph))
    print('should be same as')
    print(len(nx.transitive_reduction(graph)))

if not nx.is_directed_acyclic_graph(graph):
    raise ValueError('Circular connections found! This is not a DAG :(')

total_ancestors = 0

for node in graph:
    n_ancestors = len(nx.ancestors(graph, node))
    total_ancestors += n_ancestors
    if debug:
        print(f'Node {node} has {n_ancestors} ancestors')

print(f'Total orbitees: {total_ancestors}')
예제 #14
0
 def remove_transitive_edges(self):
     """Removes all transitive edges from the graph."""
     self._remove_all_edges_except(
         set(nx.transitive_reduction(self.graph).edges))
     return self
예제 #15
0
nodes = list(G.nodes)
numEdges = {}
for node1 in nodes:
    for node2 in nodes:
        if node1 != node2:
            if G.number_of_edges(node1, node2) > 0:
                numEdges[(node1, node2)] = G.number_of_edges(node1, node2)
# print(numEdges)

for node in nodes:
    if G.number_of_edges(node, node) > 0:
        for i in range(G.number_of_edges(node, node)):
            G.remove_edge(node, node)

# print(G.edges())
G = nx.transitive_reduction(G)
# for x in nodes:
#     for y in nodes:
#         if x!=y:
#             max = -1
#             maxIndex = -1
#             paths = nx.all_simple_paths(G, x, y)
#             for i in range(len(paths)):
#                 if len(paths[i]) > max:
#                     max = len(paths[i])
#                     maxIndex = i
#             for i in range(len(paths)):
#                 if i != maxIndex:
#                     for j in range(len(paths[i])-1):
#                         # for k in range(k, len(paths[i])):
#                         G.remove_edge(paths[j], paths[j+1])
예제 #16
0
def weak_unmerge(subclusters, ssi_threshold):
    """Removes merged clusters using directed graphs approach.
    
    Parameters
    ----------
    subclusters : array, shape = [n_subclusters]
         Set of n_subclusters subsets of the data.
    
    ssi_threshold : float
         Minimum Simpson's similarity between two subclusters for edge drawing.
         
    Returns
    -------
    unmerged_subclusters : array, shape = [n_unmerged_subclusters]
         Subset of subclusters of size n_unmerged_subclusters.
    
    """
    indices = np.asarray([i for i in range(len(subclusters))])
    di_graph_ = nx.DiGraph()
    di_graph_.add_nodes_from(indices)
    
    chains = []
    while (len(indices) > 0):
        index = indices[0]
        chain = [index]
        indices = indices[1:]
        for i in range(len(indices)):
            if ssi(subclusters[index], subclusters[indices[i]]) >= ssi_threshold:
                chain.append(indices[i])
        chains.append(chain)

    for chain in chains:
        for i in range(len(chain)-1):
            di_graph_.add_edge(chain[0], chain[i+1])

    di_graph_ = nx.transitive_reduction(di_graph_)
    
    potentially_unmerged_subclusters = []
    unmerged_subclusters = []
    merged_subclusters = []
    for i in range(len(subclusters)):
        if di_graph_.in_degree(i) <= 1:
            potentially_unmerged_subclusters.append(list(subclusters[i]))
        elif di_graph_.in_degree(i) > 1:
            merged_subclusters.append(list(subclusters[i]))
        else:
            pass
        
    for subcluster in potentially_unmerged_subclusters:
        for merged_subcluster in merged_subclusters:
            if len(subcluster) >= len(merged_subcluster):
                if ssi(subcluster, merged_subcluster) >= ssi_threshold:
                    break
        else:
            continue
        unmerged_subclusters.append(subcluster)
    
    unmerged_subclusters = unique(unmerged_subclusters)
    unmerged_subclusters = np.asarray(unmerged_subclusters)
    
    return unmerged_subclusters
예제 #17
0
def prepare_agraph():
    node_str = request.values.get('nodes')
    model = request.values.get('model', 'default')
    info = models.get(model, default_model)
    nodes, errors = info.nodes(node_str, report_errors=True)
    if errors:
        flash(
            'Die folgenden zentralen Knoten wurden nicht gefunden: ' +
            ', '.join(errors), 'warning')
    context = request.values.get('context', False)
    abs_dates = request.values.get('abs_dates', False)
    extra, errors = info.nodes(request.values.get('extra', ''),
                               report_errors=True)
    if errors:
        flash(
            'Die folgenden Pfadziele wurden nicht gefunden: ' +
            ', '.join(errors), 'warning')
    induced_edges = request.values.get('induced_edges', False)
    ignored_edges = request.values.get('ignored_edges', False)
    direct_assertions = request.values.get('assertions', False)
    paths_wo_timeline = request.values.get('paths_wo_timeline', False)
    no_edge_labels = request.values.get('no_edge_labels', False)
    tred = request.values.get('tred', False)
    nohl = request.values.get('nohl', False)
    syn = request.values.get('syn', False)
    inscriptions = request.values.get('inscriptions', False)
    order = request.values.get('order', False)
    collapse = request.values.get('collapse', False)
    direction = request.values.get('dir', 'LR').upper()
    central_paths = request.values.get('central_paths', 'all').lower()
    if direction not in {'LR', 'RL', 'TB', 'BT'}:
        direction = 'LR'
    if nodes:
        g = info.subgraph(*nodes,
                          context=context,
                          abs_dates=abs_dates,
                          paths=extra,
                          keep_timeline=True,
                          paths_between_nodes=central_paths,
                          paths_without_timeline=paths_wo_timeline,
                          direct_assertions=direct_assertions,
                          include_syn_clusters=syn,
                          include_inscription_clusters=inscriptions)
        if induced_edges:
            g = info.base.subgraph(g.nodes).copy()
        if not ignored_edges or tred:
            g = remove_edges(
                g,
                lambda u, v, attr: attr.get('ignore', False) and not attr.get(
                    'kind', '') == 'temp-syn')
        if not syn:
            g = remove_edges(
                g, lambda u, v, attr: attr.get('kind', None) == "temp-syn")
        if tred:
            g = remove_edges(g, lambda u, v, attr: attr.get('delete', False))
        if tred:
            if nx.is_directed_acyclic_graph(g):
                reduction = nx.transitive_reduction(g)
                g = g.edge_subgraph([
                    (u, v, k)
                    for u, v, k, _ in expand_edges(g, reduction.edges)
                ])
            else:
                flash('Cannot produce DAG – subgraph is not acyclic!?',
                      'error')
        g = simplify_timeline(g)
        if collapse:
            g = collapse_parallel_edges(g)
        g.add_nodes_from(nodes)
        if order:
            g = info.order_graph(g)
        agraph = write_dot(g,
                           target=None,
                           highlight=None if nohl else nodes,
                           edge_labels=not no_edge_labels)
        agraph.graph_attr['basename'] = ",".join([
            str(node.filename.stem if hasattr(node, 'filename') else node)
            for node in nodes
        ])
        agraph.graph_attr['bgcolor'] = 'transparent'
        agraph.graph_attr['rankdir'] = direction
        if order:
            agraph.graph_attr['ranksep'] = '0.2'
        return agraph
    else:
        raise NoNodes('No nodes in graph')
def group_substructures(mols,
                        patterns=None,
                        mol_instantiator=unsanitized_mol_from_smiles,
                        pattern_instantiator=mol_from_smarts,
                        matcher=has_query_query_match,
                        reduce=True):

    try:
        import networkx as nx
    except ImportError:
        raise ImportError('Please install networkx')

    # Instantiate mols and their "pattern" representation
    # Must document that, when already provided Chem.Mol objects, instantiators usually are no-ops
    if pattern_instantiator is not None:
        patterns = list(to_rdkit_mols(mols, pattern_instantiator))
    if mol_instantiator is not None:
        mols = list(to_rdkit_mols(mols, mol_instantiator))

    if patterns is None:
        patterns = mols

    # Sort substructures by decreasing number of atoms
    num_atoms = [mol.GetNumAtoms() for mol in mols]
    descending_number_of_atoms_order = np.argsort(num_atoms)[::-1]

    representative = [None] * len(mols)  # For duplicates
    graph = nx.DiGraph()  # Directed graph, if (p1, p2) on it,

    # Nasty stuff that would not happen if cheminformatics were logical
    # noinspection PyUnusedLocal
    has_equal_nonequal = has_cycles = False

    for p1, p2 in combinations(descending_number_of_atoms_order, 2):
        p2_in_p1, p1_in_p2 = matcher(mols[p1], patterns[p2]), matcher(
            mols[p2], patterns[p1])
        representative[p1] = representative[p1] or p1
        representative[p2] = representative[p2] or p2
        if p2_in_p1 and p1_in_p2:
            representative[p2] = representative[p1]
        elif p2_in_p1:
            if num_atoms[p1] == num_atoms[p2] and not has_equal_nonequal:
                has_equal_nonequal = True
                logger.info(
                    'mindblowingly, with equal number of atoms, one contains the other but not viceversa'
                )
            graph.add_edge(representative[p1], representative[p2])
        elif p1_in_p2:
            if num_atoms[p1] == num_atoms[p2] and not has_equal_nonequal:
                has_equal_nonequal = True
                logger.info(
                    'mindblowingly, with equal number of atoms, one contains the other but not viceversa'
                )
            graph.add_edge(representative[p2], representative[p1])
        else:
            graph.add_node(representative[p1])
            graph.add_node(representative[p2])

    # Cycles?
    try:
        nx.find_cycle(graph)
        has_cycles = True
        logger.info('containment graph has cycles')
    except nx.NetworkXNoCycle:
        has_cycles = False

    if reduce:
        graph = nx.transitive_reduction(graph)

    groups = list(nx.weakly_connected_components(graph))
    # noinspection PyCallingNonCallable
    roots = [node for node, degree in graph.in_degree() if 0 == degree]
    # noinspection PyCallingNonCallable
    leaves = [node for node, degree in graph.out_degree() if 0 == degree]

    return graph, groups, representative, roots, leaves, num_atoms, has_cycles, has_equal_nonequal
예제 #19
0
        else:
            print "PROBLEMATIC CYCLE", cycle

    #nx.write_graphml(subg, "subgraph_%s.graphml" % ccc)

    # add here widths for each node
    for nod in dch.nodes():
        dch.node[nod]["width"] = gg_orig.node[":".join(nod.split(":")[:-1]) + "_2"]["width"]


    print dch.nodes(), "NODES OF DCH"


    is_dag = nx.dag.is_directed_acyclic_graph(dch)
    if is_dag:
        dch2 = nx.transitive_reduction(dch)
        toremove = []
        for x, y in dch.edges():
            if not dch2.has_edge(x, y):
                toremove.append((x, y))
        transitively_removed_edges[id(dch)] = {}
        for x, y in toremove:
            transitively_removed_edges[id(dch)][(x, y)] = max(0, dch[x][y]["dist"])
            dch.remove_edge(x, y)
    else:
        while True:
            try:
                cycle = list(nx.find_cycle(dch))
            except nx.exception.NetworkXNoCycle:
                cycle = []
            if cycle == []:
예제 #20
0
def agraph(nodeinfo: NodeInput = Depends(),
           context: bool = False,
           abs_dates: bool = False,
           induced_edges: bool = False,
           ignored_edges: bool = False,
           assertions: bool = False,
           extra: str = '',
           paths_wo_timeline: bool = False,
           tred: bool = False,
           nohl: bool = False,
           syn: bool = False,
           inscriptions: bool = False,
           order: bool = False,
           collapse: bool = False,
           dir: Direction = Direction.LR,
           central_paths: CentralPaths = CentralPaths.ALL,
           no_edge_labels: bool = False) -> _AGraphInfo:
    """
    Creates the actual graph.
    """

    # retrieve the nodes by string
    model = models[nodeinfo.model]
    nodes, unknown_nodes = model.nodes(nodeinfo.nodes, report_errors=True)
    extra_nodes, unknown_extra_nodes = model.nodes(extra, report_errors=True)

    # extract the basic subgraph
    g = model.subgraph(*nodes, context=context, abs_dates=abs_dates, paths=extra_nodes,
                       paths_without_timeline=paths_wo_timeline,
                       paths_between_nodes=central_paths.value,
                       direct_assertions=assertions, include_syn_clusters=syn,
                       include_inscription_clusters=inscriptions)

    if induced_edges:  # TODO refactor into model.subgraph?
        g = model.base.subgraph(g.nodes).copy()

    # now remove ignored or conflicting edges, depending on the options
    if not ignored_edges or tred:
        g = remove_edges(g, lambda u, v, attr: attr.get('ignore', False) and not attr.get('kind', '') == 'temp-syn')
    if not syn:
        g = remove_edges(g, lambda u, v, attr: attr.get('kind', None) == 'temp-syn')
    if tred:
        g = remove_edges(g, lambda u, v, attr: attr.get('delete', False))

        # now actual tred
        if nx.is_directed_acyclic_graph(g):
            reduction = nx.transitive_reduction(g)
            g = g.edge_subgraph([(u, v, k) for u, v, k, _ in expand_edges(g, reduction.edges)])
        else:
            raise HTTPException(500, dict(
                    error="not-acyclic",
                    msg='Cannot produce transitive reduction – the subgraph is not acyclic after removing conflict edges!',
                    dot=write_dot(g, target=None, highlight=nodes).to_string()))

    g = simplify_timeline(g)

    if collapse:
        g = collapse_parallel_edges(g)

    # make sure the central nodes are actually in the subgraph. They might theoretically have fallen out by
    # one of the reduction operations before, e.g., the edge subgraph required for tred
    g.add_nodes_from(nodes)

    # now we have our subgraph. All following operations are visualisation focused
    if order:
        g = model.order_graph(g)  # adjusts weights & invisible edges to make graphviz layout the nodes in
        # a straight line according to the order of the model
    agraph = write_dot(g, target=None, highlight=None if nohl else nodes, edge_labels=not no_edge_labels)
    basename = ",".join(
            [str(node.filename.stem if hasattr(node, 'filename') else node) for node in nodes])
    agraph.graph_attr['bgcolor'] = 'transparent'
    agraph.graph_attr['rankdir'] = dir.value
    if order:
        agraph.graph_attr['ranksep'] = '0.2'
    return _AGraphInfo(agraph, nodes, extra_nodes, unknown_nodes + unknown_extra_nodes, basename)