U
    tdv                     @   sH   d dl mZ d dlmZ ddlmZ ddlmZmZ G dd de	Z
dS )	   )	_c_leiden)(LinearResolutionParameterVertexPartition    )
namedtuple)logsqrtc                   @   s,  e Zd ZdZdd Zedd Zejdd Zedd Zejd	d Zed
d Z	e	jdd Z	edd Z
e
jdd Z
edd Zejdd Zedd Zejdd Zedd Zejdd Zdd Zd0ddZd1dd Zd2d!d"Zd3d#d$Zd4d%d&Zd5d'd(Zdd)d* d+d,d-d+fd.d/ZdS )6	Optimisera$   Class for doing community detection using the Leiden algorithm.

  The Leiden algorithm [1] derives from the Louvain algorithm [2]. The Louvain
  algorithm has an elegant formulation. It consists of two phases: (1) move
  nodes between communities; (2) aggregate the graph. It then repeats these
  phases on the aggregate graph. The Leiden algorithm consists of three phases:
  (1) move nodes; (2) refine communities; (3) aggregate the graph based on the
  refinement. The Louvain algorithm can lead to arbitrarily badly connected
  communities, whereas the Leiden algorithm guarantees communities are
  well-connected. In fact, it converges towards a partition in which all
  subsets of all communities are locally optimally assigned. Finally, the
  Leiden algorithm is also much faster, because it relies on a fast local move
  routine.

  There is one, rather technical, difference with the original Leiden
  algorithm. This implementation provides a general optimisation
  routine for any quality function. There is one aspect of the original Leiden
  algorithm that cannot be translated well in this framework: when merging
  subcommunities in the refinement procedure, it does not consider whether they
  are sufficiently well connected to the rest of the community. This
  implementation therefore does not guarantee subpartition :math:`\gamma`-density.
  However, all other guarantees still hold:

  After each iteration
    1. :math:`\gamma`-separation
    2. :math:`\gamma`-connectivity

  After a stable iteration
    3. Node optimality
    4. Some subsets are locally optimally assigned

  Asymptotically
    5. Uniform :math:`\gamma`-density
    6. Subset optimality

  The optimiser class provides a number of different methods for optimising a
  given partition. The overall optimisation procedure
  :func:`optimise_partition` calls either :func:`move_nodes` or
  :func:`merge_nodes` (which is controlled by :attr:`optimise_routine`) then
  aggregates the graph and repeats the same procedure. Possible, indicated by
  :attr:`refine_partition` the partition is refined before aggregating, meaning
  that subsets of communities are considered for moving around. Which routine
  is used for the refinement is indicated by :attr:`refine_routine`. For
  calculating the actual improvement of moving a node (corresponding a subset
  of nodes in the aggregate graph), the code relies on
  :func:`~VertexPartition.MutableVertexPartition.diff_move` which provides
  different values for different methods (e.g. modularity or CPM). The default
  settings are consistent with the Leiden algorithm. By not doing the
  refinement, you essentially get the Louvain algorithm with a fast local move.
  Finally, the Optimiser class provides a routine to construct a
  :func:`resolution_profile` on a resolution parameter.

  References
  ----------

  .. [1] Traag, V.A., Waltman. L., Van Eck, N.-J. (2018). From Louvain to
         Leiden: guaranteeing well-connected communities.
         `arXiv:1810.08473 <https://arxiv.org/abs/1810.08473>`_

  .. [2] Blondel, V. D., Guillaume, J.-L., Lambiotte, R., & Lefebvre, E.
         (2008). Fast unfolding of communities in large networks. Journal of
         Statistical Mechanics: Theory and Experiment, 10008(10), 6.
         `10.1088/1742-5468/2008/10/P10008 <https://doi.org/10.1088/1742-5468/2008/10/P10008>`_
  c                 C   s   t  | _dS )z Create a new Optimiser object N)r   Z_new_Optimiser
