6
6
7
7
8
8
class Node ():
9
+ """Stores data for Octree nodes."""
10
+
9
11
def __init__ (self , middle , dimension ):
12
+ """Method that sets up a node.
13
+
14
+ Method that sets up and declares variables for an octree node.
15
+
16
+ Args:
17
+ middle: Position of center of the position.
18
+ dimension: Length of sides of a node.
19
+ """
10
20
self .particle = None
11
21
self .middle = middle
12
22
self .dimension = dimension
@@ -26,6 +36,15 @@ def get_subnode(self, quad):
26
36
return (self .subnodes [quad [0 ]][quad [1 ]][quad [2 ]])
27
37
28
38
def create_subnode (self , quad ):
39
+ """Method that creates a subnode.
40
+
41
+ Method that determines the middle and dimension of the subnode
42
+ of a specific quadrant of the node. Initializes a subnode and adds
43
+ that subnode to the nodes.
44
+
45
+ Args:
46
+ quad: Quadrant of node.
47
+ """
29
48
dimension = self .dimension / 2
30
49
31
50
x , y , z = 1 , 1 , 1
@@ -36,9 +55,9 @@ def create_subnode(self, quad):
36
55
if quad [2 ] == 1 :
37
56
z = - 1
38
57
39
- middle = [self .middle [0 ] + ((dimension / 2 ) * x ), # quad[0] == 0: value 1, right
40
- self .middle [1 ] + ((dimension / 2 ) * y ), # quad[1] == 0: value 1, front
41
- self .middle [2 ] + ((dimension / 2 ) * z )] # quad[2] == 0: value 1, top
58
+ middle = [self .middle [0 ] + ((dimension / 2 ) * x ), # value 1, right
59
+ self .middle [1 ] + ((dimension / 2 ) * y ), # value 1, front
60
+ self .middle [2 ] + ((dimension / 2 ) * z )] # value 1, top
42
61
node = Node (middle , dimension )
43
62
self .subnodes [quad [0 ]][quad [1 ]][quad [2 ]] = node
44
63
self .nodes .append (node )
@@ -54,6 +73,11 @@ def get_quad(self, point):
54
73
return [x , y , z ]
55
74
56
75
def get_corners (self ):
76
+ """Method that gets corners of a node.
77
+
78
+ Method that get corners of a node for visualization. Iterates through
79
+ the top and bottom for front and back for right and left.
80
+ """
57
81
if self .corners is None :
58
82
self .corners = []
59
83
for x in [1 , - 1 ]: # right or left
@@ -67,13 +91,22 @@ def get_corners(self):
67
91
68
92
def in_bounds (self , point ):
69
93
val = False
70
- if point [0 ] <= self .middle [0 ] + (self .dimension / 2 ) and point [0 ] >= self .middle [0 ] - (self .dimension / 2 ) and \
71
- point [1 ] <= self .middle [1 ] + (self .dimension / 2 ) and point [1 ] >= self .middle [1 ] - (self .dimension / 2 ) and \
72
- point [2 ] <= self .middle [2 ] + (self .dimension / 2 ) and point [2 ] >= self .middle [2 ] - (self .dimension / 2 ):
94
+ if point [0 ] <= self .middle [0 ] + (self .dimension / 2 ) and \
95
+ point [0 ] >= self .middle [0 ] - (self .dimension / 2 ) and \
96
+ point [1 ] <= self .middle [1 ] + (self .dimension / 2 ) and \
97
+ point [1 ] >= self .middle [1 ] - (self .dimension / 2 ) and \
98
+ point [2 ] <= self .middle [2 ] + (self .dimension / 2 ) and \
99
+ point [2 ] >= self .middle [2 ] - (self .dimension / 2 ):
73
100
val = True
74
101
return val
75
102
76
103
def compute_mass_distribution (self ):
104
+ """Method that calculates the mass distribution.
105
+
106
+ Method that calculates the mass distribution of the node based on
107
+ the mass posistions of the subnode weighted by weights of
108
+ the subnodes.
109
+ """
77
110
if self .particle is not None :
78
111
self .center_of_mass = np .array ([* self .particle .position ])
79
112
self .mass = self .particle .mass
@@ -91,41 +124,61 @@ def compute_mass_distribution(self):
91
124
92
125
93
126
class Octree ():
94
- def __init__ (self , particles , root_node , theta ):
127
+ """Handles setup and calculations of the Barnes-Hut octree."""
128
+
129
+ def __init__ (self , particles , root_node , theta , node_type ):
130
+ """Method that sets up an octree.
131
+
132
+ Method that sets up the variables for the octree. Calls functions
133
+ for creation of the octree.
134
+
135
+ Args:
136
+ particles: List of particles that are inserted.
137
+ root_node: Root node of the octree.
138
+ theta: Theta that determines the accuracy of the simulations.
139
+ """
95
140
self .theta = theta
96
141
self .root_node = root_node
97
142
self .particles = particles
143
+ self .node_type = node_type
98
144
for particle in self .particles :
99
145
self .insert_to_node (self .root_node , particle )
100
146
101
147
def insert_to_node (self , node , particle ):
148
+ """Recursive method that inserts particles into the octree.
149
+
150
+ Recursive method that inserts bodies into the octree.
151
+ Checks if particle is in the current node to prevent bounds issues.
152
+ Determines the appropriate child node and gets that subnode.
153
+ If that subnode is empty insert the particle and stop.
154
+ If the child node point is a point node (one particle) turn it into
155
+ a regional node by inserting both particles into it.
156
+ If the child node is a regional node insert the particle.
157
+
158
+ Args:
159
+ node: Quadrant of node.
160
+ particle: Simulation body.
161
+ """
102
162
# check if point is in cuboid of present node
103
163
if not node .in_bounds (particle .position ) and not np .array_equal (particle .position , self .root_node .middle ):
104
164
print ("error particle not in bounds" )
105
165
print (f"middle: { node .middle } , dimension: { node .dimension } , particle position: { particle .position } , type: { type (particle )} " )
106
166
return
107
167
108
- # determine the appropriate child node
109
168
quad = node .get_quad (particle .position )
110
169
if node .get_subnode (quad ) is None :
111
170
node .create_subnode (quad )
112
171
subnode = node .get_subnode (quad )
113
172
114
- # if subnode is empty, insert point, stop insertion
115
173
if subnode .particle is None and len (subnode .nodes ) == 0 : # case empty node
116
174
subnode .insert_particle (particle )
117
175
118
- # If the child node is a point node, replace it with a region node.
119
- # Call insert for the point that just got replaced.
120
- # Set current node as the newly formed regionnode.
121
176
elif subnode .particle is not None : # case point node
122
177
old_particle = subnode .particle
123
178
subnode .insert_particle (None )
124
179
self .insert_to_node (subnode , old_particle )
125
180
self .insert_to_node (subnode , particle )
126
181
127
- # If the child node is a point node, replace it with a region node.
128
- # Call insert for the point that just got replaced. Set current node as the newly formed region node.
129
182
elif subnode .particle is None and len (subnode .nodes ) >= 1 : # case region node
130
183
self .insert_to_node (subnode , particle )
131
184
@@ -143,15 +196,28 @@ def update_forces_collisions(self):
143
196
del self .collision_dic [particle ]
144
197
145
198
def calc_forces (self , node , particle ):
146
- # Gravitational force and collision between two particles
199
+ """Method that calculates the force on an octree particle.
200
+
201
+ Method that calculates the force on an octree particle by iterating
202
+ through the octree.
203
+ If the node is a point node that doesnt hold the body itelf, directly
204
+ calculate the forces.
205
+ If its a regional node and the dimension/distance ratio is smaller
206
+ than theta and the center of mass position is not the same as the
207
+ particle position, calculate the force between the node and
208
+ the particle.
209
+
210
+ Args:
211
+ node: Quadrant of node.
212
+ particle: Simulation body.
213
+ """
147
214
if node .particle is not None and node .particle != particle :
148
215
force , e_pot , distance = self .gravitational_force (particle , node , np .array ([]), np .array ([]))
149
216
particle .force -= force
150
217
particle .e_pot -= e_pot
151
218
if distance < particle .radius + node .particle .radius and particle .mass > node .particle .mass :
152
219
self .collision_dic [particle ].append (node .particle )
153
220
154
- # find regional node were particle is not the center of mass (subnodes particle is particle)
155
221
elif node .particle is None and not np .array_equal (particle .position , node .center_of_mass ):
156
222
distance = np .array ([* particle .position ]) - np .array ([* node .center_of_mass ])
157
223
r = math .sqrt (np .dot (distance , distance ))
@@ -165,6 +231,22 @@ def calc_forces(self, node, particle):
165
231
self .calc_forces (subnode , particle )
166
232
167
233
def gravitational_force (self , particle , node , distance_vec , distance_val ): # can be ragional or point node
234
+ """Method that calculates the force between two particles.
235
+
236
+ Method that calculates the force acted on the particle by
237
+ another particle or a node. Only calculates the distance and vector
238
+ did not have to be calculated for theta.
239
+
240
+ Args:
241
+ particle: Simulation body.
242
+ node: Node of the octree.
243
+ distance_vec: Vector of distance between the bodies.
244
+ distance_val: Magnitude of distance betweent the bodies
245
+
246
+ Returns:
247
+ The force, potential energy and distance between two bodies or
248
+ the body and the node.
249
+ """
168
250
force = np .array ([0. , 0. , 0. ])
169
251
if len (distance_vec ) == 0 and len (distance_val ) == 0 :
170
252
distance = np .array ([* particle .position ]) - np .array ([* node .center_of_mass ])
@@ -179,14 +261,15 @@ def gravitational_force(self, particle, node, distance_vec, distance_val): # ca
179
261
force = (distance / distance_mag ) * force_mag
180
262
return force , e_pot , distance_mag
181
263
182
- def get_all_nodes (self , node , lst ): # all point nodes, could include regional nodes
264
+ def get_all_nodes (self , node , lst ):
183
265
184
266
if node .particle is None and len (node .nodes ) >= 1 or node .particle is not None :
185
267
if len (node .nodes ) >= 1 :
186
- # lst.append(node.get_corners()) # if regional are shown aswell
268
+ if self .node_type == "regional" or self .node_type == "both" :
269
+ lst .append (node .get_corners ())
187
270
for subnode in node .nodes :
188
271
self .get_all_nodes (subnode , lst )
189
- if node .particle is not None :
272
+ if node .particle is not None and ( self . node_type == "point" or self . node_type == "both" ) :
190
273
lst .append (node .get_corners ())
191
274
192
275
0 commit comments