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/.dylibs/libgcc_s.1.1.dylib +0 -0
- cosmic/.dylibs/libgfortran.5.dylib +0 -0
- cosmic/.dylibs/libquadmath.0.dylib +0 -0
- cosmic/Match.py +191 -0
- cosmic/__init__.py +32 -0
- cosmic/_commit_hash.py +1 -0
- cosmic/_evolvebin.cpython-313-darwin.so +0 -0
- cosmic/_version.py +1 -0
- cosmic/bse_utils/__init__.py +18 -0
- cosmic/bse_utils/zcnsts.py +570 -0
- cosmic/bse_utils/zdata.py +596 -0
- cosmic/checkstate.py +128 -0
- cosmic/data/cosmic-settings.json +1635 -0
- cosmic/evolve.py +607 -0
- cosmic/filter.py +214 -0
- cosmic/get_commit_hash.py +15 -0
- cosmic/output.py +466 -0
- cosmic/plotting.py +680 -0
- cosmic/sample/__init__.py +26 -0
- cosmic/sample/cmc/__init__.py +18 -0
- cosmic/sample/cmc/elson.py +411 -0
- cosmic/sample/cmc/king.py +260 -0
- cosmic/sample/initialbinarytable.py +251 -0
- cosmic/sample/initialcmctable.py +449 -0
- cosmic/sample/sampler/__init__.py +25 -0
- cosmic/sample/sampler/cmc.py +418 -0
- cosmic/sample/sampler/independent.py +1252 -0
- cosmic/sample/sampler/multidim.py +882 -0
- cosmic/sample/sampler/sampler.py +130 -0
- cosmic/test_evolve.py +108 -0
- cosmic/test_match.py +30 -0
- cosmic/test_sample.py +580 -0
- cosmic/test_utils.py +198 -0
- cosmic/utils.py +1574 -0
- cosmic_popsynth-3.6.2.data/scripts/cosmic-pop +544 -0
- cosmic_popsynth-3.6.2.dist-info/METADATA +55 -0
- cosmic_popsynth-3.6.2.dist-info/RECORD +38 -0
- cosmic_popsynth-3.6.2.dist-info/WHEEL +6 -0
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)
|