File: bisectionDecomposition.py

package info (click to toggle)
yade 2025.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 33,308 kB
  • sloc: cpp: 93,298; python: 50,409; sh: 577; makefile: 162
file content (210 lines) | stat: -rw-r--r-- 7,512 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
'''
Module for domain decomposition based on the Orthogonal Recursive bisection Algorithm, see [1] and [2] for full details. 
Working : The master processor assigns subdomains determines the min and max bounds from all the bodies (excluding wall, subdomain and box), 
This space is the subdivided into regions based on a splitting axis. The axis of split is along the largest dimension and each split results into 
2 subregions** until a certain 'depth' based on the number of workers is reached.
The maximum depth/number of levels is defined as log2(N_{w}), where N_{w} is the number of workers. The algorithm can be used for any number of workers.

(c) Deepak Kunhappan, deepak.kn1990@gmail.com, deepak.kunhappan@3sr-grenoble.fr 

'''

from math import ceil, log10, log
from yade.wrapper import *
from yade.minieigenHP import Vector3
# Note: importing mpi4py will scan environment variable for MPI flags (e.g. OMPI_MCA), so because of this import here bisectionDecomposition can't be imported before mpy.configure(), which may change env variables
from mpi4py import MPI
import sys

sys.stderr.write = sys.stdout.write
minVal = -1e+50
maxVal = 1e+50
maxDim = 3
_TAG_DATA_ = 999
_TOL_ = 1e-08

# for coloring bodies.
import yade.coloring as col


