Source code for optiwindnet.clustering

# SPDX-License-Identifier: MIT
# https://gitlab.windenergy.dtu.dk/TOPFARM/OptiWindNet/

import math
from heapq import heappop, heappush

import networkx as nx

__all__ = ('clusterize',)


[docs] def clusterize(A: nx.Graph, capacity: int) -> tuple[list[set[int]], list[int]]: """Partition the terminals of A into one cluster per root. For the moment, it enforces the minimum number of feeders, i.e. ceil(T/capacity). The algorithm guarantees that the number of feeders for the entire location is not increased by the clustering. This means only one partition may have a subtree with capacity slack. It does not attempt to make uniform-sized clusters, terminals tend to be allocated to the closest root (distance measured in `P_paths` - see `make_planar_embedding()`). """ R, T = (A.graph[k] for k in 'RT') d2roots = A.graph['d2roots'] mainheap = [] idx_ = [tuple(range(-R, i)) + tuple(range(i + 1, 0)) for i in range(-R, 0)] expel_ = [[] for _ in range(R)] cluster_ = [set() for _ in range(R)] # initialize mainheap for n, r in enumerate(A.graph['closest_root']): d = d2roots[n, r] heappush(mainheap, (d, n, r)) total_feeders = math.ceil(T / capacity) num_slack = total_feeders * capacity - T # expeller function def expel_from(r, blocked=[]): expel = expel_[r] cluster = cluster_[r] saved_ = [] while expel: tradeoff_exp, n_exp, r_exp = heappop(expel) if r_exp in blocked: # blocked prevents a node being expelled back and forth saved_.append((tradeoff_exp, n_exp, r_exp)) continue if n_exp in cluster: break for saved in saved_: heappush(expel, saved) was_cluster_exp_round = (len(cluster_[r_exp]) % capacity) == 0 cluster.remove(n_exp) # print(f'{n_exp}: {r} -> {r_exp}', end='|') cluster_[r_exp].add(n_exp) d_exp = d2roots[n_exp, r_exp] for i in idx_[r_exp]: heappush(expel_[r_exp], (d2roots[n_exp, i] - d_exp, n_exp, i)) # print(f'({d2roots[n_exp, i]:.0f}, {d_exp:.0f})', end='|') if was_cluster_exp_round: expel_from(r_exp, blocked + [r]) while mainheap: d, n, r = heappop(mainheap) cluster = cluster_[r] expel = expel_[r] was_cluster_round = (len(cluster) % capacity) == 0 # the (- 1) is because we just popped a node but have not assigned it threshold = ( sum((capacity - len(clu) % capacity) for clu in cluster_) - num_slack - 1 ) # add first and expel later if necessary cluster.add(n) for i in idx_[r]: heappush(expel, (d2roots[n, i] - d, n, i)) if ( len(mainheap) <= threshold and was_cluster_round and not all((len(cluster_[i]) % capacity == 0) for i in idx_[r]) ): # cluster is overfull: expel expel_from(r) # this only works because the slack is allocated in a single cluster num_slack_ = [num_slack if (len(cluster) % capacity) else 0 for cluster in cluster_] return cluster_, num_slack_