_optimiserself r   L/home/sam/Atlas/atlas_env/lib/python3.8/site-packages/leidenalg/Optimiser.py__init__G   s    zOptimiser.__init__c                 C   s   t | jS )a   Determine how alternative communities are considered for moving
    a node for *optimising* a partition.

    Nodes will only move to alternative communities that improve the given
    quality function.

    Notes
    -------
    This attribute should be set to one of the following values

    * :attr:`leidenalg.ALL_NEIGH_COMMS`
      Consider all neighbouring communities for moving.

    * :attr:`leidenalg.ALL_COMMS`
      Consider all communities for moving. This is especially useful in the
      case of negative links, in which case it may be better to move a node to
      a non-neighbouring community.

    * :attr:`leidenalg.RAND_NEIGH_COMM`
      Consider a random neighbour community for moving. The probability to
      choose a community is proportional to the number of neighbours a node has
      in that community.

    * :attr:`leidenalg.RAND_COMM`
      Consider a random community for moving. The probability to choose a
      community is proportional to the number of nodes in that community.
    )r   Z_Optimiser_get_consider_commsr	   r
   r   r   r   consider_commsM   s    zOptimiser.consider_commsc                 C   s   t | j| d S N)r   Z_Optimiser_set_consider_commsr	   r   valuer   r   r   r   l   s    c                 C   s   t | jS )a   Determine how alternative communities are considered for moving
    a node when *refining* a partition.

    Nodes will only move to alternative communities that improve the given
    quality function.

    Notes
    -------
    This attribute should be set to one of the following values

    * :attr:`leidenalg.ALL_NEIGH_COMMS`
      Consider all neighbouring communities for moving.

    * :attr:`leidenalg.ALL_COMMS`
      Consider all communities for moving. This is especially useful in the
      case of negative links, in which case it may be better to move a node to
      a non-neighbouring community.

    * :attr:`leidenalg.RAND_NEIGH_COMM`
      Consider a random neighbour community for moving. The probability to
      choose a community is proportional to the number of neighbours a node has
      in that community.

    * :attr:`leidenalg.RAND_COMM`
      Consider a random community for moving. The probability to choose a
      community is proportional to the number of nodes in that community.
    )r   Z$_Optimiser_get_refine_consider_commsr	   r
   r   r   r   refine_consider_commsr   s    zOptimiser.refine_consider_commsc                 C   s   t | j| d S r   )r   Z$_Optimiser_set_refine_consider_commsr	   r   r   r   r   r      s    c                 C   s   t | jS )a   Determine the routine to use for *optimising* a partition.

    Notes
    -------
    This attribute should be set to one of the following values

    * :attr:`leidenalg.MOVE_NODES`
      Use :func:`move_nodes`.

    * :attr:`leidenalg.MERGE_NODES`
      Use :func:`merge_nodes`.
    )r   Z_Optimiser_get_optimise_routiner	   r
   r   r   r   optimise_routine   s    zOptimiser.optimise_routinec                 C   s   t | j| d S r   )r   Z_Optimiser_set_optimise_routiner	   r   r   r   r   r      s    c                 C   s   t | jS )a   Determine the routine to use for *refining* a partition.

    Notes
    -------
    This attribute should be set to one of the following values

    * :attr:`leidenalg.MOVE_NODES`
      Use :func:`move_nodes`.

    * :attr:`leidenalg.MERGE_NODES`
      Use :func:`merge_nodes`.
    )r   Z_Optimiser_get_refine_routiner	   r
   r   r   r   refine_routine   s    zOptimiser.refine_routinec                 C   s   t | j| d S r   )r   Z_Optimiser_set_refine_routiner	   r   r   r   r   r      s    c                 C   s   t | jS )z; boolean: if ``True`` refine partition before aggregation. )r   Z_Optimiser_get_refine_partitionr	   r
   r   r   r   refine_partition   s    zOptimiser.refine_partitionc                 C   s   t | j| d S r   )r   Z_Optimiser_set_refine_partitionr	   r   r   r   r   r      s    c                 C   s   t | jS )zV boolean: if ``True`` consider also moving nodes to an empty community
    (default). )r   Z'_Optimiser_get_consider_empty_communityr	   r
   r   r   r   consider_empty_community   s    z"Optimiser.consider_empty_communityc                 C   s   t | j| d S r   )r   Z'_Optimiser_set_consider_empty_communityr	   r   r   r   r   r      s    c                 C   s   t | jS )z Constrain the maximal community size.

    By default (zero), communities can be of any size. If this is set to a
    positive integer value, then communities will be constrained to be at most
    this total size.
    )r   Z_Optimiser_get_max_comm_sizer	   r
   r   r   r   max_comm_size   s    zOptimiser.max_comm_sizec                 C   s&   |dk rt d| t| j| d S )Nr   znegative max_comm_size: %s)