class decompBodiesSerial:

	def __init__(self, mpiComm):

		self.comm = mpiComm
		self.numWorkers = self.comm.Get_size() - 1
		# for coloring bodies
		self.colorscale = col.getCol(self.numWorkers)

	def isEqual(self, a, b):
		'''function to check if 2 floats are equal'''

		return abs(a - b) < _TOL_

	def ifPw2(self, numThreads):
		''' Determine if the number of workers is a power of 2 '''

		if numThreads == 0:
			return False
		return numThreads & (numThreads - 1) == 0

	def numThreadsinPw2(self, numThreads):
		''' Express the number of workers as sum in poowers of 2, e.g: 11 = 2^{3} + 2^{1} + 2^{0} '''
		parts = []
		i = 1
		while i <= numThreads:
			if i & numThreads:
				parts.append(i)
			i <<= 1
		return parts

	def setPartList(self, lst, numThreads):
		''' Used when the number of workers are not a power of 2. Bisectioning divides the space into 2 regions, hence equal divisions are created when the number of workers are in power of 2. If not
		one or more workers end up having more number of bodies compared to other workers, leading to a load imbalance. In-order to overcome this we first express the numW in terms sums of 2^{n}, with each 2^{n} defining 
		a sub group in which bisectioning can be done. 
		For example : we have 11 workers, so : 11 = 2^{0} + 2^{1} + 2^{3} = [1, 2, 8] 
		with 1 worker from 1 level of bisectioning,(a part of the domain is assigned to 1 worker) 
		2 workers from 1 level/depth of bisectioning
		8 workers from 3 levels/depth of bisectioning
		'''

		nb = len(lst) // numThreads
		parts = self.numThreadsinPw2(numThreads)
		wIds = [[] for x in range(len(parts))]
		for i in range(len(parts) - 1):
			splitAxis = self.domainPreProc(lst)
			lst = sorted(lst, key=lambda x: x[0][splitAxis])
			splitPt = len(lst) - (nb * parts[i])
			wIds[i] = lst[splitPt:]
			lst = lst[:splitPt]
		wIds[len(parts) - 1] = lst
		return parts, wIds

	def _partitionDomain(self, bList, numW, strideRank):
		''' The partitioning algorithm, sorts the list of bodies in increasing order based on a predetermined axis, (split axis)
		for each level. With one half allocated to one worker and the other to another
		at the final level. '''

		workerIds = [[] for x in range(numW)]
		workerIds[0] = bList
		wProcs = [n for n in range(numW)]
		numLevels = int(log(numW, 2))
		nlevel = 0
		depth = 0
		while (nlevel <= numLevels):
			lpW2 = 1 << nlevel
			lpW2_1 = 1 << (nlevel + 1)
			sIndxs = [x for x in wProcs if x < lpW2]
			rIndxs = [x for x in wProcs if x >= lpW2 and x < lpW2_1]
			for s, r in zip(sIndxs, rIndxs):
				workerIds[s], workerIds[r] = self.partData(workerIds[s], nlevel)
			nlevel += 1

		for i, x in enumerate(workerIds):
			workerIds[i] = [bId[1] for bId in x]

		for i, blst in enumerate(workerIds):
			for x in blst:
				O.bodies[x].subdomain = (i + strideRank) + 1
				O.bodies[x].shape.color = self.colorscale[(i + strideRank) - 1]

	def partData(self, bList, nlevel):
		''' function to sort data and divide into 2 parts. The data is a list containing a tuple of (b.state.pos, b.id) '''

		splitAxis = self.domainPreProc(bList)
		sortD = sorted(bList, key=lambda x: x[0][splitAxis])
		splitPt = len(sortD) // 2
		return sortD[:splitPt], sortD[splitPt:]

	def domainPreProc(self, data):
		''' determines the max extends of the domain, the splitting axis is based on the axis of the largest extend. Also used to determine the dimensionality of the 
		configuration, eg: 2D or 3D. 
		'''

		domainMin = Vector3(maxVal, maxVal, maxVal)
		domainMax = Vector3(minVal, minVal, minVal)
		ndim = -1
		symIndex = -1
		domain2D = False
		splitPlaneIndex = -1
		maxDim = 3

		for x in data:
			pos = x[0]

			domainMin[0] = min(domainMin[0], pos[0])
			domainMin[1] = min(domainMin[1], pos[1])
			domainMin[2] = min(domainMin[2], pos[2])

			domainMax[0] = max(domainMax[0], pos[0])
			domainMax[1] = max(domainMax[1], pos[1])
			domainMax[2] = max(domainMax[2], pos[2])

		for j in range(maxDim):
			if (self.isEqual(domainMin[j], domainMax[j])):
				domain2D = True
				symIndex = j
				ndim = 2
			else:
				ndim = 3

		domainAxisL = [abs(domainMax[0] - domainMin[0])] + [abs(domainMax[1] - domainMin[1])] + [abs(domainMax[2] - domainMin[2])]
		splitPlaneIndex = domainAxisL.index(max(domainAxisL))

		#TODO: find minimum of intersection areas between subdomains, the idea is to use this as a condition to determine if subdomains have to be reset.
		interSectionAreas = [domainAxisL[1] * domainAxisL[2]] + [domainAxisL[0] * domainAxisL[2]] + [domainAxisL[0] * domainAxisL[1]]
		areaMin = None
		if ndim == 3:
			areaMin = interSectionAreas.index(min(interSectionAreas))

		else:
			if symIndex == 0:
				areaMin = min(domainAxisL[1], domainAxisL[2])
			if symIndex == 1:
				areaMin = min(domainAxisL[2], domainAxisL[0])
			if symIndex == 2:
				areaMin = min(domainAxisL[0], domainAxisL[1])

		return splitPlaneIndex

	def checkIfMasterBody(self, b):
		''' check if the body is a boundary body or bodies that would be typically be in master. '''
		if (isinstance(b.shape, Wall)):
			return True
		elif (isinstance(b.shape, Box)):
			return True
		elif (isinstance(b.shape, FluidDomainBbox)):
			return True
		elif (isinstance(b.shape, Facet)):
			return True
		elif (isinstance(b.shape, Subdomain)):
			return True
		else:
			return False

	def partitionDomain(self, fibreList=None):
		''' The main driver function  '''

		blst = []
		if not fibreList:
			blst = [(b.state.pos, b.id) for b in O.bodies if not self.checkIfMasterBody(b)]
		else:
			blst = [(O.bodies[j.cntrId].state.pos, j.cntrId) for j in fibreList]

		if self.ifPw2(self.numWorkers):
			self._partitionDomain(blst, self.numWorkers, strideRank=0)
		else:
			parts, wIds = self.setPartList(blst, self.numWorkers)
			nr = self.numWorkers + 1
			for i in range(len(parts)):
				nr = nr - parts[i]
				self._partitionDomain(wIds[i], parts[i], nr - 1)

		if fibreList:
			for fib in fibreList:
				for j in fib.nodes:
					O.bodies[j].subdomain = O.bodies[fib.cntrId].subdomain
					O.bodies[j].shape.color = O.bodies[fib.cntrId].shape.color
					for intr in O.bodies[j].intrs():
						O.bodies[intr.geom.connectionBody.id].subdomain = O.bodies[fib.cntrId].subdomain
						O.bodies[intr.geom.connectionBody.id].shape.color = O.bodies[fib.cntrId].shape.color