topologicpy 0.5.9__py3-none-any.whl → 6.0.0__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.
Files changed (94) hide show
  1. topologicpy/Aperture.py +72 -72
  2. topologicpy/Cell.py +2169 -2169
  3. topologicpy/CellComplex.py +1137 -1137
  4. topologicpy/Cluster.py +1288 -1280
  5. topologicpy/Color.py +423 -423
  6. topologicpy/Context.py +79 -79
  7. topologicpy/DGL.py +3213 -3240
  8. topologicpy/Dictionary.py +698 -698
  9. topologicpy/Edge.py +1187 -1187
  10. topologicpy/EnergyModel.py +1180 -1152
  11. topologicpy/Face.py +2141 -2141
  12. topologicpy/Graph.py +7768 -7768
  13. topologicpy/Grid.py +353 -353
  14. topologicpy/Helper.py +507 -507
  15. topologicpy/Honeybee.py +461 -461
  16. topologicpy/Matrix.py +271 -271
  17. topologicpy/Neo4j.py +521 -521
  18. topologicpy/Plotly.py +2 -2
  19. topologicpy/Polyskel.py +541 -541
  20. topologicpy/Shell.py +1768 -1768
  21. topologicpy/Speckle.py +508 -508
  22. topologicpy/Topology.py +7060 -7002
  23. topologicpy/Vector.py +905 -905
  24. topologicpy/Vertex.py +1585 -1585
  25. topologicpy/Wire.py +3050 -3050
  26. topologicpy/__init__.py +22 -38
  27. topologicpy/version.py +1 -0
  28. {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/LICENSE +661 -704
  29. topologicpy-6.0.0.dist-info/METADATA +751 -0
  30. topologicpy-6.0.0.dist-info/RECORD +32 -0
  31. topologicpy/bin/linux/topologic/__init__.py +0 -2
  32. topologicpy/bin/linux/topologic/libTKBO-6bdf205d.so.7.7.0 +0 -0
  33. topologicpy/bin/linux/topologic/libTKBRep-2960a069.so.7.7.0 +0 -0
  34. topologicpy/bin/linux/topologic/libTKBool-c44b74bd.so.7.7.0 +0 -0
  35. topologicpy/bin/linux/topologic/libTKFillet-9a670ba0.so.7.7.0 +0 -0
  36. topologicpy/bin/linux/topologic/libTKG2d-8f31849e.so.7.7.0 +0 -0
  37. topologicpy/bin/linux/topologic/libTKG3d-4c6bce57.so.7.7.0 +0 -0
  38. topologicpy/bin/linux/topologic/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
  39. topologicpy/bin/linux/topologic/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
  40. topologicpy/bin/linux/topologic/libTKMath-72572fa8.so.7.7.0 +0 -0
  41. topologicpy/bin/linux/topologic/libTKMesh-2a060427.so.7.7.0 +0 -0
  42. topologicpy/bin/linux/topologic/libTKOffset-6cab68ff.so.7.7.0 +0 -0
  43. topologicpy/bin/linux/topologic/libTKPrim-eb1262b3.so.7.7.0 +0 -0
  44. topologicpy/bin/linux/topologic/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
  45. topologicpy/bin/linux/topologic/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
  46. topologicpy/bin/linux/topologic/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
  47. topologicpy/bin/linux/topologic/libgcc_s-32c1665e.so.1 +0 -0
  48. topologicpy/bin/linux/topologic/libstdc++-672d7b41.so.6.0.30 +0 -0
  49. topologicpy/bin/linux/topologic/topologic.cpython-310-x86_64-linux-gnu.so +0 -0
  50. topologicpy/bin/linux/topologic/topologic.cpython-311-x86_64-linux-gnu.so +0 -0
  51. topologicpy/bin/linux/topologic/topologic.cpython-38-x86_64-linux-gnu.so +0 -0
  52. topologicpy/bin/linux/topologic/topologic.cpython-39-x86_64-linux-gnu.so +0 -0
  53. topologicpy/bin/linux/topologic.libs/libTKBO-6bdf205d.so.7.7.0 +0 -0
  54. topologicpy/bin/linux/topologic.libs/libTKBRep-2960a069.so.7.7.0 +0 -0
  55. topologicpy/bin/linux/topologic.libs/libTKBool-c44b74bd.so.7.7.0 +0 -0
  56. topologicpy/bin/linux/topologic.libs/libTKFillet-9a670ba0.so.7.7.0 +0 -0
  57. topologicpy/bin/linux/topologic.libs/libTKG2d-8f31849e.so.7.7.0 +0 -0
  58. topologicpy/bin/linux/topologic.libs/libTKG3d-4c6bce57.so.7.7.0 +0 -0
  59. topologicpy/bin/linux/topologic.libs/libTKGeomAlgo-26066fd9.so.7.7.0 +0 -0
  60. topologicpy/bin/linux/topologic.libs/libTKGeomBase-2116cabe.so.7.7.0 +0 -0
  61. topologicpy/bin/linux/topologic.libs/libTKMath-72572fa8.so.7.7.0 +0 -0
  62. topologicpy/bin/linux/topologic.libs/libTKMesh-2a060427.so.7.7.0 +0 -0
  63. topologicpy/bin/linux/topologic.libs/libTKOffset-6cab68ff.so.7.7.0 +0 -0
  64. topologicpy/bin/linux/topologic.libs/libTKPrim-eb1262b3.so.7.7.0 +0 -0
  65. topologicpy/bin/linux/topologic.libs/libTKShHealing-e67e5cc7.so.7.7.0 +0 -0
  66. topologicpy/bin/linux/topologic.libs/libTKTopAlgo-e4c96c33.so.7.7.0 +0 -0
  67. topologicpy/bin/linux/topologic.libs/libTKernel-fb7fe3b7.so.7.7.0 +0 -0
  68. topologicpy/bin/linux/topologic.libs/libgcc_s-32c1665e.so.1 +0 -0
  69. topologicpy/bin/linux/topologic.libs/libstdc++-672d7b41.so.6.0.30 +0 -0
  70. topologicpy/bin/macos/topologic/__init__.py +0 -2
  71. topologicpy/bin/windows/topologic/TKBO-f6b191de.dll +0 -0
  72. topologicpy/bin/windows/topologic/TKBRep-e56a600e.dll +0 -0
  73. topologicpy/bin/windows/topologic/TKBool-7b8d47ae.dll +0 -0
  74. topologicpy/bin/windows/topologic/TKFillet-0ddbf0a8.dll +0 -0
  75. topologicpy/bin/windows/topologic/TKG2d-2e2dee3d.dll +0 -0
  76. topologicpy/bin/windows/topologic/TKG3d-6674513d.dll +0 -0
  77. topologicpy/bin/windows/topologic/TKGeomAlgo-d240e370.dll +0 -0
  78. topologicpy/bin/windows/topologic/TKGeomBase-df87aba5.dll +0 -0
  79. topologicpy/bin/windows/topologic/TKMath-45bd625a.dll +0 -0
  80. topologicpy/bin/windows/topologic/TKMesh-d6e826b1.dll +0 -0
  81. topologicpy/bin/windows/topologic/TKOffset-79b9cc94.dll +0 -0
  82. topologicpy/bin/windows/topologic/TKPrim-aa430a86.dll +0 -0
  83. topologicpy/bin/windows/topologic/TKShHealing-bb48be89.dll +0 -0
  84. topologicpy/bin/windows/topologic/TKTopAlgo-7d0d1e22.dll +0 -0
  85. topologicpy/bin/windows/topologic/TKernel-08c8cfbb.dll +0 -0
  86. topologicpy/bin/windows/topologic/__init__.py +0 -2
  87. topologicpy/bin/windows/topologic/topologic.cp310-win_amd64.pyd +0 -0
  88. topologicpy/bin/windows/topologic/topologic.cp311-win_amd64.pyd +0 -0
  89. topologicpy/bin/windows/topologic/topologic.cp38-win_amd64.pyd +0 -0
  90. topologicpy/bin/windows/topologic/topologic.cp39-win_amd64.pyd +0 -0
  91. topologicpy-0.5.9.dist-info/METADATA +0 -86
  92. topologicpy-0.5.9.dist-info/RECORD +0 -91
  93. {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/WHEEL +0 -0
  94. {topologicpy-0.5.9.dist-info → topologicpy-6.0.0.dist-info}/top_level.txt +0 -0
topologicpy/Cluster.py CHANGED
@@ -1,1281 +1,1289 @@
1
- # Copyright (C) 2024
2
- # Wassim Jabi <wassim.jabi@gmail.com>
3
- #
4
- # This program is free software: you can redistribute it and/or modify it under
5
- # the terms of the GNU Affero General Public License as published by the Free Software
6
- # Foundation, either version 3 of the License, or (at your option) any later
7
- # version.
8
- #
9
- # This program is distributed in the hope that it will be useful, but WITHOUT
10
- # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
- # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
12
- # details.
13
- #
14
- # You should have received a copy of the GNU Affero General Public License along with
15
- # this program. If not, see <https://www.gnu.org/licenses/>.
16
-
17
- from topologicpy.Topology import Topology
18
- import topologic
19
- import os
20
- import warnings
21
-
22
- try:
23
- import numpy as np
24
- except:
25
- print("Cluster - Installing required numpy library.")
26
- try:
27
- os.system("pip install numpy")
28
- except:
29
- os.system("pip install numpy --user")
30
- try:
31
- import numpy as np
32
- print("Cluster - numpy library installed correctly.")
33
- except:
34
- warnings.warn("Cluster - Error: Could not import numpy.")
35
-
36
- try:
37
- from scipy.spatial.distance import pdist, squareform
38
- except:
39
- print("Cluster - Installing required scipy library.")
40
- try:
41
- os.system("pip install scipy")
42
- except:
43
- os.system("pip install scipy --user")
44
- try:
45
- from scipy.spatial.distance import pdist, squareform
46
- print("Cluster - scipy library installed correctly.")
47
- except:
48
- warnings.warn("Cluster - Error: Could not import scipy.")
49
-
50
- class Cluster(Topology):
51
- @staticmethod
52
- def ByFormula(formula, xRange=None, yRange=None, xString="X", yString="Y"):
53
- """
54
- Creates a cluster of vertices by vvaluating the input formula for a range of x and, optionally, a range of y values.
55
-
56
- Parameters
57
- ----------
58
- formula : str
59
- A string representing the formula to be evaluated.
60
- For 2D formulas, use 'X' for the independent variable. For 3D formulas, use 'X' and 'Y' for the independent variables.
61
- You can use standard math functions like 'sin', 'cos', 'tan', 'sqrt', etc.
62
- For example, 'X**2 + 2*X - sqrt(X)' or 'cos(abs(X)+abs(Y))'
63
- xRange : tuple , optional
64
- A tuple (start, end, step) representing the range of X values
65
- for which the formula should be evaluated.
66
- For example, to evaluate for X values from -5 to 5 with a step of 0.1: (-5, 5, 0.1)
67
- If the xRange is set to None or not specified:
68
- The method assumes that the formula uses the yString (e.g. 'Y' as in 'Y**2 + 2*Y - sqrt(Y)')
69
- The method will attempt to use the YRange instead for the independent variable.
70
- yRange : tuple , optional
71
- A tuple (start, end, step) representing the range of Y values
72
- for which the formula should be evaluated.
73
- For example, to evaluate for x values from -5 to 5 with a step of 0.1:
74
- (-5, 5, 0.1)
75
-
76
- Returns:
77
- topologic.Cluster
78
- The created cluster of vertices.
79
- """
80
- from topologicpy.Vertex import Vertex
81
- import math
82
- if xRange == None and yRange == None:
83
- print("Cluster.ByFormula - Error: Both ranges cannot be None at the same time. Returning None.")
84
- return None
85
- if xString.islower():
86
- print("Cluster.ByFormula - Error: the input xString cannot lowercase. Please consider using uppercase (e.g. X). Returning None.")
87
- return None
88
- if yString == 'y':
89
- print("Cluster.ByFormula - Error: the input yString cannot be lowercase. Please consider using uppercase (e.g. Y). Returning None.")
90
- return None
91
-
92
- x_values = []
93
- y_values = []
94
- if not xRange == None:
95
- x_start, x_end, x_step = xRange
96
- x = x_start
97
- while x < x_end:
98
- x_values.append(x)
99
- x = x + x_step
100
- x_values.append(x_end)
101
-
102
- if not yRange == None:
103
- y_start, y_end, y_step = yRange
104
- y = y_start
105
- while y < y_end:
106
- y_values.append(y)
107
- y = y + y_step
108
- y_values.append(y_end)
109
-
110
- # Evaluate the formula for each x and y value
111
- x_return = []
112
- y_return = []
113
- z_return = []
114
- if len(x_values) > 0 and len(y_values) > 0: # Both X and Y exist, compute Z.
115
- for x in x_values:
116
- for y in y_values:
117
- x_return.append(x)
118
- y_return.append(y)
119
- formula1 = formula.replace(xString, str(x)).replace(yString, str(y)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
120
- z_return.append(eval(formula1))
121
- elif len(x_values) == 0 and len(y_values) > 0: # Only Y exists, compute X, Z is always 0.
122
- for y in y_values:
123
- y_return.append(y)
124
- formula1 = formula.replace(xString, str(y)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
125
- x_return.append(eval(formula1))
126
- z_return.append(0)
127
- else: # Only X exists, compute Y, Z is always 0.
128
- for x in x_values:
129
- x_return.append(x)
130
- formula1 = formula.replace(xString, str(x)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
131
- y_return.append(eval(formula1))
132
- z_return.append(0)
133
- vertices = []
134
- for i in range(len(x_return)):
135
- vertices.append(Vertex.ByCoordinates(x_return[i], y_return[i], z_return[i]))
136
- return Cluster.ByTopologies(vertices)
137
-
138
- @staticmethod
139
- def ByTopologies(*args, transferDictionaries: bool = False) -> topologic.Cluster:
140
- """
141
- Creates a topologic Cluster from the input list of topologies. The input can be individual topologies each as an input argument or a list of topologies stored in one input argument.
142
-
143
- Parameters
144
- ----------
145
- topologies : list
146
- The list of topologies.
147
- transferDictionaries : bool , optional
148
- If set to True, the dictionaries from the input topologies are merged and transferred to the cluster. Otherwise they are not. The default is False.
149
-
150
- Returns
151
- -------
152
- topologic.Cluster
153
- The created topologic Cluster.
154
-
155
- """
156
- from topologicpy.Dictionary import Dictionary
157
- from topologicpy.Topology import Topology
158
- from topologicpy.Helper import Helper
159
-
160
- if len(args) == 0:
161
- print("Cluster.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
162
- return None
163
- if len(args) == 1:
164
- topologies = args[0]
165
- if isinstance(topologies, list):
166
- if len(topologies) == 0:
167
- print("Cluster.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
168
- return None
169
- else:
170
- topologyList = [x for x in topologies if isinstance(x, topologic.Topology)]
171
- if len(topologies) == 0:
172
- print("Cluster.ByTopologies - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
173
- return None
174
- else:
175
- print("Cluster.ByTopologies - Warning: The input topologies parameter contains only one topology. Returning the same topology.")
176
- return topologies
177
- else:
178
- topologyList = Helper.Flatten(list(args))
179
- topologyList = [x for x in topologyList if isinstance(x, topologic.Topology)]
180
- if len(topologyList) == 0:
181
- print("Cluster.ByTopologies - Error: The input parameters do not contain any valid topologies. Returning None.")
182
- return None
183
- cluster = topologic.Cluster.ByTopologies(topologyList, False)
184
- dictionaries = []
185
- for t in topologyList:
186
- d = Topology.Dictionary(t)
187
- keys = Dictionary.Keys(d)
188
- if isinstance(keys, list):
189
- if len(keys) > 0:
190
- dictionaries.append(d)
191
- if len(dictionaries) > 0:
192
- if len(dictionaries) > 1:
193
- d = Dictionary.ByMergedDictionaries(dictionaries)
194
- else:
195
- d = dictionaries[0]
196
- cluster = Topology.SetDictionary(cluster, d)
197
- return cluster
198
-
199
- @staticmethod
200
- def CellComplexes(cluster: topologic.Cluster) -> list:
201
- """
202
- Returns the cellComplexes of the input cluster.
203
-
204
- Parameters
205
- ----------
206
- cluster : topologic.Cluster
207
- The input cluster.
208
-
209
- Returns
210
- -------
211
- list
212
- The list of cellComplexes.
213
-
214
- """
215
- if not isinstance(cluster, topologic.Cluster):
216
- print("Cluster.CellComplexes - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
217
- return None
218
- cellComplexes = []
219
- _ = cluster.CellComplexes(None, cellComplexes)
220
- return cellComplexes
221
-
222
- @staticmethod
223
- def Cells(cluster: topologic.Cluster) -> list:
224
- """
225
- Returns the cells of the input cluster.
226
-
227
- Parameters
228
- ----------
229
- cluster : topologic.Cluster
230
- The input cluster.
231
-
232
- Returns
233
- -------
234
- list
235
- The list of cells.
236
-
237
- """
238
- if not isinstance(cluster, topologic.Cluster):
239
- print("Cluster.Cells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
240
- return None
241
- cells = []
242
- _ = cluster.Cells(None, cells)
243
- return cells
244
-
245
-
246
- @staticmethod
247
- def DBSCAN(topologies, selectors=None, keys=["x", "y", "z"], epsilon: float = 0.5, minSamples: int = 2):
248
- """
249
- Clusters the input vertices based on the Density-Based Spatial Clustering of Applications with Noise (DBSCAN) method. See https://en.wikipedia.org/wiki/DBSCAN
250
-
251
- Parameters
252
- ----------
253
- topologies : list
254
- The input list of topologies to be clustered.
255
- selectors : list , optional
256
- If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies.
257
- If set to None, the list of topologies is expected to be a list of vertices. The default is None.
258
- keys : list, optional
259
- The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included,
260
- make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
261
- epsilon : float , optional
262
- The maximum radius around a data point within which other points are considered to be part of the same sense region (cluster). The default is 0.5.
263
- minSamples : int , optional
264
- The minimum number of points required to form a dense region (cluster). The default is 2.
265
-
266
- Returns
267
- -------
268
- list, list
269
- The list of clusters and the list of vertices considered to be noise if any (otherwise returns None).
270
-
271
- """
272
- from topologicpy.Vertex import Vertex
273
- from topologicpy.Topology import Topology
274
- from topologicpy.Dictionary import Dictionary
275
-
276
- def dbscan_3d_indices(data, eps, min_samples):
277
- """
278
- DBSCAN clustering algorithm for 3D points.
279
-
280
- Parameters:
281
- - data: NumPy array, input data points with X, Y, and Z coordinates.
282
- - eps: float, maximum distance between two samples for one to be considered as in the neighborhood of the other.
283
- - min_samples: int, the number of samples (or total weight) in a neighborhood for a point to be considered as a core point.
284
-
285
- Returns:
286
- - clusters: List of lists, each list containing the indices of points in a cluster.
287
- - noise: List of indices, indices of points labeled as noise.
288
- """
289
-
290
- # Compute pairwise distances
291
- dists = squareform(pdist(data))
292
-
293
- # Initialize labels and cluster ID
294
- labels = np.full(data.shape[0], -1)
295
- cluster_id = 0
296
-
297
- # Iterate through each point
298
- for i in range(data.shape[0]):
299
- if labels[i] != -1:
300
- continue # Skip already processed points
301
-
302
- # Find neighbors within epsilon distance
303
- neighbors = np.where(dists[i] < eps)[0]
304
-
305
- if len(neighbors) < min_samples:
306
- # Label as noise
307
- labels[i] = -1
308
- else:
309
- # Expand cluster
310
- cluster_id += 1
311
- expand_cluster_3d_indices(labels, dists, i, neighbors, cluster_id, eps, min_samples)
312
-
313
- # Organize indices into clusters and noise
314
- clusters = [list(np.where(labels == cid)[0]) for cid in range(1, cluster_id + 1)]
315
- noise = list(np.where(labels == -1)[0])
316
-
317
- return clusters, noise
318
-
319
- def expand_cluster_3d_indices(labels, dists, point_index, neighbors, cluster_id, eps, min_samples):
320
- """
321
- Expand the cluster around a core point for 3D points.
322
-
323
- Parameters:
324
- - labels: NumPy array, cluster labels for each data point.
325
- - dists: NumPy array, pairwise distances between data points.
326
- - point_index: int, index of the core point.
327
- - neighbors: NumPy array, indices of neighbors.
328
- - cluster_id: int, current cluster ID.
329
- - eps: float, maximum distance between two samples for one to be considered as in the neighborhood of the other.
330
- - min_samples: int, the number of samples (or total weight) in a neighborhood for a point to be considered as a core point.
331
- """
332
- labels[point_index] = cluster_id
333
-
334
- i = 0
335
- while i < len(neighbors):
336
- current_neighbor = neighbors[i]
337
-
338
- if labels[current_neighbor] == -1:
339
- labels[current_neighbor] = cluster_id
340
-
341
- new_neighbors = np.where(dists[current_neighbor] < eps)[0]
342
- if len(new_neighbors) >= min_samples:
343
- neighbors = np.concatenate([neighbors, new_neighbors])
344
-
345
- elif labels[current_neighbor] == 0:
346
- labels[current_neighbor] = cluster_id
347
-
348
- i += 1
349
-
350
- if not isinstance(topologies, list):
351
- print("Cluster.DBSCAN - Error: The input vertices parameter is not a valid list. Returning None.")
352
- return None, None
353
- topologyList = [t for t in topologies if isinstance(t, topologic.Topology)]
354
- if len(topologyList) < 1:
355
- print("Cluster.DBSCAN - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
356
- return None, None
357
- if len(topologyList) < minSamples:
358
- print("Cluster.DBSCAN - Error: The input minSamples parameter cannot be larger than the number of vertices. Returning None.")
359
- return None, None
360
-
361
- if not isinstance(selectors, list):
362
- check_vertices = [t for t in topologyList if not isinstance(t, topologic.Vertex)]
363
- if len(check_vertices) > 0:
364
- print("Cluster.DBSCAN - Error: The input selectors parameter is not a valid list and this is needed since the list of topologies contains objects of type other than a topologic.Vertex. Returning None.")
365
- return None, None
366
- else:
367
- selectors = [s for s in selectors if isinstance(s, topologic.Vertex)]
368
- if len(selectors) < 1:
369
- check_vertices = [t for t in topologyList if not isinstance(t, topologic.Vertex)]
370
- if len(check_vertices) > 0:
371
- print("Cluster.DBSCAN - Error: The input selectors parameter does not contain any valid vertices and this is needed since the list of topologies contains objects of type other than a topologic.Vertex. Returning None.")
372
- return None, None
373
- if not len(selectors) == len(topologyList):
374
- print("Cluster.DBSCAN - Error: The input topologies and selectors parameters do not have the same length. Returning None.")
375
- return None, None
376
- if not isinstance(keys, list):
377
- print("Cluster.DBSCAN - Error: The input keys parameter is not a valid list. Returning None.")
378
- return None
379
-
380
-
381
- data = []
382
- if selectors == None:
383
- for t in topologyList:
384
- elements = []
385
- if keys:
386
- d = Topology.Dictionary(t)
387
- for key in keys:
388
- if key.lower() == "x":
389
- value = Vertex.X(t)
390
- elif key.lower() == "y":
391
- value = Vertex.Y(t)
392
- elif key.lower() == "z":
393
- value = Vertex.Z(t)
394
- else:
395
- value = Dictionary.ValueAtKey(d, key)
396
- if value != None:
397
- elements.append(value)
398
- data.append(elements)
399
- else:
400
- for i, s in enumerate(selectors):
401
- elements = []
402
- if keys:
403
- d = Topology.Dictionary(topologyList[i])
404
- for key in keys:
405
- if key.lower() == "x":
406
- value = Vertex.X(s)
407
- elif key.lower() == "y":
408
- value = Vertex.Y(s)
409
- elif key.lower() == "z":
410
- value = Vertex.Z(s)
411
- else:
412
- value = Dictionary.ValueAtKey(d, key)
413
- if value != None:
414
- elements.append(value)
415
- data.append(elements)
416
- #coords = [[Vertex.X(v), Vertex.Y(v), Vertex.Z(v)] for v in vertexList]
417
- clusters, noise = dbscan_3d_indices(np.array(data), epsilon, minSamples)
418
- tp_clusters = []
419
- for cluster in clusters:
420
- tp_clusters.append(Cluster.ByTopologies([topologyList[i] for i in cluster]))
421
- vert_group = []
422
- tp_noise = None
423
- if len(noise) > 0:
424
- tp_noise = Cluster.ByTopologies([topologyList[i] for i in noise])
425
- return tp_clusters, tp_noise
426
-
427
- @staticmethod
428
- def Edges(cluster: topologic.Cluster) -> list:
429
- """
430
- Returns the edges of the input cluster.
431
-
432
- Parameters
433
- ----------
434
- cluster : topologic.Cluster
435
- The input cluster.
436
-
437
- Returns
438
- -------
439
- list
440
- The list of edges.
441
-
442
- """
443
- if not isinstance(cluster, topologic.Cluster):
444
- print("Cluster.Edges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
445
- return None
446
- edges = []
447
- _ = cluster.Edges(None, edges)
448
- return edges
449
-
450
- @staticmethod
451
- def Faces(cluster: topologic.Cluster) -> list:
452
- """
453
- Returns the faces of the input cluster.
454
-
455
- Parameters
456
- ----------
457
- cluster : topologic.Cluster
458
- The input cluster.
459
-
460
- Returns
461
- -------
462
- list
463
- The list of faces.
464
-
465
- """
466
- if not isinstance(cluster, topologic.Cluster):
467
- print("Cluster.Faces - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
468
- return None
469
- faces = []
470
- _ = cluster.Faces(None, faces)
471
- return faces
472
-
473
- @staticmethod
474
- def FreeCells(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
475
- """
476
- Returns the free cells of the input cluster that are not part of a higher topology.
477
-
478
- Parameters
479
- ----------
480
- cluster : topologic.Cluster
481
- The input cluster.
482
- tolerance : float , optional
483
- The desired tolerance. The default is 0.0001.
484
-
485
- Returns
486
- -------
487
- list
488
- The list of free cells.
489
-
490
- """
491
- from topologicpy.CellComplex import CellComplex
492
- from topologicpy.Topology import Topology
493
-
494
- if not isinstance(cluster, topologic.Cluster):
495
- print("Cluster.FreeCells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
496
- return None
497
- allCells = []
498
- _ = cluster.Cells(None, allCells)
499
- if len(allCells) < 1:
500
- return []
501
- allCellsCluster = Cluster.ByTopologies(allCells)
502
- freeCells = []
503
- cellComplexes = []
504
- _ = cluster.CellComplexes(None, cellComplexes)
505
- cellComplexesCells = []
506
- for cellComplex in cellComplexes:
507
- tempCells = CellComplex.Cells(cellComplex)
508
- cellComplexesCells += tempCells
509
- if len(cellComplexesCells) == 0:
510
- return allCells
511
- cellComplexesCluster = Cluster.ByTopologies(cellComplexesCells)
512
- resultingCluster = Topology.Boolean(allCellsCluster, cellComplexesCluster, operation="difference", tolerance=tolerance)
513
- if resultingCluster == None:
514
- return []
515
- if isinstance(resultingCluster, topologic.Cell):
516
- return [resultingCluster]
517
- result = Topology.SubTopologies(resultingCluster, subTopologyType="cell")
518
- if result == None:
519
- return [] #Make sure you return an empty list instead of None
520
- return result
521
-
522
- @staticmethod
523
- def FreeShells(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
524
- """
525
- Returns the free shells of the input cluster that are not part of a higher topology.
526
-
527
- Parameters
528
- ----------
529
- cluster : topologic.Cluster
530
- The input cluster.
531
- tolerance : float, optional
532
- The desired tolerance. The default is 0.0001.
533
-
534
- Returns
535
- -------
536
- list
537
- The list of free shells.
538
-
539
- """
540
- from topologicpy.Cell import Cell
541
- from topologicpy.Topology import Topology
542
-
543
- if not isinstance(cluster, topologic.Cluster):
544
- print("Cluster.FreeShells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
545
- return None
546
- allShells = []
547
- _ = cluster.Shells(None, allShells)
548
- if len(allShells) < 1:
549
- return []
550
- allShellsCluster = Cluster.ByTopologies(allShells)
551
- cells = []
552
- _ = cluster.Cells(None, cells)
553
- cellsShells = []
554
- for cell in cells:
555
- tempShells = Cell.Shells(cell)
556
- cellsShells += tempShells
557
- if len(cellsShells) == 0:
558
- return allShells
559
- cellsCluster = Cluster.ByTopologies(cellsShells)
560
- resultingCluster = Topology.Boolean(allShellsCluster, cellsCluster, operation="difference", tolerance=tolerance)
561
- if resultingCluster == None:
562
- return []
563
- if isinstance(resultingCluster, topologic.Shell):
564
- return [resultingCluster]
565
- result = Topology.SubTopologies(resultingCluster, subTopologyType="shell")
566
- if result == None:
567
- return [] #Make sure you return an empty list instead of None
568
- return result
569
-
570
- @staticmethod
571
- def FreeFaces(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
572
- """
573
- Returns the free faces of the input cluster that are not part of a higher topology.
574
-
575
- Parameters
576
- ----------
577
- cluster : topologic.Cluster
578
- The input cluster.
579
- tolerance : float , optional
580
- The desired tolerance. The default is 0.0001.
581
-
582
- Returns
583
- -------
584
- list
585
- The list of free faces.
586
-
587
- """
588
- from topologicpy.Shell import Shell
589
- from topologicpy.Topology import Topology
590
-
591
- if not isinstance(cluster, topologic.Cluster):
592
- print("Cluster.FreeFaces - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
593
- return None
594
- allFaces = []
595
- _ = cluster.Faces(None, allFaces)
596
- if len(allFaces) < 1:
597
- return []
598
- allFacesCluster = Cluster.ByTopologies(allFaces)
599
- shells = []
600
- _ = cluster.Shells(None, shells)
601
- shellFaces = []
602
- for shell in shells:
603
- tempFaces = Shell.Faces(shell)
604
- shellFaces += tempFaces
605
- if len(shellFaces) == 0:
606
- return allFaces
607
- shellCluster = Cluster.ByTopologies(shellFaces)
608
- resultingCluster = Topology.Boolean(allFacesCluster, shellCluster, operation="difference", tolerance=tolerance)
609
- if resultingCluster == None:
610
- return []
611
- if isinstance(resultingCluster, topologic.Face):
612
- return [resultingCluster]
613
- result = Topology.SubTopologies(resultingCluster, subTopologyType="face")
614
- if result == None:
615
- return [] #Make sure you return an empty list instead of None
616
- return result
617
-
618
- @staticmethod
619
- def FreeWires(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
620
- """
621
- Returns the free wires of the input cluster that are not part of a higher topology.
622
-
623
- Parameters
624
- ----------
625
- cluster : topologic.Cluster
626
- The input cluster.
627
- tolerance : float , optional
628
- The desired tolerance. The default is 0.0001.
629
-
630
- Returns
631
- -------
632
- list
633
- The list of free wires.
634
-
635
- """
636
- from topologicpy.Face import Face
637
- from topologicpy.Topology import Topology
638
-
639
- if not isinstance(cluster, topologic.Cluster):
640
- print("Cluster.FreeWires - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
641
- return None
642
- allWires = []
643
- _ = cluster.Wires(None, allWires)
644
- if len(allWires) < 1:
645
- return []
646
- allWiresCluster = Cluster.ByTopologies(allWires)
647
- faces = []
648
- _ = cluster.Faces(None, faces)
649
- facesWires = []
650
- for face in faces:
651
- tempWires = Face.Wires(face)
652
- facesWires += tempWires
653
- if len(facesWires) == 0:
654
- return allWires
655
- facesCluster = Cluster.ByTopologies(facesWires)
656
- resultingCluster = Topology.Boolean(allWiresCluster, facesCluster, operation="difference", tolerance=tolerance)
657
- if resultingCluster == None:
658
- return []
659
- if isinstance(resultingCluster, topologic.Wire):
660
- return [resultingCluster]
661
- result = Topology.SubTopologies(resultingCluster, subTopologyType="wire")
662
- if not result:
663
- return [] #Make sure you return an empty list instead of None
664
- return result
665
-
666
- @staticmethod
667
- def FreeEdges(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
668
- """
669
- Returns the free edges of the input cluster that are not part of a higher topology.
670
-
671
- Parameters
672
- ----------
673
- cluster : topologic.Cluster
674
- The input cluster.
675
- tolerance : float, optional
676
- The desired tolerance. The default is 0.0001.
677
-
678
- Returns
679
- -------
680
- list
681
- The list of free edges.
682
-
683
- """
684
- from topologicpy.Wire import Wire
685
- from topologicpy.Topology import Topology
686
-
687
- if not isinstance(cluster, topologic.Cluster):
688
- print("Cluster.FreeEdges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
689
- return None
690
- allEdges = []
691
- _ = cluster.Edges(None, allEdges)
692
- if len(allEdges) < 1:
693
- return []
694
- allEdgesCluster = Cluster.ByTopologies(allEdges)
695
- wires = []
696
- _ = cluster.Wires(None, wires)
697
- wireEdges = []
698
- for wire in wires:
699
- tempEdges = Wire.Edges(wire)
700
- wireEdges += tempEdges
701
- if len(wireEdges) == 0:
702
- return allEdges
703
- wireCluster = Cluster.ByTopologies(wireEdges)
704
- resultingCluster = Topology.Boolean(allEdgesCluster, wireCluster, operation="difference", tolerance=tolerance)
705
- if resultingCluster == None:
706
- return []
707
- if isinstance(resultingCluster, topologic.Edge):
708
- return [resultingCluster]
709
- result = Topology.SubTopologies(resultingCluster, subTopologyType="edge")
710
- if result == None:
711
- return [] #Make sure you return an empty list instead of None
712
- return result
713
-
714
- @staticmethod
715
- def FreeVertices(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
716
- """
717
- Returns the free vertices of the input cluster that are not part of a higher topology.
718
-
719
- Parameters
720
- ----------
721
- cluster : topologic.Cluster
722
- The input cluster.
723
- tolerance : float , optional
724
- The desired tolerance. The default is 0.0001.
725
-
726
- Returns
727
- -------
728
- list
729
- The list of free vertices.
730
-
731
- """
732
- from topologicpy.Edge import Edge
733
- from topologicpy.Topology import Topology
734
-
735
- if not isinstance(cluster, topologic.Cluster):
736
- print("Cluster.FreeVertices - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
737
- return None
738
- allVertices = []
739
- _ = cluster.Vertices(None, allVertices)
740
- if len(allVertices) < 1:
741
- return []
742
- allVerticesCluster = Cluster.ByTopologies(allVertices)
743
- edges = []
744
- _ = cluster.Edges(None, edges)
745
- edgesVertices = []
746
- for edge in edges:
747
- tempVertices = Edge.Vertices(edge)
748
- edgesVertices += tempVertices
749
- if len(edgesVertices) == 0:
750
- return allVertices
751
- edgesCluster = Cluster.ByTopologies(edgesVertices)
752
- resultingCluster = Topology.Boolean(allVerticesCluster, edgesCluster, operation="difference", tolerance=tolerance)
753
- if isinstance(resultingCluster, topologic.Vertex):
754
- return [resultingCluster]
755
- if resultingCluster == None:
756
- return []
757
- result = Topology.SubTopologies(resultingCluster, subTopologyType="vertex")
758
- if result == None:
759
- return [] #Make sure you return an empty list instead of None
760
- return result
761
-
762
- @staticmethod
763
- def FreeTopologies(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
764
- """
765
- Returns the free topologies of the input cluster that are not part of a higher topology.
766
-
767
- Parameters
768
- ----------
769
- cluster : topologic.Cluster
770
- The input cluster.
771
- tolerance : float , optional
772
- The desired tolerance. The default is 0.0001.
773
-
774
- Returns
775
- -------
776
- list
777
- The list of free topologies.
778
-
779
- """
780
- topologies = Cluster.FreeVertices(cluster, tolerance=tolerance)
781
- topologies += Cluster.FreeEdges(cluster, tolerance=tolerance)
782
- topologies += Cluster.FreeWires(cluster, tolerance=tolerance)
783
- topologies += Cluster.FreeFaces(cluster, tolerance=tolerance)
784
- topologies += Cluster.FreeShells(cluster, tolerance=tolerance)
785
- topologies += Cluster.FreeCells(cluster, tolerance=tolerance)
786
- topologies += Cluster.CellComplexes(cluster)
787
-
788
- return topologies
789
-
790
- @staticmethod
791
- def HighestType(cluster: topologic.Cluster) -> int:
792
- """
793
- Returns the type of the highest dimension subtopology found in the input cluster.
794
-
795
- Parameters
796
- ----------
797
- cluster : topologic.Cluster
798
- The input cluster.
799
-
800
- Returns
801
- -------
802
- int
803
- The type of the highest dimension subtopology found in the input cluster.
804
-
805
- """
806
- if not isinstance(cluster, topologic.Cluster):
807
- print("Cluster.HighestType - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
808
- return None
809
- cellComplexes = Cluster.CellComplexes(cluster)
810
- if len(cellComplexes) > 0:
811
- return topologic.CellComplex.Type()
812
- cells = Cluster.Cells(cluster)
813
- if len(cells) > 0:
814
- return topologic.Cell.Type()
815
- shells = Cluster.Shells(cluster)
816
- if len(shells) > 0:
817
- return topologic.Shell.Type()
818
- faces = Cluster.Faces(cluster)
819
- if len(faces) > 0:
820
- return topologic.Face.Type()
821
- wires = Cluster.Wires(cluster)
822
- if len(wires) > 0:
823
- return topologic.Wire.Type()
824
- edges = Cluster.Edges(cluster)
825
- if len(edges) > 0:
826
- return topologic.Edge.Type()
827
- vertices = Cluster.Vertices(cluster)
828
- if len(vertices) > 0:
829
- return topologic.Vertex.Type()
830
-
831
- @staticmethod
832
- def K_Means(topologies, selectors=None, keys=["x", "y", "z"], k=4, maxIterations=100, centroidKey="k_centroid"):
833
- """
834
- Clusters the input topologies using K-Means clustering. See https://en.wikipedia.org/wiki/K-means_clustering
835
-
836
- Parameters
837
- ----------
838
- topologies : list
839
- The input list of topologies. If this is not a list of topologic vertices then please provide a list of selectors
840
- selectors : list , optional
841
- If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies.
842
- If set to None, the list of topologies is expected to be a list of vertices. The default is None.
843
- keys : list, optional
844
- The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included,
845
- make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
846
- k : int , optional
847
- The desired number of clusters. The default is 4.
848
- maxIterations : int , optional
849
- The desired maximum number of iterations for the clustering algorithm
850
- centroidKey : str , optional
851
- The desired dictionary key under which to store the cluster's centroid (this is not to be confused with the actual geometric centroid of the cluster). The default is "k_centroid"
852
-
853
- Returns
854
- -------
855
- list
856
- The created list of clusters.
857
-
858
- """
859
- from topologicpy.Helper import Helper
860
- from topologicpy.Vertex import Vertex
861
- from topologicpy.Dictionary import Dictionary
862
- from topologicpy.Topology import Topology
863
-
864
-
865
- def k_means(data, vertices, k=4, maxIterations=100):
866
- import random
867
- def euclidean_distance(p, q):
868
- return sum((pi - qi) ** 2 for pi, qi in zip(p, q)) ** 0.5
869
-
870
- # Initialize k centroids randomly
871
- centroids = random.sample(data, k)
872
-
873
- for _ in range(maxIterations):
874
- # Assign each data point to the nearest centroid
875
- clusters = [[] for _ in range(k)]
876
- clusters_v = [[] for _ in range(k)]
877
- for i, point in enumerate(data):
878
- distances = [euclidean_distance(point, centroid) for centroid in centroids]
879
- nearest_centroid_index = distances.index(min(distances))
880
- clusters[nearest_centroid_index].append(point)
881
- clusters_v[nearest_centroid_index].append(vertices[i])
882
-
883
- # Compute the new centroids as the mean of the points in each cluster
884
- new_centroids = []
885
- for cluster in clusters:
886
- if not cluster:
887
- # If a cluster is empty, keep the previous centroid
888
- new_centroids.append(centroids[clusters.index(cluster)])
889
- else:
890
- new_centroids.append([sum(dim) / len(cluster) for dim in zip(*cluster)])
891
-
892
- # Check if the centroids have converged
893
- if new_centroids == centroids:
894
- break
895
-
896
- centroids = new_centroids
897
-
898
- return {'clusters': clusters, 'clusters_v': clusters_v, 'centroids': centroids}
899
-
900
-
901
-
902
- if not isinstance(topologies, list):
903
- print("Cluster.K_Means - Error: The input topologies parameter is not a valid list. Returning None.")
904
- return None
905
- topologies = [t for t in topologies if isinstance(t, topologic.Topology)]
906
- if len(topologies) < 1:
907
- print("Cluster.K_Means - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
908
- return None
909
- if not isinstance(selectors, list):
910
- check_vertices = [v for v in topologies if not isinstance(v, topologic.Vertex)]
911
- if len(check_vertices) > 0:
912
- print("Cluster.K_Means - Error: The input selectors parameter is not a valid list and this is needed since the list of topologies contains objects of type other than a topologic.Vertex. Returning None.")
913
- return None
914
- else:
915
- selectors = [s for s in selectors if isinstance(s, topologic.Vertex)]
916
- if len(selectors) < 1:
917
- check_vertices = [v for v in topologies if not isinstance(v, topologic.Vertex)]
918
- if len(check_vertices) > 0:
919
- print("Cluster.K_Means - Error: The input selectors parameter does not contain any valid vertices and this is needed since the list of topologies contains objects of type other than a topologic.Vertex. Returning None.")
920
- return None
921
- if not len(selectors) == len(topologies):
922
- print("Cluster.K_Means - Error: The input topologies and selectors parameters do not have the same length. Returning None.")
923
- return None
924
- if not isinstance(keys, list):
925
- print("Cluster.K_Means - Error: The input keys parameter is not a valid list. Returning None.")
926
- return None
927
- if not isinstance(k , int):
928
- print("Cluster.K_Means - Error: The input k parameter is not a valid integer. Returning None.")
929
- return None
930
- if k < 1:
931
- print("Cluster.K_Means - Error: The input k parameter is less than one. Returning None.")
932
- return None
933
- if len(topologies) < k:
934
- print("Cluster.K_Means - Error: The input topologies parameter is less than the specified number of clusters. Returning None.")
935
- return None
936
- if len(topologies) == k:
937
- t_clusters = []
938
- for topology in topologies:
939
- t_cluster = Cluster.ByTopologies([topology])
940
- for key in keys:
941
- if key.lower() == "x":
942
- value = Vertex.X(t)
943
- elif key.lower() == "y":
944
- value = Vertex.Y(t)
945
- elif key.lower() == "z":
946
- value = Vertex.Z(t)
947
- else:
948
- value = Dictionary.ValueAtKey(d, key)
949
- if value != None:
950
- elements.append(value)
951
- d = Dictionary.ByKeysValues([centroidKey], [elements])
952
- t_cluster = Topology.SetDictionary(t_cluster, d)
953
- t_clusters.append(t_cluster)
954
- return t_clusters
955
-
956
- data = []
957
- if selectors == None:
958
- for t in topologies:
959
- elements = []
960
- if keys:
961
- d = Topology.Dictionary(t)
962
- for key in keys:
963
- if key.lower() == "x":
964
- value = Vertex.X(t)
965
- elif key.lower() == "y":
966
- value = Vertex.Y(t)
967
- elif key.lower() == "z":
968
- value = Vertex.Z(t)
969
- else:
970
- value = Dictionary.ValueAtKey(d, key)
971
- if value != None:
972
- elements.append(value)
973
- data.append(elements)
974
- else:
975
- for i, s in enumerate(selectors):
976
- elements = []
977
- if keys:
978
- d = Topology.Dictionary(topologies[i])
979
- for key in keys:
980
- if key.lower() == "x":
981
- value = Vertex.X(s)
982
- elif key.lower() == "y":
983
- value = Vertex.Y(s)
984
- elif key.lower() == "z":
985
- value = Vertex.Z(s)
986
- else:
987
- value = Dictionary.ValueAtKey(d, key)
988
- if value != None:
989
- elements.append(value)
990
- data.append(elements)
991
- if len(data) == 0:
992
- print("Cluster.K_Means - Error: Could not perform the operation. Returning None.")
993
- return None
994
- if selectors:
995
- dict = k_means(data, selectors, k=k, maxIterations=maxIterations)
996
- else:
997
- dict = k_means(data, topologies, k=k, maxIterations=maxIterations)
998
- clusters = dict['clusters_v']
999
- centroids = dict['centroids']
1000
- t_clusters = []
1001
- for i, cluster in enumerate(clusters):
1002
- cluster_vertices = []
1003
- for v in cluster:
1004
- if selectors == None:
1005
- cluster_vertices.append(v)
1006
- else:
1007
- index = selectors.index(v)
1008
- cluster_vertices.append(topologies[index])
1009
- cluster = Cluster.ByTopologies(cluster_vertices)
1010
- d = Dictionary.ByKeysValues([centroidKey], [centroids[i]])
1011
- cluster = Topology.SetDictionary(cluster, d)
1012
- t_clusters.append(cluster)
1013
- return t_clusters
1014
-
1015
- @staticmethod
1016
- def MergeCells(cells, tolerance=0.0001):
1017
- """
1018
- Creates a cluster that contains cellComplexes where it can create them plus any additional free cells.
1019
-
1020
- Parameters
1021
- ----------
1022
- cells : list
1023
- The input list of cells.
1024
- tolerance : float , optional
1025
- The desired tolerance. The default is 0.0001.
1026
-
1027
- Returns
1028
- -------
1029
- topologic.Cluster
1030
- The created cluster with merged cells as possible.
1031
-
1032
- """
1033
-
1034
- from topologicpy.CellComplex import CellComplex
1035
- from topologicpy.Topology import Topology
1036
-
1037
- def find_cell_complexes(cells, adjacency_test, tolerance=0.0001):
1038
- cell_complexes = []
1039
- remaining_cells = set(cells)
1040
-
1041
- def explore_complex(cell_complex, remaining, tolerance=0.0001):
1042
- new_cells = set()
1043
- for cell in remaining:
1044
- if any(adjacency_test(cell, existing_cell, tolerance=tolerance) for existing_cell in cell_complex):
1045
- new_cells.add(cell)
1046
- return new_cells
1047
-
1048
- while remaining_cells:
1049
- current_cell = remaining_cells.pop()
1050
- current_complex = {current_cell}
1051
- current_complex.update(explore_complex(current_complex, remaining_cells, tolerance=tolerance))
1052
- cell_complexes.append(current_complex)
1053
- remaining_cells -= current_complex
1054
-
1055
- return cell_complexes
1056
-
1057
- # Example adjacency test function (replace this with your actual implementation)
1058
- def adjacency_test(cell1, cell2, tolerance=0.0001):
1059
- return isinstance(Topology.Merge(cell1, cell2, tolerance=tolerance), topologic.CellComplex)
1060
-
1061
- if not isinstance(cells, list):
1062
- print("Cluster.MergeCells - Error: The input cells parameter is not a valid list of cells. Returning None.")
1063
- return None
1064
- #cells = [cell for cell in cells if isinstance(cell, topologic.Cell)]
1065
- if len(cells) < 1:
1066
- print("Cluster.MergeCells - Error: The input cells parameter does not contain any valid cells. Returning None.")
1067
- return None
1068
-
1069
- complexes = find_cell_complexes(cells, adjacency_test)
1070
- cellComplexes = []
1071
- cells = []
1072
- for aComplex in complexes:
1073
- aComplex = list(aComplex)
1074
- if len(aComplex) > 1:
1075
- cc = CellComplex.ByCells(aComplex, silent=True)
1076
- if isinstance(cc, topologic.CellComplex):
1077
- cellComplexes.append(cc)
1078
- elif len(aComplex) == 1:
1079
- if isinstance(aComplex[0], topologic.Cell):
1080
- cells.append(aComplex[0])
1081
- return Cluster.ByTopologies(cellComplexes+cells)
1082
-
1083
- @staticmethod
1084
- def MysticRose(wire: topologic.Wire = None, origin: topologic.Vertex = None, radius: float = 0.5, sides: int = 16, perimeter: bool = True, direction: list = [0, 0, 1], placement:str = "center", tolerance: float = 0.0001) -> topologic.Cluster:
1085
- """
1086
- Creates a mystic rose.
1087
-
1088
- Parameters
1089
- ----------
1090
- wire : topologic.Wire , optional
1091
- The input Wire. if set to None, a circle with the input parameters is created. Otherwise, the input parameters are ignored.
1092
- origin : topologic.Vertex , optional
1093
- The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
1094
- radius : float , optional
1095
- The radius of the mystic rose. The default is 1.
1096
- sides : int , optional
1097
- The number of sides of the mystic rose. The default is 16.
1098
- perimeter : bool , optional
1099
- If True, the perimeter edges are included in the output. The default is True.
1100
- direction : list , optional
1101
- The vector representing the up direction of the mystic rose. The default is [0, 0, 1].
1102
- placement : str , optional
1103
- The description of the placement of the origin of the mystic rose. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
1104
- tolerance : float , optional
1105
- The desired tolerance. The default is 0.0001.
1106
-
1107
- Returns
1108
- -------
1109
- topologic.cluster
1110
- The created mystic rose (cluster of edges).
1111
-
1112
- """
1113
- import topologicpy
1114
- from topologicpy.Vertex import Vertex
1115
- from topologicpy.Edge import Edge
1116
- from topologicpy.Wire import Wire
1117
- from topologicpy.Cluster import Cluster
1118
- from itertools import combinations
1119
-
1120
- if wire == None:
1121
- wire = Wire.Circle(origin=origin, radius=radius, sides=sides, fromAngle=0, toAngle=360, close=True, direction=direction, placement=placement, tolerance=tolerance)
1122
- if not Wire.IsClosed(wire):
1123
- print("Cluster.MysticRose - Error: The input wire parameter is not a closed topologic wire. Returning None.")
1124
- return None
1125
- vertices = Wire.Vertices(wire)
1126
- indices = list(range(len(vertices)))
1127
- combs = [[comb[0],comb[1]] for comb in combinations(indices, 2) if not (abs(comb[0]-comb[1]) == 1) and not (abs(comb[0]-comb[1]) == len(indices)-1)]
1128
- edges = []
1129
- if perimeter:
1130
- edges = Wire.Edges(wire)
1131
- for comb in combs:
1132
- edges.append(Edge.ByVertices([vertices[comb[0]], vertices[comb[1]]], tolerance=tolerance))
1133
- return Cluster.ByTopologies(edges)
1134
-
1135
- @staticmethod
1136
- def Shells(cluster: topologic.Cluster) -> list:
1137
- """
1138
- Returns the shells of the input cluster.
1139
-
1140
- Parameters
1141
- ----------
1142
- cluster : topologic.Cluster
1143
- The input cluster.
1144
-
1145
- Returns
1146
- -------
1147
- list
1148
- The list of shells.
1149
-
1150
- """
1151
- if not isinstance(cluster, topologic.Cluster):
1152
- print("Cluster.Shells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
1153
- return None
1154
- shells = []
1155
- _ = cluster.Shells(None, shells)
1156
- return shells
1157
-
1158
- @staticmethod
1159
- def Simplify(cluster: topologic.Cluster):
1160
- """
1161
- Simplifies the input cluster if possible. For example, if the cluster contains only one cell, that cell is returned.
1162
-
1163
- Parameters
1164
- ----------
1165
- cluster : topologic.Cluster
1166
- The input cluster.
1167
-
1168
- Returns
1169
- -------
1170
- topologic.Topology or list
1171
- The simplification of the cluster.
1172
-
1173
- """
1174
- if not isinstance(cluster, topologic.Cluster):
1175
- print("Cluster.Simplify - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
1176
- return None
1177
- resultingTopologies = []
1178
- topCC = []
1179
- _ = cluster.CellComplexes(None, topCC)
1180
- topCells = []
1181
- _ = cluster.Cells(None, topCells)
1182
- topShells = []
1183
- _ = cluster.Shells(None, topShells)
1184
- topFaces = []
1185
- _ = cluster.Faces(None, topFaces)
1186
- topWires = []
1187
- _ = cluster.Wires(None, topWires)
1188
- topEdges = []
1189
- _ = cluster.Edges(None, topEdges)
1190
- topVertices = []
1191
- _ = cluster.Vertices(None, topVertices)
1192
- if len(topCC) == 1:
1193
- cc = topCC[0]
1194
- ccVertices = []
1195
- _ = cc.Vertices(None, ccVertices)
1196
- if len(topVertices) == len(ccVertices):
1197
- resultingTopologies.append(cc)
1198
- if len(topCC) == 0 and len(topCells) == 1:
1199
- cell = topCells[0]
1200
- ccVertices = []
1201
- _ = cell.Vertices(None, ccVertices)
1202
- if len(topVertices) == len(ccVertices):
1203
- resultingTopologies.append(cell)
1204
- if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 1:
1205
- shell = topShells[0]
1206
- ccVertices = []
1207
- _ = shell.Vertices(None, ccVertices)
1208
- if len(topVertices) == len(ccVertices):
1209
- resultingTopologies.append(shell)
1210
- if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 1:
1211
- face = topFaces[0]
1212
- ccVertices = []
1213
- _ = face.Vertices(None, ccVertices)
1214
- if len(topVertices) == len(ccVertices):
1215
- resultingTopologies.append(face)
1216
- if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 1:
1217
- wire = topWires[0]
1218
- ccVertices = []
1219
- _ = wire.Vertices(None, ccVertices)
1220
- if len(topVertices) == len(ccVertices):
1221
- resultingTopologies.append(wire)
1222
- if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 0 and len(topEdges) == 1:
1223
- edge = topEdges[0]
1224
- ccVertices = []
1225
- _ = wire.Vertices(None, ccVertices)
1226
- if len(topVertices) == len(ccVertices):
1227
- resultingTopologies.append(edge)
1228
- if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 0 and len(topEdges) == 0 and len(topVertices) == 1:
1229
- vertex = topVertices[0]
1230
- resultingTopologies.append(vertex)
1231
- if len(resultingTopologies) == 1:
1232
- return resultingTopologies[0]
1233
- return cluster
1234
-
1235
- @staticmethod
1236
- def Vertices(cluster: topologic.Cluster) -> list:
1237
- """
1238
- Returns the vertices of the input cluster.
1239
-
1240
- Parameters
1241
- ----------
1242
- cluster : topologic.Cluster
1243
- The input cluster.
1244
-
1245
- Returns
1246
- -------
1247
- list
1248
- The list of vertices.
1249
-
1250
- """
1251
- if not isinstance(cluster, topologic.Cluster):
1252
- print("Cluster.Vertices - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
1253
- return None
1254
- vertices = []
1255
- _ = cluster.Vertices(None, vertices)
1256
- return vertices
1257
-
1258
- @staticmethod
1259
- def Wires(cluster: topologic.Cluster) -> list:
1260
- """
1261
- Returns the wires of the input cluster.
1262
-
1263
- Parameters
1264
- ----------
1265
- cluster : topologic.Cluster
1266
- The input cluster.
1267
-
1268
- Returns
1269
- -------
1270
- list
1271
- The list of wires.
1272
-
1273
- """
1274
- if not isinstance(cluster, topologic.Cluster):
1275
- print("Cluster.Wires - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
1276
- return None
1277
- wires = []
1278
- _ = cluster.Wires(None, wires)
1279
- return wires
1280
-
1
+ # Copyright (C) 2024
2
+ # Wassim Jabi <wassim.jabi@gmail.com>
3
+ #
4
+ # This program is free software: you can redistribute it and/or modify it under
5
+ # the terms of the GNU Affero General Public License as published by the Free Software
6
+ # Foundation, either version 3 of the License, or (at your option) any later
7
+ # version.
8
+ #
9
+ # This program is distributed in the hope that it will be useful, but WITHOUT
10
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
12
+ # details.
13
+ #
14
+ # You should have received a copy of the GNU Affero General Public License along with
15
+ # this program. If not, see <https://www.gnu.org/licenses/>.
16
+
17
+ from topologicpy.Topology import Topology
18
+ import topologic_core as topologic
19
+ import os
20
+ import warnings
21
+
22
+ try:
23
+ import numpy as np
24
+ except:
25
+ print("Cluster - Installing required numpy library.")
26
+ try:
27
+ os.system("pip install numpy")
28
+ except:
29
+ os.system("pip install numpy --user")
30
+ try:
31
+ import numpy as np
32
+ print("Cluster - numpy library installed correctly.")
33
+ except:
34
+ warnings.warn("Cluster - Error: Could not import numpy.")
35
+
36
+ try:
37
+ from scipy.spatial.distance import pdist, squareform
38
+ except:
39
+ print("Cluster - Installing required scipy library.")
40
+ try:
41
+ os.system("pip install scipy")
42
+ except:
43
+ os.system("pip install scipy --user")
44
+ try:
45
+ from scipy.spatial.distance import pdist, squareform
46
+ print("Cluster - scipy library installed correctly.")
47
+ except:
48
+ warnings.warn("Cluster - Error: Could not import scipy.")
49
+
50
+ class Cluster(Topology):
51
+ @staticmethod
52
+ def ByFormula(formula, xRange=None, yRange=None, xString="X", yString="Y"):
53
+ """
54
+ Creates a cluster of vertices by evaluating the input formula for a range of x values and, optionally, a range of y values.
55
+
56
+ Parameters
57
+ ----------
58
+ formula : str
59
+ A string representing the formula to be evaluated.
60
+ For 2D formulas (i.e. Z = 0), use either 'X' (uppercase) or 'Y' (uppercase) for the independent variable.
61
+ For 3D formulas, use 'X' and 'Y' (uppercase) for the independent variables. The Z value will be evaluated.
62
+ For 3D formulas, both xRange and yRange MUST be specified.
63
+ You can use standard math functions like 'sin', 'cos', 'tan', 'sqrt', etc.
64
+ For example, 'X**2 + 2*X - sqrt(X)' or 'cos(abs(X)+abs(Y))'
65
+ xRange : tuple , optional
66
+ A tuple (start, end, step) representing the range of X values for which the formula should be evaluated.
67
+ For example, to evaluate Y for X values from -5 to 5 with a step of 0.1, you should specify xRange=(-5, 5, 0.1).
68
+ If the xRange is set to None or not specified:
69
+ The method assumes that the formula uses the yString (e.g. 'Y' as in 'Y**2 + 2*Y - sqrt(Y)')
70
+ The method will attempt to evaluate X based on the specified yRange.
71
+ xRange and yRange CANNOT be None or unspecified at the same time. One or the other must be specified.
72
+ yRange : tuple , optional
73
+ A tuple (start, end, step) representing the range of Y values for which the formula should be evaluated.
74
+ For example, to evaluate X for Y values from -5 to 5 with a step of 0.1, you should specify yRange=(-5,5,0.1).
75
+ If the yRange is set to None or not specified:
76
+ The method assumes that the formula uses the xString (e.g. 'X' as in 'X**2 + 2*X - sqrt(X)')
77
+ The method will attempt to evaluate Y based on the specified xRange.
78
+ xRange and yRange CANNOT be None or unspecified at the same time. One or the other must be specified.
79
+ xString : str , optional
80
+ The string used to represent the X independent variable. The default is 'X' (uppercase).
81
+ yString : str , optional
82
+ The string used to represent the Y independent variable. The default is 'Y' (uppercase).
83
+
84
+ Returns:
85
+ topologic.Cluster
86
+ The created cluster of vertices.
87
+ """
88
+ from topologicpy.Vertex import Vertex
89
+ import math
90
+ if xRange == None and yRange == None:
91
+ print("Cluster.ByFormula - Error: Both ranges cannot be None at the same time. Returning None.")
92
+ return None
93
+ if xString.islower():
94
+ print("Cluster.ByFormula - Error: the input xString cannot lowercase. Please consider using uppercase (e.g. X). Returning None.")
95
+ return None
96
+ if yString == 'y':
97
+ print("Cluster.ByFormula - Error: the input yString cannot be lowercase. Please consider using uppercase (e.g. Y). Returning None.")
98
+ return None
99
+
100
+ x_values = []
101
+ y_values = []
102
+ if not xRange == None:
103
+ x_start, x_end, x_step = xRange
104
+ x = x_start
105
+ while x < x_end:
106
+ x_values.append(x)
107
+ x = x + x_step
108
+ x_values.append(x_end)
109
+
110
+ if not yRange == None:
111
+ y_start, y_end, y_step = yRange
112
+ y = y_start
113
+ while y < y_end:
114
+ y_values.append(y)
115
+ y = y + y_step
116
+ y_values.append(y_end)
117
+
118
+ # Evaluate the formula for each x and y value
119
+ x_return = []
120
+ y_return = []
121
+ z_return = []
122
+ if len(x_values) > 0 and len(y_values) > 0: # Both X and Y exist, compute Z.
123
+ for x in x_values:
124
+ for y in y_values:
125
+ x_return.append(x)
126
+ y_return.append(y)
127
+ formula1 = formula.replace(xString, str(x)).replace(yString, str(y)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
128
+ z_return.append(eval(formula1))
129
+ elif len(x_values) == 0 and len(y_values) > 0: # Only Y exists, compute X, Z is always 0.
130
+ for y in y_values:
131
+ y_return.append(y)
132
+ formula1 = formula.replace(xString, str(y)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
133
+ x_return.append(eval(formula1))
134
+ z_return.append(0)
135
+ else: # Only X exists, compute Y, Z is always 0.
136
+ for x in x_values:
137
+ x_return.append(x)
138
+ formula1 = formula.replace(xString, str(x)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
139
+ y_return.append(eval(formula1))
140
+ z_return.append(0)
141
+ vertices = []
142
+ for i in range(len(x_return)):
143
+ vertices.append(Vertex.ByCoordinates(x_return[i], y_return[i], z_return[i]))
144
+ return Cluster.ByTopologies(vertices)
145
+
146
+ @staticmethod
147
+ def ByTopologies(*args, transferDictionaries: bool = False) -> topologic.Cluster:
148
+ """
149
+ Creates a topologic Cluster from the input list of topologies. The input can be individual topologies each as an input argument or a list of topologies stored in one input argument.
150
+
151
+ Parameters
152
+ ----------
153
+ topologies : list
154
+ The list of topologies.
155
+ transferDictionaries : bool , optional
156
+ If set to True, the dictionaries from the input topologies are merged and transferred to the cluster. Otherwise they are not. The default is False.
157
+
158
+ Returns
159
+ -------
160
+ topologic.Cluster
161
+ The created topologic Cluster.
162
+
163
+ """
164
+ from topologicpy.Dictionary import Dictionary
165
+ from topologicpy.Topology import Topology
166
+ from topologicpy.Helper import Helper
167
+
168
+ if len(args) == 0:
169
+ print("Cluster.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
170
+ return None
171
+ if len(args) == 1:
172
+ topologies = args[0]
173
+ if isinstance(topologies, list):
174
+ if len(topologies) == 0:
175
+ print("Cluster.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
176
+ return None
177
+ else:
178
+ topologyList = [x for x in topologies if isinstance(x, topologic.Topology)]
179
+ if len(topologies) == 0:
180
+ print("Cluster.ByTopologies - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
181
+ return None
182
+ else:
183
+ print("Cluster.ByTopologies - Warning: The input topologies parameter contains only one topology. Returning the same topology.")
184
+ return topologies
185
+ else:
186
+ topologyList = Helper.Flatten(list(args))
187
+ topologyList = [x for x in topologyList if isinstance(x, topologic.Topology)]
188
+ if len(topologyList) == 0:
189
+ print("Cluster.ByTopologies - Error: The input parameters do not contain any valid topologies. Returning None.")
190
+ return None
191
+ cluster = topologic.Cluster.ByTopologies(topologyList, False)
192
+ dictionaries = []
193
+ for t in topologyList:
194
+ d = Topology.Dictionary(t)
195
+ keys = Dictionary.Keys(d)
196
+ if isinstance(keys, list):
197
+ if len(keys) > 0:
198
+ dictionaries.append(d)
199
+ if len(dictionaries) > 0:
200
+ if len(dictionaries) > 1:
201
+ d = Dictionary.ByMergedDictionaries(dictionaries)
202
+ else:
203
+ d = dictionaries[0]
204
+ cluster = Topology.SetDictionary(cluster, d)
205
+ return cluster
206
+
207
+ @staticmethod
208
+ def CellComplexes(cluster: topologic.Cluster) -> list:
209
+ """
210
+ Returns the cellComplexes of the input cluster.
211
+
212
+ Parameters
213
+ ----------
214
+ cluster : topologic.Cluster
215
+ The input cluster.
216
+
217
+ Returns
218
+ -------
219
+ list
220
+ The list of cellComplexes.
221
+
222
+ """
223
+ if not isinstance(cluster, topologic.Cluster):
224
+ print("Cluster.CellComplexes - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
225
+ return None
226
+ cellComplexes = []
227
+ _ = cluster.CellComplexes(None, cellComplexes)
228
+ return cellComplexes
229
+
230
+ @staticmethod
231
+ def Cells(cluster: topologic.Cluster) -> list:
232
+ """
233
+ Returns the cells of the input cluster.
234
+
235
+ Parameters
236
+ ----------
237
+ cluster : topologic.Cluster
238
+ The input cluster.
239
+
240
+ Returns
241
+ -------
242
+ list
243
+ The list of cells.
244
+
245
+ """
246
+ if not isinstance(cluster, topologic.Cluster):
247
+ print("Cluster.Cells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
248
+ return None
249
+ cells = []
250
+ _ = cluster.Cells(None, cells)
251
+ return cells
252
+
253
+
254
+ @staticmethod
255
+ def DBSCAN(topologies, selectors=None, keys=["x", "y", "z"], epsilon: float = 0.5, minSamples: int = 2):
256
+ """
257
+ Clusters the input vertices based on the Density-Based Spatial Clustering of Applications with Noise (DBSCAN) method. See https://en.wikipedia.org/wiki/DBSCAN
258
+
259
+ Parameters
260
+ ----------
261
+ topologies : list
262
+ The input list of topologies to be clustered.
263
+ selectors : list , optional
264
+ If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies.
265
+ If set to None, the list of topologies is expected to be a list of vertices. The default is None.
266
+ keys : list, optional
267
+ The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included,
268
+ make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
269
+ epsilon : float , optional
270
+ The maximum radius around a data point within which other points are considered to be part of the same sense region (cluster). The default is 0.5.
271
+ minSamples : int , optional
272
+ The minimum number of points required to form a dense region (cluster). The default is 2.
273
+
274
+ Returns
275
+ -------
276
+ list, list
277
+ The list of clusters and the list of vertices considered to be noise if any (otherwise returns None).
278
+
279
+ """
280
+ from topologicpy.Vertex import Vertex
281
+ from topologicpy.Topology import Topology
282
+ from topologicpy.Dictionary import Dictionary
283
+
284
+ def dbscan_3d_indices(data, eps, min_samples):
285
+ """
286
+ DBSCAN clustering algorithm for 3D points.
287
+
288
+ Parameters:
289
+ - data: NumPy array, input data points with X, Y, and Z coordinates.
290
+ - eps: float, maximum distance between two samples for one to be considered as in the neighborhood of the other.
291
+ - min_samples: int, the number of samples (or total weight) in a neighborhood for a point to be considered as a core point.
292
+
293
+ Returns:
294
+ - clusters: List of lists, each list containing the indices of points in a cluster.
295
+ - noise: List of indices, indices of points labeled as noise.
296
+ """
297
+
298
+ # Compute pairwise distances
299
+ dists = squareform(pdist(data))
300
+
301
+ # Initialize labels and cluster ID
302
+ labels = np.full(data.shape[0], -1)
303
+ cluster_id = 0
304
+
305
+ # Iterate through each point
306
+ for i in range(data.shape[0]):
307
+ if labels[i] != -1:
308
+ continue # Skip already processed points
309
+
310
+ # Find neighbors within epsilon distance
311
+ neighbors = np.where(dists[i] < eps)[0]
312
+
313
+ if len(neighbors) < min_samples:
314
+ # Label as noise
315
+ labels[i] = -1
316
+ else:
317
+ # Expand cluster
318
+ cluster_id += 1
319
+ expand_cluster_3d_indices(labels, dists, i, neighbors, cluster_id, eps, min_samples)
320
+
321
+ # Organize indices into clusters and noise
322
+ clusters = [list(np.where(labels == cid)[0]) for cid in range(1, cluster_id + 1)]
323
+ noise = list(np.where(labels == -1)[0])
324
+
325
+ return clusters, noise
326
+
327
+ def expand_cluster_3d_indices(labels, dists, point_index, neighbors, cluster_id, eps, min_samples):
328
+ """
329
+ Expand the cluster around a core point for 3D points.
330
+
331
+ Parameters:
332
+ - labels: NumPy array, cluster labels for each data point.
333
+ - dists: NumPy array, pairwise distances between data points.
334
+ - point_index: int, index of the core point.
335
+ - neighbors: NumPy array, indices of neighbors.
336
+ - cluster_id: int, current cluster ID.
337
+ - eps: float, maximum distance between two samples for one to be considered as in the neighborhood of the other.
338
+ - min_samples: int, the number of samples (or total weight) in a neighborhood for a point to be considered as a core point.
339
+ """
340
+ labels[point_index] = cluster_id
341
+
342
+ i = 0
343
+ while i < len(neighbors):
344
+ current_neighbor = neighbors[i]
345
+
346
+ if labels[current_neighbor] == -1:
347
+ labels[current_neighbor] = cluster_id
348
+
349
+ new_neighbors = np.where(dists[current_neighbor] < eps)[0]
350
+ if len(new_neighbors) >= min_samples:
351
+ neighbors = np.concatenate([neighbors, new_neighbors])
352
+
353
+ elif labels[current_neighbor] == 0:
354
+ labels[current_neighbor] = cluster_id
355
+
356
+ i += 1
357
+
358
+ if not isinstance(topologies, list):
359
+ print("Cluster.DBSCAN - Error: The input vertices parameter is not a valid list. Returning None.")
360
+ return None, None
361
+ topologyList = [t for t in topologies if isinstance(t, topologic.Topology)]
362
+ if len(topologyList) < 1:
363
+ print("Cluster.DBSCAN - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
364
+ return None, None
365
+ if len(topologyList) < minSamples:
366
+ print("Cluster.DBSCAN - Error: The input minSamples parameter cannot be larger than the number of vertices. Returning None.")
367
+ return None, None
368
+
369
+ if not isinstance(selectors, list):
370
+ check_vertices = [t for t in topologyList if not isinstance(t, topologic.Vertex)]
371
+ if len(check_vertices) > 0:
372
+ print("Cluster.DBSCAN - Error: The input selectors parameter is not a valid list and this is needed since the list of topologies contains objects of type other than a topologic.Vertex. Returning None.")
373
+ return None, None
374
+ else:
375
+ selectors = [s for s in selectors if isinstance(s, topologic.Vertex)]
376
+ if len(selectors) < 1:
377
+ check_vertices = [t for t in topologyList if not isinstance(t, topologic.Vertex)]
378
+ if len(check_vertices) > 0:
379
+ print("Cluster.DBSCAN - Error: The input selectors parameter does not contain any valid vertices and this is needed since the list of topologies contains objects of type other than a topologic.Vertex. Returning None.")
380
+ return None, None
381
+ if not len(selectors) == len(topologyList):
382
+ print("Cluster.DBSCAN - Error: The input topologies and selectors parameters do not have the same length. Returning None.")
383
+ return None, None
384
+ if not isinstance(keys, list):
385
+ print("Cluster.DBSCAN - Error: The input keys parameter is not a valid list. Returning None.")
386
+ return None
387
+
388
+
389
+ data = []
390
+ if selectors == None:
391
+ for t in topologyList:
392
+ elements = []
393
+ if keys:
394
+ d = Topology.Dictionary(t)
395
+ for key in keys:
396
+ if key.lower() == "x":
397
+ value = Vertex.X(t)
398
+ elif key.lower() == "y":
399
+ value = Vertex.Y(t)
400
+ elif key.lower() == "z":
401
+ value = Vertex.Z(t)
402
+ else:
403
+ value = Dictionary.ValueAtKey(d, key)
404
+ if value != None:
405
+ elements.append(value)
406
+ data.append(elements)
407
+ else:
408
+ for i, s in enumerate(selectors):
409
+ elements = []
410
+ if keys:
411
+ d = Topology.Dictionary(topologyList[i])
412
+ for key in keys:
413
+ if key.lower() == "x":
414
+ value = Vertex.X(s)
415
+ elif key.lower() == "y":
416
+ value = Vertex.Y(s)
417
+ elif key.lower() == "z":
418
+ value = Vertex.Z(s)
419
+ else:
420
+ value = Dictionary.ValueAtKey(d, key)
421
+ if value != None:
422
+ elements.append(value)
423
+ data.append(elements)
424
+ #coords = [[Vertex.X(v), Vertex.Y(v), Vertex.Z(v)] for v in vertexList]
425
+ clusters, noise = dbscan_3d_indices(np.array(data), epsilon, minSamples)
426
+ tp_clusters = []
427
+ for cluster in clusters:
428
+ tp_clusters.append(Cluster.ByTopologies([topologyList[i] for i in cluster]))
429
+ vert_group = []
430
+ tp_noise = None
431
+ if len(noise) > 0:
432
+ tp_noise = Cluster.ByTopologies([topologyList[i] for i in noise])
433
+ return tp_clusters, tp_noise
434
+
435
+ @staticmethod
436
+ def Edges(cluster: topologic.Cluster) -> list:
437
+ """
438
+ Returns the edges of the input cluster.
439
+
440
+ Parameters
441
+ ----------
442
+ cluster : topologic.Cluster
443
+ The input cluster.
444
+
445
+ Returns
446
+ -------
447
+ list
448
+ The list of edges.
449
+
450
+ """
451
+ if not isinstance(cluster, topologic.Cluster):
452
+ print("Cluster.Edges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
453
+ return None
454
+ edges = []
455
+ _ = cluster.Edges(None, edges)
456
+ return edges
457
+
458
+ @staticmethod
459
+ def Faces(cluster: topologic.Cluster) -> list:
460
+ """
461
+ Returns the faces of the input cluster.
462
+
463
+ Parameters
464
+ ----------
465
+ cluster : topologic.Cluster
466
+ The input cluster.
467
+
468
+ Returns
469
+ -------
470
+ list
471
+ The list of faces.
472
+
473
+ """
474
+ if not isinstance(cluster, topologic.Cluster):
475
+ print("Cluster.Faces - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
476
+ return None
477
+ faces = []
478
+ _ = cluster.Faces(None, faces)
479
+ return faces
480
+
481
+ @staticmethod
482
+ def FreeCells(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
483
+ """
484
+ Returns the free cells of the input cluster that are not part of a higher topology.
485
+
486
+ Parameters
487
+ ----------
488
+ cluster : topologic.Cluster
489
+ The input cluster.
490
+ tolerance : float , optional
491
+ The desired tolerance. The default is 0.0001.
492
+
493
+ Returns
494
+ -------
495
+ list
496
+ The list of free cells.
497
+
498
+ """
499
+ from topologicpy.CellComplex import CellComplex
500
+ from topologicpy.Topology import Topology
501
+
502
+ if not isinstance(cluster, topologic.Cluster):
503
+ print("Cluster.FreeCells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
504
+ return None
505
+ allCells = []
506
+ _ = cluster.Cells(None, allCells)
507
+ if len(allCells) < 1:
508
+ return []
509
+ allCellsCluster = Cluster.ByTopologies(allCells)
510
+ freeCells = []
511
+ cellComplexes = []
512
+ _ = cluster.CellComplexes(None, cellComplexes)
513
+ cellComplexesCells = []
514
+ for cellComplex in cellComplexes:
515
+ tempCells = CellComplex.Cells(cellComplex)
516
+ cellComplexesCells += tempCells
517
+ if len(cellComplexesCells) == 0:
518
+ return allCells
519
+ cellComplexesCluster = Cluster.ByTopologies(cellComplexesCells)
520
+ resultingCluster = Topology.Boolean(allCellsCluster, cellComplexesCluster, operation="difference", tolerance=tolerance)
521
+ if resultingCluster == None:
522
+ return []
523
+ if isinstance(resultingCluster, topologic.Cell):
524
+ return [resultingCluster]
525
+ result = Topology.SubTopologies(resultingCluster, subTopologyType="cell")
526
+ if result == None:
527
+ return [] #Make sure you return an empty list instead of None
528
+ return result
529
+
530
+ @staticmethod
531
+ def FreeShells(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
532
+ """
533
+ Returns the free shells of the input cluster that are not part of a higher topology.
534
+
535
+ Parameters
536
+ ----------
537
+ cluster : topologic.Cluster
538
+ The input cluster.
539
+ tolerance : float, optional
540
+ The desired tolerance. The default is 0.0001.
541
+
542
+ Returns
543
+ -------
544
+ list
545
+ The list of free shells.
546
+
547
+ """
548
+ from topologicpy.Cell import Cell
549
+ from topologicpy.Topology import Topology
550
+
551
+ if not isinstance(cluster, topologic.Cluster):
552
+ print("Cluster.FreeShells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
553
+ return None
554
+ allShells = []
555
+ _ = cluster.Shells(None, allShells)
556
+ if len(allShells) < 1:
557
+ return []
558
+ allShellsCluster = Cluster.ByTopologies(allShells)
559
+ cells = []
560
+ _ = cluster.Cells(None, cells)
561
+ cellsShells = []
562
+ for cell in cells:
563
+ tempShells = Cell.Shells(cell)
564
+ cellsShells += tempShells
565
+ if len(cellsShells) == 0:
566
+ return allShells
567
+ cellsCluster = Cluster.ByTopologies(cellsShells)
568
+ resultingCluster = Topology.Boolean(allShellsCluster, cellsCluster, operation="difference", tolerance=tolerance)
569
+ if resultingCluster == None:
570
+ return []
571
+ if isinstance(resultingCluster, topologic.Shell):
572
+ return [resultingCluster]
573
+ result = Topology.SubTopologies(resultingCluster, subTopologyType="shell")
574
+ if result == None:
575
+ return [] #Make sure you return an empty list instead of None
576
+ return result
577
+
578
+ @staticmethod
579
+ def FreeFaces(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
580
+ """
581
+ Returns the free faces of the input cluster that are not part of a higher topology.
582
+
583
+ Parameters
584
+ ----------
585
+ cluster : topologic.Cluster
586
+ The input cluster.
587
+ tolerance : float , optional
588
+ The desired tolerance. The default is 0.0001.
589
+
590
+ Returns
591
+ -------
592
+ list
593
+ The list of free faces.
594
+
595
+ """
596
+ from topologicpy.Shell import Shell
597
+ from topologicpy.Topology import Topology
598
+
599
+ if not isinstance(cluster, topologic.Cluster):
600
+ print("Cluster.FreeFaces - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
601
+ return None
602
+ allFaces = []
603
+ _ = cluster.Faces(None, allFaces)
604
+ if len(allFaces) < 1:
605
+ return []
606
+ allFacesCluster = Cluster.ByTopologies(allFaces)
607
+ shells = []
608
+ _ = cluster.Shells(None, shells)
609
+ shellFaces = []
610
+ for shell in shells:
611
+ tempFaces = Shell.Faces(shell)
612
+ shellFaces += tempFaces
613
+ if len(shellFaces) == 0:
614
+ return allFaces
615
+ shellCluster = Cluster.ByTopologies(shellFaces)
616
+ resultingCluster = Topology.Boolean(allFacesCluster, shellCluster, operation="difference", tolerance=tolerance)
617
+ if resultingCluster == None:
618
+ return []
619
+ if isinstance(resultingCluster, topologic.Face):
620
+ return [resultingCluster]
621
+ result = Topology.SubTopologies(resultingCluster, subTopologyType="face")
622
+ if result == None:
623
+ return [] #Make sure you return an empty list instead of None
624
+ return result
625
+
626
+ @staticmethod
627
+ def FreeWires(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
628
+ """
629
+ Returns the free wires of the input cluster that are not part of a higher topology.
630
+
631
+ Parameters
632
+ ----------
633
+ cluster : topologic.Cluster
634
+ The input cluster.
635
+ tolerance : float , optional
636
+ The desired tolerance. The default is 0.0001.
637
+
638
+ Returns
639
+ -------
640
+ list
641
+ The list of free wires.
642
+
643
+ """
644
+ from topologicpy.Face import Face
645
+ from topologicpy.Topology import Topology
646
+
647
+ if not isinstance(cluster, topologic.Cluster):
648
+ print("Cluster.FreeWires - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
649
+ return None
650
+ allWires = []
651
+ _ = cluster.Wires(None, allWires)
652
+ if len(allWires) < 1:
653
+ return []
654
+ allWiresCluster = Cluster.ByTopologies(allWires)
655
+ faces = []
656
+ _ = cluster.Faces(None, faces)
657
+ facesWires = []
658
+ for face in faces:
659
+ tempWires = Face.Wires(face)
660
+ facesWires += tempWires
661
+ if len(facesWires) == 0:
662
+ return allWires
663
+ facesCluster = Cluster.ByTopologies(facesWires)
664
+ resultingCluster = Topology.Boolean(allWiresCluster, facesCluster, operation="difference", tolerance=tolerance)
665
+ if resultingCluster == None:
666
+ return []
667
+ if isinstance(resultingCluster, topologic.Wire):
668
+ return [resultingCluster]
669
+ result = Topology.SubTopologies(resultingCluster, subTopologyType="wire")
670
+ if not result:
671
+ return [] #Make sure you return an empty list instead of None
672
+ return result
673
+
674
+ @staticmethod
675
+ def FreeEdges(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
676
+ """
677
+ Returns the free edges of the input cluster that are not part of a higher topology.
678
+
679
+ Parameters
680
+ ----------
681
+ cluster : topologic.Cluster
682
+ The input cluster.
683
+ tolerance : float, optional
684
+ The desired tolerance. The default is 0.0001.
685
+
686
+ Returns
687
+ -------
688
+ list
689
+ The list of free edges.
690
+
691
+ """
692
+ from topologicpy.Wire import Wire
693
+ from topologicpy.Topology import Topology
694
+
695
+ if not isinstance(cluster, topologic.Cluster):
696
+ print("Cluster.FreeEdges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
697
+ return None
698
+ allEdges = []
699
+ _ = cluster.Edges(None, allEdges)
700
+ if len(allEdges) < 1:
701
+ return []
702
+ allEdgesCluster = Cluster.ByTopologies(allEdges)
703
+ wires = []
704
+ _ = cluster.Wires(None, wires)
705
+ wireEdges = []
706
+ for wire in wires:
707
+ tempEdges = Wire.Edges(wire)
708
+ wireEdges += tempEdges
709
+ if len(wireEdges) == 0:
710
+ return allEdges
711
+ wireCluster = Cluster.ByTopologies(wireEdges)
712
+ resultingCluster = Topology.Boolean(allEdgesCluster, wireCluster, operation="difference", tolerance=tolerance)
713
+ if resultingCluster == None:
714
+ return []
715
+ if isinstance(resultingCluster, topologic.Edge):
716
+ return [resultingCluster]
717
+ result = Topology.SubTopologies(resultingCluster, subTopologyType="edge")
718
+ if result == None:
719
+ return [] #Make sure you return an empty list instead of None
720
+ return result
721
+
722
+ @staticmethod
723
+ def FreeVertices(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
724
+ """
725
+ Returns the free vertices of the input cluster that are not part of a higher topology.
726
+
727
+ Parameters
728
+ ----------
729
+ cluster : topologic.Cluster
730
+ The input cluster.
731
+ tolerance : float , optional
732
+ The desired tolerance. The default is 0.0001.
733
+
734
+ Returns
735
+ -------
736
+ list
737
+ The list of free vertices.
738
+
739
+ """
740
+ from topologicpy.Edge import Edge
741
+ from topologicpy.Topology import Topology
742
+
743
+ if not isinstance(cluster, topologic.Cluster):
744
+ print("Cluster.FreeVertices - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
745
+ return None
746
+ allVertices = []
747
+ _ = cluster.Vertices(None, allVertices)
748
+ if len(allVertices) < 1:
749
+ return []
750
+ allVerticesCluster = Cluster.ByTopologies(allVertices)
751
+ edges = []
752
+ _ = cluster.Edges(None, edges)
753
+ edgesVertices = []
754
+ for edge in edges:
755
+ tempVertices = Edge.Vertices(edge)
756
+ edgesVertices += tempVertices
757
+ if len(edgesVertices) == 0:
758
+ return allVertices
759
+ edgesCluster = Cluster.ByTopologies(edgesVertices)
760
+ resultingCluster = Topology.Boolean(allVerticesCluster, edgesCluster, operation="difference", tolerance=tolerance)
761
+ if isinstance(resultingCluster, topologic.Vertex):
762
+ return [resultingCluster]
763
+ if resultingCluster == None:
764
+ return []
765
+ result = Topology.SubTopologies(resultingCluster, subTopologyType="vertex")
766
+ if result == None:
767
+ return [] #Make sure you return an empty list instead of None
768
+ return result
769
+
770
+ @staticmethod
771
+ def FreeTopologies(cluster: topologic.Cluster, tolerance: float = 0.0001) -> list:
772
+ """
773
+ Returns the free topologies of the input cluster that are not part of a higher topology.
774
+
775
+ Parameters
776
+ ----------
777
+ cluster : topologic.Cluster
778
+ The input cluster.
779
+ tolerance : float , optional
780
+ The desired tolerance. The default is 0.0001.
781
+
782
+ Returns
783
+ -------
784
+ list
785
+ The list of free topologies.
786
+
787
+ """
788
+ topologies = Cluster.FreeVertices(cluster, tolerance=tolerance)
789
+ topologies += Cluster.FreeEdges(cluster, tolerance=tolerance)
790
+ topologies += Cluster.FreeWires(cluster, tolerance=tolerance)
791
+ topologies += Cluster.FreeFaces(cluster, tolerance=tolerance)
792
+ topologies += Cluster.FreeShells(cluster, tolerance=tolerance)
793
+ topologies += Cluster.FreeCells(cluster, tolerance=tolerance)
794
+ topologies += Cluster.CellComplexes(cluster)
795
+
796
+ return topologies
797
+
798
+ @staticmethod
799
+ def HighestType(cluster: topologic.Cluster) -> int:
800
+ """
801
+ Returns the type of the highest dimension subtopology found in the input cluster.
802
+
803
+ Parameters
804
+ ----------
805
+ cluster : topologic.Cluster
806
+ The input cluster.
807
+
808
+ Returns
809
+ -------
810
+ int
811
+ The type of the highest dimension subtopology found in the input cluster.
812
+
813
+ """
814
+ if not isinstance(cluster, topologic.Cluster):
815
+ print("Cluster.HighestType - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
816
+ return None
817
+ cellComplexes = Cluster.CellComplexes(cluster)
818
+ if len(cellComplexes) > 0:
819
+ return topologic.CellComplex.Type()
820
+ cells = Cluster.Cells(cluster)
821
+ if len(cells) > 0:
822
+ return topologic.Cell.Type()
823
+ shells = Cluster.Shells(cluster)
824
+ if len(shells) > 0:
825
+ return topologic.Shell.Type()
826
+ faces = Cluster.Faces(cluster)
827
+ if len(faces) > 0:
828
+ return topologic.Face.Type()
829
+ wires = Cluster.Wires(cluster)
830
+ if len(wires) > 0:
831
+ return topologic.Wire.Type()
832
+ edges = Cluster.Edges(cluster)
833
+ if len(edges) > 0:
834
+ return topologic.Edge.Type()
835
+ vertices = Cluster.Vertices(cluster)
836
+ if len(vertices) > 0:
837
+ return topologic.Vertex.Type()
838
+
839
+ @staticmethod
840
+ def K_Means(topologies, selectors=None, keys=["x", "y", "z"], k=4, maxIterations=100, centroidKey="k_centroid"):
841
+ """
842
+ Clusters the input topologies using K-Means clustering. See https://en.wikipedia.org/wiki/K-means_clustering
843
+
844
+ Parameters
845
+ ----------
846
+ topologies : list
847
+ The input list of topologies. If this is not a list of topologic vertices then please provide a list of selectors
848
+ selectors : list , optional
849
+ If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies.
850
+ If set to None, the list of topologies is expected to be a list of vertices. The default is None.
851
+ keys : list, optional
852
+ The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included,
853
+ make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
854
+ k : int , optional
855
+ The desired number of clusters. The default is 4.
856
+ maxIterations : int , optional
857
+ The desired maximum number of iterations for the clustering algorithm
858
+ centroidKey : str , optional
859
+ The desired dictionary key under which to store the cluster's centroid (this is not to be confused with the actual geometric centroid of the cluster). The default is "k_centroid"
860
+
861
+ Returns
862
+ -------
863
+ list
864
+ The created list of clusters.
865
+
866
+ """
867
+ from topologicpy.Helper import Helper
868
+ from topologicpy.Vertex import Vertex
869
+ from topologicpy.Dictionary import Dictionary
870
+ from topologicpy.Topology import Topology
871
+
872
+
873
+ def k_means(data, vertices, k=4, maxIterations=100):
874
+ import random
875
+ def euclidean_distance(p, q):
876
+ return sum((pi - qi) ** 2 for pi, qi in zip(p, q)) ** 0.5
877
+
878
+ # Initialize k centroids randomly
879
+ centroids = random.sample(data, k)
880
+
881
+ for _ in range(maxIterations):
882
+ # Assign each data point to the nearest centroid
883
+ clusters = [[] for _ in range(k)]
884
+ clusters_v = [[] for _ in range(k)]
885
+ for i, point in enumerate(data):
886
+ distances = [euclidean_distance(point, centroid) for centroid in centroids]
887
+ nearest_centroid_index = distances.index(min(distances))
888
+ clusters[nearest_centroid_index].append(point)
889
+ clusters_v[nearest_centroid_index].append(vertices[i])
890
+
891
+ # Compute the new centroids as the mean of the points in each cluster
892
+ new_centroids = []
893
+ for cluster in clusters:
894
+ if not cluster:
895
+ # If a cluster is empty, keep the previous centroid
896
+ new_centroids.append(centroids[clusters.index(cluster)])
897
+ else:
898
+ new_centroids.append([sum(dim) / len(cluster) for dim in zip(*cluster)])
899
+
900
+ # Check if the centroids have converged
901
+ if new_centroids == centroids:
902
+ break
903
+
904
+ centroids = new_centroids
905
+
906
+ return {'clusters': clusters, 'clusters_v': clusters_v, 'centroids': centroids}
907
+
908
+
909
+
910
+ if not isinstance(topologies, list):
911
+ print("Cluster.K_Means - Error: The input topologies parameter is not a valid list. Returning None.")
912
+ return None
913
+ topologies = [t for t in topologies if isinstance(t, topologic.Topology)]
914
+ if len(topologies) < 1:
915
+ print("Cluster.K_Means - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
916
+ return None
917
+ if not isinstance(selectors, list):
918
+ check_vertices = [v for v in topologies if not isinstance(v, topologic.Vertex)]
919
+ if len(check_vertices) > 0:
920
+ print("Cluster.K_Means - Error: The input selectors parameter is not a valid list and this is needed since the list of topologies contains objects of type other than a topologic.Vertex. Returning None.")
921
+ return None
922
+ else:
923
+ selectors = [s for s in selectors if isinstance(s, topologic.Vertex)]
924
+ if len(selectors) < 1:
925
+ check_vertices = [v for v in topologies if not isinstance(v, topologic.Vertex)]
926
+ if len(check_vertices) > 0:
927
+ print("Cluster.K_Means - Error: The input selectors parameter does not contain any valid vertices and this is needed since the list of topologies contains objects of type other than a topologic.Vertex. Returning None.")
928
+ return None
929
+ if not len(selectors) == len(topologies):
930
+ print("Cluster.K_Means - Error: The input topologies and selectors parameters do not have the same length. Returning None.")
931
+ return None
932
+ if not isinstance(keys, list):
933
+ print("Cluster.K_Means - Error: The input keys parameter is not a valid list. Returning None.")
934
+ return None
935
+ if not isinstance(k , int):
936
+ print("Cluster.K_Means - Error: The input k parameter is not a valid integer. Returning None.")
937
+ return None
938
+ if k < 1:
939
+ print("Cluster.K_Means - Error: The input k parameter is less than one. Returning None.")
940
+ return None
941
+ if len(topologies) < k:
942
+ print("Cluster.K_Means - Error: The input topologies parameter is less than the specified number of clusters. Returning None.")
943
+ return None
944
+ if len(topologies) == k:
945
+ t_clusters = []
946
+ for topology in topologies:
947
+ t_cluster = Cluster.ByTopologies([topology])
948
+ for key in keys:
949
+ if key.lower() == "x":
950
+ value = Vertex.X(t)
951
+ elif key.lower() == "y":
952
+ value = Vertex.Y(t)
953
+ elif key.lower() == "z":
954
+ value = Vertex.Z(t)
955
+ else:
956
+ value = Dictionary.ValueAtKey(d, key)
957
+ if value != None:
958
+ elements.append(value)
959
+ d = Dictionary.ByKeysValues([centroidKey], [elements])
960
+ t_cluster = Topology.SetDictionary(t_cluster, d)
961
+ t_clusters.append(t_cluster)
962
+ return t_clusters
963
+
964
+ data = []
965
+ if selectors == None:
966
+ for t in topologies:
967
+ elements = []
968
+ if keys:
969
+ d = Topology.Dictionary(t)
970
+ for key in keys:
971
+ if key.lower() == "x":
972
+ value = Vertex.X(t)
973
+ elif key.lower() == "y":
974
+ value = Vertex.Y(t)
975
+ elif key.lower() == "z":
976
+ value = Vertex.Z(t)
977
+ else:
978
+ value = Dictionary.ValueAtKey(d, key)
979
+ if value != None:
980
+ elements.append(value)
981
+ data.append(elements)
982
+ else:
983
+ for i, s in enumerate(selectors):
984
+ elements = []
985
+ if keys:
986
+ d = Topology.Dictionary(topologies[i])
987
+ for key in keys:
988
+ if key.lower() == "x":
989
+ value = Vertex.X(s)
990
+ elif key.lower() == "y":
991
+ value = Vertex.Y(s)
992
+ elif key.lower() == "z":
993
+ value = Vertex.Z(s)
994
+ else:
995
+ value = Dictionary.ValueAtKey(d, key)
996
+ if value != None:
997
+ elements.append(value)
998
+ data.append(elements)
999
+ if len(data) == 0:
1000
+ print("Cluster.K_Means - Error: Could not perform the operation. Returning None.")
1001
+ return None
1002
+ if selectors:
1003
+ dict = k_means(data, selectors, k=k, maxIterations=maxIterations)
1004
+ else:
1005
+ dict = k_means(data, topologies, k=k, maxIterations=maxIterations)
1006
+ clusters = dict['clusters_v']
1007
+ centroids = dict['centroids']
1008
+ t_clusters = []
1009
+ for i, cluster in enumerate(clusters):
1010
+ cluster_vertices = []
1011
+ for v in cluster:
1012
+ if selectors == None:
1013
+ cluster_vertices.append(v)
1014
+ else:
1015
+ index = selectors.index(v)
1016
+ cluster_vertices.append(topologies[index])
1017
+ cluster = Cluster.ByTopologies(cluster_vertices)
1018
+ d = Dictionary.ByKeysValues([centroidKey], [centroids[i]])
1019
+ cluster = Topology.SetDictionary(cluster, d)
1020
+ t_clusters.append(cluster)
1021
+ return t_clusters
1022
+
1023
+ @staticmethod
1024
+ def MergeCells(cells, tolerance=0.0001):
1025
+ """
1026
+ Creates a cluster that contains cellComplexes where it can create them plus any additional free cells.
1027
+
1028
+ Parameters
1029
+ ----------
1030
+ cells : list
1031
+ The input list of cells.
1032
+ tolerance : float , optional
1033
+ The desired tolerance. The default is 0.0001.
1034
+
1035
+ Returns
1036
+ -------
1037
+ topologic.Cluster
1038
+ The created cluster with merged cells as possible.
1039
+
1040
+ """
1041
+
1042
+ from topologicpy.CellComplex import CellComplex
1043
+ from topologicpy.Topology import Topology
1044
+
1045
+ def find_cell_complexes(cells, adjacency_test, tolerance=0.0001):
1046
+ cell_complexes = []
1047
+ remaining_cells = set(cells)
1048
+
1049
+ def explore_complex(cell_complex, remaining, tolerance=0.0001):
1050
+ new_cells = set()
1051
+ for cell in remaining:
1052
+ if any(adjacency_test(cell, existing_cell, tolerance=tolerance) for existing_cell in cell_complex):
1053
+ new_cells.add(cell)
1054
+ return new_cells
1055
+
1056
+ while remaining_cells:
1057
+ current_cell = remaining_cells.pop()
1058
+ current_complex = {current_cell}
1059
+ current_complex.update(explore_complex(current_complex, remaining_cells, tolerance=tolerance))
1060
+ cell_complexes.append(current_complex)
1061
+ remaining_cells -= current_complex
1062
+
1063
+ return cell_complexes
1064
+
1065
+ # Example adjacency test function (replace this with your actual implementation)
1066
+ def adjacency_test(cell1, cell2, tolerance=0.0001):
1067
+ return isinstance(Topology.Merge(cell1, cell2, tolerance=tolerance), topologic.CellComplex)
1068
+
1069
+ if not isinstance(cells, list):
1070
+ print("Cluster.MergeCells - Error: The input cells parameter is not a valid list of cells. Returning None.")
1071
+ return None
1072
+ #cells = [cell for cell in cells if isinstance(cell, topologic.Cell)]
1073
+ if len(cells) < 1:
1074
+ print("Cluster.MergeCells - Error: The input cells parameter does not contain any valid cells. Returning None.")
1075
+ return None
1076
+
1077
+ complexes = find_cell_complexes(cells, adjacency_test)
1078
+ cellComplexes = []
1079
+ cells = []
1080
+ for aComplex in complexes:
1081
+ aComplex = list(aComplex)
1082
+ if len(aComplex) > 1:
1083
+ cc = CellComplex.ByCells(aComplex, silent=True)
1084
+ if isinstance(cc, topologic.CellComplex):
1085
+ cellComplexes.append(cc)
1086
+ elif len(aComplex) == 1:
1087
+ if isinstance(aComplex[0], topologic.Cell):
1088
+ cells.append(aComplex[0])
1089
+ return Cluster.ByTopologies(cellComplexes+cells)
1090
+
1091
+ @staticmethod
1092
+ def MysticRose(wire: topologic.Wire = None, origin: topologic.Vertex = None, radius: float = 0.5, sides: int = 16, perimeter: bool = True, direction: list = [0, 0, 1], placement:str = "center", tolerance: float = 0.0001) -> topologic.Cluster:
1093
+ """
1094
+ Creates a mystic rose.
1095
+
1096
+ Parameters
1097
+ ----------
1098
+ wire : topologic.Wire , optional
1099
+ The input Wire. if set to None, a circle with the input parameters is created. Otherwise, the input parameters are ignored.
1100
+ origin : topologic.Vertex , optional
1101
+ The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
1102
+ radius : float , optional
1103
+ The radius of the mystic rose. The default is 1.
1104
+ sides : int , optional
1105
+ The number of sides of the mystic rose. The default is 16.
1106
+ perimeter : bool , optional
1107
+ If True, the perimeter edges are included in the output. The default is True.
1108
+ direction : list , optional
1109
+ The vector representing the up direction of the mystic rose. The default is [0, 0, 1].
1110
+ placement : str , optional
1111
+ The description of the placement of the origin of the mystic rose. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
1112
+ tolerance : float , optional
1113
+ The desired tolerance. The default is 0.0001.
1114
+
1115
+ Returns
1116
+ -------
1117
+ topologic.cluster
1118
+ The created mystic rose (cluster of edges).
1119
+
1120
+ """
1121
+ import topologicpy
1122
+ from topologicpy.Vertex import Vertex
1123
+ from topologicpy.Edge import Edge
1124
+ from topologicpy.Wire import Wire
1125
+ from topologicpy.Cluster import Cluster
1126
+ from itertools import combinations
1127
+
1128
+ if wire == None:
1129
+ wire = Wire.Circle(origin=origin, radius=radius, sides=sides, fromAngle=0, toAngle=360, close=True, direction=direction, placement=placement, tolerance=tolerance)
1130
+ if not Wire.IsClosed(wire):
1131
+ print("Cluster.MysticRose - Error: The input wire parameter is not a closed topologic wire. Returning None.")
1132
+ return None
1133
+ vertices = Wire.Vertices(wire)
1134
+ indices = list(range(len(vertices)))
1135
+ combs = [[comb[0],comb[1]] for comb in combinations(indices, 2) if not (abs(comb[0]-comb[1]) == 1) and not (abs(comb[0]-comb[1]) == len(indices)-1)]
1136
+ edges = []
1137
+ if perimeter:
1138
+ edges = Wire.Edges(wire)
1139
+ for comb in combs:
1140
+ edges.append(Edge.ByVertices([vertices[comb[0]], vertices[comb[1]]], tolerance=tolerance))
1141
+ return Cluster.ByTopologies(edges)
1142
+
1143
+ @staticmethod
1144
+ def Shells(cluster: topologic.Cluster) -> list:
1145
+ """
1146
+ Returns the shells of the input cluster.
1147
+
1148
+ Parameters
1149
+ ----------
1150
+ cluster : topologic.Cluster
1151
+ The input cluster.
1152
+
1153
+ Returns
1154
+ -------
1155
+ list
1156
+ The list of shells.
1157
+
1158
+ """
1159
+ if not isinstance(cluster, topologic.Cluster):
1160
+ print("Cluster.Shells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
1161
+ return None
1162
+ shells = []
1163
+ _ = cluster.Shells(None, shells)
1164
+ return shells
1165
+
1166
+ @staticmethod
1167
+ def Simplify(cluster: topologic.Cluster):
1168
+ """
1169
+ Simplifies the input cluster if possible. For example, if the cluster contains only one cell, that cell is returned.
1170
+
1171
+ Parameters
1172
+ ----------
1173
+ cluster : topologic.Cluster
1174
+ The input cluster.
1175
+
1176
+ Returns
1177
+ -------
1178
+ topologic.Topology or list
1179
+ The simplification of the cluster.
1180
+
1181
+ """
1182
+ if not isinstance(cluster, topologic.Cluster):
1183
+ print("Cluster.Simplify - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
1184
+ return None
1185
+ resultingTopologies = []
1186
+ topCC = []
1187
+ _ = cluster.CellComplexes(None, topCC)
1188
+ topCells = []
1189
+ _ = cluster.Cells(None, topCells)
1190
+ topShells = []
1191
+ _ = cluster.Shells(None, topShells)
1192
+ topFaces = []
1193
+ _ = cluster.Faces(None, topFaces)
1194
+ topWires = []
1195
+ _ = cluster.Wires(None, topWires)
1196
+ topEdges = []
1197
+ _ = cluster.Edges(None, topEdges)
1198
+ topVertices = []
1199
+ _ = cluster.Vertices(None, topVertices)
1200
+ if len(topCC) == 1:
1201
+ cc = topCC[0]
1202
+ ccVertices = []
1203
+ _ = cc.Vertices(None, ccVertices)
1204
+ if len(topVertices) == len(ccVertices):
1205
+ resultingTopologies.append(cc)
1206
+ if len(topCC) == 0 and len(topCells) == 1:
1207
+ cell = topCells[0]
1208
+ ccVertices = []
1209
+ _ = cell.Vertices(None, ccVertices)
1210
+ if len(topVertices) == len(ccVertices):
1211
+ resultingTopologies.append(cell)
1212
+ if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 1:
1213
+ shell = topShells[0]
1214
+ ccVertices = []
1215
+ _ = shell.Vertices(None, ccVertices)
1216
+ if len(topVertices) == len(ccVertices):
1217
+ resultingTopologies.append(shell)
1218
+ if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 1:
1219
+ face = topFaces[0]
1220
+ ccVertices = []
1221
+ _ = face.Vertices(None, ccVertices)
1222
+ if len(topVertices) == len(ccVertices):
1223
+ resultingTopologies.append(face)
1224
+ if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 1:
1225
+ wire = topWires[0]
1226
+ ccVertices = []
1227
+ _ = wire.Vertices(None, ccVertices)
1228
+ if len(topVertices) == len(ccVertices):
1229
+ resultingTopologies.append(wire)
1230
+ if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 0 and len(topEdges) == 1:
1231
+ edge = topEdges[0]
1232
+ ccVertices = []
1233
+ _ = wire.Vertices(None, ccVertices)
1234
+ if len(topVertices) == len(ccVertices):
1235
+ resultingTopologies.append(edge)
1236
+ if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 0 and len(topEdges) == 0 and len(topVertices) == 1:
1237
+ vertex = topVertices[0]
1238
+ resultingTopologies.append(vertex)
1239
+ if len(resultingTopologies) == 1:
1240
+ return resultingTopologies[0]
1241
+ return cluster
1242
+
1243
+ @staticmethod
1244
+ def Vertices(cluster: topologic.Cluster) -> list:
1245
+ """
1246
+ Returns the vertices of the input cluster.
1247
+
1248
+ Parameters
1249
+ ----------
1250
+ cluster : topologic.Cluster
1251
+ The input cluster.
1252
+
1253
+ Returns
1254
+ -------
1255
+ list
1256
+ The list of vertices.
1257
+
1258
+ """
1259
+ if not isinstance(cluster, topologic.Cluster):
1260
+ print("Cluster.Vertices - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
1261
+ return None
1262
+ vertices = []
1263
+ _ = cluster.Vertices(None, vertices)
1264
+ return vertices
1265
+
1266
+ @staticmethod
1267
+ def Wires(cluster: topologic.Cluster) -> list:
1268
+ """
1269
+ Returns the wires of the input cluster.
1270
+
1271
+ Parameters
1272
+ ----------
1273
+ cluster : topologic.Cluster
1274
+ The input cluster.
1275
+
1276
+ Returns
1277
+ -------
1278
+ list
1279
+ The list of wires.
1280
+
1281
+ """
1282
+ if not isinstance(cluster, topologic.Cluster):
1283
+ print("Cluster.Wires - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
1284
+ return None
1285
+ wires = []
1286
+ _ = cluster.Wires(None, wires)
1287
+ return wires
1288
+
1281
1289