ValueErrorr   Z_Optimiser_set_max_comm_sizer	   r   r   r   r   r      s    c                 C   s   t | j| dS )z Set the random seed for the random number generator.

    Parameters
    ----------
    value
      The integer seed used in the random number generator
    N)r   Z_Optimiser_set_rng_seedr	   r   r   r   r   set_rng_seed   s    zOptimiser.set_rng_seed   Nc                 C   sh   d}d}||k p|dk }|r\t j| j|j|d}||7 }|d7 }|dk rR|dk}q||k }q|  |S )a   Optimise the given partition.

    Parameters
    ----------
    partition
      The :class:`~VertexPartition.MutableVertexPartition` to optimise.

    n_iterations : int
      Number of iterations to run the Leiden algorithm. By default, 2 iterations
      are run. If the number of iterations is negative, the Leiden algorithm is
      run until an iteration in which there was no improvement.

    is_membership_fixed: list of bools or None
      Boolean list of nodes that are not allowed to change community. The
      length of this list must be equal to the number of nodes. By default
      (None) all nodes can change community during the optimization.

    Returns
    -------
    float
      Improvement in quality function.

    Examples
    --------

    >>> G = ig.Graph.Famous('Zachary')
    >>> optimiser = la.Optimiser()
    >>> partition = la.ModularityVertexPartition(G)
    >>> diff = optimiser.optimise_partition(partition)

    or, fixing the membership of some nodes:

    >>> is_membership_fixed = [False for v in G.vs]
    >>> is_membership_fixed[4] = True
    >>> is_membership_fixed[6] = True
    >>> diff = optimiser.optimise_partition(partition, is_membership_fixed=is_membership_fixed)
    r   )is_membership_fixedr   )r   Z_Optimiser_optimise_partitionr	   
_partition_update_internal_membership)r   	partitionn_iterationsr   itrdiffcontinue_iterationdiff_incr   r   r   optimise_partition   s     '

zOptimiser.optimise_partitionc           
      C   s   |sdgt | }d}d}||k p(|dk }|rvt| jdd |D ||}||7 }|d7 }|dk rl|dk}q*||k }q*|D ]}	|	  qz|S )a   Optimise the given partitions simultaneously.

    Parameters
    ----------
    partitions
      List of :class:`~VertexPartition.MutableVertexPartition` layers to optimise.

    layer_weights
      List of weights of layers.

    is_membership_fixed: list of bools or None
      Boolean list of nodes that are not allowed to change community. The
      length of this list must be equal to the number of nodes. By default
      (None) all nodes can change community during the optimization.

    n_iterations : int
      Number of iterations to run the Leiden algorithm. By default, 2 iterations
      are run. If the number of iterations is negative, the Leiden algorithm is
      run until an iteration in which there was no improvement.

    Returns
    -------
    float
      Improvement in quality of combined partitions, see `Notes <#notes-multiplex>`_.


    .. _notes-multiplex:

    Notes
    -----

    This method assumes that the partitions are defined for graphs with the
    same vertices. The connections between the vertices may be different, but
    the vertices themselves should be identical. In other words, all vertices
    should have identical indices in all graphs (i.e. node `i` is assumed to be
    the same node in all graphs). The quality of the overall partition is
    simply the sum of the individual qualities for the various partitions,
    weighted by the layer_weight. If we denote by :math:`Q_k` the quality of
    layer :math:`k` and the weight by :math:`\lambda_k`, the overall quality
    is then

    .. math:: Q = \sum_k \lambda_k Q_k.

    This is particularly useful for graphs containing negative links. When
    separating the graph in two graphs, the one containing only the positive
    links, and the other only the negative link, by supplying a negative weight
    to the latter layer, we try to find relatively many positive links within a
    community and relatively many negative links between communities. Note that
    in this case it may be better to assign a node to a community to which it
    is not connected so that :attr:`consider_comms` may be better set to
    :attr:`leidenalg.ALL_COMMS`.

    Besides multiplex graphs where each node is assumed to have a single
    community, it is also useful in the case of for example multiple time
    slices, or in situations where nodes can have different communities in
    different slices. The package includes some special helper functions for
    using :func:`optimise_partition_multiplex` in such cases, where there is a
    conversion required from (time) slices to layers suitable for use in this
    function.

    See Also
    --------
    :func:`slices_to_layers`

    :func:`time_slices_to_layers`

    :func:`find_partition_multiplex`

    :func:`find_partition_temporal`

    Examples
    --------
    >>> G_pos = ig.Graph.SBM(100, pref_matrix=[[0.5, 0.1], [0.1, 0.5]], block_sizes=[50, 50])
    >>> G_neg = ig.Graph.SBM(100, pref_matrix=[[0.1, 0.5], [0.5, 0.1]], block_sizes=[50, 50])
    >>> optimiser = la.Optimiser()
    >>> partition_pos = la.ModularityVertexPartition(G_pos)
    >>> partition_neg = la.ModularityVertexPartition(G_neg)
    >>> diff = optimiser.optimise_partition_multiplex(
    ...                     partitions=[partition_pos, partition_neg],
    ...                     layer_weights=[1,-1])

    r   r   c                 S   s   g | ]
}|j qS r   )r   ).0r   r   r   r   
<listcomp>  s     z:Optimiser.optimise_partition_multiplex.<locals>.<listcomp>)lenr   Z'_Optimiser_optimise_partition_multiplexr	   r   )
r   Z
partitionsZlayer_weightsr    r   r!   r"   r#   r$   r   r   r   r   optimise_partition_multiplex0  s(    S


