def threshold_multilayer_network(M,
                                 intralayer_density=0.05,
                                 interlayer_density=0.05,
                                 replace_interlayer_weights_with_ones=True):
    layers = sorted(list(M.get_layers()))
    assert (layers == list(range(min(layers), max(layers) + 1)))
    #pairwise_layersets = zip(*(range(len(layers))[ii:] for ii in range(2)))

    # threshold first layer in network:
    first_layer = layers[0]
    ordered_edges = sorted(pn.subnet(M, M.iter_nodes(), [first_layer]).edges,
                           key=lambda w: w[4],
                           reverse=True)
    assert (all([w[4] is not np.nan for w in ordered_edges]))
    max_edges = (len(list(M.iter_nodes(first_layer))) *
                 (len(list(M.iter_nodes(first_layer))) - 1)) / 2.0
    thresholded_edges = ordered_edges[
        0:int(np.floor(max_edges * intralayer_density))]
    for edge in ordered_edges:
        if edge in thresholded_edges:
            M[edge[0], edge[1], edge[2], edge[3]] = 1
        else:
            M[edge[0], edge[1], edge[2], edge[3]] = 0

    # threshold subsequent layers and intralayer networks between current layer and the previous one
    del (layers[0])
    for layer in layers:
        ordered_edges = sorted(pn.subnet(M, M.iter_nodes(), [layer]).edges,
                               key=lambda w: w[4],
                               reverse=True)
        assert (all([w[4] is not np.nan for w in ordered_edges]))
        max_edges = (len(list(M.iter_nodes(layer))) *
                     (len(list(M.iter_nodes(layer))) - 1)) / 2.0
        thresholded_edges = ordered_edges[
            0:int(np.floor(max_edges * intralayer_density))]
        for edge in ordered_edges:
            if edge in thresholded_edges:
                M[edge[0], edge[1], edge[2], edge[3]] = 1
            else:
                M[edge[0], edge[1], edge[2], edge[3]] = 0
        interlayer_ordered_edges = sorted([
            edge
            for edge in pn.subnet(M, M.iter_nodes(), [layer, layer - 1]).edges
            if edge[2] != edge[3]
        ],
                                          key=lambda w: w[4],
                                          reverse=True)
        interlayer_max_edges = len(list(M.iter_nodes(layer))) * len(
            list(M.iter_nodes(layer - 1)))
        interlayer_thresholded_edges = interlayer_ordered_edges[
            0:int(np.floor(interlayer_max_edges * interlayer_density))]
        for edge in interlayer_ordered_edges:
            if edge in interlayer_thresholded_edges:
                if replace_interlayer_weights_with_ones:
                    M[edge[0], edge[1], edge[2], edge[3]] = 1
            else:
                M[edge[0], edge[1], edge[2], edge[3]] = 0

    return M
def subtrahend(orbit1, orbit2, nets, auts, invs, allowed_aspects='all'):
    '''
    Returns the subtrahend for orbit2 in orbit equations (the value that is
    substracted in the upper part of the binomial coefficient on theleft sides
    of the orbit equation)
    
    Parameters
    ----------
    orbit1, orbit2 : tuple (n_nodes, net_index, node_orbit_index)
        These can extract from the output of orbit_equations, n_nodex, net_index,
        and node_orbit_index should match those of parameter auts
    nets : dict (key: n_nodes, value: list of networks)
        graphlets, as produced by graphlets
    auts : dd (key: (n_nodes, net_index, node), value: node_orbit_index)
        as produced by automorphism_orbits
    
    Returns
    -------
    sub_max : int
    
    Notes
    -----
    assumes orbit2 has at most the same number of nodes as orbit1
    '''

    sub_max = 0
    n_nodes1 = orbit1[0]
    n_nodes2 = orbit2[0]
    net1 = nets[n_nodes1][orbit1[1]]
    net2 = nets[n_nodes2][orbit2[1]]
    the_node = orbit1[2]
    layers = net1.slices[1]
    nodes = net1.slices[0] - set([the_node])
    for partition in partitions_with_remainder(nodes, n_nodes2 - 1):
        sub = 0
        for nodes_s in partition:
            nodes_g = set(nodes_s) | set([the_node])
            sub_net = pymnet.subnet(net1, nodes_g, layers)
            ci_sub = str(
                pymnet.get_complete_invariant(sub_net,
                                              allowed_aspects=allowed_aspects))
            if ci_sub in invs and invs[ci_sub] == (n_nodes2, orbit2[1]):
                iso = pymnet.get_isomorphism(sub_net,
                                             net2,
                                             allowed_aspects=allowed_aspects)
                if the_node in iso[0]:
                    iso_node = iso[0][the_node]
                else:
                    iso_node = the_node

                if auts[n_nodes2, orbit2[1], iso_node] == orbit2[2]:
                    sub += 1

        sub_max = max(sub_max, sub)

    return sub_max
def add_possible_edges(both_orbit_nodes, net):
    '''
    Adds all possible edge combinations between nodes from different orbits
    
    Parameters
    ----------
    both_orbit_nodes : list of nodes
        nodes that belong to both orbits
    net : network
    
    Returns
    -------
    nets : list of networks
    
    Notes
    -----
    assumes negative nodes belong to the same orbit
    returned networks can be isomorphic
    '''

    nodes = net.slices[0] - set(both_orbit_nodes)
    nodes1 = []
    nodes2 = []
    for node in nodes:
        if node < 0:
            nodes2.append(node)
        else:
            nodes1.append(node)

    edges = list(itertools.product(nodes1, nodes2))
    n_edges = len(edges)

    net_nodes = net.slices[0]
    net_layers = net.slices[1]
    layer_combs = layer_combinations(net_layers)

    nets = []

    for n_e in range(1, n_edges + 1):
        for edge_comb in itertools.combinations(edges, n_e):
            edge_layers = [layer_combs] * n_e
            for edge_layer_comb in itertools.product(*edge_layers):
                new_net = pymnet.subnet(net, net_nodes, net_layers)
                for edge_i in range(n_e):
                    edge = edge_comb[edge_i]
                    for layer in edge_layer_comb[edge_i]:
                        new_net[edge[0], edge[1], layer] = 1

                nets.append(new_net)

    return nets
