cosmic-popsynth 3.6.2__cp313-cp313-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.
cosmic/test_sample.py ADDED
@@ -0,0 +1,580 @@
1
+ """Unit test for cosmic
2
+ """
3
+
4
+ __author__ = 'Katie Breivik <katie.breivik@gmail.com>'
5
+
6
+ import os
7
+ import unittest
8
+ import numpy as np
9
+ import pandas as pd
10
+ from cosmic.sample import InitialBinaryTable
11
+ from cosmic.sample.sampler.independent import Sample
12
+ from cosmic.sample.sampler.multidim import MultiDim
13
+ from cosmic.sample.sampler.cmc import CMCSample
14
+ from cosmic.sample.cmc import elson
15
+ from cosmic.sample.initialcmctable import InitialCMCTable
16
+ from scipy.optimize import curve_fit
17
+ from cosmic.utils import a_from_p, get_porb_norm
18
+
19
+ SAMPLECLASS = Sample()
20
+ MULTIDIMSAMPLECLASS = MultiDim()
21
+ CMCSAMPLECLASS = CMCSample()
22
+ TEST_DATA_DIR = os.path.join(os.path.split(__file__)[0], 'data')
23
+
24
+ #distribution slopes/power laws
25
+ KROUPA_93_HI = -2.7
26
+ KROUPA_93_MID = -2.2
27
+ KROUPA_93_LO = -1.3
28
+ KROUPA_01_HI = -2.3
29
+ KROUPA_01_LO = -1.3
30
+ SALPETER_55 = -2.35
31
+ SANA12_PORB_POWER_LAW = -0.55
32
+ FLAT_SLOPE = 0.0
33
+ # Since we integrate up to 0.7 in ecc, the slope is 4.0 rounded at the 0th decimal
34
+ THERMAL_SLOPE = 4.0
35
+ SANA12_ECC_POWER_LAW = -0.45
36
+ SANA12_ECC_POWER_LAW_ROUND = -0.5
37
+ MEAN_RAGHAVAN = 4.9
38
+ SIGMA_RAGHAVAN = 2.3
39
+ CLOSE_BINARY_FRAC = 0.42
40
+
41
+
42
+ N_BINARY_SELECT = 85
43
+ VANHAAFTEN_BINFRAC_MAX = 0.9989087986493874
44
+ VANHAAFTEN_BINFRAC_MIN = 0.6192803136799157
45
+ OFFNER_MASS_RANGES = [(0.075,0.15), (0.15,0.30), (0.3,0.6), (0.75,1.00), (0.85,1.25), (1.00,1.25), (1.6,2.4), (3,5), (5,8), (8,17), (17,50)]
46
+ OFFNER_DATA = [0.19, 0.23, 0.30, 0.42, 0.47, 0.50, 0.68, 0.81, 0.89, 0.93, 0.96]
47
+ OFFNER_ERRORS = [0.03, 0.02, 0.02, 0.03, 0.03, 0.04, 0.07, 0.06, 0.05, 0.04, 0.04]
48
+ MULTIDIM_BINFRAC_MAX = 0.6146916774140262
49
+ MULTIDIM_BINFRAC_MIN = 0.13786300908773025
50
+ CONST_SFR_SUM = 460028.2453521937
51
+ BURST_SFR_SUM = 946002.8245352195
52
+ KSTAR_SOLAR = 1.0
53
+ MOE_TOTAL_MASS = 20.27926225850954
54
+ METALLICITY_1000 = 0.02
55
+ METALLICITY_13000 = 0.02*0.15
56
+
57
+ QMIN_LOWMASS = 0.1
58
+ QMIN_HIGHMASS = 0.7
59
+ M2MIN_LOWMASS = 0.08
60
+ M2MIN_HIGHMASS = 12.0
61
+ BINFRAC_LOWMASS = 499139
62
+ BINFRAC_HIGHMASS = 2205
63
+
64
+ KING_TEST_DATA = np.load(os.path.join(TEST_DATA_DIR, "cmc_king_test.npz"))
65
+ ELSON_TEST_DATA = np.load(os.path.join(TEST_DATA_DIR, "cmc_elson_test.npz"))
66
+ PLUMMER_TEST_DATA = np.load(os.path.join(TEST_DATA_DIR, "cmc_plummer_test.npz"))
67
+ R_PLUMMER_TEST_ARRAY, VR_PLUMMER_TEST_ARRAY, VT_PLUMMER_TEST_ARRAY = PLUMMER_TEST_DATA["arr_0"], PLUMMER_TEST_DATA["arr_1"], PLUMMER_TEST_DATA["arr_2"]
68
+ R_ELSON_TEST_ARRAY, VR_ELSON_TEST_ARRAY, VT_ELSON_TEST_ARRAY = ELSON_TEST_DATA["arr_0"], ELSON_TEST_DATA["arr_1"], ELSON_TEST_DATA["arr_2"]
69
+ R_KING_TEST_ARRAY, VR_KING_TEST_ARRAY, VT_KING_TEST_ARRAY = KING_TEST_DATA["arr_0"], KING_TEST_DATA["arr_1"], KING_TEST_DATA["arr_2"]
70
+
71
+ REFF_TEST_ARRAY = np.array([3.94190562, 5.99895482])
72
+
73
+ SINGLES_CMC_FITS, BINARIES_CMC_FITS = InitialCMCTable.read(filename=os.path.join(TEST_DATA_DIR, "input_cmc.fits"))
74
+ SINGLES_CMC_HDF5, BINARIES_CMC_HDF5 = InitialCMCTable.read(filename=os.path.join(TEST_DATA_DIR, "input_cmc.hdf5"))
75
+
76
+ def power_law_fit(data, n_bins=100, return_intercept=False):
77
+ def line(x, a, b):
78
+ return x*a + b
79
+ def center_bins(bins):
80
+ mid_bin = []
81
+ for bin_lo, bin_hi in zip(bins[:-1], bins[1:]):
82
+ mid_bin.append(bin_lo + (bin_hi-bin_lo)/2)
83
+ return mid_bin
84
+
85
+ hist, bins = np.histogram(data, bins=n_bins)
86
+ bins = center_bins(bins)
87
+
88
+ xdata = np.log10(bins)
89
+ ydata = np.log10(hist)
90
+
91
+ popt, pcov = curve_fit(line, xdata, ydata)
92
+
93
+ slope, intercept = popt[0], popt[1]
94
+
95
+ if return_intercept:
96
+ return (slope, intercept)
97
+ else:
98
+ return slope
99
+
100
+ def linear_fit(data):
101
+ def line(x, a, b):
102
+ return x*a + b
103
+ def center_bins(bins):
104
+ mid_bin = []
105
+ for bin_lo, bin_hi in zip(bins[:-1], bins[1:]):
106
+ mid_bin.append(bin_lo + (bin_hi-bin_lo)/2)
107
+ return mid_bin
108
+
109
+ hist, bins = np.histogram(data, bins=100, density=True)
110
+ bins = center_bins(bins)
111
+ popt, pcov = curve_fit(line, bins, hist)
112
+
113
+ slope, intercept = popt[0], popt[1]
114
+
115
+ return slope
116
+
117
+ class TestSample(unittest.TestCase):
118
+ """`TestCase` for the cosmic Sample class, which generates several
119
+ independent initial parameters drawn from specified distributions
120
+ """
121
+
122
+ def test_sample_primary_kroupa93(self):
123
+ np.random.seed(2)
124
+ # Check that the sample_primary function samples mass correctly
125
+ mass, total_mass = SAMPLECLASS.sample_primary(primary_model='kroupa93', size=1000000)
126
+ mass_hi = mass[mass > 1.0]
127
+ # filter out highest masses because they kill us with the histograms
128
+ mass_hi = mass_hi[mass_hi < 10.0]
129
+ mass_mid = mass[(mass <= 1.0)]
130
+ mass_mid = mass_mid[mass_mid > 0.5]
131
+ mass_lo = mass[mass <= 0.5]
132
+
133
+ #few bins for the most massive stars
134
+ power_slope_hi = power_law_fit(mass_hi, n_bins=50)
135
+ power_slope_mid = power_law_fit(mass_mid)
136
+ power_slope_lo = power_law_fit(mass_lo)
137
+
138
+ self.assertEqual(np.round(power_slope_hi, 1), KROUPA_93_HI)
139
+ self.assertEqual(np.round(power_slope_mid, 1), KROUPA_93_MID)
140
+ self.assertEqual(np.round(power_slope_lo, 1), KROUPA_93_LO)
141
+
142
+
143
+ def test_sample_primary_kroupa01(self):
144
+ np.random.seed(2)
145
+ # Check that the sample_primary function samples mass correctly
146
+ mass, total_mass = SAMPLECLASS.sample_primary(primary_model='kroupa01', size=1000000)
147
+ mass_hi = mass[mass > 0.5]
148
+ # filter out highest masses because they kill us with the histograms
149
+ mass_hi = mass_hi[mass_hi < 10.0]
150
+ mass_lo = mass[mass <= 0.5]
151
+
152
+ #few bins for the most massive stars
153
+ power_slope_hi = power_law_fit(mass_hi, n_bins=50)
154
+ power_slope_lo = power_law_fit(mass_lo)
155
+
156
+ self.assertEqual(np.round(power_slope_hi, 1), KROUPA_01_HI)
157
+ self.assertEqual(np.round(power_slope_lo, 1), KROUPA_01_LO)
158
+
159
+ def test_sample_primary_salpeter55(self):
160
+ np.random.seed(3)
161
+ # Check that the sample_primary function samples mass correctly
162
+ mass, total_mass = SAMPLECLASS.sample_primary(primary_model='salpeter55', size=10000000)
163
+ #filter on mass to get better statistics
164
+ power_slope = power_law_fit(mass[mass < 1.0], n_bins=50)
165
+ self.assertEqual(np.round(power_slope, 2), SALPETER_55)
166
+
167
+ def test_sample_secondary(self):
168
+ np.random.seed(2)
169
+ # Check that the sample_secondary function samples secondary mass correctly
170
+ mass1, total_mass = SAMPLECLASS.sample_primary(primary_model='salpeter55', size=10000000)
171
+ ind_massive, = np.where(mass1 > 15.0)
172
+ ind_not_massive, = np.where(mass1 < 15.0)
173
+
174
+ np.random.seed(2)
175
+ mass2 = SAMPLECLASS.sample_secondary(primary_mass=mass1, qmin=0.1)
176
+ q = mass2[ind_massive] / mass1[ind_massive]
177
+ slope = linear_fit(q)
178
+ self.assertEqual(np.round(slope, 1), FLAT_SLOPE)
179
+
180
+ np.random.seed(2)
181
+ mass2 = SAMPLECLASS.sample_secondary(primary_mass=mass1, qmin=-1)
182
+ q = mass2[ind_not_massive] / mass1[ind_not_massive]
183
+ slope = linear_fit(q)
184
+ self.assertEqual(np.round(slope, 1), FLAT_SLOPE)
185
+
186
+ np.random.seed(2)
187
+ mass2 = SAMPLECLASS.sample_secondary(primary_mass=mass1, m2_min=0.08)
188
+ q = mass2[ind_massive] / mass1[ind_massive]
189
+ slope = linear_fit(q)
190
+ self.assertEqual(np.round(slope, 1), FLAT_SLOPE)
191
+
192
+ # This is now redundant since you should only sample with qmin or m2_min
193
+ #np.random.seed(2)
194
+ #mass2 = SAMPLECLASS.sample_secondary(primary_mass=mass1, qmin=0.1, m2_min=0.08)
195
+ #q = mass2[ind_massive] / mass1[ind_massive]
196
+ #slope = linear_fit(q)
197
+ #self.assertEqual(np.round(slope, 1), FLAT_SLOPE)
198
+
199
+ def test_sample_q(self):
200
+ """Test you can sample different mass ratio distributions"""
201
+ np.random.seed(2)
202
+ mass1, total_mass = SAMPLECLASS.sample_primary(primary_model='kroupa01', size=10000000)
203
+ for slope in [0, 1, 2]:
204
+ mass2 = SAMPLECLASS.sample_secondary(primary_mass=mass1, q_power_law=slope, qmin=0.0)
205
+ q = mass2 / mass1
206
+ fit_slope = power_law_fit(q)
207
+ self.assertEqual(np.round(fit_slope, 1), slope)
208
+
209
+ def test_binary_select(self):
210
+ np.random.seed(2)
211
+ # Check that the binary select function chooses binarity properly
212
+ m1_b, m1_s, binfrac, bin_index = SAMPLECLASS.binary_select(primary_mass=np.arange(1,100), binfrac_model=0.0)
213
+ self.assertEqual(len(m1_b), 0)
214
+ m1_b, m1_s, binfrac, bin_index = SAMPLECLASS.binary_select(primary_mass=np.arange(1,100), binfrac_model=1.0)
215
+ self.assertEqual(len(m1_b), 99)
216
+ m1_b, m1_s, binfrac, bin_index = SAMPLECLASS.binary_select(primary_mass=np.arange(1,100), binfrac_model='vanHaaften')
217
+ self.assertEqual(len(m1_b), N_BINARY_SELECT)
218
+
219
+ def test_binary_fraction(self):
220
+ np.random.seed(2)
221
+ # Check that the binary fraction tracking is correct
222
+ m1_b, m1_s, binfrac, bin_index = SAMPLECLASS.binary_select(primary_mass=np.arange(1,100), binfrac_model=0.5)
223
+ self.assertEqual(binfrac.max(), 0.5)
224
+ m1_b, m1_s, binfrac, bin_index = SAMPLECLASS.binary_select(primary_mass=np.arange(1,100), binfrac_model='vanHaaften')
225
+ self.assertEqual(binfrac.max(), VANHAAFTEN_BINFRAC_MAX)
226
+ self.assertEqual(binfrac.min(), VANHAAFTEN_BINFRAC_MIN)
227
+
228
+ primary_mass = np.array([float(x) for x in np.logspace(np.log10(0.08), np.log10(150), num=100000)])
229
+ m1_b, m1_s, binfrac, bin_index = SAMPLECLASS.binary_select(primary_mass=primary_mass, binfrac_model='offner23')
230
+ for i in range(len(OFFNER_MASS_RANGES)):
231
+ low, high = OFFNER_MASS_RANGES[i][0], OFFNER_MASS_RANGES[i][1]
232
+ offner_value = OFFNER_DATA[i]
233
+ offner_error = OFFNER_ERRORS[i]
234
+ bins_count = len(m1_b[(m1_b >= low) & (m1_b <= high)])
235
+ singles_count = len(m1_s[(m1_s >= low) & (m1_s <= high)])
236
+ bin_frac = bins_count / (bins_count + singles_count)
237
+ error = abs(offner_value - bin_frac)
238
+ self.assertLess(error, offner_error)
239
+
240
+ def test_msort(self):
241
+ np.random.seed(2)
242
+ mass1, total_mass = SAMPLECLASS.sample_primary(primary_model='kroupa01', size=1000000)
243
+ # Check that qmin_msort and m2_min_msort are workings as expected
244
+ mass2 = SAMPLECLASS.sample_secondary(primary_mass = mass1, qmin=0.1, msort=15, qmin_msort=0.7, m2_min_msort=12)
245
+ ind_light, = np.where(mass1 < 15.0)
246
+ ind_massive, = np.where(mass1 >= 15.0)
247
+ m2_light = mass2[ind_light]
248
+ m2_massive = mass2[ind_massive]
249
+ q_light = mass2[ind_light]/mass1[ind_light]
250
+ q_massive = mass2[ind_massive]/mass1[ind_massive]
251
+ assert m2_light.min() > np.min(mass1[ind_light]) * 0.1
252
+ assert m2_massive.min() > np.min(mass1[ind_massive]) * 0.7
253
+ assert q_light.min() > QMIN_LOWMASS
254
+ assert q_massive.min() > QMIN_HIGHMASS
255
+ # Check that the binary fraction tracking is correct when using msort
256
+ m1_b, m1_s, binfrac, bin_index = SAMPLECLASS.binary_select(primary_mass=mass1, binfrac_model=0.5, msort=15, binfrac_model_msort=0.8)
257
+ assert np.sum(binfrac==0.5) == BINFRAC_LOWMASS
258
+ assert np.sum(binfrac==0.8) == BINFRAC_HIGHMASS
259
+
260
+ #
261
+ def test_sample_porb(self):
262
+ # next do Sana12
263
+ np.random.seed(4)
264
+ mass1, total_mass = SAMPLECLASS.sample_primary(primary_model='kroupa01', size=100000)
265
+ mass2 = SAMPLECLASS.sample_secondary(primary_mass = mass1, qmin=0.1)
266
+ rad1 = SAMPLECLASS.set_reff(mass=mass1, metallicity=0.02)
267
+ rad2 = SAMPLECLASS.set_reff(mass=mass2, metallicity=0.02)
268
+ porb,aRL_over_a = SAMPLECLASS.sample_porb(
269
+ mass1, mass2, rad1, rad2, 'sana12', size=mass1.size
270
+ )
271
+ power_slope = power_law_fit(np.log10(porb))
272
+ self.assertEqual(np.round(power_slope, 2), SANA12_PORB_POWER_LAW)
273
+
274
+ # now some custom power laws
275
+ for slope in [-0.5, 0.5, 1]:
276
+ porb,aRL_over_a = SAMPLECLASS.sample_porb(
277
+ mass1, mass2, rad1, rad2, porb_model={
278
+ "min": 0.15, "max": 5, "slope": slope
279
+ }, size=mass1.size
280
+ )
281
+ power_slope = power_law_fit(np.log10(porb))
282
+ self.assertEqual(np.round(power_slope, 1), slope)
283
+
284
+ np.random.seed(5)
285
+ # next do Renzo+19
286
+ m1_high = mass1+15
287
+ rad1_high = SAMPLECLASS.set_reff(mass=m1_high, metallicity=0.02)
288
+ porb,aRL_over_a = SAMPLECLASS.sample_porb(
289
+ m1_high, mass2, rad1_high, rad2, 'renzo19', size=m1_high.size
290
+ )
291
+ porb_cut = porb[porb > 2.5]
292
+ power_slope = power_law_fit(np.log10(porb_cut))
293
+ self.assertAlmostEqual(np.round(power_slope, 2), SANA12_PORB_POWER_LAW)
294
+
295
+ porb,aRL_over_a = SAMPLECLASS.sample_porb(
296
+ mass1, mass2, rad1, rad2, 'renzo19', size=mass1.size
297
+ )
298
+ ind_log_uniform, = np.where(mass1 <= 15)
299
+ porb_log_uniform = porb[ind_log_uniform]
300
+ uniform_slope = linear_fit(np.log10(porb_log_uniform))
301
+ self.assertEqual(np.round(uniform_slope, 1), FLAT_SLOPE)
302
+
303
+ # next check the log uniform
304
+ porb,aRL_over_a = SAMPLECLASS.sample_porb(
305
+ mass1, mass2, rad1, rad2, 'log_uniform', size=mass1.size
306
+ )
307
+ power_slope = linear_fit(np.log10(porb))
308
+ sep = a_from_p(porb, mass1, mass2)
309
+ sep = sep[sep > 10]
310
+ uniform_slope = linear_fit(np.log10(sep))
311
+ self.assertEqual(np.round(uniform_slope, 1), FLAT_SLOPE)
312
+
313
+ # next check raghavan10
314
+ porb,aRL_over_a = SAMPLECLASS.sample_porb(
315
+ mass1, mass2, rad1, rad2, 'raghavan10', size=mass1.size
316
+ )
317
+ log_porb_mean = np.mean(np.log10(porb))
318
+ log_porb_sigma = np.std(np.log10(porb))
319
+ self.assertTrue(np.round(log_porb_mean, 1) >= MEAN_RAGHAVAN-0.15)
320
+ self.assertEqual(np.round(log_porb_sigma, 0), np.round(SIGMA_RAGHAVAN, 0))
321
+
322
+ # next check martinez26
323
+ porb,aRL_over_a = SAMPLECLASS.sample_porb(
324
+ mass1, mass2, rad1, rad2, 'martinez26', size=mass1.size
325
+ )
326
+ # the part of the model with m1 < 8 M_sun should follow the Raghavan10 distribution
327
+ porb_low_mass = porb[mass1 < 8]
328
+ log_porb_mean = np.mean(np.log10(porb_low_mass))
329
+ log_porb_sigma = np.std(np.log10(porb_low_mass))
330
+ self.assertTrue(np.round(log_porb_mean, 1) >= MEAN_RAGHAVAN-0.15)
331
+ self.assertEqual(np.round(log_porb_sigma, 0), np.round(SIGMA_RAGHAVAN, 0))
332
+ # the part of the model with m1 >= 8 M_sun should follow the same power law as Sana12
333
+ m1_high = mass1+8
334
+ rad1_high = SAMPLECLASS.set_reff(mass=m1_high, metallicity=0.02)
335
+ porb,aRL_over_a = SAMPLECLASS.sample_porb(
336
+ m1_high, mass2, rad1_high, rad2, 'martinez26', size=m1_high.size
337
+ )
338
+ porb_high_mass = porb[(m1_high >= 8) & (np.log10(porb) > 0.5)]
339
+ power_slope = power_law_fit(np.log10(porb_high_mass), n_bins=25)
340
+ self.assertEqual(np.round(power_slope, 2), SANA12_PORB_POWER_LAW)
341
+
342
+ # next check moe19
343
+ from cosmic.utils import get_met_dep_binfrac
344
+ from scipy.interpolate import interp1d
345
+ from scipy.stats import kstest
346
+ metallicity = 0.001
347
+ # this is a metallicity dependent population:
348
+ binfrac = get_met_dep_binfrac(metallicity)
349
+ mass1, total_mass = SAMPLECLASS.sample_primary(primary_model='kroupa01', size=100000)
350
+ (mass1_binaries, mass_single, binfrac_binaries, binary_index,) = SAMPLECLASS.binary_select(
351
+ mass1, binfrac_model=binfrac,
352
+ )
353
+ mass2_binaries = SAMPLECLASS.sample_secondary(
354
+ primary_mass=mass1_binaries, qmin=0.1
355
+ )
356
+ rad1 = SAMPLECLASS.set_reff(mass=mass1_binaries, metallicity=metallicity)
357
+ rad2 = SAMPLECLASS.set_reff(mass=mass2_binaries, metallicity=metallicity)
358
+
359
+
360
+ porb,aRL_over_a = SAMPLECLASS.sample_porb(
361
+ mass1_binaries, mass2_binaries, rad1, rad2, 'moe19', size=mass1_binaries.size, met=metallicity
362
+ )
363
+
364
+ binary_frac = len(porb) / (len(mass1))
365
+ self.assertAlmostEqual(np.round(binary_frac, 2), binfrac)
366
+
367
+
368
+
369
+ def test_sample_ecc(self):
370
+ np.random.seed(2)
371
+ # Check that the sample_ecc function samples ecc properly
372
+
373
+ # first sample orbital periods
374
+ np.random.seed(4)
375
+ mass1, total_mass = SAMPLECLASS.sample_primary(primary_model='kroupa01', size=100000)
376
+ mass2 = SAMPLECLASS.sample_secondary(primary_mass = mass1, qmin=0.1)
377
+ rad1 = SAMPLECLASS.set_reff(mass=mass1, metallicity=0.02)
378
+ rad2 = SAMPLECLASS.set_reff(mass=mass2, metallicity=0.02)
379
+ porb,aRL_over_a = SAMPLECLASS.sample_porb(
380
+ mass1, mass2, rad1, rad2, 'sana12', size=mass1.size
381
+ )
382
+
383
+ # now we feed aRL_over_a into sample_ecc
384
+ ecc = SAMPLECLASS.sample_ecc(aRL_over_a, ecc_model='thermal', size=mass1.size)
385
+ ecc_cut = ecc[ecc < 0.7]
386
+ slope = linear_fit(ecc_cut)
387
+ self.assertEqual(np.round(slope, 0), THERMAL_SLOPE)
388
+
389
+ ecc = SAMPLECLASS.sample_ecc(aRL_over_a, ecc_model='sana12', size=mass1.size)
390
+ ecc_cut = ecc[ecc < 0.91]
391
+ power_slope = power_law_fit(ecc_cut)
392
+ self.assertEqual(np.round(power_slope, 1), SANA12_ECC_POWER_LAW_ROUND)
393
+
394
+ ecc = SAMPLECLASS.sample_ecc(aRL_over_a, ecc_model='circular', size=mass1.size)
395
+ self.assertEqual(np.mean(ecc), 0.0)
396
+
397
+
398
+ def test_sample_SFH(self):
399
+ np.random.seed(2)
400
+ # Check that the sample SFH function samples SFH='const' correctly
401
+ times, met = SAMPLECLASS.sample_SFH(SF_start=10000.0,\
402
+ SF_duration=10000.0,\
403
+ met = 0.02, size=100)
404
+ self.assertEqual(times.sum(), CONST_SFR_SUM)
405
+ self.assertAlmostEqual(np.mean(met), 0.02)
406
+
407
+ np.random.seed(2)
408
+ # Check that the sample SFH function samples SFH='burst' correctly
409
+ times, met = SAMPLECLASS.sample_SFH(SF_start=10000.0,\
410
+ SF_duration=1000.0,\
411
+ met = 0.02, size=100)
412
+ self.assertEqual(times.sum(), BURST_SFR_SUM)
413
+ self.assertAlmostEqual(np.mean(met), 0.02)
414
+
415
+ # Check that the sample SFH function samples SFH='delta_burst' correctly
416
+ times, met = SAMPLECLASS.sample_SFH(SF_start=10000.0,\
417
+ SF_duration=0.0,\
418
+ met = 0.02, size=100)
419
+ self.assertEqual(times.sum(), 100*10000.0)
420
+ self.assertAlmostEqual(np.mean(met), 0.02)
421
+
422
+ def test_set_kstar(self):
423
+ # Check that the kstar is selected properly
424
+ kstar = SAMPLECLASS.set_kstar(pd.DataFrame([1.0, 1.0, 1.0, 1.0, 1.0]))
425
+ self.assertEqual(np.mean(kstar), KSTAR_SOLAR)
426
+
427
+ def test_Moe_sample(self):
428
+ # Test the multidim sampler and system-by-system binary fraction
429
+ m1, m2, porb, ecc, single_mass_list, mass_singles, mass_binaries, n_singles, n_binaries, binfrac = MULTIDIMSAMPLECLASS.initial_sample(rand_seed = 2, size=10, nproc=1, mp_seeds=[0])
430
+ self.assertEqual(np.sum(mass_singles), MOE_TOTAL_MASS)
431
+ self.assertAlmostEqual(binfrac.max(), MULTIDIM_BINFRAC_MAX)
432
+ self.assertAlmostEqual(binfrac.min(), MULTIDIM_BINFRAC_MIN)
433
+
434
+ def test_sample_MultiDim_SFH(self):
435
+ np.random.seed(2)
436
+ # Check that the sample SFH function samples SFH='const' correctly
437
+ times, met = MULTIDIMSAMPLECLASS.sample_SFH(SF_start=10000.0,\
438
+ SF_duration=10000.0,\
439
+ met = 0.02, size=100)
440
+ self.assertEqual(times.sum(), CONST_SFR_SUM)
441
+ self.assertAlmostEqual(np.mean(met), 0.02)
442
+
443
+ np.random.seed(2)
444
+ # Check that the sample SFH function samples SFH='burst' correctly
445
+ times, met = MULTIDIMSAMPLECLASS.sample_SFH(SF_start=10000.0,\
446
+ SF_duration=1000.0,\
447
+ met = 0.02, size=100)
448
+ self.assertEqual(times.sum(), BURST_SFR_SUM)
449
+ self.assertAlmostEqual(np.mean(met), 0.02)
450
+
451
+ # Check that the sample SFH function samples SFH='delta_burst' correctly
452
+ times, met = MULTIDIMSAMPLECLASS.sample_SFH(SF_start=10000.0,\
453
+ SF_duration=0.0,\
454
+ met = 0.02, size=100)
455
+ self.assertEqual(times.sum(), 100*10000.0)
456
+ self.assertAlmostEqual(np.mean(met), 0.02)
457
+
458
+ def test_set_kstar_MultiDim(self):
459
+ # Check that the kstar is selected properly
460
+ kstar = MULTIDIMSAMPLECLASS.set_kstar(pd.DataFrame([1.0, 1.0, 1.0, 1.0, 1.0]))
461
+ self.assertEqual(np.mean(kstar), KSTAR_SOLAR)
462
+
463
+ def test_sampling_binfrac_zero(self):
464
+ # check that you can't sample based on size with a binary fraction of 0.0
465
+ it_fails = False
466
+ try:
467
+ InitialBinaryTable.sampler('independent', np.arange(16), np.arange(16),
468
+ primary_model='kroupa01', ecc_model='thermal',
469
+ porb_model='sana12', binfrac_model=0.0,
470
+ SF_start=10.0, SF_duration=0.0, met=0.02,
471
+ size=1000)
472
+ except ValueError:
473
+ it_fails = True
474
+ self.assertTrue(it_fails)
475
+
476
+ def test_sampling_targets_bad_input(self):
477
+ # check that you get an error if you don't supply a size or total_mass
478
+ it_fails = False
479
+ try:
480
+ InitialBinaryTable.sampler('independent', np.arange(16), np.arange(16),
481
+ primary_model='kroupa01', ecc_model='thermal',
482
+ porb_model='sana12', binfrac_model=0.5,
483
+ SF_start=10.0, SF_duration=0.0, met=0.02,
484
+ sampling_target="total_mass")
485
+ except ValueError:
486
+ it_fails = True
487
+ self.assertTrue(it_fails)
488
+
489
+ it_fails = False
490
+ try:
491
+ InitialBinaryTable.sampler('independent', np.arange(16), np.arange(16),
492
+ primary_model='kroupa01', ecc_model='thermal',
493
+ porb_model='sana12', binfrac_model=0.5,
494
+ SF_start=10.0, SF_duration=0.0, met=0.02,
495
+ total_mass=None, sampling_target="total_mass")
496
+ except ValueError:
497
+ it_fails = True
498
+ self.assertTrue(it_fails)
499
+
500
+ it_fails = False
501
+ try:
502
+ InitialBinaryTable.sampler('independent', np.arange(16), np.arange(16),
503
+ primary_model='kroupa01', ecc_model='thermal',
504
+ porb_model='sana12', binfrac_model=0.5,
505
+ SF_start=10.0, SF_duration=0.0, met=0.02,
506
+ size=None, total_mass=None, sampling_target="size")
507
+ except ValueError:
508
+ it_fails = True
509
+ self.assertTrue(it_fails)
510
+
511
+ def test_sampling_targets_size(self):
512
+ # check that you can sample based on size
513
+ for size in np.random.randint(100, 1000, size=100):
514
+ samples = InitialBinaryTable.sampler('independent', np.arange(16), np.arange(16),
515
+ primary_model='kroupa01', ecc_model='thermal',
516
+ porb_model='sana12', binfrac_model=0.5,
517
+ SF_start=10.0, SF_duration=0.0, met=0.02,
518
+ size=size)
519
+ self.assertGreaterEqual(len(samples[0]), size)
520
+
521
+ def test_sampling_targets_mass(self):
522
+ # check that you can sample based on total mass
523
+ for mass in np.random.randint(100, 1000, size=100):
524
+ samples = InitialBinaryTable.sampler('independent', np.arange(16), np.arange(16),
525
+ primary_model='kroupa01', ecc_model='thermal',
526
+ porb_model='sana12', binfrac_model=0.5,
527
+ SF_start=10.0, SF_duration=0.0, met=0.02,
528
+ size=None, total_mass=mass, sampling_target="total_mass")
529
+ self.assertGreaterEqual(samples[1] + samples[2], mass)
530
+
531
+ def test_sampling_targets_mass_trimmed(self):
532
+ # check that you can sample based on total mass and that the samples are trimmed properly
533
+ for mass in np.random.randint(100, 1000, size=100):
534
+ samples = InitialBinaryTable.sampler('independent', np.arange(16), np.arange(16),
535
+ primary_model='kroupa01', ecc_model='thermal',
536
+ porb_model='sana12', binfrac_model=0.5,
537
+ SF_start=10.0, SF_duration=0.0, met=0.02,
538
+ size=None, total_mass=mass, sampling_target="total_mass",
539
+ trim_extra_samples=True)
540
+ self.assertLessEqual(abs(samples[1] + samples[2] - mass), 300)
541
+
542
+ class TestCMCSample(unittest.TestCase):
543
+ def test_plummer_profile(self):
544
+ np.random.seed(2)
545
+ r, vr, vt = CMCSAMPLECLASS.set_r_vr_vt('plummer',N=100, r_max=300)
546
+ np.testing.assert_allclose(VR_PLUMMER_TEST_ARRAY, vr, rtol=1e-5)
547
+ np.testing.assert_allclose(VT_PLUMMER_TEST_ARRAY, vt, rtol=1e-5)
548
+ np.testing.assert_allclose(R_PLUMMER_TEST_ARRAY, r, rtol=1e-5)
549
+
550
+ def test_elson_profile(self):
551
+ np.random.seed(2)
552
+ r, vr, vt = CMCSAMPLECLASS.set_r_vr_vt('elson',N=100, r_max=300, gamma=3)
553
+ np.testing.assert_allclose(VR_ELSON_TEST_ARRAY, vr, rtol=1e-5)
554
+ np.testing.assert_allclose(VT_ELSON_TEST_ARRAY, vt, rtol=1e-5)
555
+ np.testing.assert_allclose(R_ELSON_TEST_ARRAY, r, rtol=1e-5)
556
+
557
+ def test_king_profile(self):
558
+ np.random.seed(2)
559
+ r, vr, vt = CMCSAMPLECLASS.set_r_vr_vt('king',N=100, w_0=5)
560
+ np.testing.assert_allclose(VR_KING_TEST_ARRAY, vr, rtol=1e-5)
561
+ np.testing.assert_allclose(VT_KING_TEST_ARRAY, vt, rtol=1e-5)
562
+ np.testing.assert_allclose(R_KING_TEST_ARRAY, r, rtol=1e-5)
563
+
564
+ def test_set_reff(self):
565
+ reff = CMCSAMPLECLASS.set_reff(mass=np.array([10.0, 20.0]), metallicity=0.02)
566
+ np.testing.assert_allclose(REFF_TEST_ARRAY, reff)
567
+
568
+ def test_cmc_sampler(self):
569
+ np.random.seed(2)
570
+ # Test generating CMC initial conditions and test saving the output to files
571
+ Singles, Binaries = InitialCMCTable.sampler('cmc', binfrac_model=0.2, primary_model='kroupa01', ecc_model='sana12', porb_model='sana12', cluster_profile='plummer', met=0.014, size=20, params=os.path.join(TEST_DATA_DIR,'Params.ini'), gamma=4, r_max=100, qmin=0.1)
572
+ InitialCMCTable.write(Singles, Binaries, filename="input.hdf5")
573
+ InitialCMCTable.write(Singles, Binaries, filename="input.fits")
574
+ Singles, Binaries = InitialCMCTable.read(filename="input.fits")
575
+ # read the test files and compare to the static unit tests files
576
+ pd.testing.assert_frame_equal(Singles, SINGLES_CMC_FITS)
577
+ pd.testing.assert_frame_equal(Binaries, BINARIES_CMC_FITS)
578
+ Singles, Binaries = InitialCMCTable.read(filename="input.hdf5")
579
+ pd.testing.assert_frame_equal(Singles, SINGLES_CMC_HDF5)
580
+ pd.testing.assert_frame_equal(Binaries, BINARIES_CMC_HDF5)