z&Optimiser.optimise_partition_multiplexc                 C   s.   |dkr| j }t| j|j||}|  |S )a   Move nodes to alternative communities for *optimising* the partition.

    Parameters
    ----------
    partition
      The partition for which to move nodes.

    is_membership_fixed: list of bools or None
      Boolean list of nodes that are not allowed to change community. The
      length of this list must be equal to the number of nodes. By default
      (None) all nodes can change community during the optimization.

    consider_comms
      If ``None`` uses :attr:`consider_comms`, but can be set to
      something else.

    Returns
    -------
    float
      Improvement in quality function.

    Notes
    -----
    When moving nodes, the function loops over nodes and considers moving the
    node to an alternative community. Which community depends on
    ``consider_comms``. The function terminates when no more nodes can be moved
    to an alternative community.

    See Also
    --------
    :func:`Optimiser.move_nodes_constrained`

    :func:`Optimiser.merge_nodes`

    Examples
    --------
    >>> G = ig.Graph.Famous('Zachary')
    >>> optimiser = la.Optimiser()
    >>> partition = la.ModularityVertexPartition(G)
    >>> diff = optimiser.move_nodes(partition)

    N)r   r   Z_Optimiser_move_nodesr	   r   r   r   r   r   r   r"   r   r   r   
move_nodes  s    +   zOptimiser.move_nodesc                 C   s0   |dkr| j }t| j|j|j|}|  |S )a   Move nodes to alternative communities for *refining* the partition.

    Parameters
    ----------
    partition
      The partition for which to move nodes.

    constrained_partition
      The partition within which we may move nodes.

    consider_comms
      If ``None`` uses :attr:`refine_consider_comms`, but can be set
      to something else.

    Returns
    -------
    float
      Improvement in quality function.

    Notes
    -----
    The idea is constrain the movement of nodes to alternative communities to
    another partition. In other words, if there is a partition ``P`` which we
    want to refine, we can then initialize a new singleton partition, and move
    nodes in that partition constrained to ``P``.

    See Also
    --------
    :func:`Optimiser.move_nodes`

    :func:`Optimiser.merge_nodes_constrained`

    Examples
    --------
    >>> G = ig.Graph.Famous('Zachary')
    >>> optimiser = la.Optimiser()
    >>> partition = la.ModularityVertexPartition(G)
    >>> diff = optimiser.optimise_partition(partition)
    >>> refine_partition = la.ModularityVertexPartition(G)
    >>> diff = optimiser.move_nodes_constrained(refine_partition, partition)

    N)r   r   Z!_Optimiser_move_nodes_constrainedr	   r   r   r   r   Zconstrained_partitionr   r"   r   r   r   move_nodes_constrained  s
    +z Optimiser.move_nodes_constrainedc                 C   s.   |dkr| j }t| j|j||}|  |S )an   Merge nodes for *optimising* the partition.

    Parameters
    ----------
    partition
      The partition for which to merge nodes.

    is_membership_fixed: list of bools or None
      Boolean list of nodes that are not allowed to change community. The
      length of this list must be equal to the number of nodes. By default
      (None) all nodes can change community during the optimization.

    consider_comms
      If ``None`` uses :attr:`consider_comms`, but can be set to
      something else.

    Returns
    -------
    float
      Improvement in quality function.

    Notes
    -----
    This function loop over all nodes once and tries to merge them with another
    community.  Merging in this case implies that a node will never be removed
    from a community, only merged with other communities.

    See Also
    --------
    :func:`Optimiser.move_nodes`

    :func:`Optimiser.merge_nodes_constrained`

    Examples
    --------
    >>> G = ig.Graph.Famous('Zachary')
    >>> optimiser = la.Optimiser()
    >>> partition = la.ModularityVertexPartition(G)
    >>> diff = optimiser.merge_nodes(partition)

    N)r   r   Z_Optimiser_merge_nodesr	   r   r   r*   r   r   r   merge_nodes  s    *   zOptimiser.merge_nodesc                 C   s0   |dkr| j }t| j|j|j|}|  |S )ap   Merge nodes for *refining* the partition.

    Parameters
    ----------
    partition
      The partition for which to merge nodes.

    constrained_partition
      The partition within which we may merge nodes.

    consider_comms
      If ``None`` uses :attr:`refine_consider_comms`, but can be set
      to something else.

    Returns
    -------
    float
      Improvement in quality function.

    Notes
    -----
    The idea is constrain the merging of nodes to another partition. In other
    words, if there is a partition ``P`` which we want to refine, we can then
    initialize a new singleton partition, and move nodes in that partition
    constrained to ``P``.

    See Also
    --------
    :func:`Optimiser.move_nodes_constrained`

    :func:`Optimiser.merge_nodes`

    Examples
    --------
    >>> G = ig.Graph.Famous('Zachary')
    >>> optimiser = la.Optimiser()
    >>> partition = la.ModularityVertexPartition(G)
    >>> diff = optimiser.optimise_partition(partition)
    >>> refine_partition = la.ModularityVertexPartition(G)
    >>> diff = optimiser.move_nodes_constrained(refine_partition, partition)

    N)r   r   Z"_Optimiser_merge_nodes_constrainedr	   r   r   r,   r   r   r   merge_nodes_constrained/  s
    +z!Optimiser.merge_nodes_constrainedc                 C   s   |   S r   bisect_value)pr   r   r   <lambda>e      zOptimiser.<lambda>r   gMbP?Fc
                    sz  dd }dd }d fdd	}t |ts0tdi }g }|| td	d