def save_subnet_with_complete_invariant(M, nodes, layers, writefile,
                                        examples_dict, allowed_aspects):
    if allowed_aspects != 'all':
        # relabel layers to 0...k
        sorted_layers = tuple(sorted(layers))
        relabeling = dict()
        for ii, orig_layer in enumerate(sorted_layers):
            relabeling[orig_layer] = ii
        subnet = pn.transforms.relabel(pn.subnet(M, nodes, layers),
                                       layerNames=relabeling)
        complete_invariant = pn.get_complete_invariant(
            subnet, allowed_aspects=allowed_aspects)
    else:
        subnet = pn.subnet(M, nodes, layers)
        complete_invariant = pn.get_complete_invariant(
            subnet, allowed_aspects=allowed_aspects)
    writefile.write(
        str(nodes) + '\t' + str(layers) + '\t' + str(complete_invariant) +
        '\n')
    if isinstance(examples_dict, dict):
        if complete_invariant not in examples_dict:
            examples_dict[complete_invariant] = subnet
    return
def orbit_counts(n,
                 node0,
                 net,
                 nets,
                 orbits,
                 auts,
                 invs,
                 orbit_list,
                 allowed_aspects='all'):
    '''
    Computes the orbit counts for node0 in net
    
    Parameters
    ----------
    node0 : node
    net : network
    nets : dict (key: n_nodes, value: list of networks)
        graphlets
    orbits : dd (key: (node, orbit), value: count)
        dictionary where the counts will be stored
    auts : dd (key: (n_nodes, net_index, node), value: node)
        automorphism orbits
    invs : dict (key: str(complete invariant), value: tuple(n_nodes, net_index in nets))
        complete invariants of the graphlets
    orbit_list : list of orbits
    allowed_aspects : list, string
        the aspects that can be permutated when computing isomorphisms
    '''

    for orbit in orbit_list:
        orbits[node0, orbit] = 0

    layers = net.slices[1]
    node_sets = touching_orbit_nodes(node0, net, n)
    for nodes_s in node_sets:
        sub_net = pymnet.subnet(net, nodes_s, layers)
        ci_sub = str(
            pymnet.get_complete_invariant(sub_net,
                                          allowed_aspects=allowed_aspects))
        i = invs[ci_sub][1]
        n_nodes = invs[ci_sub][0]
        nw = nets[n_nodes][i]
        iso = pymnet.get_isomorphism(sub_net,
                                     nw,
                                     allowed_aspects=allowed_aspects)
        if node0 in iso[0]:
            orbits[node0, (n_nodes, i, auts[n_nodes, i, iso[0][node0]])] += 1
        else:
            orbits[node0, (n_nodes, i, auts[n_nodes, i, node0])] += 1
def add_subnet_to_aggregated_dict(M, nodes, layers, aggregated_dict,
                                  examples_dict, allowed_aspects):
    # modifies aggregated_dict directly
    sorted_layers = tuple(sorted(layers))
    if allowed_aspects != 'all':
        # relabel layers to 0...k
        relabeling = dict()
        for ii, orig_layer in enumerate(sorted_layers):
            relabeling[orig_layer] = ii
        subnet = pn.transforms.relabel(pn.subnet(M, nodes, layers),
                                       layerNames=relabeling)
        complete_invariant = pn.get_complete_invariant(
            subnet, allowed_aspects=allowed_aspects)
    else:
        subnet = pn.subnet(M, nodes, layers)
        complete_invariant = pn.get_complete_invariant(
            subnet, allowed_aspects=allowed_aspects)
    aggregated_dict[complete_invariant][
        sorted_layers] = aggregated_dict[complete_invariant].get(
            sorted_layers, 0) + 1
    if isinstance(examples_dict, dict):
        if complete_invariant not in examples_dict:
            examples_dict[complete_invariant] = subnet
    return
