tsam 2.1.0__py3-none-any.whl → 2.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,133 +1,133 @@
1
- # -*- coding: utf-8 -*-
2
-
3
- import numpy as np
4
- import time
5
- import pyomo.environ as pyomo
6
- import pyomo.opt as opt
7
- import networkx as nx
8
- from tsam.utils.k_medoids_exact import (
9
- _setup_k_medoids,
10
- KMedoids,
11
- _solve_given_pyomo_model,
12
- )
13
-
14
-
15
- # class KMedoids_contiguity(KMedoids):
16
-
17
-
18
- def k_medoids_contiguity(distances, n_clusters, adjacency, max_iter=500, solver="cbc"):
19
- """Declares a k-medoids model and iteratively adds cutting planes to hold on adjacency/contiguity
20
-
21
- The algorithm is based on: Oehrlein and Hauner (2017): A cutting-plane method for adjacency-constrained spatial aggregation
22
- """
23
- # First transform the network to a networkx instance which is required for cut generation
24
- G = _contiguity_to_graph(adjacency, distances=distances)
25
-
26
- # check if inputs are correct
27
- np.size(distances) == np.size(adjacency)
28
-
29
- # and test for connectivity
30
- if not nx.is_connected(G):
31
- raise ValueError("The give adjacency matrix is not connected.")
32
-
33
- # Initial setup of k medoids
34
- M = _setup_k_medoids(distances, n_clusters)
35
-
36
- M.adjacency = adjacency
37
-
38
- # Add constraintlist for the cuts later added
39
- M.cuts = pyomo.ConstraintList()
40
-
41
- # Loop over the relaxed k-medoids problem and add cuts until the problem fits
42
- _all_cluster_connected = False
43
- _iter = 0
44
- _cuts_added = []
45
- while not _all_cluster_connected and _iter < max_iter:
46
- # first solve instance
47
- t_presolve = time.time()
48
- print(str(_iter) + " iteration: Solving instance")
49
- r_x, r_y, obj = _solve_given_pyomo_model(M, solver=solver)
50
- t_aftersolve = time.time()
51
- print(
52
- "Total distance: "
53
- + str(obj)
54
- + " with solving time: "
55
- + str(t_aftersolve - t_presolve)
56
- )
57
-
58
- candidates, labels = np.where(r_x == 1)
59
- # claim that the resulting clusters are connected
60
- _all_cluster_connected = True
61
- _new_cuts_added = []
62
- for label in np.unique(labels):
63
- # extract the cluster
64
- cluster = G.subgraph(np.where(labels == label)[0])
65
- # Identify if the cluster is contineous, instead of validating the constraints such as Validi and Oehrlein.
66
- if not nx.is_connected(cluster):
67
- _all_cluster_connected = False
68
- # if not add contiguity constraints based on c-v (Oehrlein) o a-b (Validi) separators
69
- for candidate in cluster.nodes:
70
- # It is not clear in Validi and Oehrlein, if cuts between all cluster candidates or just the center and the candidates shall be made. The latter one does not converge for the test system wherefore the first one is chosen.
71
- for node in cluster.nodes:
72
- # different to Validi et al. (2021) and Oehrlein and Haunert (2017), check first and just add continuity constraints for the not connected candidates to increase performance
73
- if nx.node_connectivity(cluster, node, candidate) == 0:
74
- # check that the cut was not added so far for the cluster
75
- if (label, candidate, node) not in _cuts_added:
76
- # include the cut in the cut list
77
- _new_cuts_added.append((label, candidate, node))
78
- # Cuts to Separators - Appendix A Minimum-weight vertex separators (Oehrlein and Haunert, 2017)
79
- # Validi uses an own cut generator and Oehrlein falls back to a Java library, here we use simple max flow cutting
80
- # TODO: Check performance for large networks
81
- cut_set = nx.minimum_node_cut(G, node, candidate)
82
- # (Eq. 13 - Oehrlein and Haunert, 2017)
83
- M.cuts.add(
84
- sum(M.z[u, node] for u in cut_set)
85
- >= M.z[candidate, node]
86
- )
87
- else:
88
- raise ValueError(
89
- "Minimal cluster,candidate separation/minimum cut does not seem sufficient. Adding additional separators is could help."
90
- )
91
- # Total cuts
92
- _cuts_added.extend(_new_cuts_added)
93
- _iter += 1
94
- t_afteradding = time.time()
95
-
96
- print(
97
- str(len(_new_cuts_added))
98
- + " contiguity constraints/cuts added, adding to a total number of "
99
- + str(len(_cuts_added))
100
- + " cuts within time: "
101
- + str(t_afteradding - t_aftersolve)
102
- )
103
-
104
- labels = np.where(r_x == 1)
105
-
106
- return (r_y, r_x.T, obj)
107
-
108
-
109
- def _contiguity_to_graph(adjacency, distances=None):
110
- """Transforms a adjacency matrix to a networkx.Graph
111
-
112
- Args:
113
- adjacency (np.ndarray): 2-diimensional adjacency matrix
114
- distances (np.ndarray, optional): If provided, delivers the distances between the nodes. Defaults to None.
115
-
116
- Returns:
117
- nx.Graph: Graph with every index as node name.
118
- """
119
- rows, cols = np.where(adjacency == 1)
120
- G = nx.Graph()
121
- if distances is None:
122
- edges = zip(rows.tolist(), cols.tolist())
123
- G.add_edges_from(edges)
124
- else:
125
- normed_distances = distances / np.max(distances)
126
- weights = 1 - normed_distances
127
- if np.any(weights < 0) or np.any(weights > 1):
128
- raise ValueError("Weight calculation went wrong.")
129
-
130
- edge_weights = weights[rows, cols]
131
- edges = zip(rows.tolist(), cols.tolist(), edge_weights.tolist())
132
- G.add_weighted_edges_from(edges)
133
- return G
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import numpy as np
4
+ import time
5
+ import pyomo.environ as pyomo
6
+ import pyomo.opt as opt
7
+ import networkx as nx
8
+ from tsam.utils.k_medoids_exact import (
9
+ _setup_k_medoids,
10
+ KMedoids,
11
+ _solve_given_pyomo_model,
12
+ )
13
+
14
+
15
+ # class KMedoids_contiguity(KMedoids):
16
+
17
+
18
+ def k_medoids_contiguity(distances, n_clusters, adjacency, max_iter=500, solver="highs"):
19
+ """Declares a k-medoids model and iteratively adds cutting planes to hold on adjacency/contiguity
20
+
21
+ The algorithm is based on: Oehrlein and Hauner (2017): A cutting-plane method for adjacency-constrained spatial aggregation
22
+ """
23
+ # First transform the network to a networkx instance which is required for cut generation
24
+ G = _contiguity_to_graph(adjacency, distances=distances)
25
+
26
+ # check if inputs are correct
27
+ np.size(distances) == np.size(adjacency)
28
+
29
+ # and test for connectivity
30
+ if not nx.is_connected(G):
31
+ raise ValueError("The give adjacency matrix is not connected.")
32
+
33
+ # Initial setup of k medoids
34
+ M = _setup_k_medoids(distances, n_clusters)
35
+
36
+ M.adjacency = adjacency
37
+
38
+ # Add constraintlist for the cuts later added
39
+ M.cuts = pyomo.ConstraintList()
40
+
41
+ # Loop over the relaxed k-medoids problem and add cuts until the problem fits
42
+ _all_cluster_connected = False
43
+ _iter = 0
44
+ _cuts_added = []
45
+ while not _all_cluster_connected and _iter < max_iter:
46
+ # first solve instance
47
+ t_presolve = time.time()
48
+ print(str(_iter) + " iteration: Solving instance")
49
+ r_x, r_y, obj = _solve_given_pyomo_model(M, solver=solver)
50
+ t_aftersolve = time.time()
51
+ print(
52
+ "Total distance: "
53
+ + str(obj)
54
+ + " with solving time: "
55
+ + str(t_aftersolve - t_presolve)
56
+ )
57
+
58
+ candidates, labels = np.where(r_x == 1)
59
+ # claim that the resulting clusters are connected
60
+ _all_cluster_connected = True
61
+ _new_cuts_added = []
62
+ for label in np.unique(labels):
63
+ # extract the cluster
64
+ cluster = G.subgraph(np.where(labels == label)[0])
65
+ # Identify if the cluster is contineous, instead of validating the constraints such as Validi and Oehrlein.
66
+ if not nx.is_connected(cluster):
67
+ _all_cluster_connected = False
68
+ # if not add contiguity constraints based on c-v (Oehrlein) o a-b (Validi) separators
69
+ for candidate in cluster.nodes:
70
+ # It is not clear in Validi and Oehrlein, if cuts between all cluster candidates or just the center and the candidates shall be made. The latter one does not converge for the test system wherefore the first one is chosen.
71
+ for node in cluster.nodes:
72
+ # different to Validi et al. (2021) and Oehrlein and Haunert (2017), check first and just add continuity constraints for the not connected candidates to increase performance
73
+ if nx.node_connectivity(cluster, node, candidate) == 0:
74
+ # check that the cut was not added so far for the cluster
75
+ if (label, candidate, node) not in _cuts_added:
76
+ # include the cut in the cut list
77
+ _new_cuts_added.append((label, candidate, node))
78
+ # Cuts to Separators - Appendix A Minimum-weight vertex separators (Oehrlein and Haunert, 2017)
79
+ # Validi uses an own cut generator and Oehrlein falls back to a Java library, here we use simple max flow cutting
80
+ # TODO: Check performance for large networks
81
+ cut_set = nx.minimum_node_cut(G, node, candidate)
82
+ # (Eq. 13 - Oehrlein and Haunert, 2017)
83
+ M.cuts.add(
84
+ sum(M.z[u, node] for u in cut_set)
85
+ >= M.z[candidate, node]
86
+ )
87
+ else:
88
+ raise ValueError(
89
+ "Minimal cluster,candidate separation/minimum cut does not seem sufficient. Adding additional separators is could help."
90
+ )
91
+ # Total cuts
92
+ _cuts_added.extend(_new_cuts_added)
93
+ _iter += 1
94
+ t_afteradding = time.time()
95
+
96
+ print(
97
+ str(len(_new_cuts_added))
98
+ + " contiguity constraints/cuts added, adding to a total number of "
99
+ + str(len(_cuts_added))
100
+ + " cuts within time: "
101
+ + str(t_afteradding - t_aftersolve)
102
+ )
103
+
104
+ labels = np.where(r_x == 1)
105
+
106
+ return (r_y, r_x.T, obj)
107
+
108
+
109
+ def _contiguity_to_graph(adjacency, distances=None):
110
+ """Transforms a adjacency matrix to a networkx.Graph
111
+
112
+ Args:
113
+ adjacency (np.ndarray): 2-diimensional adjacency matrix
114
+ distances (np.ndarray, optional): If provided, delivers the distances between the nodes. Defaults to None.
115
+
116
+ Returns:
117
+ nx.Graph: Graph with every index as node name.
118
+ """
119
+ rows, cols = np.where(adjacency == 1)
120
+ G = nx.Graph()
121
+ if distances is None:
122
+ edges = zip(rows.tolist(), cols.tolist())
123
+ G.add_edges_from(edges)
124
+ else:
125
+ normed_distances = distances / np.max(distances)
126
+ weights = 1 - normed_distances
127
+ if np.any(weights < 0) or np.any(weights > 1):
128
+ raise ValueError("Weight calculation went wrong.")
129
+
130
+ edge_weights = weights[rows, cols]
131
+ edges = zip(rows.tolist(), cols.tolist(), edge_weights.tolist())
132
+ G.add_weighted_edges_from(edges)
133
+ return G