cosmic-popsynth 3.4.15__cp39-cp39-macosx_11_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,873 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) Katelyn Breivik (2017 - 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
+ """`multidim`
20
+ """
21
+ from schwimmbad import MultiPool, MPIPool
22
+
23
+ from .sampler import register_sampler
24
+ from .. import InitialBinaryTable
25
+ from ... import utils
26
+
27
+ import numpy as np
28
+
29
+ __author__ = "Katelyn Breivik <katie.breivik@gmail.com>"
30
+ __credits__ = "Scott Coughlin <scott.coughlin@ligo.org>"
31
+ __all__ = ["get_multidim_sampler", "MultiDim"]
32
+
33
+
34
+ def get_multidim_sampler(
35
+ final_kstar1,
36
+ final_kstar2,
37
+ rand_seed,
38
+ nproc,
39
+ SF_start,
40
+ SF_duration,
41
+ met,
42
+ size,
43
+ **kwargs
44
+ ):
45
+ """adapted version of Maxwell Moe's IDL code that generates a population of single and binary stars
46
+
47
+ Below is the adapted version of Maxwell Moe's IDL code
48
+ that generates a population of single and binary stars
49
+ based on the paper Mind your P's and Q's
50
+ By Maxwell Moe and Rosanne Di Stefano
51
+
52
+ The python code has been adopted by Mads Sørensen
53
+
54
+ Version history:
55
+ V. 0.1; 2017/02/03
56
+ By Mads Sørensen
57
+ - This is a pure adaption from IDL to Python.
58
+ - The function idl_tabulate is similar to
59
+ the IDL function int_tabulated except, this function seems to be slightly
60
+ more exact in its solution.
61
+ Therefore, relative to the IDL code, there are small numerical differences.
62
+
63
+ Comments below beginning with ; is the original nodes by Maxwell Moe.
64
+ Please read these careful for understanding the script.
65
+ ; NOTE - This version produces only the statistical distributions of
66
+ ; single stars, binaries, and inner binaries in hierarchical triples.
67
+ ; Outer tertiaries in hierarchical triples are NOT generated.
68
+ ; Moreover, given a set of companions, all with period P to
69
+ ; primary mass M1, this version currently uses an approximation to
70
+ ; determine the fraction of those companions that are inner binaries
71
+ ; vs. outer triples. Nevertheless, this approximation reproduces
72
+ ; the overall multiplicity statistics.
73
+ ; Step 1 - Tabulate probably density functions of periods,
74
+ ; mass ratios, and eccentricities based on
75
+ ; analytic fits to corrected binary star populations.
76
+ ; Step 2 - Implement Monte Carlo method to generate stellar
77
+ ; population from those density functions.
78
+
79
+ Parameters
80
+ ----------
81
+ final_kstar1 : `list` or `int`
82
+ Int or list of final kstar1
83
+
84
+ final_kstar2 : `list` or `int`
85
+ Int or list of final kstar2
86
+
87
+ rand_seed : `int`
88
+ Int to seed random number generator
89
+
90
+ nproc : `int`
91
+ Number of processors to use to generate population
92
+
93
+ SF_start : `float`
94
+ Time in the past when star formation initiates in Myr
95
+
96
+ SF_duration : `float`
97
+ Duration of constant star formation beginning from SF_Start in Myr
98
+
99
+ met : `float`
100
+ Sets the metallicity of the binary population where solar metallicity is 0.02
101
+
102
+ size : `int`
103
+ Size of the population to sample
104
+
105
+ **porb_lo : `float`
106
+ Lower limit in days for the orbital period distribution
107
+
108
+ **porb_hi: `float`
109
+ Upper limit in days for the orbital period distribution
110
+
111
+ Returns
112
+ -------
113
+ InitialBinaryTable : `pandas.DataFrame`
114
+ DataFrame in the format of the InitialBinaryTable
115
+
116
+ mass_singles : `float`
117
+ Total mass in single stars needed to generate population
118
+
119
+ mass_binaries : `float`
120
+ Total mass in binaries needed to generate population
121
+
122
+ n_singles : `int`
123
+ Number of single stars needed to generate a population
124
+
125
+ n_binaries : `int`
126
+ Number of binaries needed to generate a population
127
+ """
128
+
129
+ if type(final_kstar1) in [int, float]:
130
+ final_kstar1 = [final_kstar1]
131
+ if type(final_kstar2) in [int, float]:
132
+ final_kstar2 = [final_kstar2]
133
+ porb_lo = kwargs.pop("porb_lo", 0.15)
134
+ porb_hi = kwargs.pop("porb_hi", 8.0)
135
+ pool = kwargs.pop("pool", None)
136
+ mp_seeds = kwargs.pop("mp_seeds", None)
137
+
138
+ primary_min, primary_max, secondary_min, secondary_max = utils.mass_min_max_select(
139
+ final_kstar1, final_kstar2
140
+ )
141
+
142
+ initconditions = MultiDim()
143
+
144
+ (
145
+ mass1_binary,
146
+ mass2_binary,
147
+ porb,
148
+ ecc,
149
+ mass_singles,
150
+ mass_binaries,
151
+ n_singles,
152
+ n_binaries,
153
+ binfrac,
154
+ ) = initconditions.initial_sample(
155
+ primary_min,
156
+ secondary_min,
157
+ primary_max,
158
+ secondary_max,
159
+ porb_lo,
160
+ porb_hi,
161
+ rand_seed,
162
+ size=size,
163
+ nproc=nproc,
164
+ pool=pool,
165
+ mp_seeds=mp_seeds,
166
+ )
167
+
168
+ tphysf, metallicity = initconditions.sample_SFH(
169
+ SF_start=SF_start, SF_duration=SF_duration, met=met, size=mass1_binary.size
170
+ )
171
+
172
+ kstar1 = initconditions.set_kstar(mass1_binary)
173
+ kstar2 = initconditions.set_kstar(mass2_binary)
174
+ metallicity[metallicity < 1e-4] = 1e-4
175
+ metallicity[metallicity > 0.03] = 0.03
176
+
177
+ if kwargs.pop("keep_singles", False):
178
+ binary_table = InitialBinaryTable.InitialBinaries(
179
+ mass1_binary,
180
+ mass2_binary,
181
+ porb,
182
+ ecc,
183
+ tphysf,
184
+ kstar1,
185
+ kstar2,
186
+ metallicity,
187
+ binfrac=binfrac,
188
+ )
189
+ tphysf, metallicity = initconditions.sample_SFH(
190
+ SF_start=SF_start, SF_duration=SF_duration, met=met, size=mass_singles.size
191
+ )
192
+ metallicity[metallicity < 1e-4] = 1e-4
193
+ metallicity[metallicity > 0.03] = 0.03
194
+ kstar1 = initconditions.set_kstar(mass_singles)
195
+ singles_table = InitialBinaryTable.InitialBinaries(
196
+ mass_singles,
197
+ np.ones_like(mass_singles)*0,
198
+ np.ones_like(mass_singles)*-1,
199
+ np.ones_like(mass_singles)*-1,
200
+ tphysf,
201
+ kstar1,
202
+ np.ones_like(mass_singles)*0,
203
+ metallicity,
204
+ )
205
+ binary_table = binary_table.append(singles_table)
206
+ else:
207
+ binary_table = InitialBinaryTable.InitialBinaries(
208
+ mass1_binary,
209
+ mass2_binary,
210
+ porb,
211
+ ecc,
212
+ tphysf,
213
+ kstar1,
214
+ kstar2,
215
+ metallicity,
216
+ binfrac=binfrac,
217
+ )
218
+
219
+ return (
220
+ binary_table,
221
+ mass_singles,
222
+ mass_binaries,
223
+ n_singles,
224
+ n_binaries,
225
+ )
226
+
227
+ register_sampler(
228
+ "multidim",
229
+ InitialBinaryTable,
230
+ get_multidim_sampler,
231
+ usage="final_kstar1, final_kstar2, rand_seed, nproc, SFH_model, component_age, metallicity, size, binfrac",
232
+ )
233
+
234
+
235
+ class MultiDim:
236
+
237
+ # -----------------------------------
238
+ # Below is the adapted version of Maxwell Moe's IDL code
239
+ # that generates a population of single and binary stars
240
+ # based on the paper Mind your P's and Q's
241
+ # By Maxwell Moe and Rosanne Di Stefano
242
+ #
243
+ # The python code has been adopted by Mads Sørensen
244
+ # -----------------------------------
245
+ # Version history:
246
+ # V. 0.1; 2017/02/03
247
+ # By Mads Sørensen
248
+ # - This is a pure adaption from IDL to Python.
249
+ # - The function idl_tabulate is similar to
250
+ # the IDL function int_tabulated except, this function seems to be slightly
251
+ # more exact in its solution.
252
+ # Therefore, relative to the IDL code, there are small numerical differences.
253
+ # -----------------------------------
254
+
255
+ #
256
+ # Comments below beginning with ; is the original nodes by Maxwell Moe.
257
+ # Please read these careful for understanding the script.
258
+ # ; NOTE - This version produces only the statistical distributions of
259
+ # ; single stars, binaries, and inner binaries in hierarchical triples.
260
+ # ; Outer tertiaries in hierarchical triples are NOT generated.
261
+ # ; Moreover, given a set of companions, all with period P to
262
+ # ; primary mass M1, this version currently uses an approximation to
263
+ # ; determine the fraction of those companions that are inner binaries
264
+ # ; vs. outer triples. Nevertheless, this approximation reproduces
265
+ # ; the overall multiplicity statistics.
266
+ # ; Step 1 - Tabulate probably density functions of periods,
267
+ # ; mass ratios, and eccentricities based on
268
+ # ; analytic fits to corrected binary star populations.
269
+ # ; Step 2 - Implement Monte Carlo method to generate stellar
270
+ # ; population from those density functions.
271
+ # ;
272
+ def initial_sample(
273
+ self,
274
+ M1min=0.08,
275
+ M2min=0.08,
276
+ M1max=150.0,
277
+ M2max=150.0,
278
+ porb_lo=0.15,
279
+ porb_hi=8.0,
280
+ rand_seed=0,
281
+ size=None,
282
+ nproc=1,
283
+ pool=None,
284
+ mp_seeds=None,
285
+ ):
286
+ """Sample initial binary distribution according to Moe & Di Stefano (2017)
287
+ <http://adsabs.harvard.edu/abs/2017ApJS..230...15M>`_
288
+
289
+ Parameters
290
+ ----------
291
+ M1min : `float`
292
+ minimum primary mass to sample [Msun]
293
+ DEFAULT: 0.08
294
+ M2min : `float`
295
+ minimum secondary mass to sample [Msun]
296
+ DEFAULT: 0.08
297
+ M1max : `float`
298
+ maximum primary mass to sample [Msun]
299
+ DEFAULT: 150.0
300
+ M2max : `float`
301
+ maximum primary mass to sample [Msun]
302
+ DEFAULT: 150.0
303
+ porb_lo : `float`
304
+ minimum orbital period to sample [log10(days)]
305
+ porb_hi : `float`
306
+ maximum orbital period to sample [log10(days)]
307
+ rand_seed : int
308
+ random seed generator
309
+ DEFAULT: 0
310
+ size : int, optional
311
+ number of evolution times to sample
312
+ NOTE: this is set in cosmic-pop call as Nstep
313
+
314
+ Returns
315
+ -------
316
+ primary_mass_list : array
317
+ array of primary masses with size=size
318
+ secondary_mass_list : array
319
+ array of secondary masses with size=size
320
+ porb_list : array
321
+ array of orbital periods in days with size=size
322
+ ecc_list : array
323
+ array of eccentricities with size=size
324
+ mass_singles : `float`
325
+ Total mass in single stars needed to generate population
326
+ mass_binaries : `float`
327
+ Total mass in binaries needed to generate population
328
+ n_singles : `int`
329
+ Number of single stars needed to generate a population
330
+ n_binaries : `int`
331
+ Number of binaries needed to generate a population
332
+ binfrac_list : array
333
+ array of binary probabilities based on primary mass and period with size=size
334
+ """
335
+ if pool is None:
336
+ with MultiPool(processes=nproc) as pool:
337
+ if mp_seeds is not None:
338
+ if len(list(mp_seeds)) != nproc:
339
+ raise ValueError("Must supply a list of random seeds with length equal to number of processors")
340
+ else:
341
+ mp_seeds = [nproc * (task._identity[0]-1) for task in pool._pool]
342
+
343
+ inputs = [(M1min, M2min, M1max, M2max, porb_hi, porb_lo, size/nproc, rand_seed + mp_seed)
344
+ for mp_seed in mp_seeds]
345
+ worker = Worker()
346
+ results = list(pool.map(worker, inputs))
347
+ else:
348
+ if mp_seeds is not None:
349
+ if len(list(mp_seeds)) != nproc:
350
+ raise ValueError("Must supply a list of random seeds with length equal to number of processors")
351
+ else:
352
+ if isinstance(pool, MPIPool):
353
+ mp_seeds = [nproc * (task - 1) for task in pool.workers]
354
+ elif isinstance(pool, MultiPool):
355
+ mp_seeds = [nproc * (task._identity[0] - 1) for task in pool._pool]
356
+ else:
357
+ mp_seeds = [0 for i in range(nproc)]
358
+
359
+ inputs = [(M1min, M2min, M1max, M2max, porb_hi, porb_lo, size/nproc, rand_seed + mp_seed) for mp_seed in mp_seeds]
360
+ worker = Worker()
361
+ results = list(pool.map(worker, inputs))
362
+
363
+ dat_lists = [[], [], [], [], [], [], [], [], []]
364
+
365
+ for output_list in results:
366
+ ii = 0
367
+ for dat_list in output_list:
368
+ dat_lists[ii].append(dat_list)
369
+ ii += 1
370
+
371
+ primary_mass_list = np.hstack(dat_lists[0])
372
+ secondary_mass_list = np.hstack(dat_lists[1])
373
+ porb_list = np.hstack(dat_lists[2])
374
+ ecc_list = np.hstack(dat_lists[3])
375
+ mass_singles = np.sum(dat_lists[4])
376
+ mass_binaries = np.sum(dat_lists[5])
377
+ n_singles = np.sum(dat_lists[6])
378
+ n_binaries = np.sum(dat_lists[7])
379
+ binfrac_list = np.hstack(dat_lists[8])
380
+
381
+ return (
382
+ primary_mass_list,
383
+ secondary_mass_list,
384
+ porb_list,
385
+ ecc_list,
386
+ mass_singles,
387
+ mass_binaries,
388
+ n_singles,
389
+ n_binaries,
390
+ binfrac_list
391
+ )
392
+
393
+ def sample_SFH(self, SF_start=13700.0, SF_duration=0.0, met=0.02, size=None):
394
+ """Sample an evolution time for each binary based on a user-specified
395
+ time at the start of star formation and the duration of star formation.
396
+ The default is a burst of star formation 13,700 Myr in the past.
397
+
398
+ Parameters
399
+ ----------
400
+ SF_start : float
401
+ Time in the past when star formation initiates in Myr
402
+ SF_duration : float
403
+ Duration of constant star formation beginning from SF_Start in Myr
404
+ met : float
405
+ metallicity of the population [Z_sun = 0.02]
406
+ Default: 0.02
407
+ size : int, optional
408
+ number of evolution times to sample
409
+ NOTE: this is set in cosmic-pop call as Nstep
410
+
411
+ Returns
412
+ -------
413
+ tphys : array
414
+ array of evolution times of size=size
415
+ metallicity : array
416
+ array of metallicities
417
+ """
418
+
419
+ if (SF_start > 0.0) & (SF_duration >= 0.0):
420
+ tphys = np.random.uniform(SF_start - SF_duration, SF_start, size)
421
+ metallicity = np.ones(size)*met
422
+ return tphys, metallicity
423
+ else:
424
+ raise ValueError('SF_start and SF_duration must be positive and SF_start must be greater than 0.0')
425
+
426
+ def set_kstar(self, mass):
427
+ """Initialize stellar types according to BSE classification
428
+ kstar=1 if M>=0.7 Msun; kstar=0 if M<0.7 Msun
429
+
430
+ Parameters
431
+ ----------
432
+ mass : array
433
+ array of masses
434
+
435
+ Returns
436
+ -------
437
+ kstar : array
438
+ array of initial stellar types
439
+ """
440
+
441
+ kstar = np.zeros(mass.size)
442
+ low_cutoff = 0.7
443
+ lowIdx = np.where(mass < low_cutoff)[0]
444
+ hiIdx = np.where(mass >= low_cutoff)[0]
445
+
446
+ kstar[lowIdx] = 0
447
+ kstar[hiIdx] = 1
448
+
449
+ return kstar
450
+
451
+
452
+ class Worker(object):
453
+ def __call__(self, task):
454
+ M1min, M2min, M1max, M2max, porb_hi, porb_lo, size, seed = task
455
+ return self._sample_initial_pop(M1min, M2min, M1max, M2max, porb_hi, porb_lo, size, seed)
456
+
457
+ def _sample_initial_pop(self, M1min, M2min, M1max, M2max, porb_hi, porb_lo, size, seed):
458
+ # Tabulate probably density functions of periods,
459
+ # mass ratios, and eccentricities based on
460
+ # analytic fits to corrected binary star populations.
461
+
462
+ numM1 = 101
463
+ # use binwidths to maintain structure of original array
464
+ # default size is: numlogP=158
465
+ bwlogP = 0.05
466
+ numq = 91
467
+ nume = 100
468
+
469
+ # ; Vector of primary masses M1 (Msun), logarithmic orbital period P (days),
470
+ # ; mass ratios q = Mcomp/M1, and eccentricities e
471
+ #
472
+ # ; 0.8 < M1 < 40 (where we have statistics corrected for selection effects)
473
+ M1_lo = 0.8
474
+ M1_hi = 40
475
+
476
+ M1v = np.logspace(np.log10(M1_lo), np.log10(M1_hi), numM1)
477
+ # ; 0.15 < log P < 8.0
478
+ # ; or use user specified values
479
+ log10_porb_lo = porb_lo
480
+ log10_porb_hi = porb_hi
481
+ logPv = np.arange(log10_porb_lo, log10_porb_hi + bwlogP, bwlogP)
482
+ numlogP = len(logPv)
483
+
484
+ # ; 0.10 < q < 1.00
485
+ q_lo = 0.1
486
+ q_hi = 1.0
487
+ qv = np.linspace(q_lo, q_hi, numq)
488
+
489
+ # ; 0.0001 < e < 0.9901
490
+ # ; set minimum to non-zero value to avoid numerical errors
491
+ e_lo = 0.0
492
+ e_hi = 0.99
493
+ ev = np.linspace(e_lo, e_hi, nume) + 0.0001
494
+ # ; Note that companions outside this parameter space (e.g., q < 0.1,
495
+ # ; log P (days) > 8.0 are not constrained in M+D16 and therefore
496
+ # ; not considered.
497
+
498
+ # ; Distribution functions - define here, but evaluate within for loops.
499
+
500
+ # ; Frequency of companions with q > 0.1 per decade of orbital period.
501
+ # ; Bottom panel in Fig. 37 of M+D17
502
+ flogP_sq = np.zeros([numlogP, numM1])
503
+
504
+ # ; Given M1 and P, the cumulative distribution of mass ratios q
505
+ cumqdist = np.zeros([numq, numlogP, numM1])
506
+
507
+ # ; Given M1 and P, the cumulative distribution of eccentricities e
508
+ cumedist = np.zeros([nume, numlogP, numM1])
509
+
510
+ # ; Given M1 and P, the probability that the companion
511
+ # ; is a member of the inner binary (currently an approximation).
512
+ # ; 100% for log P < 1.5, decreases with increasing P
513
+ probbin = np.zeros([numlogP, numM1])
514
+
515
+ # ; Given M1, the cumulative period distribution of the inner binary
516
+ # ; Normalized so that max(cumPbindist) = total binary frac. (NOT unity)
517
+ cumPbindist = np.zeros([numlogP, numM1])
518
+ # ; Slope alpha of period distribution across intermediate periods
519
+ # ; 2.7 - DlogP < log P < 2.7 + DlogP, see Section 9.3 and Eqn. 23.
520
+ # ; Slightly updated from version 1.
521
+ alpha = 0.018
522
+ DlogP = 0.7
523
+
524
+ # ; Heaviside function for twins with 0.95 < q < 1.00
525
+ H = np.zeros(numq)
526
+ ind = np.where(qv >= 0.95)
527
+ H[ind] = 1.0
528
+ H = H / utils.idl_tabulate(qv, H) # ;normalize so that integral is unity
529
+
530
+ # ; Relevant indices with respect to mass ratio
531
+ indlq = np.where(qv >= 0.3)
532
+ indsq = np.where(qv < 0.3)
533
+ indq0p3 = np.min(indlq)
534
+
535
+ # FILL IN THE MULTIDIMENSIONAL DISTRIBUTION FUNCTIONS
536
+ # ; Loop through primary mass
537
+ for i in range(0, numM1):
538
+ myM1 = M1v[i]
539
+ # ; Twin fraction parameters that are dependent on M1 only; section 9.1
540
+ FtwinlogPle1 = 0.3 - 0.15 * np.log10(myM1) # ; Eqn. 6
541
+ logPtwin = 8.0 - myM1 # ; Eqn. 7a
542
+ if myM1 >= 6.5:
543
+ logPtwin = 1.5 # ; Eqn. 7b
544
+ # ; Frequency of companions with q > 0.3 at different orbital periods
545
+ # ; and dependent on M1 only; section 9.3 (slightly modified since v1)
546
+ flogPle1 = (
547
+ 0.020 + 0.04 * np.log10(myM1) + 0.07 * (np.log10(myM1)) ** 2.0
548
+ ) # ; Eqn. 20
549
+ flogPeq2p7 = (
550
+ 0.039 + 0.07 * np.log10(myM1) + 0.01 * (np.log10(myM1)) ** 2.0
551
+ ) # ; Eqn. 21
552
+ flogPeq5p5 = (
553
+ 0.078 - 0.05 * np.log10(myM1) + 0.04 * (np.log10(myM1)) ** 2.0
554
+ ) # ; Eqn. 22
555
+ # ; Loop through orbital period P
556
+ for j in range(0, numlogP):
557
+ mylogP = logPv[j]
558
+ # ; Given M1 and P, set excess twin fraction; section 9.1 and Eqn. 5
559
+ if mylogP <= 1.0:
560
+ Ftwin = FtwinlogPle1
561
+ else:
562
+ Ftwin = FtwinlogPle1 * (1.0 - (mylogP - 1.0) / (logPtwin - 1.0))
563
+ if mylogP >= logPtwin:
564
+ Ftwin = 0.0
565
+
566
+ # ; Power-law slope gamma_largeq for M1 < 1.2 Msun and various P; Eqn. 9
567
+ if mylogP <= 5.0:
568
+ gl_1p2 = -0.5
569
+ if mylogP > 5.0:
570
+ gl_1p2 = -0.5 - 0.3 * (mylogP - 5.0)
571
+
572
+ # ; Power-law slope gamma_largeq for M1 = 3.5 Msun and various P; Eqn. 10
573
+ if mylogP <= 1.0:
574
+ gl_3p5 = -0.5
575
+ if (mylogP > 1.0) and (mylogP <= 4.5):
576
+ gl_3p5 = -0.5 - 0.2 * (mylogP - 1.0)
577
+ if (mylogP > 4.5) and (mylogP <= 6.5):
578
+ gl_3p5 = -1.2 - 0.4 * (mylogP - 4.5)
579
+ if mylogP > 6.5:
580
+ gl_3p5 = -2.0
581
+
582
+ # ; Power-law slope gamma_largeq for M1 > 6 Msun and various P; Eqn. 11
583
+ if mylogP <= 1.0:
584
+ gl_6 = -0.5
585
+ if (mylogP > 1.0) and (mylogP <= 2.0):
586
+ gl_6 = -0.5 - 0.9 * (mylogP - 1.0)
587
+ if (mylogP > 2.0) and (mylogP <= 4.0):
588
+ gl_6 = -1.4 - 0.3 * (mylogP - 2.0)
589
+
590
+ if mylogP > 4.0:
591
+ gl_6 = -2.0
592
+
593
+ # ; Given P, interpolate gamma_largeq w/ respect to M1 at myM1
594
+ if myM1 <= 1.2:
595
+ gl = gl_1p2
596
+ if (myM1 > 1.2) and (myM1 <= 3.5):
597
+ gl = np.interp(
598
+ np.log10(myM1), np.log10([1.2, 3.5]), [gl_1p2, gl_3p5]
599
+ )
600
+ if (myM1 > 3.5) and (myM1 <= 6.0):
601
+ gl = np.interp(np.log10(myM1), np.log10([3.5, 6.0]), [gl_3p5, gl_6])
602
+ if myM1 > 6.0:
603
+ gl = gl_6
604
+
605
+ # ; Power-law slope gamma_smallq for M1 < 1.2 Msun and all P; Eqn. 13
606
+ gs_1p2 = 0.3
607
+
608
+ # ; Power-law slope gamma_smallq for M1 = 3.5 Msun and various P; Eqn. 14
609
+ if mylogP <= 2.5:
610
+ gs_3p5 = 0.2
611
+ if (mylogP > 2.5) and (mylogP <= 5.5):
612
+ gs_3p5 = 0.2 - 0.3 * (mylogP - 2.5)
613
+ if mylogP > 5.5:
614
+ gs_3p5 = -0.7 - 0.2 * (mylogP - 5.5)
615
+
616
+ # ; Power-law slope gamma_smallq for M1 > 6 Msun and various P; Eqn. 15
617
+ if mylogP <= 1.0:
618
+ gs_6 = 0.1
619
+ if (mylogP > 1.0) and (mylogP <= 3.0):
620
+ gs_6 = 0.1 - 0.15 * (mylogP - 1.0)
621
+ if (mylogP > 3.0) and (mylogP <= 5.6):
622
+ gs_6 = -0.2 - 0.50 * (mylogP - 3.0)
623
+ if mylogP > 5.6:
624
+ gs_6 = -1.5
625
+
626
+ # ; Given P, interpolate gamma_smallq w/ respect to M1 at myM1
627
+ if myM1 <= 1.2:
628
+ gs = gs_1p2
629
+ if (myM1 > 1.2) and (myM1 <= 3.5):
630
+ gs = np.interp(
631
+ np.log10(myM1), np.log10([1.2, 3.5]), [gs_1p2, gs_3p5]
632
+ )
633
+ if (myM1 > 3.5) and (myM1 <= 6.0):
634
+ gs = np.interp(np.log10(myM1), np.log10([3.5, 6.0]), [gs_3p5, gs_6])
635
+ if myM1 > 6.0:
636
+ gs = gs_6
637
+
638
+ # ; Given Ftwin, gamma_smallq, and gamma_largeq at the specified M1 & P,
639
+ # ; tabulate the cumulative mass ratio distribution across 0.1 < q < 1.0
640
+ fq = qv ** gl # ; slope across 0.3 < q < 1.0
641
+ fq = fq / utils.idl_tabulate(
642
+ qv[indlq], fq[indlq]
643
+ ) # ; normalize to 0.3 < q < 1.0
644
+ fq = fq * (1.0 - Ftwin) + H * Ftwin # ; add twins
645
+ fq[indsq] = (
646
+ fq[indq0p3] * (qv[indsq] / 0.3) ** gs
647
+ ) # ; slope across 0.1 < q < 0.3
648
+ cumfq = np.cumsum(fq) - fq[0] # ; cumulative distribution
649
+ cumfq = cumfq / np.max(cumfq) # ; normalize cumfq(q=1.0) = 1
650
+ cumqdist[:, j, i] = cumfq # ; save to grid
651
+
652
+ # ; Given M1 and P, q_factor is the ratio of all binaries 0.1 < q < 1.0
653
+ # ; to those with 0.3 < q < 1.0
654
+ q_factor = utils.idl_tabulate(qv, fq)
655
+
656
+ # ; Given M1 & P, calculate power-law slope eta of eccentricity dist.
657
+ if mylogP >= 0.7:
658
+ # ; For log P > 0.7 use fits in Section 9.2.
659
+ # ; Power-law slope eta for M1 < 3 Msun and log P > 0.7
660
+ eta_3 = 0.6 - 0.7 / (mylogP - 0.5) # ; Eqn. 17
661
+ # ; Power-law slope eta for M1 > 7 Msun and log P > 0.7
662
+ eta_7 = 0.9 - 0.2 / (mylogP - 0.5) # ; Eqn. 18
663
+ else:
664
+ # ; For log P < 0.7, set eta to fitted values at log P = 0.7
665
+ eta_3 = -2.9
666
+ eta_7 = -0.1
667
+
668
+ # ; Given P, interpolate eta with respect to M1 at myM1
669
+ if myM1 <= 3.0:
670
+ eta = eta_3
671
+ if (myM1 > 3.0) and (myM1 <= 7.0):
672
+ eta = np.interp(
673
+ np.log10(myM1), np.log10([3.0, 7.0]), [eta_3, eta_7]
674
+ )
675
+ if myM1 > 7.0:
676
+ eta = eta_7
677
+
678
+ # ; Given eta at the specified M1 and P, tabulate eccentricity distribution
679
+ if 10 ** mylogP <= 2.0:
680
+ # ; For P < 2 days, assume all systems are close to circular
681
+ # ; For adopted ev (spacing and minimum value), eta = -3.2 satisfies this
682
+ fe = ev ** (-3.2)
683
+ else:
684
+ fe = ev ** eta
685
+ e_max = 1.0 - (10 ** mylogP / 2.0) ** (
686
+ -2.0 / 3.0
687
+ ) # ; maximum eccentricity for given P
688
+ ind = np.where(ev >= e_max)
689
+ fe[ind] = 0.0 # ; set dist. = 0 for e > e_max
690
+ # ; Assume e dist. has power-law slope eta for 0.0 < e / e_max < 0.8 and
691
+ # ; then linear turnover between 0.8 < e / e_max < 1.0 so that dist.
692
+ # ; is continuous at e / e_max = 0.8 and zero at e = e_max
693
+ ind = np.where((ev >= 0.8 * e_max) & (ev <= 1.0 * e_max))
694
+ ind_cont = np.min(ind) - 1
695
+ fe[ind] = np.interp(
696
+ ev[ind], [0.8 * e_max, 1.0 * e_max], [fe[ind_cont], 0.0]
697
+ )
698
+
699
+ cumfe = np.cumsum(fe) - fe[0] # ; cumulative distribution
700
+ cumfe = cumfe / np.max(cumfe) # ; normalize cumfe(e=e_max) = 1
701
+ cumedist[:, j, i] = cumfe # ; save to grid
702
+
703
+ # ; Given constants alpha and DlogP and
704
+ # ; M1 dependent values flogPle1, flogPeq2p7, and flogPeq5p5,
705
+ # ; calculate frequency flogP of companions with q > 0.3 per decade
706
+ # ; of orbital period at given P (Section 9.3 and Eqn. 23)
707
+ if mylogP <= 1.0:
708
+ flogP = flogPle1
709
+ if (mylogP > 1.0) and (mylogP <= 2.7 - DlogP):
710
+ flogP = flogPle1 + (mylogP - 1.0) / (1.7 - DlogP) * (
711
+ flogPeq2p7 - flogPle1 - alpha * DlogP
712
+ )
713
+ if (mylogP > 2.7 - DlogP) and (mylogP <= 2.7 + DlogP):
714
+ flogP = flogPeq2p7 + alpha * (mylogP - 2.7)
715
+ if (mylogP > 2.7 + DlogP) and (mylogP <= 5.5):
716
+ flogP = (
717
+ flogPeq2p7
718
+ + alpha * DlogP
719
+ + (mylogP - 2.7 - DlogP)
720
+ / (2.8 - DlogP)
721
+ * (flogPeq5p5 - flogPeq2p7 - alpha * DlogP)
722
+ )
723
+ if mylogP > 5.5:
724
+ flogP = flogPeq5p5 * np.exp(-0.3 * (mylogP - 5.5))
725
+
726
+ # ; Convert frequency of companions with q > 0.3 to frequency of
727
+ # ; companions with q > 0.1 according to q_factor; save to grid
728
+ flogP_sq[j, i] = flogP * q_factor
729
+
730
+ # ; Calculate prob. that a companion to M1 with period P is the
731
+ # ; inner binary. Currently this is an approximation.
732
+ # ; 100% for log P < 1.5
733
+ # ; For log P > 1.5 adopt functional form that reproduces M1 dependent
734
+ # ; multiplicity statistics in Section 9.4, including a
735
+ # ; 41% binary star faction (59% single star fraction) for M1 = 1 Msun and
736
+ # ; 96% binary star fraction (4% single star fraction) for M1 = 28 Msun
737
+ if mylogP <= 1.5:
738
+ probbin[j, i] = 1.0
739
+ else:
740
+ probbin[j, i] = (
741
+ 1.0 - 0.11 * (mylogP - 1.5) ** 1.43 * (myM1 / 10.0) ** 0.56
742
+ )
743
+ if probbin[j, i] <= 0.0:
744
+ probbin[j, i] = 0.0
745
+
746
+ # ; Given M1, calculate cumulative binary period distribution
747
+ mycumPbindist = (
748
+ np.cumsum(flogP_sq[:, i] * probbin[:, i])
749
+ - flogP_sq[0, i] * probbin[0, i]
750
+ )
751
+ # ; Normalize so that max(cumPbindist) = total binary star fraction (NOT 1)
752
+ mycumPbindist = (
753
+ mycumPbindist
754
+ / np.max(mycumPbindist)
755
+ * utils.idl_tabulate(logPv, flogP_sq[:, i] * probbin[:, i])
756
+ )
757
+ cumPbindist[:, i] = mycumPbindist # ;save to grid
758
+
759
+ # ; Step 2
760
+ # ; Implement Monte Carlo method / random number generator to select
761
+ # ; single stars and binaries from the grids of distributions
762
+
763
+ # ; Create vector for PRIMARY mass function, which is the mass distribution
764
+ # ; of single stars and primaries in binaries.
765
+ # ; This is NOT the IMF, which is the mass distribution of single stars,
766
+ # ; primaries in binaries, and secondaries in binaries.
767
+
768
+ np.random.seed(seed)
769
+
770
+ mass_singles = 0.0
771
+ mass_binaries = 0.0
772
+ n_singles = 0
773
+ n_binaries = 0
774
+ primary_mass_list = []
775
+ secondary_mass_list = []
776
+ porb_list = []
777
+ ecc_list = []
778
+ binfrac_list = []
779
+
780
+ # Full primary mass vector across 0.08 < M1 < 150
781
+ M1 = np.linspace(0, 150, 150000) + 0.08
782
+ # Slope = -2.3 for M1 > 1 Msun
783
+ fM1 = M1**(-2.3)
784
+ # Slope = -1.6 for M1 = 0.5 - 1.0 Msun
785
+ ind = np.where(M1 <= 1.0)
786
+ fM1[ind] = M1[ind]**(-1.6)
787
+ # Slope = -0.8 for M1 = 0.15 - 0.5 Msun
788
+ ind = np.where(M1 <= 0.5)
789
+ fM1[ind] = M1[ind]**(-0.8) / 0.5**(1.6 - 0.8)
790
+ # Cumulative primary mass distribution function
791
+ cumfM1 = np.cumsum(fM1) - fM1[0]
792
+ cumfM1 = cumfM1 / np.max(cumfM1)
793
+ # Value of primary mass CDF where M1 = M1min
794
+ # Minimum primary mass to generate (must be >0.080 Msun)
795
+ cumf_M1min = np.interp(0.08, M1, cumfM1)
796
+ while len(primary_mass_list) < size:
797
+
798
+ # Select primary M1 > M1min from primary mass function
799
+ myM1 = np.interp(cumf_M1min + (1.0 - cumf_M1min) * np.random.rand(), cumfM1, M1)
800
+
801
+ # Find index of M1v that is closest to myM1.
802
+ # ; For M1 = 40 - 150 Msun, adopt binary statistics of M1 = 40 Msun.
803
+ # ; For M1 = 0.08 - 0.8 Msun, adopt P and e dist of M1 = 0.8Msun,
804
+ # ; scale and interpolate the companion frequencies so that the
805
+ # ; binary star fraction of M1 = 0.08 Msun primaries is zero,
806
+ # ; and truncate the q distribution so that q > q_min = 0.08/M1
807
+ indM1 = np.where(abs(myM1 - M1v) == min(abs(myM1 - M1v)))
808
+ indM1 = indM1[0]
809
+
810
+ # ; Given M1, determine cumulative binary period distribution
811
+ mycumPbindist_flat = (cumPbindist[:, indM1]).flatten()
812
+ # If M1 < 0.8 Msun, rescale to appropriate binary star fraction
813
+ if(myM1 <= 0.8):
814
+ mycumPbindist_flat = mycumPbindist_flat * np.interp(np.log10(myM1), np.log10([0.08, 0.8]), [0.0, 1.0])
815
+
816
+ # ; Given M1, determine the binary star fraction
817
+ mybinfrac = np.max(mycumPbindist_flat)
818
+
819
+ # ; Generate random number myrand between 0 and 1
820
+ myrand = np.random.rand()
821
+ # If random number < binary star fraction, generate a binary
822
+ if(myrand < mybinfrac):
823
+ # Given myrand, select P and corresponding index in logPv
824
+ mylogP = np.interp(myrand, mycumPbindist_flat, logPv)
825
+ indlogP = np.where(abs(mylogP - logPv) == min(abs(mylogP - logPv)))
826
+ indlogP = indlogP[0]
827
+
828
+ # Given M1 & P, select e from eccentricity distribution
829
+ mye = np.interp(np.random.rand(), cumedist[:, indlogP, indM1].flatten(), ev)
830
+
831
+ # Given M1 & P, determine mass ratio distribution.
832
+ # If M1 < 0.8 Msun, truncate q distribution and consider
833
+ # only mass ratios q > q_min = 0.08 / M1
834
+ mycumqdist = cumqdist[:, indlogP, indM1].flatten()
835
+ if(myM1 < 0.8):
836
+ q_min = 0.08 / myM1
837
+ # Calculate cumulative probability at q = q_min
838
+ cum_qmin = np.interp(q_min, qv, mycumqdist)
839
+ # Rescale and renormalize cumulative distribution for q > q_min
840
+ mycumqdist = mycumqdist - cum_qmin
841
+ mycumqdist = mycumqdist / max(mycumqdist)
842
+ # Set probability = 0 where q < q_min
843
+ indq = np.where(qv <= q_min)
844
+ mycumqdist[indq] = 0.0
845
+
846
+ # Given M1 & P, select q from cumulative mass ratio distribution
847
+ myq = np.interp(np.random.rand(), mycumqdist, qv)
848
+
849
+ if ((myM1 > M1min) and (myq * myM1 > M2min) and (myM1 < M1max) and
850
+ (myq * myM1 < M2max) and (mylogP < porb_hi) and (mylogP > porb_lo)):
851
+ primary_mass_list.append(myM1)
852
+ secondary_mass_list.append(myq * myM1)
853
+ porb_list.append(10**mylogP)
854
+ ecc_list.append(mye)
855
+ binfrac_list.append(mybinfrac)
856
+ mass_binaries += myM1
857
+ mass_binaries += myq * myM1
858
+ n_binaries += 1
859
+ else:
860
+ mass_singles += myM1
861
+ n_singles += 1
862
+
863
+ return (
864
+ primary_mass_list,
865
+ secondary_mass_list,
866
+ porb_list,
867
+ ecc_list,
868
+ mass_singles,
869
+ mass_binaries,
870
+ n_singles,
871
+ n_binaries,
872
+ binfrac_list
873
+ )