Example #7
0
def default_check_reqs(network,
                       nodelist,
                       layerlist,
                       sizes,
                       intersections,
                       nnodes=None,
                       nlayers=None,
                       intersection_type="strict"):
    u"""Checks whether a multilayer induced subgraph of the form [nodelist][layerlist] is connected,
    whether it has no empty layers or nodes, and whether it fulfills the given sizes and intersections
    requirements. Works on one-aspect multilayer networks.
    
    Parameters
    ----------
    network : MultilayerNetwork
        A one-aspect multilayer network containing the induced subgraph.
    nodelist : list of node names
        The nodes in the induced subgraph.
    layerlist : list of layer names
        The layers in the induced subgraph.
    sizes : list of ints > 0
        How many nodes should be on each layer of an acceptable induced subgraph.
        One integer for each layer of an acceptable subgraph.
    intersections : list of ints >= 0 or Nones
        How many nodes should be shared between sets of layers in an acceptable
        induced subgraph. If an entry in the list is None, any number of shared
        nodes is accepted. The order of the intersections is taken to follow the
        order of layers in sizes, with two-layer intersections being listed first,
        then three-layer intersections, etc. For more details, see section
        "Constructing the requirements".
    nnodes : int
        How many nodes an acceptable subgraph should have. If not provided, it
        will be calculated based on the sizes and intersections parameters.
        Required if there are Nones in intersections or if intersection_type
        is not "strict". If you cannot guarantee the correctness of this
        number, do not use this parameter. Can be used in scripts to bypass
        calculating the correct number of nodes based on the sizes and intersections
        parameters.
    nlayers : int
        How many layers an acceptable subgraph should have. If not provided, it
        will be calculated based on the sizes and intersections parameters.
        Required if there are Nones in intersections.
        If you cannot guarantee the correctness of this number, do not use this
        parameter. Can be used in scripts to bypass calculating the correct number
        of layers based on the sizes and intersections parameters.
    intersection_type : string, "strict" or "less_or_equal"
        If intersection_type is "strict", all intersections must be exactly equal
        to entries in the intersections parameter. If intersection_type is
        "less_or_equal", an intersection is allowed to be less than or equal to the corresponding
        entry in the intersections parameter. Usage is case-sensitive.
        
    Returns
    -------
    True if the requirements are fulfilled, the induced subgraph has no empty nodes
    or layers, and the induced subgraph is connected. False otherwise.
    
    Empty nodes or layers
    ---------------------
    The phrase 'does not contain any empty layers or nodes' means that for each
    layer, there is at least one nodelayer in the induced subgraph, and that for
    each node, there is at least one nodelayer in the induced subgraph.
    In other words, each node in the nodelist and each layer in the layerlist
    appears at least once as the node identity or the layer identity, respectively,
    among the nodelayers present in the induced subgraph.
    
    Constructing the requirements
    -----------------------------
    The sizes list contains the desired number of nodes on each layer in any order.
    This means that the layers in the actual found sub-network can be in any order.
    However, the order of entries in sizes determines the order of entries in intersections.
    The intersections list is constructed as follows:
        First, think of each size in the size list as having a specific role:
        the first entry in sizes corresponds to layer role A, the second to role
        B, the third to role C, and so on. This order determines how intersections
        in the intersections list are interpreted when checking if the requirements
        are fulfilled.
        
        Then, construct the intersections list so that first all size-two intersections
        are listed, then size-three intersections, and so on, until the intersection
        between all layers is reached. The entry for each intersection can be an integer
        or None. An integer signifies the number of nodes in the intersection (the cardinality
        of the intersection set), and it can be followed strictly or less strictly, depending
        on the intersection_type parameter. The value None signifies that the intersection in
        question can have any size in an acceptable induced subgraph. If intersections
        contains Nones, the nnodes and nlayers parameters must also be specified.
        
        The order of listing size-n intersections is such that the closer the role is
        to the beginning of the size list, the later it is iterated over. More specifically,
        this is the order that itertools.combinations() uses to iterate. Since we signify
        the roles by letters A, B, C and so on, this means that the intersections are
        listed in "alphabetical order" within each size category.
    
    For example, suppose we have a length-four sizes list. Now, we think of the first
    size entry as layer (role) A, the second as layer B, the third as layer C, and the fourth
    as layer D. The intersections list is then assumed to contain the intersections
    in the following order:
    
    intersections = [A∩B, A∩C, A∩D, B∩C, B∩D, C∩D, A∩B∩C, A∩B∩D, A∩C∩D, B∩C∩D, A∩B∩C∩D]
    
    When checking whether the size and intersection requirements are fulfilled,
    each possible set of role assginments to the actual layers is checked. If even
    one is suitable, the function returns True.
    
    Continuing from the example above, if the actual induced subgraph has layers [X,Y,Z,W],
    all possible role assignment combinations are checked (X takes role from the set {A,B,C,D}, Y 
    takes role from {A,B,C,D} minus {role(X)}, Z takes role from {A,B,C,D} minus {role(X),role(Y)}, W
    takes role from {A,B,C,D} minus {role(X),role(Y),role(Z)}). The role assignment is iterated
    until an acceptable role assignment is found -- at which point the function returns True --
    or until all possible role assignments have been considered without success -- at which
    point the function returns False.
    
    This also means that the orderings of the [nodelist] and [layerlist] of the induced subgraph
    to be tested do not matter (i.e calling this function with nodelist = [1,2] and layerlist = ['X','Y']
    gives the exact same return value as nodelist = [2,1] and layerlist = ['Y','X'], etc.).
    
    Using Nones
    -----------
    If we only care about the cardinalities of some specific intersections, we can set
    the rest to None. For example, calling
    
    >>> default_check_reqs(some_network,some_nodelist,some_layerlist,[1,2,3],[None,None,2,None],nnodes=4,nlayers=3)
    
    means that the algorithm will find the induced subgraphs which are connected, have 4 nodes and
    3 layers, have no empty layers or nodes, have one node on one layer, two nodes on another layer,
    three nodes on the third layer, and have a cardinality-2 (size-2) intersection between the layer
    that has two nodes and the layer that has three nodes (with no constraints on the cardinalities
    of the other intersections).
    
    When using Nones, nnodes and nlayers have to be specified, since if all intersection
    cardinalities are not unambiguous, the nnodes and nlayers cannot be calculated based
    on the sizes and intersections alone. It is up to the user to provide nnodes and nlayers
    that are sensible (for example, nnodes cannot sensibly be more than the sum of all sizes).
    It is also up to the user to not give contradictory requirements.
    Technically, only nnodes would be required, but both have to be given to make the function
    call more explicit and more intuitive to read.
    
    Example
    -------
    Suppose we have the multilayer network N:
    
    (1,'X')----(2,'X')    (3,'X')
                  |
                  |
               (2,'Y')
             
    where (a,b) are nodelayer tuples with a = node identity and b = layer identity.
    Now,
    
    >>> default_check_reqs(N,[1,2],['X','Y'],[1,2],[1])
    
    returns True, as do
    
    >>> default_check_reqs(N,[2,1],['Y','X'],[1,2],[1])
    
    and
    
    >>> default_check_reqs(N,[1,2],['Y','X'],[2,1],[1])
    
    (and naturally so do also all the other ways of defining the same induced subgraph
    and the same requirements).
    
    On the other hand,
    
    >>> default_check_reqs(N,[2,3],['X','Y'],[1,2],[1])
    
    returns False, since the induced subgraph is not connected.
    """
    if intersection_type == "strict":
        if nnodes != None and nlayers != None:
            req_nodelist_len = nnodes
            req_layerlist_len = nlayers
        else:
            if None in intersections:
                raise TypeError(
                    "Please provide nnodes and nlayers when including Nones in intersections"
                )
            try:
                req_nodelist_len, req_layerlist_len = default_calculate_required_lengths(
                    sizes, intersections)
            except AssertionError:
                raise

    elif intersection_type == "less_or_equal":
        assert nnodes != None, "Please provide nnodes when using less_or_equal intersection type"
        if nlayers != None:
            req_nodelist_len = nnodes
            req_layerlist_len = nlayers
        else:
            if None in intersections:
                raise TypeError(
                    "Please provide nnodes and nlayers when including Nones in intersections"
                )
            req_nodelist_len = nnodes
            try:
                _, req_layerlist_len = default_calculate_required_lengths(
                    sizes, intersections)
            except AssertionError:
                raise

    else:
        raise TypeError(
            "Please specify either strict or less_or_equal as intersection type"
        )

    assert len(nodelist) == req_nodelist_len, "Wrong number of nodes"
    assert len(layerlist) == req_layerlist_len, "Wrong number of layers"
    assert all(i >= 1 for i in sizes), "Inappropriate sizes"
    induced_graph = pymnet.subnet(network, nodelist, layerlist)
    try:
        graph_is_connected = nx.is_connected(
            pymnet.transforms.get_underlying_graph(induced_graph))
    except nx.networkx.NetworkXPointlessConcept:
        return False
    if graph_is_connected:

        # check for empty nodes or layers
        nls = set(induced_graph.iter_node_layers())
        for layer in layerlist:
            no_nodelayers = True
            for node in nodelist:
                if (node, layer) in nls:
                    no_nodelayers = False
                    break
            if no_nodelayers:
                return False
        for node in nodelist:
            no_nodelayers = True
            for layer in layerlist:
                if (node, layer) in nls:
                    no_nodelayers = False
                    break
            if no_nodelayers:
                return False

        d = dict()  # keys: layers, values: nodes
        for nodelayer in nls:
            d.setdefault(nodelayer[1], [])
            d[nodelayer[1]].append(nodelayer[0])
        if len(d) != req_layerlist_len:
            return False
        d_isect = dict()  # layer intersections, roles 0,1,2,...
        indexer = 0
        for jj in range(2, len(layerlist) + 1):
            for combination in list(
                    itertools.combinations(list(range(0, len(layerlist))),
                                           jj)):
                d_isect[combination] = intersections[indexer]
                indexer = indexer + 1
        for permutation in list(itertools.permutations(layerlist)):
            # index in permutation determines role of layer
            goto_next_perm = False
            # check all intersections and sizes
            for ii in range(1, len(layerlist) + 1):  # A, B etc. AB, AC etc.
                for combination in list(itertools.combinations(
                        permutation, ii)):  # try all role combinations
                    assert len(
                        combination
                    ) >= 1, "Empty combination list, this shouldn't happen"
                    if len(combination) == 1:  # check sizes
                        if len(d[combination[0]]) != sizes[permutation.index(
                                combination[0])]:
                            goto_next_perm = True
                            break
                    elif len(combination) > 1:
                        rolelist = []
                        nodeset = set(d[combination[0]])
                        for layer in combination:
                            rolelist.append(permutation.index(layer))
                            nodeset = nodeset & set(d[layer])
                        rolelist.sort()
                        if intersection_type == "strict":
                            if d_isect[tuple(rolelist)] != None and len(
                                    nodeset) != d_isect[tuple(rolelist)]:
                                goto_next_perm = True
                                break
                        elif intersection_type == "less_or_equal":
                            if d_isect[tuple(rolelist)] != None and len(
                                    nodeset) > d_isect[tuple(rolelist)]:
                                goto_next_perm = True
                                break
                if goto_next_perm:
                    break
            if not goto_next_perm:
                return True
    return False
