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