cosmic-popsynth 3.4.17__cp310-cp310-macosx_14_0_arm64.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.
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # Copyright (C) Carl Rodriguez (2017 - 2020)
4
+ #
5
+ # This file is part of COSMIC
6
+ #
7
+ # COSMIC is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # COSMIC is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU General Public License
18
+ # along with COSMIC. If not, see <http://www.gnu.org/licenses/>
@@ -0,0 +1,411 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) Carl Rodriguez (2020 - 2021)
3
+ #
4
+ # This file is part of cosmic.
5
+ #
6
+ # cosmic is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # cosmic is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with cosmic. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ """`Elson`
20
+ """
21
+
22
+ from scipy.interpolate import interp1d, CubicSpline
23
+ from scipy.integrate import quad
24
+ from scipy.special import hyp2f1
25
+ from scipy.optimize import brentq
26
+ import numpy as np
27
+ from numpy.random import uniform, normal
28
+ from scipy.stats import maxwell
29
+
30
+ __author__ = "Carl Rodriguez <carllouisrodriguez@gmail.com>"
31
+ __credits__ = "Carl Rodriguez <carllouisrodriguez@gmail.com>"
32
+
33
+
34
+ def M_enclosed(r, gamma, rho_0):
35
+ """
36
+ Compute the mass enclosed in an Elson profile at radius r with slope
37
+ gamma, central concentration rho_0, and assumed scale factor a = 1
38
+
39
+ In practice, this is only used to sample the positions of stars, so rho_0 is
40
+ just picked to normalize the distribution (i.e. rho_0 s.t. M_enclosed(rmax) = 1)
41
+ """
42
+
43
+ prefactor = 4 * np.pi * rho_0 * r * r * r / 3.0
44
+
45
+ hypergeometric = hyp2f1(1.5, (gamma + 1.0) / 2.0, 2.5, -(r ** 2))
46
+
47
+ return prefactor * hypergeometric
48
+
49
+ def phi_r(r, gamma, rho_0):
50
+ """
51
+ Compute the gravitational potential of an Elson profile at radius r with slope
52
+ gamma, central concentration rho_0, and assumed scale factor a = 1
53
+
54
+ Needed to compute the escape speed (for resampling stars)
55
+ """
56
+
57
+ prefactor = - 4 * np.pi * rho_0 / (gamma - 1)
58
+
59
+ hypergeometric = hyp2f1(0.5, (gamma - 1.0) / 2.0, 1.5, -(r ** 2))
60
+
61
+ return prefactor * hypergeometric
62
+
63
+ def rho_r(r, gamma, rho_0):
64
+ """
65
+ Compute the density of the Elson profile at radius r
66
+ Best to use the same normalized rho_0 from M_enclosed
67
+ """
68
+
69
+
70
+ return rho_0 * pow(1 + r * r, -(gamma + 1.0) / 2.0)
71
+
72
+ def virial_radius_analytic(gamma, r_max):
73
+ """
74
+ Virial radius is best calculated directly, since rmax may be pretty far from
75
+ infinity. Directly integrate 4*pi*r*rho*m_enclosed from 0 to rMax to get the
76
+ binding energy, then just divide 0.5 by that.
77
+ """
78
+
79
+ rho_0 = 1.0 / M_enclosed(r_max, gamma, 1)
80
+
81
+ integral, error = quad(lambda r: rho_r(r, gamma, rho_0) * r * 4 * np.pi * M_enclosed(r, gamma, rho_0), 0, r_max)
82
+
83
+ return 1 / 2.0 / integral
84
+
85
+
86
+ def find_rmax_vir(r_max, gamma):
87
+ """
88
+ This is a little tricky: because the virial radius of the Elson profile
89
+ depends on the maximum radius, if the profile is very flat (e.g. gamma~2)
90
+ then you need a large maximum radius to get a large number of virial radius
91
+ in there. This basically finds r such that r / rVir(r) = r_max. In other
92
+ word, how far out do we need to go to have r_max number of virial radii in the
93
+ sample.
94
+ """
95
+ rOrvirMin = 1.0
96
+ rOrvirMax = 20000.0
97
+
98
+ # rvir ~ 1 for gamma > 4, and the M_enclosed integral doesn't converge.
99
+ # Here's a cheap fix for that...
100
+ if gamma > 4:
101
+ rOrvirMax /= 10
102
+
103
+
104
+ def y_zero(r):
105
+ return r / virial_radius_analytic(gamma, r) - r_max
106
+
107
+ yMin = y_zero(rOrvirMin)
108
+ yMax = y_zero(rOrvirMax)
109
+
110
+ rmax_vir = brentq(y_zero, rOrvirMin,rOrvirMax)
111
+
112
+ return rmax_vir
113
+
114
+
115
+ def find_sigma_sqr(r, r_max_cluster, gamma):
116
+ """
117
+ Find the 1D velocity dispersion at a given radius r using one of the
118
+ spherial Jeans equations (and assuming velocity is isotropic)
119
+ """
120
+
121
+ rho_0 = 1.0 / M_enclosed(r_max_cluster, gamma, 1)
122
+
123
+ jeans_integrand = (
124
+ lambda rr: rho_r(rr, gamma, rho_0) * M_enclosed(rr, gamma, rho_0) / rr / rr
125
+ )
126
+
127
+ integral, error = quad(jeans_integrand, r, r_max_cluster)
128
+
129
+ return integral / rho_r(r, gamma, rho_0)
130
+
131
+
132
+ def get_positions(N, r_max_cluster, gamma):
133
+ """
134
+ This one's easy: the mass enclosed function is just the CDF of the mass
135
+ density, so just invert that, and you've got positions.
136
+ """
137
+
138
+ # First normalize the central density s.t. M_enc(r_max) = 1
139
+ rho_0 = 1.0 / M_enclosed(r_max_cluster, gamma, 1)
140
+
141
+ radii_grid = np.logspace(-3, np.log10(r_max_cluster), 1000)
142
+
143
+ # Add r=0 to the array
144
+ radii_grid[0] = 0
145
+
146
+ mass_enclosed_grid = M_enclosed(radii_grid, gamma, rho_0)
147
+
148
+ # if it's a steep profile, you'll get a lot of similar numbers (to machine precision)
149
+ # remove them here
150
+ mass_enclosed_grid, unique_idx = np.unique(mass_enclosed_grid,return_index=True)
151
+ radii_grid = radii_grid[unique_idx]
152
+
153
+ # Use an interpolator, but flip x and y for the CDF
154
+ interpolator = interp1d(mass_enclosed_grid, radii_grid, kind="cubic")
155
+
156
+ X = uniform(size=N)
157
+
158
+ positions = interpolator(X)
159
+
160
+ return positions
161
+
162
+ def get_velocities_old(r, r_max_cluster, gamma):
163
+ """
164
+ Uses the spherical Jeans functions to sample the velocity dispersion for the
165
+ cluster at different radii, then draws a random, isotropic velocity for each
166
+ star
167
+
168
+ NOTE: this gives you correct velocity dispersion and produces a cluster in virial
169
+ equilibrium, but it's not strictly speaking correct (the distribution should
170
+ have some kurtosis, in addition to getting the variance of the Gaussian correct).
171
+ You can see the disagreement in the tail of the distributions when sampling a
172
+ Plummer sphere. This is what mcluster does...
173
+
174
+ Use the new get_velocities function, which generates a distribution function
175
+ directly from rho and samples velocities from it
176
+
177
+ This is kept around because it's super useful to sample from when the rejection sampling
178
+ gets too slow at the last X samples (e.g. gamma ~ 10)
179
+
180
+ returns (vr,vt) with same length as r
181
+ """
182
+
183
+ N = len(r)
184
+
185
+ rho_0 = 1.0 / M_enclosed(r_max_cluster, gamma, 1)
186
+
187
+ # Rather than calculate the integral for every stellar position, just
188
+ # create an interpolator of 1000 or so points, then sample from the
189
+ # interpolated curve
190
+ radii_grid = np.logspace(np.log10(min(r) * 0.999), np.log10(max(r) * 1.001), 1000)
191
+ sigma_sqr_grid = np.array(
192
+ [find_sigma_sqr(rr, r_max_cluster, gamma) for rr in radii_grid]
193
+ )
194
+
195
+ interpolator = interp1d(radii_grid, sigma_sqr_grid, kind="cubic")
196
+
197
+ # Draw the 1D velocity dispersions
198
+ sigma = np.sqrt(interpolator(r))
199
+
200
+ # Then sample an isotropic gaussian in vx,vy,vz using those dispersions
201
+ vx = normal(scale=sigma, size=N)
202
+ vy = normal(scale=sigma, size=N)
203
+ vz = normal(scale=sigma, size=N)
204
+
205
+ # Finally the rejection sampling: because of the Gaussian, some of the velocities
206
+ # we draw will naturally be above the local escape speed of the cluster. To avoid
207
+ # this, we flag them, and resample them from the velocity distribution
208
+ while False:
209
+ ve = np.sqrt(-2*phi_r(r, gamma, rho_0))
210
+ escapers = (np.sqrt(vx**2 + vy**2 + vz**2) > ve)
211
+
212
+ number_of_escapers = np.sum(escapers)
213
+ if number_of_escapers == 0:
214
+ break
215
+ else:
216
+ vx_temp = normal(scale=sigma[escapers], size=number_of_escapers)
217
+ vy_temp = normal(scale=sigma[escapers], size=number_of_escapers)
218
+ vz_temp = normal(scale=sigma[escapers], size=number_of_escapers)
219
+
220
+ vx[escapers] = vx_temp
221
+ vy[escapers] = vy_temp
222
+ vz[escapers] = vz_temp
223
+
224
+ return np.sqrt(vx**2 + vy**2 + vz**2)
225
+
226
+ def get_velocities(r, r_max_cluster, gamma):
227
+ """
228
+ The correct way to generate velocities: generate the distribution function from rho,
229
+ then use rejection sampling.
230
+
231
+ returns (vr,vt) with same length as r
232
+ """
233
+
234
+ N = len(r)
235
+
236
+ # If we actually want a plummer sphere, we can default to the simpler function
237
+ # using the analytic distribution function
238
+ if gamma == 4:
239
+ return get_velocities_plummer(r,r_max_cluster)
240
+ # Otherwise need to compute f(E) numerically
241
+
242
+ # First get phi and rho over 100 points or so
243
+ rho_0 = 1.0 / M_enclosed(max(r), gamma, 1)
244
+
245
+ r_stensil = np.logspace(np.log10(min(r)),np.log10(max(r)),100)
246
+ phi = phi_r(r_stensil,gamma,rho_0)
247
+ rho = rho_r(r_stensil,gamma,rho_0)
248
+
249
+ # Then construct a cubic spline of rho as a function of phi
250
+ # Technically should be psi = -phi, but FITPACK only accepts increasing values
251
+ rho_phi_spline = CubicSpline(phi,rho,extrapolate=False)
252
+
253
+ f_E = []
254
+ energies = []
255
+
256
+ # Now integrate over energies to get the distribution function
257
+ for en in np.linspace(min(-phi),max(-phi),100):
258
+
259
+ integrand = lambda psi: rho_phi_spline(-psi,2) / np.sqrt(en - psi)
260
+
261
+ # Eqn 4.46b of Binney and Tremain (2nd Edition)
262
+ # (4.140b in 1st edition)
263
+ temp_f_E = ((quad(integrand,min(-phi)*1.01,en,limit=200)[0] +
264
+ rho_phi_spline(-min(-phi),1)/np.sqrt(en)) / 17.493418) # sqrt(8)*pi^2
265
+
266
+ # Extrapolating derivatives from a cubic spline is hella dangerous
267
+ # Better to have it throw NaNs and discard them
268
+ if np.isnan(temp_f_E):
269
+ continue
270
+ else:
271
+ f_E.append(temp_f_E)
272
+ energies.append(en)
273
+
274
+ # Set f(E) to zero at zero energy
275
+ f_E = np.array([0] + f_E)
276
+ energies = np.array([0] + energies)
277
+
278
+ # Make an interpolator for f(E) and compute f and phi at every star
279
+ f_E_interp = interp1d(energies,f_E)
280
+ phi_at_r = phi_r(r,gamma,rho_0)
281
+ f_E_at_r = f_E_interp(-phi_at_r)
282
+
283
+ # Make an initial guess for every velocity
284
+ x_rand = uniform(size=N)
285
+ y_rand = uniform(size=N)
286
+ velocities = x_rand*np.sqrt(-2*phi_at_r)
287
+
288
+ resample = np.ones(N,dtype=bool)
289
+ number_to_resample = N
290
+
291
+ # Now do the rejection sampling
292
+ while number_to_resample > 0:
293
+ resample[resample] = (y_rand[resample]*f_E_at_r[resample] >
294
+ x_rand[resample]**2 * f_E_interp(-phi_at_r[resample]*(1 - x_rand[resample]**2)))
295
+ number_to_resample = np.sum(resample)
296
+ x_rand[resample] = uniform(size=number_to_resample)
297
+ y_rand[resample] = uniform(size=number_to_resample)
298
+
299
+ velocities = x_rand * np.sqrt(-2*phi_at_r)
300
+
301
+ theta = np.arccos(uniform(-1,1,N))
302
+
303
+ vr = velocities*np.cos(theta)
304
+ vt = velocities*np.sin(theta)
305
+
306
+ return vr, vt
307
+
308
+ def get_velocities_plummer(r, r_max_cluster):
309
+ """
310
+ The correct way to generate velocities
311
+
312
+ this is a special case for gamma=4, which is the plummer sphere (and has an analytic distribution function)
313
+
314
+ returns (vr,vt) with same length as r
315
+ """
316
+
317
+ N = len(r)
318
+
319
+
320
+ # Make an initial guess for every velocity
321
+ x_rand = uniform(size=N)
322
+ y_rand = uniform(size=N)
323
+ vesc = np.sqrt(2/np.sqrt(1+r**2))
324
+ velocities = x_rand * vesc
325
+
326
+ resample = np.ones(N,dtype=bool)
327
+ number_to_resample = N
328
+
329
+ # Now do the rejection sampling; this is taken from Aarseth's original algorithm using the dist function
330
+ while number_to_resample > 0:
331
+ resample[resample] = 0.1*y_rand[resample] > x_rand[resample]**2 * (1 - x_rand[resample]**2)**3.5
332
+ number_to_resample = np.sum(resample)
333
+ x_rand[resample] = uniform(size=number_to_resample)
334
+ y_rand[resample] = uniform(size=number_to_resample)
335
+
336
+ # Scale up to the local escape speed
337
+ velocities = x_rand * vesc
338
+
339
+ theta = np.arccos(uniform(-1,1,N))
340
+
341
+ vr = velocities*np.cos(theta)
342
+ vt = velocities*np.sin(theta)
343
+
344
+ return vr, vt
345
+
346
+
347
+ def scale_pos_and_vel(r,vr,vt):
348
+ """
349
+ Scale the positions and velocities to be in N-body units
350
+ If we add binaries we'll do this again in initialcmctable.py
351
+
352
+ takes r, vr, and vt as input
353
+
354
+ returns (r,vr,vt) scaled to Henon units
355
+ """
356
+
357
+ # Normalize masses to 1/Mtotal
358
+ mass = np.ones_like(r)/len(r)
359
+ cumul_mass = np.cumsum(mass)
360
+
361
+ radius = np.array(r)
362
+ radius_p1 = np.append(radius[1:], [1e100])
363
+
364
+ # Then compute the total kinetic and potential energy
365
+ # There's probably a cleaner way to do the PE (this is a one-line version
366
+ # of the for loop we use in CMC; vectorized and pythonic, but sloppy)
367
+ KE = 0.5 * sum(mass * (vr ** 2 + vt ** 2))
368
+ PE = 0.5 * sum(
369
+ mass[::-1]
370
+ * np.cumsum((cumul_mass * (1.0 / radius - 1.0 / radius_p1))[::-1])
371
+ )
372
+
373
+ # Compute the position and velocity scalings
374
+ rfac = 2 * PE
375
+ vfac = 1.0 / np.sqrt(4 * KE)
376
+
377
+ return r*rfac, vr*vfac, vt*vfac
378
+
379
+
380
+ def draw_r_vr_vt(N=100000, r_max=300, gamma=4):
381
+ """
382
+ Draw random velocities and positions from the Elson profile.
383
+
384
+ N = number of stars
385
+ r_max = maximum number of virial radii for the farthest star
386
+ gamma = steepness function of the profile
387
+
388
+ Note that gamma=4 is the Plummer profile
389
+
390
+ returns (vr,vt,r) in G=M_cluster=1 units
391
+
392
+ N.B. if you're getting a "Expect x to not have duplicates" error
393
+ with a large gamma, use a smaller r_max.
394
+ """
395
+ # First convert r_max into max number of virial radii
396
+ r_max = find_rmax_vir(r_max, gamma)
397
+
398
+ # Then draw the positions from the cumulative mass function
399
+ r = get_positions(N, r_max, gamma)
400
+
401
+ # Sort the radii (needed later)
402
+ r = np.sort(r)
403
+
404
+ # Finally, draw the velocities using the radii and one of the Jeans
405
+ # equations
406
+ vr, vt = get_velocities(r, r_max, gamma)
407
+
408
+ # And scale them to Henon units
409
+ r,vr,vt = scale_pos_and_vel(r,vr,vt)
410
+
411
+ return r, vr, vt
@@ -0,0 +1,260 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) Carl Rodriguez (2020 - 2021)
3
+ #
4
+ # This file is part of cosmic.
5
+ #
6
+ # cosmic is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # cosmic is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with cosmic. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ """`King`
20
+ """
21
+
22
+ import numpy as np
23
+ from numpy.random import uniform, normal
24
+ from scipy.integrate import RK45, quad, simpson, cumulative_trapezoid
25
+ from scipy.interpolate import interp1d
26
+ from scipy.special import erf
27
+
28
+ __author__ = "Carl Rodriguez <carllouisrodriguez@gmail.com>"
29
+ __credits__ = "Carl Rodriguez <carllouisrodriguez@gmail.com>"
30
+ __all__ = ["calc_rho", "integrate_king_profile", "virial_radius_numerical", "find_sigma_sqr", "get_positions", "get_velocities", "scale_pos_and_vel", "draw_r_vr_vt"]
31
+
32
+ def calc_rho(w):
33
+ """
34
+ Returns the density (unnormalized) given w = psi/sigma^2
35
+ Note that w(0) = w_0, the main parameter of a King profile
36
+ """
37
+ rho = np.exp(w)*erf(np.sqrt(w)) - np.sqrt(4.0*w/np.pi)*(1.0+2.0*w/3.0)
38
+ return rho
39
+
40
+ def integrate_king_profile(w0,tidal_boundary=1e-6):
41
+ """
42
+ Integrate a King Profile of a given w_0 until the density (or phi/sigma^2)
43
+ drops below tidal_boundary limit (1e-8 times the central density by default)
44
+
45
+ Let's define some things:
46
+ The King potential is often expressed in terms of w = psi / sigma^2 (psi is phi0 - phi, so just positive potential)
47
+ (note that sigma is the central velocity dispersion with an infinitely deep potential, and close otherwise)
48
+
49
+ in the center, w_0 = psi_0 / sigma^2 (the free parameter of the King profile)
50
+
51
+ The core radius is defined (King 1966) as r_c = sqrt(9 * sigma^2 / (4 pi G rho_0))
52
+
53
+ If we define new scaled quantities
54
+ r_tilda = r/r_c
55
+ rho_tilda = rho/rho_o
56
+ We can rewrite Poisson's equation, (1/r^2) d/dr (r^2 dphi/dr) = 4 pi G rho as:
57
+ d^2(r_tilda w)/dr_tilda^2 = 9 r_tilda rho_tilda
58
+
59
+ After that, all we need is initial conditions:
60
+ w(0) = w_0
61
+ w'(0) = 0
62
+
63
+ returns (radii, rho, phi, M_enclosed)
64
+ """
65
+
66
+ rho_0 = calc_rho(w0)
67
+
68
+ def ode_rhs(r_tilda,w_vec):
69
+ ## Unpack the w_vector (note I'm cheating by putting rho_0 as an element)
70
+ w,w_dot,rho_0 = w_vec
71
+
72
+ ## Compute rho and second derivative of w
73
+ rho = calc_rho(w)
74
+ w_ddot = -9*rho/rho_0 - 2*w_dot/r_tilda
75
+
76
+ ## return dw/dr_tilda, d^2w/dr_tilda^2, d(rho_tilda)/dr_tilda = 0
77
+ return (w_dot,w_ddot,0)
78
+
79
+ king_profile = RK45(ode_rhs,1e-4,[w0,0,rho_0],1e5,rtol=1e-10,atol=1e-12)
80
+
81
+ at_the_tidal_boundary = False
82
+ r = []
83
+ rho_r = []
84
+ phi_r = []
85
+
86
+ while not at_the_tidal_boundary:
87
+ w,w_dot,rho_0 = king_profile.y
88
+ rho = calc_rho(w)
89
+
90
+ r.append(king_profile.t)
91
+ rho_r.append(rho)
92
+ phi_r.append(w)
93
+
94
+ if rho/rho_0 < tidal_boundary:
95
+ at_the_tidal_boundary = True
96
+ else:
97
+ king_profile.step()
98
+
99
+ r = np.array(r)
100
+ rho_r = np.array(rho_r)
101
+
102
+ ## Finally compute the cumulative mass enclosed
103
+ M_enclosed = cumulative_trapezoid(4*np.pi*r**2*rho_r, r, initial=0)
104
+
105
+ return r, rho_r, phi_r, M_enclosed
106
+
107
+
108
+ def virial_radius_numerical(r, rho_r, M_enclosed):
109
+ """
110
+ Virial radius is best calculated directly. Directly integrate
111
+ 4*pi*r*rho*m_enclosed over the samples binding energy, then just divide 0.5 by that.
112
+ """
113
+ integral = simpson(y=rho_r * r * 4 * np.pi * M_enclosed,x=r)
114
+
115
+ return 1 / 2.0 / integral
116
+
117
+ def find_sigma_sqr(r_sample, r, rho_r, M_enclosed):
118
+ """
119
+ Find the 1D velocity dispersion at a given radius r using one of the
120
+ spherial Jeans equations (and assuming velocity is isotropic)
121
+ """
122
+
123
+ ## This needs to be done continuously, so use interpolators
124
+ rho_interp = interp1d(r,rho_r)
125
+ M_enc_interp = interp1d(r,M_enclosed)
126
+
127
+ jeans_integrand = lambda rr: rho_interp(rr) * M_enc_interp(rr) / rr / rr
128
+ jeans_integrand = interp1d(r,rho_r*M_enclosed/r/r)
129
+
130
+ integral, error = quad(jeans_integrand, r_sample, r[-1])
131
+ return integral / rho_interp(r_sample)
132
+
133
+ def get_positions(N, r, M_enclosed):
134
+ """
135
+ This one's easy: the mass enclosed function is just the CDF of the mass
136
+ density, so just invert that, and you've got positions.
137
+
138
+ Note that here we've already normalized M_enclosed to 1 at r_tidal
139
+ """
140
+
141
+ # Use an interpolator, but flip x and y for the CDF
142
+ interpolator = interp1d(M_enclosed, r, kind="cubic")
143
+
144
+ X = uniform(size=N)
145
+
146
+ positions = interpolator(X)
147
+
148
+ return positions
149
+
150
+ def get_velocities(r, r_profile, psi_profile, M_enclosed_profile):
151
+ """
152
+ The correct way to generate velocities: start from the distribution function
153
+ and use rejection sampling.
154
+
155
+ returns (vr,vt) with same length as r
156
+ """
157
+
158
+ N = len(r)
159
+
160
+ # create an interpolator for the psi
161
+ psi_r = interp1d(r_profile,psi_profile)
162
+ psi = psi_r(r)
163
+
164
+ # Make an initial guess for every velocity (some fraction of the escape speed)
165
+ x_rand = uniform(size=N)
166
+ y_rand = uniform(size=N)
167
+
168
+ resample = np.ones(N,dtype=bool)
169
+ number_to_resample = N
170
+
171
+ minF = (np.exp(psi) - 1)*2*psi
172
+ v = x_rand * np.sqrt(2*psi)
173
+ f_0 = y_rand*minF
174
+ f = (np.exp(psi-v**2/2)-1)*v**2
175
+
176
+ # Now do the rejection sampling
177
+ while number_to_resample > 0:
178
+ v[resample] = x_rand[resample] * np.sqrt(2*psi[resample])
179
+ f_0[resample] = y_rand[resample]*minF[resample]
180
+ f[resample] = (np.exp(psi[resample]-v[resample]**2/2)-1)*v[resample]**2
181
+
182
+ resample[resample] = f[resample] < f_0[resample]
183
+ number_to_resample = np.sum(resample)
184
+ x_rand[resample] = uniform(size=number_to_resample)
185
+ y_rand[resample] = uniform(size=number_to_resample)
186
+
187
+ theta = np.arccos(uniform(-1,1,N))
188
+
189
+ vr = v*np.cos(theta)
190
+ vt = v*np.sin(theta)
191
+
192
+ return vr, vt
193
+
194
+
195
+ def scale_pos_and_vel(r,vr,vt):
196
+ """
197
+ Scale the positions and velocities to be in N-body units
198
+ If we add binaries we'll do this again in initialcmctable.py
199
+
200
+ takes r, vr, and vt as input
201
+
202
+ returns (r,vr,vt) scaled to Henon units
203
+ """
204
+
205
+ # Normalize masses to 1/Mtotal
206
+ mass = np.ones_like(r)/len(r)
207
+ cumul_mass = np.cumsum(mass)
208
+
209
+ radius = np.array(r)
210
+ radius_p1 = np.append(radius[1:], [1e100])
211
+
212
+ # Then compute the total kinetic and potential energy
213
+ # There's probably a cleaner way to do the PE (this is a one-line version
214
+ # of the for loop we use in CMC; vectorized and pythonic, but sloppy)
215
+ KE = 0.5 * sum(mass * (vr ** 2 + vt ** 2))
216
+ PE = 0.5 * sum(
217
+ mass[::-1]
218
+ * np.cumsum((cumul_mass * (1.0 / radius - 1.0 / radius_p1))[::-1])
219
+ )
220
+
221
+ # Compute the position and velocity scalings
222
+ rfac = 2 * PE
223
+ vfac = 1.0 / np.sqrt(4 * KE)
224
+
225
+ return r*rfac, vr*vfac, vt*vfac
226
+
227
+ def draw_r_vr_vt(N=100000, w_0=6, tidal_boundary=1e-6):
228
+ """
229
+ Draw random velocities and positions from the King profile.
230
+
231
+ N = number of stars
232
+ w_0 = King concentration parameter (-psi/sigma^2)
233
+ tidal_boundary = ratio of rho/rho_0 where we truncate the tidal boundary
234
+
235
+ returns (vr,vt,r) in G=M_cluster=1 units
236
+ """
237
+ # First integrate the differential equation of a King profile
238
+ r_profile, rho_profile, psi_profile, M_enclosed_profile = integrate_king_profile(w_0, tidal_boundary=tidal_boundary)
239
+
240
+ ## Normalize the masses to 1 at r_tidal
241
+ rho_profile /= M_enclosed_profile[-1]
242
+ M_enclosed_profile /= M_enclosed_profile[-1]
243
+
244
+ ## Normalize r (currently in units of the King core radius) to the virial radius
245
+ r_profile /= virial_radius_numerical(r_profile, rho_profile, M_enclosed_profile)
246
+
247
+ # Then draw the positions from the cumulative mass function
248
+ r = get_positions(N, r_profile, M_enclosed_profile)
249
+
250
+ # Sort the radii (needed later)
251
+ r = np.sort(r)
252
+
253
+ # Finally, draw the velocities using the radii and one of the Jeans
254
+ # equations
255
+ vr, vt = get_velocities(r, r_profile, psi_profile, M_enclosed_profile)
256
+
257
+ # Scale into Henon units
258
+ r, vr, vt = scale_pos_and_vel(r,vr,vt)
259
+
260
+ return r, vr, vt