Example #8
0
def relaxed_check_reqs(network, nodelist, layerlist):
    """Checks whether a multilayer induced subgraph of the form [nodelist][layerlist] is connected
    and does not contain any empty layers or nodes. Works on one-aspect multilayer networks.
    See section Details for more information.
    
    Parameters
    ----------
    network : MultilayerNetwork
        A one-aspect multilayer network containing the induced subgraph.
    nodelist : list of node names
        The nodes in the induced subgraph.
    layerlist : list of layer names
        The layers in the induced subgraph.
        
    Returns
    -------
    True if the induced subgraph is connected and does not contain empty layers
    or nodes, False otherwise.
    
    Details
    -------
    The phrase 'does not contain any empty layers or nodes' means that for each
    layer, there is at least one nodelayer in the induced subgraph, and that for
    each node, there is at least one nodelayer in the induced subgraph.
    In other words, each node in the nodelist and each layer in the layerlist
    appears at least once as the node identity or the layer identity, respectively,
    among the nodelayers present in the induced subgraph.
    
    Example
    -------
    Suppose we have the multilayer network N:
    
    (1,'X')----(2,'X')
                  |
                  |
               (2,'Y')
               
    Calling
    
    >>> relaxed_check_reqs(N,[1,2],['X','Y'])
    
    returns True, but calling
    
    >>> relaxed_check_reqs(N,[1,2],['Y'])
    
    returns False, because node 1 is empty.
    """
    induced_graph = pymnet.subnet(network, nodelist, layerlist)
    try:
        graph_is_connected = nx.is_connected(
            pymnet.transforms.get_underlying_graph(induced_graph))
    except nx.networkx.NetworkXPointlessConcept:
        return False
    if graph_is_connected:
        nls = set(induced_graph.iter_node_layers())
        for layer in layerlist:
            no_nodelayers = True
            for node in nodelist:
                if (node, layer) in nls:
                    no_nodelayers = False
                    break
            if no_nodelayers:
                return False
        for node in nodelist:
            no_nodelayers = True
            for layer in layerlist:
                if (node, layer) in nls:
                    no_nodelayers = False
                    break
            if no_nodelayers:
                return False
        return True
    return False