dg}|| f||||d d|
}||||d||d < || f||||d d|
}||||d||d < zddlm} |tdd}W n   d}Y nX |rB| }t||d  j	||d  j	 }|d dkrR|d dkrR|sRt
|d |d  }nt|d |d  }||kr||kr|d dkr|d dkr|st|d |d  }nt|d }||d |f |||d f ||kr|| |f|||d|
}||||d||< |dk	r6|d |j|dd ||| q|dk	rT|  || tdd | D dd dS )aI   Use bisectioning on the resolution parameter in order to construct a
    resolution profile.

    Parameters
    ----------
    graph
      The graph for which to construct a resolution profile.

    partition_type
      The type of :class:`~VertexPartition.MutableVertexPartition` used
      to find a partition (must support resolution parameters obviously).

    resolution_range
      The range of resolution values that we would like to scan.

    weights
      If provided, indicates the edge attribute to use as a weight.

    Returns
    -------
    list of :class:`~VertexPartition.MutableVertexPartition`
      A list of partitions for different resolutions.

    Other Parameters
    ----------------
    bisect_func
      The function used for bisectioning. For the methods currently
      implemented, this should usually not be altered.

    min_diff_bisect_value
      The difference in the value returned by the bisect_func below which the
      bisectioning stops (i.e. by default, a difference of a single edge does
      not trigger further bisectioning).

    min_diff_resolution
      The difference in resolution below which the bisectioning stops. For
      positive differences, the logarithmic difference is used by default, i.e.
      ``diff = log(res_1) - log(res_2) = log(res_1/res_2)``, for which ``diff >
      min_diff_resolution`` to continue bisectioning. Set the linear_bisection
      to true in order to use only linear bisectioning (in the case of negative
      resolution parameters for example, which can happen with negative
      weights).

    linear_bisection
      Whether the bisectioning will be done on a linear or on a logarithmic
      basis (if possible).

    number_iterations
      Indicates the number of iterations of the algorithm to run. If negative
      (or zero) the algorithm is run until a stable iteration.

    Examples
    --------
    >>> G = ig.Graph.Famous('Zachary')
    >>> optimiser = la.Optimiser()
    >>> profile = optimiser.resolution_profile(G, la.CPMVertexPartition,
    ...                                        resolution_range=(0,1))
    c                 S   s   |   D ]Z\}}|}|j|}|   D ](\}}|j||kr(|}|j|}q(||kr|| |< qtdd |   D dd d}t||dd  D ]\\}}	\}}
