Peano
Loading...
Searching...
No Matches
smltools.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""!
3Tools related to the (computation of) the smoothing length.
4"""
5
6from .sph_kernels import (
7 sph_kernel_W,
8 sph_kernel_dWdr,
9 sph_kernel_get_H,
10 sph_kernel_root,
11)
12from numpy import ndarray, sqrt, cbrt, pi, sum
13
14
15def _neighbour_loop_vectorized(r: ndarray, h: float, kernel: str, ndim: int):
16 """!
17 A neighbour interaction loop used to determine the smoothing lengths.
18
19 Parameters
20 ----------
21
22 r: ndarray
23 distances from the particle to the neighbours
24
25 h: float
26 current guess for the particle's smoothing length
27
28 kernel: str
29 which SPH kernel to use.
30
31 ndim: int
32 number of dimensions to work for.
33
34
35 Returns
36 -------
37
38 wcount: float
39 sum of particle's kernel weight at neighbour positions
40
41 wcount_dh: float
42 sum of particle's kernel derivative weight at neighbour positions
43 """
44 wcount = 0.0
45 wcount_dh = 0.0
46 H = sph_kernel_get_H(h, kernel, ndim)
47
48 q = r / H
49 w_all = sph_kernel_W(r, h, kernel, ndim)
50 dwdr_all = sph_kernel_dWdr(r, h, kernel, ndim)
51 wcount = w_all.sum()
52 dwdh_term = sum(ndim * w_all / h + q * dwdr_all)
53 wcount_dh = -dwdh_term
54
55 # Add self contribution
56 w = sph_kernel_W(0.0, h, kernel, ndim)
57 dwdr = sph_kernel_dWdr(0.0, h, kernel, ndim)
58 wcount += w
59 wcount_dh -= ndim * w / h
60
61 return wcount, wcount_dh
62
63
64def _neighbour_loop(r: ndarray, h: float, kernel: str, ndim: int):
65 """!
66 A neighbour interaction loop used to determine the smoothing lengths.
67
68 Parameters
69 ----------
70
71 r: ndarray
72 distances from the particle to the neighbours
73
74 h: float
75 current guess for the particle's smoothing length
76
77 kernel: str
78 which SPH kernel to use.
79
80 ndim: int
81 number of dimensions to work for.
82
83
84 Returns
85 -------
86
87 wcount: float
88 sum of particle's kernel weight at neighbour positions
89
90 wcount_dh: float
91 sum of particle's kernel derivative weight at neighbour positions
92 """
93 wcount = 0.0
94 wcount_dh = 0.0
95 H = sph_kernel_get_H(h, kernel, ndim)
96
97 # Collect neighbour values
98 for n in range(r.shape[0]):
99 q = r[n] / H
100 w = sph_kernel_W(r[n], h, kernel, ndim)
101 dwdr = sph_kernel_dWdr(r[n], h, kernel, ndim)
102 wcount += w
103 dwdh_term = ndim * w / h + q * dwdr
104 wcount_dh -= dwdh_term
105
106 # Add self contribution
107 w = sph_kernel_W(0.0, h, kernel, ndim)
108 dwdr = sph_kernel_dWdr(0.0, h, kernel, ndim)
109 wcount += w
110 wcount_dh -= ndim * w / h
111
112 return wcount, wcount_dh
113
114
115def find_smoothing_length(
116 xp: ndarray,
117 xn: ndarray,
118 kernel: str,
119 eta: float,
120 h_tolerance: float,
121 ndim: int,
122 verbose: bool = False,
123):
124 """!
125 Determine the smoothing length of a particle with coordinates xp
126 and neighbours with positions xn. This function assumes that the
127 an appropriate amount of neighbours is passed on via the `xn` array
128 of their positions, meaning that max{|xn - xp|} <= gamma * h.
129
130 Parameters
131 ----------
132
133 xp: ndarray
134 Coordiate of particle to determine smoothing length for
135
136 xn: ndarray
137 Array of coordinates of neighbour particles
138
139 kernel: str
140 Which kernel to use. Choices are given in sph_kernels.py:sph_kernel_list
141
142 eta: float
143 resolution eta to use, "smoothing scale". This determines the number of neighbours.
144
145 h_tolerance: float
146 tolerance to adapt to consider smoothing length converged
147
148 ndim: int
149 number of dimensions to work with
150
151 verbose: bool
152 whether to be verbose about what's going on
153
154
155 Returns
156 -------
157
158 h: float
159 the smoothing length
160 """
161
162 # get distances between particles
163 dx = xn - xp
164 r2 = sum(dx[:, :ndim] ** 2, axis=1)
165 r = sqrt(r2)
166
167 kernel_root = sph_kernel_root(kernel, ndim)
168
169 # Set up boundaries. We assume that the correct
170 # number of neighbour (candidates) has been passed.
171 H_over_h = sph_kernel_get_H(1.0, kernel, ndim)
172 h_max = r.max() / H_over_h
173 h_min = h_max * 1e-9
174
175 # Get initial guess for smoothing length
176 dx_average = r.max() / xn.shape[0]
177 nneigh_expect = number_of_neighbours_from_eta(eta, kernel, ndim)
178 h = nneigh_expect * dx_average
179 h = max(h_min, h)
180 h = min(h_max, h)
181
182 # safety check
183 if int(nneigh_expect) > xn.shape[0]:
184 print("WARNING: trying to find more neighbours than candidates provided")
185
186 # bisection bounds
187 right = h_max
188 right_old = right
189 left = h_min
190 left_old = left
191
192 iteration = 0
193 n_target = eta**ndim
194 if verbose:
195 print("Startup: h_init", h)
196 print("Startup: h_min", h_min)
197 print("Startup: h_max", h_max)
198 print("Startup: Neighbours: ", xn.shape[0])
199
200 # This is a very high number of iterations. However, this function
201 # is intended for confirmation checks that use untypically low
202 # convergence criteria, so allow it to do a lot of iterations.
203 while iteration < 10000:
204 iteration += 1
205
206 h_dim = h**ndim
207 h_dim_minus_one = h ** (ndim - 1.0)
208 wcount, wcount_dh = _neighbour_loop_vectorized(r, h, kernel, ndim)
209
210 if abs(wcount - kernel_root) < 1e-5 * kernel_root:
211 if verbose:
212 print(f"Caught no neighbours case, h={h}")
213 h_new = 2.0 * h
214
215 else:
216 n_sum = wcount * h_dim
217 f = n_sum - n_target
218 f_prime = wcount_dh * h_dim + ndim * wcount * h_dim_minus_one
219
220 # improve bisection bounds
221 if n_sum < n_target:
222 left_old = left
223 left = max(left, h)
224 elif n_sum > n_target:
225 right_old = right
226 right = min(right, h)
227
228 if left > right:
229 # this might be a cause to crash, actually
230 print("Error in bisection bounds. Resetting them.")
231 left = left_old
232 right = right_old
233
234 if h >= h_max and f < 0:
235 raise RuntimeError(
236 f"""
237Particle with coordinates {xp} reached h_max without converging.
238Increase the number of neighbours you pass to this script."""
239 )
240 return h_max
241
242 if h <= h_min and f > 0:
243 print(
244 f"WARNING: Particle with coordinates {xp} reached h_min. Returning that."
245 )
246 return h_min
247
248 h_new = h - f / f_prime
249
250 # don't overstep boundaries, and don't change too fast
251 if verbose:
252 print(
253 "boundaries check:",
254 h_new > 2.0 * h,
255 h_new < 0.5 * h,
256 h_new < left,
257 h_new == left,
258 h_new > right,
259 h_new == right,
260 )
261 h_new = min(2.0 * h, h_new)
262 h_new = max(0.5 * h, h_new)
263 h_new = max(h_new, left)
264 h_new = min(h_new, right)
265
266 diff = h - h_new
267
268 if verbose:
269 print("h =", h)
270 print("h_new=", h_new)
271 print("wcount", wcount)
272 print("wcount_dh", wcount_dh)
273 print("f", f)
274 print("f_prime", f_prime)
275 print("h - h_new", diff)
276
277 diff = h - h_new
278
279 # Are we done?
280 if abs(diff) < h_tolerance * h:
281 if verbose:
282 print("done h=", h_new)
283 return h_new
284
285 # We're not done...
286 # Case where we have been oscillating around the solution
287 if (h_new == left and h == right) or (h == left and h_new == right):
288 if verbose:
289 if h_new == left:
290 print("h_new == left")
291 if h == right:
292 print("h == right")
293 if h_new == right:
294 print("h_new == right")
295 if h == left:
296 print("h == left")
297 h_new = (0.5 * (left**ndim + right**ndim)) ** (1.0 / ndim)
298
299 h = h_new
300
301 h = min(h_max, h)
302 h = max(h_min, h)
303
304 if verbose:
305 print("------------------------------------")
306
307 print(
308 f"Particle with coordinates {xp} didn't converge smoothing length after {iteration} iterations"
309 )
310
311 quit()
312 return h
313
314
315def eta_from_number_of_neighbours(N: float, kernel: str, ndim: int):
316 """!
317 Compute the resolution eta from a given (approximate) number of neighbours.
318 Note that the neighbour number does not need be an integer.
319 See Price 2012, https://ui.adsabs.harvard.edu/abs/2012JCoPh.231..759P, eq. 12
320
321 Parameters
322 ----------
323
324 N: float
325 Number of neighbours
326
327 kernel: str
328 which kernel we're computing for.
329
330 ndim: int
331 how many dimensions we are working with.
332
333
334 Returns
335 -------
336
337 eta: float
338 the corresponding resolution eta
339 """
340
341 H_over_h = sph_kernel_get_H(1.0, kernel, ndim)
342
343 if ndim == 1:
344 return N / (2.0 * H_over_h)
345 elif ndim == 2:
346 return sqrt(N / pi) / H_over_h
347 elif ndim == 3:
348 return cbrt(N * 3.0 / 4.0 / pi) / H_over_h
349 else:
350 raise ValueError("Invalid number of dimensions", ndim)
351
352
353def number_of_neighbours_from_eta(eta: float, kernel: str, ndim: int):
354 """!
355 Compute the (approximate) number of neighbours from a given resolution eta.
356 Note that the neighbour number does not need be an integer.
357 See Price 2012, https://ui.adsabs.harvard.edu/abs/2012JCoPh.231..759P, eq. 12
358
359 Parameters
360 ----------
361
362 eta: float
363 resolution eta
364
365 kernel: str
366 which kernel we're computing for.
367
368 ndim: int
369 how many dimensions we are working with.
370
371
372 Returns
373 -------
374
375 N: float
376 the (approximate) number of neighbours
377 """
378
379 H_over_h = sph_kernel_get_H(1.0, kernel, ndim)
380
381 if ndim == 1:
382 return 2.0 * H_over_h * eta
383 elif ndim == 2:
384 return pi * (H_over_h * eta) ** 2
385 elif ndim == 3:
386 return 4.0 / 3.0 * pi * (H_over_h * eta) ** 3
387 else:
388 raise ValueError("Invalid number of dimensions", ndim)
_neighbour_loop(ndarray r, float h, str kernel, int ndim)
A neighbour interaction loop used to determine the smoothing lengths.
Definition smltools.py:64
_neighbour_loop_vectorized(ndarray r, float h, str kernel, int ndim)
A neighbour interaction loop used to determine the smoothing lengths.
Definition smltools.py:15