Example #9
0
def sample_multilayer_subgraphs_esu(network,results,sizes=None,intersections=None,nnodes=None,nlayers=None,p=None,seed=None,intersection_type="strict",copy_network=True,custom_check_function=None):
    u"""A one-aspect multilayer version of the Rand-EnumerateSubgraphs (Rand-ESU) algorithm
    introduced by Wernicke [1].
    
    Uniformly samples induced subgraphs of the form [nodelist][layerlist] which
    fulfill the given requirements. Each subgraph is sampled at probability
    product(p), where p is the parameter p. If all entries in p are 1, all
    such induced subgraphs in the network are found.
    
    Parameters
    ----------
    Multiple parameters can be given by the user, some of which are always required,
    some of which are sometimes required, and some of which are mutually
    exclusive. There are multiple functionalities to this function, and the choice
    is done based on the parameters passed by the user. For a description of all
    of them, see section Usage.
    
    network : MultilayerNetwork
        The multilayer network to be analyzed.
    results : list or callable
        The method of outputting the found induced subgraphs. If a list, then
        the induced subgraphs are appended to it as ([nodelist],[layerlist]) tuples.
        The results list or the [nodelist] or [layerlist] lists are not guaranteed to
        be in any specific order.
        If a callable, when an acceptable induced subgraph is found, this callable
        is called with the argument ([nodelist],[layerlist]) (that is, one argument
        which is a tuple of two lists). The callable should therefore take only one
        required parameter in the form of a tuple. If you want to pass more parameters
        to the callable, do so via e.g. an anonymous function.
    sizes : list of ints > 0
        How many nodes should be on each layer of an acceptable induced subgraph.
        One integer for each layer of an acceptable subgraph.
    intersections : list of ints >= 0 or Nones, or int
        How many nodes should be shared between sets of layers in an acceptable
        induced subgraph. If list, if an entry in the list is None, any number of shared
        nodes is accepted. The order of the intersections is taken to follow the
        order of layers in sizes, with two-layer intersections being listed first,
        then three-layer intersections, etc. If int, then this is taken to mean
        the intersection between ALL layers, and the other intersections can be anything.
        If this is an int with value x, it is equivalent to being a list with
        [None,None,...,None,x].
        For more details, see section "Constructing the requirements" in the documentation
        of the function default_check_reqs.
    nnodes : int
        How many nodes an acceptable subgraph should have. If not provided and
        sizes and intersections are provided, it
        will be calculated based on the sizes and intersections parameters.
        Required if there are Nones in intersections or if intersection_type
        is not "strict". If you cannot guarantee the correctness of this
        number, do not use this parameter.
    nlayers : int
        How many layers an acceptable subgraph should have. If not provided and
        sizes and intersections are provided,
        it will be calculated based on the sizes and intersections requirements.
        If you cannot guarantee the correctness of this number, do not use this
        parameter.
    p : list of floats 0 <= p <= 1
        List of sampling probabilities at each depth. If None, p = 1 for each
        depth is used. The probability of sampling a given induced subgraph is
        the product of the elements of p.
        It is up to the user to provide a p of correct length to
        match the depth at which desired induced subgraphs are found.
        If you know how many nodes and layers an acceptable induced subgraph should
        have (nnodes and nlayers, respectively), you can calculate the length of p by:
        len(p) = nnodes - 1 + nlayers - 1 + 1.
        This formula follows from the fact that finding an induced subgraph starts
        from a nodelayer (the + 1), and then each node and each layer have to be added
        one at a time to the nodelist and layerlist, respectively
        (nnodes - 1 and nlayers - 1, respectively). Starting from a nodelayer means
        that both nodelist and layerlist are of length 1 when the expansion of the
        subgraph is started, hence the - 1's.
    seed : int, str, bytes or bytearray
        Seed for Rand-ESU.
    intersection_type : string, "strict" or "less_or_equal"
        If intersection_type is "strict", all intersections must be exactly equal
        to entries in the intersections parameter. If intersection_type is
        "less_or_equal", an intersection is allowed to be less than or equal to the corresponding
        entry in the intersections parameter. Usage is case-sensitive.
    copy_network : boolean
        Determines whether the network is copied at the beginning of execution. If True (default),
        the network is copied and the copy is modified during the execution (the original
        network is not modified). If False, the network is not copied, and the network is
        NOT modified during the execution.
        The copying takes more memory but results in faster execution times - the default
        value (True) is the recommended setting. The modification of the copy does not affect
        the edges in the induced subgraphs that are passed to the check function. During the
        execution, if this parameter is True, as starting nodelayers are iterated through in their numerical order,
        after a nodelayer has been iterated over all edges leading to it are removed (at this
        point, it is impossible to reach the nodelayer from subsequent starting nodelayers in
        any case). Therefore, if you use a custom_check_function whose return value depends
        also on the edges OUTSIDE the induced subgraph to be tested, set this parameter to False.
    custom_check_function : callable or None
        If not None, this will be used to determine whether an induced subgraph is okay or not
        (instead of one of the built-in check functions).
        The algorithm finds induced subgraphs which have the given nnodes and nlayers, and which
        have a path spanning the induced subgraph (but are not necessarily connected). The algorithm
        then passes these to the check function, which determines whether the subgraph is acceptable
        or not. The arguments that are passed to your custom check function are the network, the nodelist
        of the induced subgraph, and the layerlist of the induced subgraph (three parameters, in this
        order). Your check function should therefore accept exactly three parameters. If you want to pass
        more parameters to the check function, do so via e.g. an anonymous function.
        If copy_network is True, the passed network is the copy of the network, which might have
        edges removed OUTSIDE of the induced subgraph (the edges inside the induced subgraph are identical
        to the original network's). The function should return True or False (the subgraph is acceptable
        or not acceptable, respectively). When this parameter is not None, you must also specify nnodes
        and nlayers.
        
    Usage
    -----
    There are multiple functionalities built-in, and determining which is used is
    done by checking which parameters have been given by the user.
    
    If you want to find induced subgraphs (ISs) that have a specified number of nodes
    on each layer and have specific intersections between layers, provide:
        - network
        - results
        - sizes
        - intersections as list without Nones
        
    If you want to find ISs that have a specific number of nodes on each layer and
    have some specific intersections between layers, and some intersections that can
    be of any cardinality, provide:
        - network
        - results
        - sizes
        - intersections as list with Nones in the elements where intersection cardinalities can be anything (even all elements can be Nones)
        - nnodes
        
    If you want to find ISs that have a specific number of nodes on each layer and
    have intersections that have at most specific cardinalities, provide:
        - network
        - results
        - sizes
        - intersections as list without Nones
        - nnodes
        - intersection_type = "less_or_equal"
        
    If you want to find ISs that have a specific number of nodes on each layer and
    have some specific intersections that have at most specific cardinalities, and some
    intersections that can be of any cardinality, provide:
        - network
        - results
        - sizes
        - intersections as list with Nones in the elements where intersection cardinalities can be anything (even all intersections can be Nones)
        - nnodes
        - intersection_type = "less_or_equal"
        
    If you want to find ISs that have a specific number of nodes on each layer and
    have a specific intersection between ALL layers (the other intersections can be anything),
    provide:
        - network
        - results
        - sizes
        - nnodes
        - intersections as int
        
    If you want to find ISs that have a specific number of nodes and a specific number
    of layers, provide:
        - network
        - results
        - nnodes
        - nlayers
        
    If you want to define your own function to determine when an IS is acceptable,
    provide:
        - network
        - results
        - nnodes
        - nlayers
        - custom_check_function
    
    For all of the above uses, if you don't want to find all ISs but only sample a
    portion of them, also provide:
        - p
    
    Of the above uses, the first five use the default_check_reqs function for checking
    subgraph validity, the sixth uses the relaxed_check_reqs function, and the seventh
    uses the user-supplied checking function.
    
    Example
    -------
    Suppose we have the multilayer network N:
    
    (1,'X')----(2,'X')----(3,'X')
                  |
                  |
               (2,'Y')
    
    where (a,b) are nodelayer tuples with a = node identity and b = layer identity.
    After calling
    
    >>> results = []
    >>> sample_multilayer_subgraphs_esu(N,results,[2,1],[1])
    
    the results list looks like [([1,2],['X','Y']),([2,3],['X','Y'])] (or some other
    order of tuples and [nodelist] and [layerlist] inside the tuples, since the output
    is not guaranteed to be in any specific order).
    
    After calling
    
    >>> results = []
    >>> sample_multilayer_subgraphs_esu(N,results,nnodes=3,nlayers=1)
    
    the results list looks like [([1,2,3],['X'])] (or again, some other ordering).
    
    Further reading
    ---------------
    The documentation of the functions default_check_reqs, default_calculate_required_lengths
    and relaxed_check_reqs offer more insight into what are considered acceptable induced subgraphs
    in different cases in the functionalities described in the Usage section. You should read these
    if you are not sure what you want to do or how to do it after reading this documentation.
    
    References
    ----------
    [1] "A Faster Algorithm for Detecting Network Motifs", S. Wernicke, WABI. Vol. 3692, pp. 165-177. Springer 2005.
    """
    if copy_network == True:
        network_copy = pymnet.subnet(network,network.get_layers(aspect=0),network.get_layers(aspect=1),newNet=pymnet.MultilayerNetwork(aspects=1,fullyInterconnected=False))
    else:
        network_copy = network
        
    if seed == None:
        random.seed()
    else:
        random.seed(seed)

    check_function = None
    assert (sizes != None and intersections != None) or (nnodes != None and nlayers != None), "Please provide either sizes and intersections or nnodes and nlayers"
    if custom_check_function != None:
        assert nnodes != None and nlayers != None, "Please provide nnodes and nlayers when using a custom check function"
        req_nodelist_len = nnodes
        req_layerlist_len = nlayers
        check_function = custom_check_function
    if sizes != None and intersections != None and check_function == None:
        if isinstance(intersections,list):
            if None in intersections:
                assert nnodes != None, "Please provide nnodes if including Nones in intersections"
                req_nodelist_len = nnodes
                req_layerlist_len = len(sizes)
            else:
                if intersection_type == "strict":
                    assert nnodes == None and nlayers == None, "You cannot provide both sizes and intersections and nnodes and nlayers, if intersections is a list"
                    req_nodelist_len, req_layerlist_len = default_calculate_required_lengths(sizes,intersections)
                elif intersection_type == "less_or_equal":
                    assert nnodes != None and nlayers == None, "please provide nnodes (and not nlayers) if using less_or_equal intersection type"
                    req_nodelist_len = nnodes
                    req_layerlist_len = len(sizes)
            check_function = lambda x,y,z: default_check_reqs(x,y,z,sizes,intersections,req_nodelist_len,req_layerlist_len,intersection_type)
        elif isinstance(intersections,int):
            assert intersections >= 0, "Please provide nonnegative common intersection size"
            assert nnodes != None and nlayers == None, "When requiring only common intersection size, please provide nnodes (and not nlayers)"
            req_nodelist_len = nnodes
            req_layerlist_len = len(sizes)
            intersections_as_list = [None]*(2**len(sizes)-len(sizes)-1)
            intersections_as_list[-1] = intersections
            check_function = lambda x,y,z: default_check_reqs(x,y,z,sizes,intersections_as_list,req_nodelist_len,req_layerlist_len,intersection_type)
    if nnodes != None and nlayers != None and check_function == None:
        assert sizes == None and intersections == None, "You cannot provide both sizes and intersections and nnodes and nlayers, if intersections is a list"
        req_nodelist_len = nnodes
        req_layerlist_len = nlayers
        assert isinstance(req_nodelist_len,int) and isinstance(req_layerlist_len,int), "Non-integer nnodes or nlayers"
        assert req_nodelist_len > 0 and req_layerlist_len > 0, "Nonpositive nnodes or nlayers"
        check_function = relaxed_check_reqs
    assert check_function != None, "Please specify a valid combination of parameters to determine method of subgraph validity checking"
        
    if p == None:
        p = [1] * (req_nodelist_len-1 + req_layerlist_len-1 + 1)
        
    depth = 0
    numberings = dict()
    inverse_numberings = dict()
    for index,nodelayer in enumerate(network_copy.iter_node_layers()):
        numberings[nodelayer] = index
    for nodelayer in numberings:
        inverse_numberings[numberings[nodelayer]] = nodelayer

    for indexnumber in range(len(numberings)):
        v = inverse_numberings[indexnumber]
        if random.random() < p[depth]:
            start_node = v[0]
            start_layer = v[1]
            V_extension_nodes = set()
            V_extension_layers = set()
            for neighbor in network_copy[v]:
                if numberings[neighbor] > numberings[v]:
                    no_node_conflicts = True
                    no_layer_conflicts = True
                    node = neighbor[0]
                    layer = neighbor[1]
                    if (node,start_layer) in numberings and numberings[(node,start_layer)] < numberings[v]:
                        no_node_conflicts = False
                    if (start_node,layer) in numberings and numberings[(start_node,layer)] < numberings[v]:
                        no_layer_conflicts = False
                    if (node != start_node
                        and no_node_conflicts
                        and node not in V_extension_nodes):
                        V_extension_nodes.add(node)
                    if (layer != start_layer
                        and no_layer_conflicts
                        and layer not in V_extension_layers):
                        V_extension_layers.add(layer)
            _extend_subgraph(network_copy,[start_node],[start_layer],check_function,V_extension_nodes,V_extension_layers,numberings,v,req_nodelist_len,req_layerlist_len,depth+1,p,results)
        if copy_network == True:
            for neighbor in list(network_copy[v]):
                network_copy[neighbor][v] = 0