|	|
kr| |= q|   D ]\}}||j_qd S )Nc                 S   s   g | ]\}}||j fqS r   r0   )r&   respartr   r   r   r'     s     zHOptimiser.resolution_profile.<locals>.clean_stepwise.<locals>.<listcomp>c                 S   s   | d S )Nr   r   xr   r   r   r3     r4   zFOptimiser.resolution_profile.<locals>.clean_stepwise.<locals>.<lambda>keyr   )itemsr   qualitysortedzipresolution_parameter)bisect_valuesr5   bisectZbest_bisectZbest_qualityZres2Zbisect2Zbisect_listZres1Zv1Zv2r   r   r   clean_stepwise  s,    

z4Optimiser.resolution_profile.<locals>.clean_stepwisec                 S   s   |   D ]0\}}| | j||j|kr| | | |< q| | j|}|}|   D ]\}}|j||krV|}qV| | | |< d S r   )r;   r   r<   )r@   new_resr5   Zbisect_partZcurrent_qualityZbest_resr   r   r   ensure_monotonicity  s    z9Optimiser.resolution_profile.<locals>.ensure_monotonicityNc                    sD   ||fd|i|}d}|  |dkr@| k s6 dkr@|d7 }q|S )Nweightsr   r   )r%   )r   graphpartition_typerE   kwargsr   Zn_itrnumber_iterationsr   r   find_partition  s    
z4Optimiser.resolution_profile.<locals>.find_partitionzIBisectioning only works on partitions with a linear resolution parameter.BisectPartitionr   r1   r   )rF   rG   rE   r?   )r   r1   r   )tqdminf)totalg       @)rG   rE   r?   F)r?   refreshc                 s   s   | ]\}}|j V  qd S r   )r   )r&   r5   rA   r   r   r   	<genexpr>'  s     z/Optimiser.resolution_profile.<locals>.<genexpr>c                 S   s   | j S r   )r?   r7   r   r   r   r3   (  r4   z.Optimiser.resolution_profile.<locals>.<lambda>r9   )N)
issubclassr   AssertionErrorappendr   rM   floatpopabsr1   r   r   sumupdateZset_postfixcloser=   r;   )r   rF   rG   Zresolution_rangerE   Zbisect_funcZmin_diff_bisect_valueZmin_diff_resolutionZlinear_bisectionrJ   rH   rB   rD   rK   r@   Zstack_res_rangerL   r   rM   progresscurrent_rangeZdiff_bisect_valueZdiff_resolutionrC   r   rI   r   resolution_profile`  s    H


 
 
""
 



zOptimiser.resolution_profile)r   N)Nr   N)NN)N)NN)N)__name__
__module____qualname____doc__r   propertyr   setterr   r   r   r   r   r   r   r%   r)   r+   r-   r.   r/   r]   r   r   r   r   r      sX   @












	


:
j
2
1
2
5r   N) r   ZVertexPartitionr   collectionsr   mathr   r   objectr   r   r   r   r   <module>   s   