def graphlets(n, layers, n_l=None, couplings=None, allowed_aspects="all"):
    '''
    Generate graphlets up to n nodes
    
    Parameters
    ----------
    n : int
        maximum number of nodes
    layers : list of layers
    n_l : int
        Number of layers in the generated graphlets, can be smaller than or equal
        to the number of elements in layers
    couplings : list, str, tuple, None, MultilayerNetwork
        Parameter determining how the layers are coupled, i.e. what 
        inter-layer edges are present.
    allowed_aspects : list, string
        the aspects that can be permutated when computing isomorphisms
    
    Returns
    -------
    nets : dict (key: n_nodes, value: list of MultiplexNetwork objects)
        graphlets
    invariants : dict (key: str(complete invariant), value: tuple(index in 'nets': n_nodes, index in the list of multiplex networks))
        complete invariants of the graphlets, the values can be used to match the graphlets in 'nets'
    '''
    if n_l == None:
        n_l = len(layers)

    nets = {}
    invariants = {}
    nets2 = []

    for net_layers in itertools.combinations(layers, n_l):
        layer_combs = layer_combinations(net_layers)
        for layer_comb in layer_combs:
            net = pymnet.MultiplexNetwork(couplings=couplings,
                                          fullyInterconnected=True)
            for layer in layer_comb:
                net[0, 1, layer] = 1

            for layer in net_layers:
                net.add_layer(layer)

            ci = pymnet.get_complete_invariant(net,
                                               allowed_aspects=allowed_aspects)
            ci_s = str(ci)
            if not ci_s in invariants:
                invariants[ci_s] = (2, len(nets2))
                nets2.append(net)

    nets[2] = nets2

    for i in range(2, n):
        nets_i = nets[i]
        nets_i_1 = []
        for net in nets_i:
            net_nodes = net.slices[0]
            net_layers = list(net.slices[1])
            net_layers.sort()
            layer_combs = layer_combinations(net_layers)
            for n_n in range(1, i + 1):
                for node_comb in itertools.combinations(range(i), n_n):
                    node_layers = [layer_combs] * n_n
                    for node_layer_comb in itertools.product(*node_layers):
                        new_net = pymnet.subnet(net, net_nodes, net_layers)
                        for node_i in range(n_n):
                            node = node_comb[node_i]
                            for layer in node_layer_comb[node_i]:
                                new_net[node, i, layer] = 1

                        for layer in net_layers:
                            new_net.add_layer(layer)
                        # check if isomorphic with a previous graph & add only if not isomorphic
                        ci = pymnet.get_complete_invariant(
                            new_net, allowed_aspects=allowed_aspects)
                        ci_s = str(ci)
                        if not ci_s in invariants:
                            invariants[ci_s] = (i + 1, len(nets_i_1))
                            nets_i_1.append(new_net)

        nets[i + 1] = nets_i_1

    return nets, invariants
def coefficient_help(nodes,
                     the_node,
                     both_orbit_nodes,
                     orbit1,
                     orbit2,
                     net,
                     nets,
                     auts,
                     invs,
                     allowed_aspects='all'):
    '''
    helper function for coefficient
    '''

    coef = 0

    nodes_a = net.slices[0]
    layers = net.slices[1]

    net1 = nets[orbit1[0]][orbit1[1]]
    net2 = nets[orbit2[0]][orbit2[1]]
    n_nodes1 = len(net1.slices[0])

    for node_comb in itertools.combinations(nodes,
                                            n_nodes1 - len(both_orbit_nodes)):
        nodes_s2 = nodes_a - set(node_comb)
        nodes_s1 = (nodes_a - nodes_s2) | set(both_orbit_nodes)
        sub1 = pymnet.subnet(net, nodes_s1, layers)
        sub2 = pymnet.subnet(net, nodes_s2, layers)
        ci_sub1 = str(
            pymnet.get_complete_invariant(sub1,
                                          allowed_aspects=allowed_aspects))
        ci_sub2 = str(
            pymnet.get_complete_invariant(sub2,
                                          allowed_aspects=allowed_aspects))
        if not ci_sub1 in invs or not ci_sub2 in invs:
            continue
        if invs[ci_sub1] == (orbit1[0],
                             orbit1[1]) and invs[ci_sub2] == (orbit2[0],
                                                              orbit2[1]):
            iso1 = pymnet.get_isomorphism(sub1,
                                          net1,
                                          allowed_aspects=allowed_aspects)
            iso2 = pymnet.get_isomorphism(sub2,
                                          net2,
                                          allowed_aspects=allowed_aspects)
            if the_node in iso1[0]:
                iso_node1 = iso1[0][the_node]
            else:
                iso_node1 = the_node

            if the_node in iso2[0]:
                iso_node2 = iso2[0][the_node]
            else:
                iso_node2 = the_node

            if auts[orbit1[0], orbit1[1],
                    iso_node1] == orbit1[2] and auts[orbit2[0], orbit2[1],
                                                     iso_node2] == orbit2[2]:
                coef += 1

    return coef
def merge_nodes(both_orbit_nodes, net, allowed_aspects='all'):
    '''
    Merges nodes from different orbits,
    each returned network has different combination of nodes merged, 
    merges only one pair of nodes
    
    Parameters
    ----------
    both_orbit_nodes : list of nodes
    net : network
    allowed_aspects : list, string
        the aspects that can be permutated when computing isomorphisms
    
    Returns
    -------
    new_nets_and_nodes : list of tuples (net, both_orbit_nodes)
    
    Notes
    -----
    assumes negative nodes belong to the same orbit
    works for both multiplex and multilayer networks (1 aspect)
    '''

    nodes_a = net.slices[0]
    nodes = nodes_a - set(both_orbit_nodes)
    nodes1 = []
    nodes2 = []
    for node in nodes:
        if node < 0:
            nodes2.append(node)
        else:
            nodes1.append(node)

    layers = net.slices[1]
    new_nets_and_nodes = []
    new_nets = []
    new_invs = set()

    for comb in itertools.product(nodes1, nodes2):
        node1 = comb[0]
        node2 = comb[1]
        l1 = set()
        l2 = set()
        for nl in net.iter_node_layers():
            if nl[0] == node1:
                l1.add(nl[1])
            elif nl[0] == node2:
                l2.add(nl[1])

        if not (l1 <= l2 and l1 >= l2):  #nodes not present in the same layers
            continue

        nodes_s1 = both_orbit_nodes + [node1]
        nodes_s2 = both_orbit_nodes + [node2]
        sub1 = pymnet.subnet(net, nodes_s1, layers)
        sub2 = pymnet.subnet(net, nodes_s2, layers)
        if not pymnet.is_isomorphic(
                sub1, sub2, allowed_aspects=[0]):  #not same links to the_node
            continue

        nodes_s = nodes_a - set([node2])
        new_net = pymnet.subnet(net, nodes_s, layers)
        for layer in layers:
            node2_o = net.__getitem__((node2, layer))
            for neighbor in node2_o.iter_total():
                if neighbor[0] != node2:
                    new_net[node1, neighbor[0], layer, neighbor[1]] = 1

        ci = str(
            pymnet.get_complete_invariant(new_net,
                                          allowed_aspects=allowed_aspects))
        if not ci in new_invs:
            new_nets.append(new_net)
            new_nets_and_nodes.append((new_net, both_orbit_nodes + [node1]))
            new_invs.add(ci)

    return new_nets_and_nodes
def combine_orbits(orbit1, orbit2, nets, allowed_aspects='all'):
    '''
    Combines orbits orbit1 and orbit2
    
    Parameters
    ----------
    orbit1, orbit2: tuple (n_nodes, net_index, node)
    nets: dict (key: n_nodes, value: list of networks)
        graphlets
    allowed_aspects : list, string
        the aspects that can be permutated when computing isomorphisms
        
    Returns
    -------
    new_nets: list of networks, None
        networks obtained by combining the orbits (no links added, no merging of nodes),
        returns None if orbits cannot be combined (graphlets have different layers)
        
    Notes
    -----
    atm works only with node-layer isomorphisms, now also vertex isomorphisms
    node1 will be the_node
    '''

    net1 = nets[orbit1[0]][orbit1[1]]
    net2 = nets[orbit2[0]][orbit2[1]]
    node1 = orbit1[2]
    node2 = orbit2[2]

    nodeNames = {}
    layerNames = {}

    nodeNames[node2] = node1
    nodes2 = net2.slices[0]
    n_nodes2 = len(nodes2)
    nodes1 = net1.slices[0]
    layers1 = net1.slices[1]
    layers2 = net2.slices[1]
    if layers1 != layers2:
        return None

    for (nodeName, newName) in zip(
            nodes2 - set([node2]),
            range(-2, -n_nodes2 - 1,
                  -1)):  # could be changed to range(-1,-n_nodes2,-1)?
        nodeNames[nodeName] = newName

    new_nets = []
    new_invs = set()
    new_net = pymnet.subnet(net1, nodes1, layers1)
    net2_r = pymnet.transforms.relabel(net2, nodeNames, layerNames)
    for e in net2_r.edges:
        if e[0] != e[1]:
            new_net[e[0], e[1], e[2], e[3]] = e[4]

    new_nets.append(new_net)
    ci = str(
        pymnet.get_complete_invariant(new_net,
                                      allowed_aspects=allowed_aspects))
    new_invs.add(ci)

    if allowed_aspects != [0]:
        for perm in itertools.permutations(layers2, len(layers2)):
            for i in range(len(perm)):
                layerNames[perm[i - 1]] = perm[i]

            new_net = pymnet.subnet(net1, nodes1, layers1)
            net2_r = pymnet.transforms.relabel(net2, nodeNames, layerNames)
            for e in net2_r.edges:
                if e[0] != e[1]:
                    new_net[e[0], e[1], e[2], e[3]] = e[4]

            ci = str(
                pymnet.get_complete_invariant(new_net,
                                              allowed_aspects=allowed_aspects))
            if not ci in new_invs:
                new_nets.append(new_net)
                new_invs.add(ci)

    return new_nets
def orbit_counts_all(net,
                     n,
                     nets,
                     invs,
                     auts,
                     orbit_list,
                     allowed_aspects='all'):
    '''
    Computes the orbit counts for all the nodes in net
    
    Parameters
    ----------
    net : network
    n : int
        max number of nodes
    nets : dict (key: n_nodes, value: list of networks)
        Graphlets, as produced by graphlets
    invs : dict (key: str(complete invariant), value: tuple(n_nodes, net_index in nets))
        complete invariants of the graphlets, as produced by graphlet
    auts : dd (key: (n_nodes, net_index, node), value: node)
        automorphisms, as produced by automorphism_orbits
    orbit_list : list of orbits
        as returned by ordered_orbit_list
    allowed_aspects : list, string
        the aspects that can be permutated when computing isomorphisms
        
    Returns
    -------
    orbits : dd (key: (node, orbit), value: count)
        Orbit counts for all the nodes
    
    Notes
    -----
    Should be faster than orbit_counts if the counts are computed for all 
    (/ most of) the nodes
    '''

    nodes = net.slices[0]
    layers = net.slices[1]

    orbits = dd()

    for node in nodes:
        for orbit in orbit_list:
            orbits[node, orbit] = 0

    processed = set()

    for node0 in nodes:
        node_sets = set()
        set_p = set([frozenset([node0])])
        for _ in range(n - 1):
            set_c = set()
            for p in set_p:
                for node_p in p:
                    for layer in layers:
                        node_o = net.__getitem__((node_p, layer))
                        for neighbor in node_o.iter_total():
                            if not (neighbor[0] in p
                                    or neighbor[0] in processed):
                                set_n = frozenset(p | set([neighbor[0]]))
                                set_c.add(set_n)

            node_sets = node_sets.union(set_c)
            set_p = set_c.copy()

        processed.add(node0)
        for node_comb in node_sets:
            sub_net = pymnet.subnet(net, node_comb, layers)
            ci_sub = str(
                pymnet.get_complete_invariant(sub_net,
                                              allowed_aspects=allowed_aspects))
            if ci_sub not in invs:
                pymnet.draw(sub_net)
                print(len(invs))
            i = invs[ci_sub][0]
            j = invs[ci_sub][1]
            nw = nets[i][j]
            iso = pymnet.get_isomorphism(sub_net,
                                         nw,
                                         allowed_aspects=allowed_aspects)
            for node in node_comb:
                if node in iso[0]:
                    orbits[node, (i, j, auts[i, j, iso[0][node]])] += 1
                else:
                    orbits[node, (i, j, auts[i, j, node])] += 1

        for layer in layers:
            nls = list(net[node0, :, layer])
            for node1 in nls:
                net[node0, node1[0], layer] = 0  #remove edges

    return orbits