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.
cosmic/utils.py ADDED
@@ -0,0 +1,1857 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright (C) Scott Coughlin (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
+ """`utils`
20
+ """
21
+ import scipy
22
+ import numpy as np
23
+ import pandas as pd
24
+ import scipy.special as ss
25
+ import astropy.stats as astrostats
26
+ import warnings
27
+ import ast
28
+ import operator
29
+ import json
30
+ import itertools
31
+ import os.path
32
+
33
+ from configparser import ConfigParser
34
+ from .bse_utils.zcnsts import zcnsts
35
+
36
+ __author__ = "Katelyn Breivik <katie.breivik@gmail.com>"
37
+ __credits__ = [
38
+ "Scott Coughlin <scott.coughlin@ligo.org>",
39
+ "Michael Zevin <zevin@northwestern.edu>",
40
+ ]
41
+ __all__ = [
42
+ "filter_bin_state",
43
+ "conv_select",
44
+ "mass_min_max_select",
45
+ "idl_tabulate",
46
+ "rndm",
47
+ "param_transform",
48
+ "dat_transform",
49
+ "dat_un_transform",
50
+ "knuth_bw_selector",
51
+ "error_check",
52
+ "check_initial_conditions",
53
+ "convert_kstar_evol_type",
54
+ "parse_inifile",
55
+ "pop_write",
56
+ "a_from_p",
57
+ "p_from_a",
58
+ "get_Z_from_FeH",
59
+ "get_FeH_from_Z",
60
+ "get_binfrac_of_Z",
61
+ "get_porb_norm",
62
+ "get_met_dep_binfrac"
63
+ ]
64
+
65
+
66
+ def filter_bin_state(bcm, bpp, method, kstar1_range, kstar2_range):
67
+ """Filter the output of bpp and bcm, where the kstar ranges
68
+ have already been selected by the conv_select module
69
+
70
+ Parameters
71
+ ----------
72
+ bcm : `pandas.DataFrame`
73
+ bcm dataframe
74
+
75
+ bpp : `pandas.DataFrame`
76
+ bpp dataframe
77
+
78
+ method : `dict`,
79
+ one or more methods by which to filter the
80
+ bpp or bcm table, e.g. ``{'binary_state' : [0,1]}``;
81
+ This means you do *not* want to select the final state of the binaries in the bcm array
82
+
83
+ kstar1_range : `list`
84
+ list containing all kstar1 values to retain
85
+
86
+ kstar2_range : `list`
87
+ list containing all kstar2 values to retain
88
+
89
+ Returns
90
+ -------
91
+ bcm : `pandas.DataFrame`
92
+ filtered bcm dataframe
93
+ """
94
+ _known_methods = ["binary_state", "timestep_conditions"]
95
+
96
+ if not set(method.keys()).issubset(set(_known_methods)):
97
+ raise ValueError(
98
+ "You have supplied an "
99
+ "unknown method to filter out "
100
+ "the bpp or bcm array. Known methods are "
101
+ "{0}".format(_known_methods)
102
+ )
103
+
104
+ for meth, use in method.items():
105
+ if meth == "binary_state":
106
+ bin_num_save = []
107
+
108
+ # in order to filter on binary state we need the last entry of the bcm array for each binary
109
+ bcm_last_entry = bcm.groupby("bin_num").last().reset_index()
110
+
111
+ # in order to find the properities of disrupted or systems
112
+ # that are alive today we can simply check the last entry in the bcm
113
+ # array for the system and see what its properities are today
114
+ bcm_0_2 = bcm_last_entry.loc[(bcm_last_entry.bin_state != 1)]
115
+ bin_num_save.extend(bcm_0_2.bin_num.tolist())
116
+
117
+ # in order to find the properities of merged systems
118
+ # we actually need to search in the BPP array for the properities
119
+ # of the objects right at merge because the bcm will report
120
+ # the post merge object only
121
+ bcm_1 = bcm_last_entry.loc[bcm_last_entry.bin_state == 1]
122
+
123
+ # We now find the product of the kstar range lists so we can match the
124
+ # merger_type column from the bcm array which tells us what objects
125
+ # merged
126
+ merger_objects_to_track = []
127
+ merger_objects_to_track.extend(
128
+ list(
129
+ map(
130
+ lambda x: "{0}{1}".format(
131
+ str(x[0]).zfill(2), str(x[1]).zfill(2)
132
+ ),
133
+ list(itertools.product(kstar1_range, kstar2_range)),
134
+ )
135
+ )
136
+ )
137
+ merger_objects_to_track.extend(
138
+ list(
139
+ map(
140
+ lambda x: "{0}{1}".format(
141
+ str(x[0]).zfill(2), str(x[1]).zfill(2)
142
+ ),
143
+ list(itertools.product(kstar2_range, kstar1_range)),
144
+ )
145
+ )
146
+ )
147
+ bin_num_save.extend(
148
+ bcm_1.loc[
149
+ bcm_1.merger_type.isin(merger_objects_to_track)
150
+ ].bin_num.tolist()
151
+ )
152
+
153
+ bcm_last_entry = bcm_last_entry.loc[
154
+ bcm_last_entry.bin_num.isin(bin_num_save)
155
+ ]
156
+
157
+ # this will tell use the binary state fraction of the systems with a certain final kstar type
158
+ # before we throw out certain binary states if a user requested that.
159
+ bin_state_fraction = bcm_last_entry.groupby("bin_state").tphys.count()
160
+ bin_states = []
161
+ for ii in range(3):
162
+ try:
163
+ bin_states.append(bin_state_fraction.loc[ii])
164
+ except Exception:
165
+ bin_states.append(0)
166
+ bin_state_fraction = pd.DataFrame([bin_states], columns=[0, 1, 2])
167
+
168
+ bcm = bcm.loc[
169
+ bcm.bin_num.isin(
170
+ bcm_last_entry.loc[bcm_last_entry.bin_state.isin(use)].bin_num
171
+ )
172
+ ]
173
+
174
+ return bcm, bin_state_fraction
175
+
176
+
177
+ def conv_select(bcm_save, bpp_save, final_kstar_1, final_kstar_2, method, conv_lims):
178
+ """Select bcm data for special convergence cases
179
+
180
+ Parameters
181
+ ----------
182
+ bcm_save : `pandas.DataFrame`
183
+ bcm dataframe containing all saved bcm data
184
+
185
+ bpp_save : `pandas.DataFrame`
186
+ bpp dataframe containing all saved bpp data
187
+
188
+ final_kstar_1 : `list`
189
+ contains list of final primary kstars specified by user
190
+
191
+ final_kstar_2 : `list`
192
+ contains list of final primary kstars specified by user
193
+
194
+ method : `str`
195
+ stage in binary evolution to check convergence for
196
+ only one method may be supplied and they are specified
197
+ in the inifile
198
+
199
+ conv_lims : `dict`
200
+ dictionary where keys are convergence params and the
201
+ values are lists containing a [lo, hi] value to filter the
202
+ convergence param between
203
+ any non-specified convergence params will not be filtered
204
+
205
+ Returns
206
+ -------
207
+ conv_save : `pandas.DataFrame`
208
+ filtered dataframe containing binaries that fulfill
209
+ user-specified convergence criteria
210
+
211
+ """
212
+ _known_methods = [
213
+ "formation",
214
+ "1_SN",
215
+ "2_SN",
216
+ "disruption",
217
+ "final_state",
218
+ "XRB_form",
219
+ ]
220
+
221
+ if method not in _known_methods:
222
+ raise ValueError(
223
+ "You have supplied an "
224
+ "unknown method to filter the "
225
+ "bcm array for convergence. Known methods are "
226
+ "{0}".format(_known_methods)
227
+ )
228
+
229
+ if method == "formation":
230
+ # filter the bpp array to find the systems that match the user-specified
231
+ # final kstars
232
+ conv_save = bpp_save.loc[
233
+ ((bpp_save.kstar_1.isin(final_kstar_1))
234
+ & (bpp_save.kstar_2.isin(final_kstar_2))
235
+ )
236
+ |
237
+ ((bpp_save.kstar_1.isin(final_kstar_2))
238
+ & (bpp_save.kstar_2.isin(final_kstar_1))
239
+ )
240
+ ]
241
+
242
+ # select the formation parameters
243
+ conv_save = conv_save.groupby("bin_num").first().reset_index()
244
+
245
+ elif method == "1_SN":
246
+ # select out the systems which will undergo a supernova
247
+ conv_sn_ind = bpp_save.loc[bpp_save.evol_type.isin([15.0, 16.0])].bin_num
248
+
249
+ # select out the systems which will produce the user specified final kstars
250
+ # and undergo a supernova
251
+ conv_sn_ind = bpp_save.loc[
252
+ (bpp_save.bin_num.isin(conv_sn_ind))
253
+ & (bpp_save.kstar_1.isin(final_kstar_1))
254
+ & (bpp_save.kstar_2.isin(final_kstar_2))
255
+ & (bpp_save.sep > 0)
256
+ ].bin_num
257
+
258
+ # select out the values just before the supernova(e)
259
+ conv_sn = bpp_save.loc[
260
+ (bpp_save.bin_num.isin(conv_sn_ind))
261
+ & (bpp_save.evol_type.isin([15.0, 16.0]))
262
+ ]
263
+
264
+ # make sure to select out only the first supernova
265
+ conv_save = conv_sn.groupby("bin_num").first().reset_index()
266
+
267
+ elif method == "2_SN":
268
+ # select out the systems which will undergo a supernova
269
+ conv_sn_ind = bpp_save.loc[bpp_save.evol_type.isin([15.0, 16.0])].bin_num
270
+
271
+ # select out the systems which will produce the user specified final kstars
272
+ # and undergo a supernova
273
+ conv_sn_ind = bpp_save.loc[
274
+ (bpp_save.bin_num.isin(conv_sn_ind))
275
+ & (bpp_save.kstar_1.isin(final_kstar_1))
276
+ & (bpp_save.kstar_2.isin(final_kstar_2))
277
+ & (bpp_save.sep > 0)
278
+ ].bin_num
279
+ # select out the values just before the supernova(e)
280
+ conv_sn = bpp_save.loc[
281
+ (bpp_save.bin_num.isin(conv_sn_ind))
282
+ & (bpp_save.evol_type.isin([15.0, 16.0]))
283
+ ]
284
+
285
+ # select out only the systems that go through 2 supernovae
286
+ conv_sn_2 = conv_sn.loc[conv_sn.groupby("bin_num").size() == 2]
287
+
288
+ # make sure to select out only the second supernova
289
+ conv_save = conv_sn_2.groupby("bin_num").nth(1).reset_index()
290
+
291
+ elif method == "disruption":
292
+ # filter the bpp array to find the systems that match the user-specified
293
+ # final kstars
294
+ conv_ind = bpp_save.loc[
295
+ (bpp_save.kstar_1.isin(final_kstar_1))
296
+ & (bpp_save.kstar_2.isin(final_kstar_2))
297
+ ].bin_num.unique()
298
+
299
+ conv_save = bpp_save.loc[(bpp_save.bin_num.isin(conv_ind))]
300
+
301
+ # select out the parameters just before disruption
302
+ # first reset the index:
303
+ conv_save_reset = conv_save.reset_index()
304
+
305
+ # next select out the index for the disrupted systems using evol_type == 11
306
+ conv_save_reset_ind = conv_save_reset.loc[
307
+ conv_save_reset.evol_type == 11.0
308
+ ].index
309
+
310
+ conv_save = conv_save_reset.iloc[conv_save_reset_ind]
311
+
312
+ elif method == "final_state":
313
+ # the bcm array is all that we need!
314
+ conv_save = bcm_save
315
+
316
+ elif method == "XRB_form":
317
+ # select out the systems which undergo a SN
318
+ conv_ind = bpp_save.loc[bpp_save.evol_type.isin([15.0, 16.0])].bin_num.unique()
319
+ conv_sn = bpp_save.loc[bpp_save.bin_num.isin(conv_ind)]
320
+
321
+ # select out systems when they first enter RLO after the 1st SN
322
+ conv_xrb = conv_sn.loc[
323
+ (conv_sn.kstar_1.isin(final_kstar_1))
324
+ & (conv_sn.kstar_2.isin(final_kstar_2))
325
+ & (conv_sn.RRLO_2 >= 1.0)
326
+ & (conv_sn.sep > 0)
327
+ ]
328
+ conv_save = conv_xrb.groupby("bin_num").first().reset_index()
329
+
330
+ if conv_lims:
331
+ for key in conv_lims.keys():
332
+ filter_lo = conv_lims[key][0]
333
+ filter_hi = conv_lims[key][1]
334
+ conv_save_lim = conv_save.loc[conv_save[key] < filter_hi]
335
+ conv_lims_bin_num = conv_save_lim.loc[conv_save[key] > filter_lo].bin_num
336
+ else:
337
+ conv_lims_bin_num = conv_save.bin_num
338
+
339
+ return conv_save, conv_lims_bin_num
340
+
341
+
342
+ def pop_write(
343
+ dat_store,
344
+ log_file,
345
+ mass_list,
346
+ number_list,
347
+ bcm,
348
+ bpp,
349
+ initC,
350
+ conv,
351
+ kick_info,
352
+ bin_state_nums,
353
+ match,
354
+ idx,
355
+ ):
356
+ """Writes all the good stuff that you want to save from runFixedPop in a
357
+ single function
358
+
359
+ Parameters
360
+ ----------
361
+ dat_store : `pandas HDFStore`
362
+ H5 file to write to
363
+
364
+ log_file : `file write`
365
+ log file to write to
366
+ mass_list : `list`
367
+ list containing the mass of the singles, mass of the binaries,
368
+ and mass of the stars
369
+
370
+ n_list : `list`
371
+ list containing the number of singles, number of binaries,
372
+ and number of stars
373
+
374
+ bcm : `pandas.DataFrame`
375
+ bcm array to write
376
+
377
+ bpp : `pandas.DataFrame`
378
+ bpp array to write
379
+
380
+ initCond : `pandas.DataFrame`
381
+ initCond array to write
382
+
383
+ conv : `pandas.DataFrame`
384
+ conv array to write
385
+
386
+ kick_info : `pandas.DataFrame`
387
+ kick_info array to write
388
+
389
+ bin_state_nums : `list`
390
+ contains the count of binstates 0,1,2
391
+
392
+ match : pandas.DataFrame
393
+ contains the match values for each conv_param
394
+
395
+ idx : `int`
396
+ contains the index of the bcm so we can pick up where we left off
397
+ if runFixedPop hits a wall time
398
+
399
+ Returns
400
+ -------
401
+ Nothing!
402
+ """
403
+
404
+ m_keys = ["mass_singles", "mass_binaries", "mass_stars"]
405
+ n_keys = ["n_singles", "n_binaries", "n_stars"]
406
+ for m_write, m_key, n_write, n_key in zip(mass_list, m_keys, number_list, n_keys):
407
+ # save the total_sampled_mass so far
408
+ dat_store.append(m_key, pd.DataFrame([m_write]))
409
+ dat_store.append(n_key, pd.DataFrame([n_write]))
410
+ log_file.write("The total mass sampled so far is: {0}\n".format(mass_list[2]))
411
+
412
+ # Save the bcm dataframe
413
+ dat_store.append("bcm", bcm)
414
+
415
+ # Save the bpp dataframe
416
+ dat_store.append("bpp", bpp)
417
+
418
+ # Save the initial binaries
419
+ # ensure that the index corresponds to bin_num
420
+ dat_store.append("initCond", initC.set_index("bin_num", drop=False))
421
+
422
+ # Save the converging dataframe
423
+ dat_store.append("conv", conv)
424
+
425
+ # Save the converging dataframe
426
+ dat_store.append("kick_info", kick_info)
427
+
428
+ # Save number of systems in each bin state
429
+ dat_store.append("bin_state_nums", bin_state_nums)
430
+
431
+ # Save the matches
432
+ dat_store.append("match", match)
433
+
434
+ # Save the index
435
+ dat_store.append("idx", pd.DataFrame([idx]))
436
+ return
437
+
438
+
439
+ def a_from_p(p, m1, m2):
440
+ """Computes the separation from orbital period with KEPLER III
441
+
442
+ Parameters
443
+ ----------
444
+ p : float/array
445
+ orbital period [day]
446
+ m1 : float/array
447
+ primary mass [msun]
448
+ m2 : float/array
449
+ secondary mass [msun]
450
+
451
+ Returns
452
+ -------
453
+ sep : float/array
454
+ separation [rsun]
455
+ """
456
+
457
+ p_yr = p / 365.25
458
+ sep_3 = p_yr ** 2 * (m1 + m2)
459
+ sep = sep_3 ** (1 / 3.0)
460
+ sep_rsun = sep * 215.032
461
+ return sep_rsun
462
+
463
+
464
+ def p_from_a(sep, m1, m2):
465
+ """Computes separation from orbital period with kepler III
466
+
467
+ Parameters
468
+ ----------
469
+ sep : float/array
470
+ separation [rsun]
471
+ m1 : float/array
472
+ primary mass [msun]
473
+ m2 : float/array
474
+ secondary mass [msun]
475
+
476
+ Returns
477
+ -------
478
+ p : float/array
479
+ orbital period [day]
480
+ """
481
+
482
+ sep_au = sep / 215.032
483
+ p_2 = sep_au ** 3 / (m1 + m2)
484
+ p_day = (p_2 ** 0.5) * 365.25
485
+ return p_day
486
+
487
+
488
+ def calc_Roche_radius(M1, M2, A):
489
+ """Get Roche lobe radius (Eggleton 1983)
490
+
491
+ Parameters
492
+ ----------
493
+ M1 : float
494
+ Primary mass [any unit]
495
+ M2 : float
496
+ Secondary mass [any unit]
497
+ A : float
498
+ Orbital separation [any unit]
499
+
500
+ Returns
501
+ -------
502
+ Roche radius : float
503
+ in units of input 'A'
504
+ """
505
+ q = M1 / M2
506
+ return (
507
+ A
508
+ * 0.49
509
+ * q ** (2.0 / 3.0)
510
+ / (0.6 * q ** (2.0 / 3.0) + np.log(1.0 + q ** (1.0 / 3.0)))
511
+ )
512
+
513
+
514
+ def mass_min_max_select(kstar_1, kstar_2, **kwargs):
515
+ """Select a minimum and maximum mass to filter out binaries in the initial
516
+ parameter sample to reduce the number of unneccessary binaries evolved
517
+ in BSE
518
+
519
+ Parameters
520
+ ----------
521
+ kstar_1 : int, list
522
+ BSE stellar type for the primary
523
+ or minimum and maximum stellar types for the primary
524
+ kstar_2 : int, list
525
+ BSE stellar type for the secondary
526
+ or minimum and maximum stellar types for the secondary
527
+
528
+ Returns
529
+ -------
530
+ min_mass[0] : float
531
+ minimum primary mass for initial sample
532
+ max_mass[0] : float
533
+ maximum primary mass for initial sample
534
+ min_mass[1] : float
535
+ minimum secondary mass for initial sample
536
+ max_mass[1] : float
537
+ maximum secondary mass for initial sample
538
+ """
539
+
540
+ primary_max = kwargs["m_max"] if "m_max" in kwargs.keys() else 150.0
541
+ secondary_max = kwargs["m_max"] if "m_max" in kwargs.keys() else 150.0
542
+
543
+ primary_min = kwargs["m1_min"] if "m1_min" in kwargs.keys() else 0.08
544
+ secondary_min = kwargs["m2_min"] if "m2_min" in kwargs.keys() else 0.08
545
+
546
+ if ((primary_min < 0.08) | (secondary_min < 0.08)):
547
+ warnings.warn("Tread carefully, BSE is not equipped to handle stellar masses less than 0.08 Msun!")
548
+ if primary_max > 150:
549
+ warnings.warn("Tread carefully, BSE is not equipped to handle stellar masses greater than 150 Msun!")
550
+
551
+ min_mass = [primary_min, secondary_min]
552
+ max_mass = [primary_max, secondary_max]
553
+
554
+ if len(kstar_1) == 1:
555
+ # there is a range of final kstar_1s to save
556
+ kstar_1_lo = kstar_1[0]
557
+ kstar_1_hi = kstar_1[0]
558
+ else:
559
+ kstar_1_lo = min(kstar_1)
560
+ kstar_1_hi = max(kstar_1)
561
+
562
+ if len(kstar_2) == 1:
563
+ # there is a range of final kstar_1s to save
564
+ kstar_2_lo = kstar_2[0]
565
+ kstar_2_hi = kstar_2[0]
566
+ else:
567
+ kstar_2_lo = min(kstar_2)
568
+ kstar_2_hi = max(kstar_2)
569
+
570
+ kstar_lo = [kstar_1_lo, kstar_2_lo]
571
+ kstar_hi = [kstar_1_hi, kstar_2_hi]
572
+
573
+ ii = 0
574
+ for k in kstar_lo:
575
+ if k == 14.0:
576
+ min_mass[ii] = 8.0
577
+ elif k == 13.0:
578
+ min_mass[ii] = 3.0
579
+ elif k == 12.0:
580
+ min_mass[ii] = 1.0
581
+ elif k == 11.0:
582
+ min_mass[ii] = 0.8
583
+ elif k == 10.0:
584
+ min_mass[ii] = 0.5
585
+ ii += 1
586
+
587
+ ii = 0
588
+ for k in kstar_hi:
589
+ if k == 13.0:
590
+ max_mass[ii] = 60.0
591
+ elif k == 12.0:
592
+ max_mass[ii] = 20.0
593
+ elif k == 11.0:
594
+ max_mass[ii] = 20.0
595
+ elif k == 10.0:
596
+ max_mass[ii] = 20.0
597
+ ii += 1
598
+
599
+ return min_mass[0], max_mass[0], min_mass[1], max_mass[1]
600
+
601
+
602
+ def idl_tabulate(x, f, p=5):
603
+ """Function that replicates the IDL int_tabulated function
604
+ which performs a p-point integration on a tabulated set of data
605
+
606
+ Parameters
607
+ ----------
608
+ x : array
609
+ tabulated x-value data
610
+ f : array
611
+ tabulated f-value data, same size as x
612
+ p : int
613
+ number of chunks to divide tabulated data into
614
+ Default: 5
615
+
616
+ Returns
617
+ -------
618
+ ret : float
619
+ Integration result
620
+ """
621
+
622
+ def newton_cotes(x, f):
623
+ if x.shape[0] < 2:
624
+ return 0
625
+ rn = (x.shape[0] - 1) * (x - x[0]) / (x[-1] - x[0])
626
+ weights = scipy.integrate.newton_cotes(rn)[0]
627
+ return (x[-1] - x[0]) / (x.shape[0] - 1) * np.dot(weights, f)
628
+
629
+ ret = 0
630
+ for idx in range(0, x.shape[0], p - 1):
631
+ ret += newton_cotes(x[idx: idx + p], f[idx: idx + p])
632
+ return ret
633
+
634
+
635
+ def rndm(a, b, g, size):
636
+ r"""Power-law generator for pdf(x)\propto x^{g} for a<=x<=b
637
+
638
+ Parameters
639
+ ----------
640
+ a : float
641
+ Minimum of range for power law
642
+ b : float
643
+ Maximum of range for power law
644
+ g : float
645
+ Index for power law
646
+ size : int
647
+ Number of data points to draw
648
+
649
+ Returns
650
+ -------
651
+ power : array
652
+ Array of data sampled from power law distribution with params
653
+ fixed by inputs
654
+ """
655
+
656
+ if g == -1:
657
+ raise ValueError("Power law index cannot be exactly -1")
658
+ r = np.random.random(size=size)
659
+ ag, bg = a ** (g + 1), b ** (g + 1)
660
+ return (ag + (bg - ag) * r) ** (1.0 / (g + 1))
661
+
662
+
663
+ def param_transform(dat):
664
+ """Transforms a data set to limits between zero and one
665
+ Leaves some wiggle room on the edges of the data set
666
+
667
+ Parameters
668
+ ----------
669
+ dat : array
670
+ array of data to transform between 0 and 1
671
+
672
+ Returns
673
+ -------
674
+ datTransformed : array
675
+ array of data with limits between 0 and 1
676
+ """
677
+
678
+ datMax = max(dat)
679
+ datMin = min(dat)
680
+ datZeroed = dat - datMin
681
+
682
+ datTransformed = datZeroed / ((datMax - datMin))
683
+ if np.max(datTransformed) == 1.0:
684
+ datTransformed[datTransformed == 1.0] = 1 - 1e-6
685
+ if np.min(datTransformed) == 0.0:
686
+ datTransformed[datTransformed == 0.0] = 1e-6
687
+ return datTransformed
688
+
689
+
690
+ def dat_transform(dat, dat_list):
691
+ """Transform a data set to have limits between zero and one using
692
+ param_transform, then transform to log space
693
+
694
+ Parameters
695
+ ----------
696
+ dat " DataFrame
697
+ Data to transform to eventually perform KDE
698
+ dat_list : list
699
+ List of DataFrame columns to include in transformation
700
+
701
+ Returns
702
+ -------
703
+ dat_trans : array
704
+ Transformed data for columns in dat_list
705
+ """
706
+
707
+ dat_trans = []
708
+ for column in dat_list:
709
+ dat_trans.append(ss.logit(param_transform(dat[column])))
710
+ dat_trans = np.vstack([dat_trans])
711
+
712
+ return dat_trans
713
+
714
+
715
+ def dat_un_transform(dat_sample, dat_set, dat_list):
716
+ """Un-transform data that was transformed in dat_transform
717
+
718
+ Parameters
719
+ ----------
720
+ dat_sample : array
721
+ Data sampled from kde generated with transformed data
722
+ dat_set : DataFrame
723
+ Un-transformed data (same as dat in dat_transform)
724
+ dat_list : list
725
+ List of DataFrame columns to include in transformation
726
+
727
+ Returns
728
+ -------
729
+ dat : array
730
+ Array of data sampled from kde that is transformed back to
731
+ bounds of the un-transformed data set the kde is generated from
732
+ """
733
+ dat = []
734
+
735
+ dat_exp = ss.expit(dat_sample)
736
+ for ii, column in zip(range(len(dat_list)), dat_list):
737
+ dat_untrans = dat_exp[ii, :] * (
738
+ max(dat_set[column]) - min(dat_set[column])
739
+ ) + min(dat_set[column])
740
+ dat.append(dat_untrans)
741
+ dat = np.vstack(dat)
742
+ return dat
743
+
744
+
745
+ def knuth_bw_selector(dat_list):
746
+ """Selects the kde bandwidth using Knuth's rule implemented in Astropy
747
+ If Knuth's rule raises error, Scott's rule is used
748
+
749
+ Parameters
750
+ ----------
751
+ dat_list : list
752
+ List of data arrays that will be used to generate a kde
753
+
754
+ Returns
755
+ -------
756
+ bw_min : float
757
+ Minimum of bandwidths for all of the data arrays in dat_list
758
+ """
759
+
760
+ bw_list = []
761
+ for dat in dat_list:
762
+ try:
763
+ bw = astrostats.knuth_bin_width(dat)
764
+ except Exception:
765
+ print("Using Scott Rule!!")
766
+ bw = astrostats.scott_bin_width(dat)
767
+ bw_list.append(bw)
768
+ return np.mean(bw_list)
769
+
770
+
771
+ def get_Z_from_FeH(FeH, Z_sun=0.02):
772
+ """
773
+ Converts from FeH to Z under the assumption that
774
+ all stars have the same abundance as the sun
775
+
776
+ Parameters
777
+ ----------
778
+ FeH : array
779
+ Fe/H values to convert
780
+ Z_sun : float
781
+ solar metallicity
782
+
783
+ Returns
784
+ -------
785
+ Z : array
786
+ metallicities corresponding to Fe/H
787
+ """
788
+ Z = 10**(FeH + np.log10(Z_sun))
789
+ return Z
790
+
791
+
792
+ def get_FeH_from_Z(Z, Z_sun=0.02):
793
+ """
794
+ Converts from Z to FeH under the assumption that
795
+ all stars have the same abundance as the sun
796
+
797
+ Parameters
798
+ ----------
799
+ Z : array
800
+ metallicities to convert to Fe/H
801
+ Z_sun : float
802
+ solar metallicity
803
+
804
+ Returns
805
+ -------
806
+ FeH : array
807
+ Fe/H corresponding to metallicities
808
+ """
809
+ FeH = np.log10(Z) - np.log10(Z_sun)
810
+ return FeH
811
+
812
+
813
+ def get_binfrac_of_Z(Z):
814
+ '''
815
+ Calculates the theoretical binary fraction as a
816
+ function of metallicity. Following Moe+2019
817
+
818
+ Parameters
819
+ ----------
820
+ Z : array
821
+ metallicity Z values
822
+
823
+ Returns
824
+ -------
825
+ binfrac : array
826
+ binary fraction values
827
+ '''
828
+ FeH = get_FeH_from_Z(Z)
829
+ FeH_low = FeH[np.where(FeH<=-1.0)]
830
+ FeH_high = FeH[np.where(FeH>-1.0)]
831
+ binfrac_low = -0.0648 * FeH_low + 0.3356
832
+ binfrac_high = -0.1977 * FeH_high + 0.2025
833
+ binfrac = np.append(binfrac_low, binfrac_high)
834
+ return binfrac
835
+
836
+
837
+ def get_porb_norm(Z, close_logP=4.0, wide_logP=6.0, binfrac_tot_solar=0.66, Z_sun=0.02):
838
+ '''Returns normalization constants to produce log normals consistent with Fig 19 of Moe+19
839
+ for the orbital period distribution
840
+
841
+ Parameters
842
+ ----------
843
+ Z : array
844
+ metallicity values
845
+ close_logP : float
846
+ divding line beween close and intermediate orbits
847
+ wide_logP : float
848
+ dividing line between intermediate and wide orbits
849
+ binfrac_tot : float
850
+ integrated total binary fraction at solar metallicity
851
+
852
+ Returns
853
+ -------
854
+ norm_wide : float
855
+ normalization factor for kde for wide binaries
856
+ norm_close : float
857
+ normalization factor for kde for wide binaries
858
+ '''
859
+ from scipy.stats import norm
860
+ from scipy.integrate import trapezoid
861
+ from scipy.interpolate import interp1d
862
+
863
+ # fix to values used in Moe+19
864
+ logP_lo_lim=0
865
+ logP_hi_lim=9
866
+ log_P = np.linspace(logP_lo_lim, logP_hi_lim, 10000)
867
+
868
+ logP_pdf = norm.pdf(log_P, loc=4.9, scale=2.3)
869
+
870
+ # set up the wide binary fraction inflection point
871
+ norm_wide = binfrac_tot_solar/trapezoid(logP_pdf, log_P)
872
+
873
+ # set up the close binary fraction inflection point
874
+ FeHclose = np.linspace(-3.0, 0.5, 100)
875
+ fclose = -0.0648 * FeHclose + 0.3356
876
+ fclose[FeHclose > -1.0] = -0.1977 * FeHclose[FeHclose > -1.0] + 0.2025
877
+ Zclose = get_Z_from_FeH(FeHclose, Z_sun=Z_sun)
878
+
879
+ fclose_interp = interp1d(Zclose, fclose)
880
+
881
+ fclose_Z = fclose_interp(Z)
882
+ norm_close = fclose_Z/trapezoid(logP_pdf[log_P < close_logP], log_P[log_P < close_logP])
883
+
884
+ return norm_wide, norm_close
885
+
886
+
887
+ def get_met_dep_binfrac(met):
888
+ '''Returns a population-wide binary fraction consistent with
889
+ Moe+19 based on the supplied metallicity
890
+
891
+ Parameters
892
+ ----------
893
+ met : float
894
+ metallicity of the population
895
+
896
+ Returns
897
+ -------
898
+ binfrac : float
899
+ binary fraction of the population based on metallicity
900
+
901
+ '''
902
+ logP_hi_lim = 9
903
+ logP_lo_lim = 0
904
+ wide_logP = 6
905
+ close_logP = 4
906
+ neval = 5000
907
+
908
+ from scipy.interpolate import interp1d
909
+ from scipy.integrate import trapezoid
910
+ from scipy.stats import norm
911
+
912
+ norm_wide, norm_close = get_porb_norm(met)
913
+ prob_wide = norm.pdf(np.linspace(wide_logP, logP_hi_lim, neval), loc=4.9, scale=2.3)*norm_wide
914
+ prob_close = norm.pdf(np.linspace(logP_lo_lim, close_logP, neval), loc=4.9, scale=2.3)*norm_close
915
+ slope = -(prob_close[-1] - prob_wide[0]) / (wide_logP - close_logP)
916
+ prob_intermediate = slope * (np.linspace(close_logP, wide_logP, neval) - close_logP) + prob_close[-1]
917
+ prob_interp_int = interp1d(np.linspace(close_logP, wide_logP, neval), prob_intermediate)
918
+
919
+ x_dat = np.hstack([np.linspace(logP_lo_lim, close_logP, neval),
920
+ np.linspace(close_logP, wide_logP, neval),
921
+ np.linspace(wide_logP, logP_hi_lim, neval),])
922
+ y_dat = np.hstack([prob_close, prob_interp_int(np.linspace(close_logP, wide_logP, neval)), prob_wide])
923
+
924
+ binfrac = trapezoid(y_dat, x_dat)/0.66 * 0.5
925
+
926
+ return float(np.round(binfrac, 2))
927
+
928
+ def error_check(BSEDict, filters=None, convergence=None, sampling=None):
929
+ """Checks that values in BSEDict, filters, and convergence are viable"""
930
+ if not isinstance(BSEDict, dict):
931
+ raise ValueError("BSE flags must be supplied via a dictionary")
932
+
933
+ if filters is not None:
934
+ if not isinstance(filters, dict):
935
+ raise ValueError("Filters criteria must be supplied via a dictionary")
936
+ for option in ["binary_state", "timestep_conditions"]:
937
+ if option not in filters.keys():
938
+ raise ValueError(
939
+ "Inifile section filters must have option {0} supplied".format(
940
+ option
941
+ )
942
+ )
943
+
944
+ if convergence is not None:
945
+ if not isinstance(convergence, dict):
946
+ raise ValueError("Convergence criteria must be supplied via a dictionary")
947
+ for option in [
948
+ "pop_select",
949
+ "convergence_params",
950
+ "convergence_limits",
951
+ "match",
952
+ "apply_convergence_limits",
953
+ ]:
954
+ if option not in convergence.keys():
955
+ raise ValueError(
956
+ "Inifile section convergence must have option {0} supplied".format(
957
+ option
958
+ )
959
+ )
960
+
961
+ if sampling is not None:
962
+ if not isinstance(sampling, dict):
963
+ raise ValueError("Sampling criteria must be supplied via a dictionary")
964
+ for option in ["sampling_method", "SF_start", "SF_duration", "metallicity", "keep_singles"]:
965
+ if option not in sampling.keys():
966
+ raise ValueError(
967
+ "Inifile section sampling must have option {0} supplied".format(
968
+ option
969
+ )
970
+ )
971
+ if ("qmin" not in sampling.keys()) & ("m2_min" not in sampling.keys()) & (sampling["sampling_method"] == 'independent'):
972
+ raise ValueError("You have not specified qmin or m2_min. At least one of these must be specified.")
973
+ # filters
974
+ if filters is not None:
975
+ flag = "binary_state"
976
+ if any(x not in [0, 1, 2] for x in filters[flag]):
977
+ raise ValueError(
978
+ "{0} needs to be a subset of [0,1,2] (you set it to {1})".format(
979
+ flag, filters[flag]
980
+ )
981
+ )
982
+ flag = "timestep_conditions"
983
+ if (type(filters[flag]) != str) and (type(filters[flag]) != list):
984
+ raise ValueError(
985
+ "{0} needs to either be a string like 'dtp=None' or a list of conditions like [['binstate==0', 'dtp=1.0']] (you set it to {1})".format(
986
+ flag, filters[flag]
987
+ )
988
+ )
989
+
990
+ # convergence
991
+ if convergence is not None:
992
+ flag = "convergence_limits"
993
+ if convergence[flag]:
994
+ for item, key in zip(convergence.items(), convergence.keys()):
995
+ if len(item) != 2:
996
+ raise ValueError(
997
+ "The value for key '{0:s}' needs to be a list of length 2, it is length: {1:i}".format(
998
+ key, len(item)
999
+ )
1000
+ )
1001
+ flag = "pop_select"
1002
+ if not convergence[flag] in [
1003
+ "formation",
1004
+ "1_SN",
1005
+ "2_SN",
1006
+ "disruption",
1007
+ "final_state",
1008
+ "XRB_form",
1009
+ ]:
1010
+ raise ValueError(
1011
+ "{0} needs to be in the list: ['formation', '1_SN', '2_SN', 'disruption', 'final_state', 'XRB_form'] "
1012
+ "(you set it to {1})".format(
1013
+ flag, convergence[flag]
1014
+ )
1015
+ )
1016
+
1017
+ flag = "match"
1018
+ if not isinstance(convergence[flag], float):
1019
+ raise ValueError(
1020
+ "{0} must be a float (you set it to {1})".format(
1021
+ flag, convergence[flag]
1022
+ )
1023
+ )
1024
+
1025
+ flag = "convergence_params"
1026
+ acceptable_convergence_params = [
1027
+ "mass_1",
1028
+ "mass_2",
1029
+ "sep",
1030
+ "porb",
1031
+ "ecc",
1032
+ "massc_1",
1033
+ "massc_2",
1034
+ "rad_1",
1035
+ "rad_2",
1036
+ ]
1037
+ for param in convergence[flag]:
1038
+ if param not in acceptable_convergence_params:
1039
+ raise ValueError(
1040
+ "Supplied convergence parameter {0} is not in list of "
1041
+ "acceptable convergence parameters {1}".format(
1042
+ param, acceptable_convergence_params
1043
+ )
1044
+ )
1045
+
1046
+ flag = "convergence_limits"
1047
+ if type(convergence[flag]) != dict:
1048
+ raise ValueError(
1049
+ "Supplied convergence limits must be passed as a dict "
1050
+ "(you passed type {0})".format(type(convergence[flag]))
1051
+ )
1052
+
1053
+ for key in convergence[flag].keys():
1054
+ if key not in convergence["convergence_params"]:
1055
+ raise ValueError(
1056
+ "Supplied convergence limits must correspond to already "
1057
+ "supplied convergence_params. The supplied convergence_params "
1058
+ "are {0}, while you supplied {1}".format(
1059
+ convergence["convergence_params"], key)
1060
+ )
1061
+ flag = "apply_convergence_limits"
1062
+ if type(convergence[flag]) != bool:
1063
+ raise ValueError(
1064
+ "apply_convergence_limits must be either True or False, "
1065
+ "you supplied {}".format(
1066
+ convergence[flag])
1067
+ )
1068
+
1069
+ # sampling
1070
+ if sampling is not None:
1071
+ flag = "sampling_method"
1072
+ acceptable_sampling = ["multidim", "independent"]
1073
+ if sampling[flag] not in acceptable_sampling:
1074
+ raise ValueError(
1075
+ "sampling_method must be one of {0} you supplied {1}.".format(
1076
+ acceptable_sampling, sampling[flag]
1077
+ )
1078
+ )
1079
+
1080
+ flag = "metallicity"
1081
+ if not isinstance(sampling[flag], float):
1082
+ raise ValueError(
1083
+ "{0} must be a float (you set it to {1})".format(flag, sampling[flag])
1084
+ )
1085
+ if sampling[flag] <= 0:
1086
+ raise ValueError(
1087
+ "{0} needs to be greater than or equal to 0 (you set it to {1})".format(
1088
+ flag, sampling[flag]
1089
+ )
1090
+ )
1091
+
1092
+ # BSEDict
1093
+ flag = "dtp"
1094
+ if flag in BSEDict.keys():
1095
+ if BSEDict[flag] < 0:
1096
+ raise ValueError(
1097
+ "'{0:s}' needs to be greater than or equal to 0 (you set it to '{1:0.2f}')".format(
1098
+ flag, BSEDict[flag]
1099
+ )
1100
+ )
1101
+ flag = "pts1"
1102
+ if flag in BSEDict.keys():
1103
+ if BSEDict[flag] <= 0:
1104
+ raise ValueError(
1105
+ "'{0:s}' needs to be greater than 0 (you set it to '{1:0.2f}')".format(
1106
+ flag, BSEDict[flag]
1107
+ )
1108
+ )
1109
+ flag = "pts2"
1110
+ if flag in BSEDict.keys():
1111
+ if BSEDict[flag] <= 0:
1112
+ raise ValueError(
1113
+ "'{0:s}' needs to be greater than 0 (you set it to '{1:0.2f}')".format(
1114
+ flag, BSEDict[flag]
1115
+ )
1116
+ )
1117
+ flag = "pts3"
1118
+ if flag in BSEDict.keys():
1119
+ if BSEDict[flag] <= 0:
1120
+ raise ValueError(
1121
+ "'{0:s}' needs to be greater than 0 (you set it to '{1:0.2f}')".format(
1122
+ flag, BSEDict[flag]
1123
+ )
1124
+ )
1125
+
1126
+ flag = "zsun"
1127
+ if flag in BSEDict.keys():
1128
+ #if BSEDict[flag] != 0.019:
1129
+ # warnings.warn(
1130
+ # f"'{flag:s}' is set to a different value than assumed in the mlwind "
1131
+ # f"prescriptions (you set it to '{BSEDict[flag]:0.3f}' and in mlwind, zsun_wind=0.019)"
1132
+ # )
1133
+ if BSEDict[flag] <= 0:
1134
+ raise ValueError(
1135
+ "'{0:s}' needs to be greater than 0 (you set it to '{1:0.2f}')".format(
1136
+ flag, BSEDict[flag]
1137
+ )
1138
+ )
1139
+
1140
+ flag = "windflag"
1141
+ if flag in BSEDict.keys():
1142
+ if BSEDict[flag] not in [0, 1, 2, 3, 4]:
1143
+ raise ValueError(
1144
+ "'{0:s}' needs to be set to either 0, 1, 2, or 3, 4 (you set it to '{1:d}')".format(
1145
+ flag, BSEDict[flag]
1146
+ )
1147
+ )
1148
+ flag = "rtmsflag"
1149
+ if flag in BSEDict.keys():
1150
+ if BSEDict[flag] not in [0, 1, 2]:
1151
+ raise ValueError(
1152
+ "'{0:s}' needs to be set to either 0, 1, or 2 (you set it to '{1:d}')".format(
1153
+ flag, BSEDict[flag]
1154
+ )
1155
+ )
1156
+ flag = "eddlimflag"
1157
+ if flag in BSEDict.keys():
1158
+ if BSEDict[flag] not in [0, 1]:
1159
+ raise ValueError(
1160
+ "'{0:s}' needs to be set to either 0 or 1 (you set it to '{1:d}')".format(
1161
+ flag, BSEDict[flag]
1162
+ )
1163
+ )
1164
+ flag = "neta"
1165
+ if flag in BSEDict.keys():
1166
+ if BSEDict[flag] <= 0:
1167
+ raise ValueError(
1168
+ "'{0:s}' needs to be greater than 0 (you set it to '{1:0.2f}')".format(
1169
+ flag, BSEDict[flag]
1170
+ )
1171
+ )
1172
+ flag = "bwind"
1173
+ if flag in BSEDict.keys():
1174
+ if BSEDict[flag] < 0:
1175
+ raise ValueError(
1176
+ "'{0:s}' needs to be greater or equal to 0 (you set it to '{1:0.2f}')".format(
1177
+ flag, BSEDict[flag]
1178
+ )
1179
+ )
1180
+ flag = "hewind"
1181
+ if flag in BSEDict.keys():
1182
+ if (BSEDict[flag] < 0) or (BSEDict[flag] > 1):
1183
+ raise ValueError(
1184
+ "'{0:s}' needs to be between 0 and 1 (you set it to '{1:0.2f}')".format(
1185
+ flag, BSEDict[flag]
1186
+ )
1187
+ )
1188
+ flag = "beta"
1189
+ # --- all numbers are valid
1190
+ flag = "xi"
1191
+ if flag in BSEDict.keys():
1192
+ if (BSEDict[flag] < 0) or (BSEDict[flag] > 1):
1193
+ raise ValueError(
1194
+ "'{0:s}' needs to be between 0 and 1 (you set it to '{1:0.2f}')".format(
1195
+ flag, BSEDict[flag]
1196
+ )
1197
+ )
1198
+ flag = "acc2"
1199
+ if flag in BSEDict.keys():
1200
+ if BSEDict[flag] < 0:
1201
+ raise ValueError(
1202
+ "'{0:s}' needs to be greater or equal to 0 (you set it to '{1:0.2f}')".format(
1203
+ flag, BSEDict[flag]
1204
+ )
1205
+ )
1206
+
1207
+ flag = "alpha1"
1208
+ if flag in BSEDict.keys():
1209
+ if BSEDict[flag] <= 0:
1210
+ raise ValueError(
1211
+ "'{0:s}' needs to be greater than 0 (you set it to '{1:0.2f}')".format(
1212
+ flag, BSEDict[flag]
1213
+ )
1214
+ )
1215
+ flag = "lambdaf"
1216
+ # --- all numbers are valid
1217
+ flag = "ceflag"
1218
+ if flag in BSEDict.keys():
1219
+ if BSEDict[flag] not in [0, 1]:
1220
+ raise ValueError(
1221
+ "'{0:s}' needs to be set to either 0 or 1 (you set it to '{1:d}')".format(
1222
+ flag, BSEDict[flag]
1223
+ )
1224
+ )
1225
+ flag = "cekickflag"
1226
+ if flag in BSEDict.keys():
1227
+ if BSEDict[flag] not in [0, 1, 2]:
1228
+ raise ValueError(
1229
+ "'{0:s}' needs to be set to either 0, 1, or 2 (you set it to '{1:d}')".format(
1230
+ flag, BSEDict[flag]
1231
+ )
1232
+ )
1233
+ flag = "cemergeflag"
1234
+ if flag in BSEDict.keys():
1235
+ if BSEDict[flag] not in [0, 1]:
1236
+ raise ValueError(
1237
+ "'{0:s}' needs to be set to either 0 or 1 (you set it to '{1:d}')".format(
1238
+ flag, BSEDict[flag]
1239
+ )
1240
+ )
1241
+ flag = "cehestarflag"
1242
+ if flag in BSEDict.keys():
1243
+ if BSEDict[flag] not in [0, 1, 2]:
1244
+ raise ValueError(
1245
+ "'{0:s}' needs to be set to either 0, 1, or 2 (you set it to '{1:d}')".format(
1246
+ flag, BSEDict[flag]
1247
+ )
1248
+ )
1249
+ flag = "grflag"
1250
+ if flag in BSEDict.keys():
1251
+ if BSEDict[flag] not in [0, 1]:
1252
+ raise ValueError(
1253
+ "'{0:s}' needs to be set to either 0 or 1 (you set it to '{1:d}')".format(
1254
+ flag, BSEDict[flag]
1255
+ )
1256
+ )
1257
+ flag = "qcflag"
1258
+ if flag in BSEDict.keys():
1259
+ if BSEDict[flag] not in [0, 1, 2, 3, 4, 5]:
1260
+ raise ValueError(
1261
+ "'{0:s}' needs to be set to 0, 1, 2, 3, 4, or 5 (you set it to '{1:0.2f}')".format(
1262
+ flag, BSEDict[flag]
1263
+ )
1264
+ )
1265
+
1266
+ flag = "qcrit_array"
1267
+ if flag in BSEDict.keys():
1268
+ if any(x < 0.0 for x in BSEDict[flag]):
1269
+ raise ValueError(
1270
+ "'{0:s}' values must be greater than or equal to zero (you set them to '[{1:d}]')".format(
1271
+ flag, *BSEDict[flag]
1272
+ )
1273
+ )
1274
+ if len(BSEDict[flag]) != 16:
1275
+ raise ValueError(
1276
+ "'{0:s}' must be supplied 16 values (you supplied '{1:d}')".format(
1277
+ flag, len(BSEDict[flag])
1278
+ )
1279
+ )
1280
+
1281
+ flag = "kickflag"
1282
+ if flag in BSEDict.keys():
1283
+ if BSEDict[flag] not in [0, -1, -2, -3]:
1284
+ raise ValueError(
1285
+ "'{0:s}' needs to be set to either 0, -1, -2, or -3 (you set it to '{1:d}')".format(
1286
+ flag, BSEDict[flag]
1287
+ )
1288
+ )
1289
+ if (BSEDict[flag] in [-1, -2]):
1290
+ if (BSEDict['ecsn'] != 2.25) or (BSEDict['ecsn_mlow'] != 1.6):
1291
+ warnings.warn("You have chosen a kick flag that assumes compact object formation "
1292
+ "according to Giacobbo & Mapelli 2020, but supplied electron "
1293
+ "capture SN (ECSN) flags that are inconsistent with this study. "
1294
+ "To maintain consistency, COSMIC will update your "
1295
+ "ECSN flags to be ecsn=2.25 and ecsn_mlow=1.6")
1296
+ BSEDict['ecsn'] = 2.25
1297
+ BSEDict['ecsn_mlow'] = 1.6
1298
+ flag = "sigma"
1299
+ if flag in BSEDict.keys():
1300
+ if BSEDict[flag] < 0:
1301
+ raise ValueError(
1302
+ "'{0:s}' needs to be greater or equal to 0 (you set it to '{1:0.2f}')".format(
1303
+ flag, BSEDict[flag]
1304
+ )
1305
+ )
1306
+ flag = "bhflag"
1307
+ if flag in BSEDict.keys():
1308
+ if BSEDict[flag] not in [0, 1, 2, 3]:
1309
+ raise ValueError(
1310
+ "'{0:s}' needs to be set to either 0, 1, 2, or 3 (you set it to '{1:d}')".format(
1311
+ flag, BSEDict[flag]
1312
+ )
1313
+ )
1314
+ flag = "ecsn"
1315
+ if flag in BSEDict.keys():
1316
+ if BSEDict[flag] < 0:
1317
+ raise ValueError(
1318
+ "'{0:s}' needs to be greater or equal to 0 (you set it to '{1:0.2f}')".format(
1319
+ flag, BSEDict[flag]
1320
+ )
1321
+ )
1322
+ flag = "ecsn_mlow"
1323
+ if flag in BSEDict.keys():
1324
+ if (BSEDict[flag] > BSEDict["ecsn"]) or (BSEDict[flag] < 0.0):
1325
+ raise ValueError(
1326
+ "'{0:s}' needs to be less than 'ecsn', and must be greater than or equal to 0 "
1327
+ "(you set it to '{1:0.2f}')".format(
1328
+ flag, BSEDict[flag]
1329
+ )
1330
+ )
1331
+ flag = "sigmadiv"
1332
+ if flag in BSEDict.keys():
1333
+ if BSEDict[flag] == 0:
1334
+ raise ValueError(
1335
+ "'{0:s}' must be positive or negative (you set it to '{1:0.2f}')".format(
1336
+ flag, BSEDict[flag]
1337
+ )
1338
+ )
1339
+ flag = "aic"
1340
+ if flag in BSEDict.keys():
1341
+ if BSEDict[flag] not in [0, 1]:
1342
+ raise ValueError(
1343
+ "'{0:s}' needs to be set to either 0 or 1 (you set it to '{1:d}')".format(
1344
+ flag, BSEDict[flag]
1345
+ )
1346
+ )
1347
+ flag = "ussn"
1348
+ if flag in BSEDict.keys():
1349
+ if BSEDict[flag] not in [0, 1]:
1350
+ raise ValueError(
1351
+ "'{0:s}' needs to be set to either 0 or 1 (you set it to '{1:d}')".format(
1352
+ flag, BSEDict[flag]
1353
+ )
1354
+ )
1355
+ flag = "pisn"
1356
+ if flag in BSEDict.keys():
1357
+ if not (
1358
+ (BSEDict[flag] >= 0)
1359
+ or (BSEDict[flag] == -1)
1360
+ or (BSEDict[flag] == -2)
1361
+ or (BSEDict[flag] == -3)
1362
+ ):
1363
+ raise ValueError(
1364
+ "'{0:s}' needs to be set to either 0, greater than 0 or equal to -1, -2, or -3 "
1365
+ "(you set it to '{1:0.2f}')".format(
1366
+ flag, BSEDict[flag]
1367
+ )
1368
+ )
1369
+ flag = "bhsigmafrac"
1370
+ if flag in BSEDict.keys():
1371
+ if (BSEDict[flag] <= 0) or (BSEDict[flag] > 1):
1372
+ raise ValueError(
1373
+ "'{0:s}' needs to be greater than 0 and less than or equal to 1 (you set it to '{1:0.2f}')".format(
1374
+ flag, BSEDict[flag]
1375
+ )
1376
+ )
1377
+ flag = "polar_kick_angle"
1378
+ if flag in BSEDict.keys():
1379
+ if (BSEDict[flag] < 0) or (BSEDict[flag] > 90):
1380
+ raise ValueError(
1381
+ "'{0:s}' needs to be within the allowed range of [0,90] (you set it to '{1:0.2f}')".format(
1382
+ flag, BSEDict[flag]
1383
+ )
1384
+ )
1385
+ flag = "natal_kick_array"
1386
+ if flag in BSEDict.keys():
1387
+ if np.array(BSEDict[flag]).shape != (2, 5):
1388
+ raise ValueError(
1389
+ "'{0:s}' must have shape (2,5) (you supplied list, or array with shape '{1}')".format(
1390
+ flag, np.array(BSEDict[flag]).shape
1391
+ )
1392
+ )
1393
+
1394
+ flag = "remnantflag"
1395
+ if flag in BSEDict.keys():
1396
+ if BSEDict[flag] not in [0, 1, 2, 3, 4]:
1397
+ raise ValueError(
1398
+ "'{0:s}' needs to be set to either 0, 1, 2, 3, or 4 (you set it to '{1:d}')".format(
1399
+ flag, BSEDict[flag]
1400
+ )
1401
+ )
1402
+ flag = "mxns"
1403
+ if flag in BSEDict.keys():
1404
+ if BSEDict[flag] <= 0:
1405
+ raise ValueError(
1406
+ "'{0:s}' needs to be greater than 0 (you set it to '{1:0.2f}')".format(
1407
+ flag, BSEDict[flag]
1408
+ )
1409
+ )
1410
+ flag = "rembar_massloss"
1411
+ if flag in BSEDict.keys():
1412
+ if BSEDict[flag] < -1:
1413
+ raise ValueError(
1414
+ "'{0:s}' needs to be between [-1,0] or greater than 0 (you set it to '{1:0.2f}')".format(
1415
+ flag, BSEDict[flag]
1416
+ )
1417
+ )
1418
+
1419
+ flag = "eddfac"
1420
+ if flag in BSEDict.keys():
1421
+ if BSEDict[flag] < 0:
1422
+ raise ValueError(
1423
+ "'{0:s}' needs to be greater or equal to 0 (you set it to '{1:0.2f}')".format(
1424
+ flag, BSEDict[flag]
1425
+ )
1426
+ )
1427
+ flag = "gamma"
1428
+ if flag in BSEDict.keys():
1429
+ if (BSEDict[flag] < 0) and (BSEDict[flag] != -1) and (BSEDict[flag] != -2) and (BSEDict[flag] != -3):
1430
+ raise ValueError(
1431
+ "'{0:s}' needs to either be set to -3, -2, -1, or a positive number (you set it to '{1:0.2f}')".format(
1432
+ flag, BSEDict[flag]
1433
+ )
1434
+ )
1435
+
1436
+ flag = "tflag"
1437
+ if flag in BSEDict.keys():
1438
+ if BSEDict[flag] not in [0, 1]:
1439
+ raise ValueError(
1440
+ "'{0:s}' needs to be set to either 0 or 1 (you set it to '{1:d}')".format(
1441
+ flag, BSEDict[flag]
1442
+ )
1443
+ )
1444
+ flag = "ifflag"
1445
+ if flag in BSEDict.keys():
1446
+ if BSEDict[flag] < 0:
1447
+ raise ValueError(
1448
+ "'{0:s}' needs to be greater or equal to 0 (you set it to '{1:0.2f}')".format(
1449
+ flag, BSEDict[flag]
1450
+ )
1451
+ )
1452
+ flag = "wdflag"
1453
+ if flag in BSEDict.keys():
1454
+ if BSEDict[flag] < 0:
1455
+ raise ValueError(
1456
+ "'{0:s}' needs to be greater or equal to 0 (you set it to '{1:0.2f}')".format(
1457
+ flag, BSEDict[flag]
1458
+ )
1459
+ )
1460
+ flag = "epsnov"
1461
+ if flag in BSEDict.keys():
1462
+ if (BSEDict[flag] < 0) or (BSEDict[flag] > 1):
1463
+ raise ValueError(
1464
+ "'{0:s}' needs to be between 0 and 1 (you set it to '{1:0.2f}')".format(
1465
+ flag, BSEDict[flag]
1466
+ )
1467
+ )
1468
+ flag = "bhspinflag"
1469
+ if flag in BSEDict.keys():
1470
+ if BSEDict[flag] not in [0, 1, 2]:
1471
+ raise ValueError(
1472
+ "'{0:s}' needs to be set to 0, 1, or 2 (you set it to '{1:0.2f}')".format(
1473
+ flag, BSEDict[flag]
1474
+ )
1475
+ )
1476
+ flag = "bhspinmag"
1477
+ if flag in BSEDict.keys():
1478
+ if (BSEDict[flag] < 0) or (BSEDict[flag] > 1):
1479
+ raise ValueError(
1480
+ "'{0:s}' needs to be between 0 and 1 (you set it to '{1:0.2f}')".format(
1481
+ flag, BSEDict[flag]
1482
+ )
1483
+ )
1484
+ flag = "bconst"
1485
+ if flag in BSEDict.keys():
1486
+ if BSEDict[flag] <= 0:
1487
+ raise ValueError(
1488
+ "'{0:s}' needs to be greater than 0 (you set it to '{1:0.2f}')".format(
1489
+ flag, BSEDict[flag]
1490
+ )
1491
+ )
1492
+ flag = "ck"
1493
+ if flag in BSEDict.keys():
1494
+ if BSEDict[flag] <= 0:
1495
+ raise ValueError(
1496
+ "'{0:s}' needs to be greater than 0 (you set it to '{1:0.2f}')".format(
1497
+ flag, BSEDict[flag]
1498
+ )
1499
+ )
1500
+
1501
+ flag = "fprimc_array"
1502
+ if flag in BSEDict.keys():
1503
+ if any(x < 0.0 for x in BSEDict[flag]):
1504
+ raise ValueError(
1505
+ "'{0:s}' values must be greater than or equal to zero (you set them to '[{1:d}]')".format(
1506
+ flag, *BSEDict[flag]
1507
+ )
1508
+ )
1509
+ if len(BSEDict[flag]) != 16:
1510
+ raise ValueError(
1511
+ "'{0:s}' must be supplied 16 values (you supplied '{1:d}')".format(
1512
+ flag, len(BSEDict[flag])
1513
+ )
1514
+ )
1515
+ flag = "rejuv_fac"
1516
+ if flag in BSEDict.keys():
1517
+ if (BSEDict[flag] > 1.0) or (BSEDict[flag] < 0.0):
1518
+ raise ValueError(
1519
+ "'{0:s}' must be between 0 and 1 (you set it to '[{1:d}]')".format(
1520
+ flag, *BSEDict[flag]
1521
+ )
1522
+ )
1523
+ flag = "rejuv_flag"
1524
+ if flag in BSEDict.keys():
1525
+ if BSEDict[flag] not in [0, 1]:
1526
+ raise ValueError(
1527
+ "'{0:s}' needs to be set to 0 or 1 (you set it to '{1:0.2f}')".format(
1528
+ flag, BSEDict[flag]
1529
+ )
1530
+ )
1531
+ flag = "htpmb"
1532
+ if flag in BSEDict.keys():
1533
+ if BSEDict[flag] not in [-1, 0, 1]:
1534
+ raise ValueError(
1535
+ "'{0:s}' needs to be set to -1, 0 or 1 (you set it to '{1:0.2f}')".format(
1536
+ flag, BSEDict[flag]
1537
+ )
1538
+ )
1539
+ flag = "bdecayfac"
1540
+ if flag in BSEDict.keys():
1541
+ if BSEDict[flag] not in [0, 1]:
1542
+ raise ValueError(
1543
+ "'{0:s}' needs to be set to 0 or 1 (you set it to '{1:0.2f}')".format(
1544
+ flag, BSEDict[flag]
1545
+ )
1546
+ )
1547
+ flag = "ST_cr"
1548
+ if flag in BSEDict.keys():
1549
+ if BSEDict[flag] not in [0, 1]:
1550
+ raise ValueError(
1551
+ "'{0:s}' needs to be set to 0 or 1 (you set it to '{1:0.2f}')".format(
1552
+ flag, BSEDict[flag]
1553
+ )
1554
+ )
1555
+ flag = "ST_tide"
1556
+ if flag in BSEDict.keys():
1557
+ if BSEDict[flag] not in [0, 1]:
1558
+ raise ValueError(
1559
+ "'{0:s}' needs to be set to 0 or 1 (you set it to '{1:0.2f}')".format(
1560
+ flag, BSEDict[flag]
1561
+ )
1562
+ )
1563
+ flag = "don_lim"
1564
+ if flag in BSEDict.keys():
1565
+ if BSEDict[flag] not in [-1, -2]:
1566
+ raise ValueError(
1567
+ "'{0:s}' needs to be set to -1 or -2 (you set it to '{1:0.2f}')".format(
1568
+ flag, BSEDict[flag]
1569
+ )
1570
+ )
1571
+ flag = "acc_lim"
1572
+ if flag in BSEDict.keys():
1573
+ if BSEDict[flag] not in [-1, -2, -3, -4]:
1574
+ if BSEDict[flag] < 0.0:
1575
+ raise ValueError(
1576
+ "'{0:s}' needs to be set to -1, -2, -3, -4 or be >=0 (you set it to '{1:0.2f}')".format(
1577
+ flag, BSEDict[flag]
1578
+ )
1579
+ )
1580
+
1581
+ flag = "wd_mass_lim"
1582
+ if flag in BSEDict.keys():
1583
+ if BSEDict[flag] not in [0, 1]:
1584
+ raise ValueError(
1585
+ "'{0:s}' needs to be set to 0 or 1 (you set it to '{1:0.2f}')".format(
1586
+ flag, BSEDict[flag]
1587
+ )
1588
+ )
1589
+
1590
+ return
1591
+
1592
+
1593
+ def check_initial_conditions(full_initial_binary_table):
1594
+ """Checks initial conditions and reports warnings
1595
+
1596
+ Only warning provided right now is if star begins in Roche lobe
1597
+ overflow
1598
+ """
1599
+
1600
+ def rzamsf(m):
1601
+ """A function to evaluate Rzams
1602
+ ( from Tout et al., 1996, MNRAS, 281, 257 ).
1603
+ """
1604
+ mx = np.sqrt(m)
1605
+ rzams = (
1606
+ (a[7] * m ** 2 + a[8] * m ** 6) * mx
1607
+ + a[9] * m ** 11
1608
+ + (a[10] + a[11] * mx) * m ** 19
1609
+ ) / (a[12] + a[13] * m ** 2 + (a[14] * m ** 8 + m ** 18 + a[15] * m ** 19) * mx)
1610
+
1611
+ return rzams
1612
+
1613
+ no_singles = ((full_initial_binary_table["mass_1"] > 0.0)
1614
+ & (full_initial_binary_table["mass_2"] > 0.0)
1615
+ & (full_initial_binary_table["porb"] > 0.0))
1616
+ initial_binary_table = full_initial_binary_table[no_singles]
1617
+
1618
+ z = np.asarray(initial_binary_table["metallicity"])
1619
+ zpars, a = zcnsts(z)
1620
+
1621
+ mass1 = np.asarray(initial_binary_table["mass_1"])
1622
+ mass2 = np.asarray(initial_binary_table["mass_2"])
1623
+
1624
+ if np.all(mass2 == 0.0):
1625
+ return
1626
+ else:
1627
+ rzams1 = rzamsf(mass1)
1628
+ rzams2 = rzamsf(mass2)
1629
+
1630
+ # assume some time step in order to calculate sep
1631
+ yeardy = 365.24
1632
+ aursun = 214.95
1633
+ tb = np.asarray(initial_binary_table["porb"]) / yeardy
1634
+ sep = aursun * (tb * tb * (mass1 + mass2)) ** (1.0 / 3.0)
1635
+
1636
+ rol1 = calc_Roche_radius(mass1, mass2, sep)
1637
+ rol2 = calc_Roche_radius(mass2, mass1, sep)
1638
+
1639
+ # check for a ZAMS that starts in RFOL
1640
+ mask = ((np.array(initial_binary_table["kstar_1"]) == 1) & (rzams1 >= rol1)) | (
1641
+ (initial_binary_table["kstar_2"] == 1) & (rzams2 >= rol2)
1642
+ )
1643
+ if mask.any():
1644
+ warnings.warn(
1645
+ "At least one of your initial binaries is starting in Roche Lobe Overflow:\n{0}".format(
1646
+ initial_binary_table[mask]
1647
+ )
1648
+ )
1649
+
1650
+ return
1651
+
1652
+
1653
+ def convert_kstar_evol_type(bpp):
1654
+ """Provides way to convert integer values to their string counterpart
1655
+
1656
+ The underlying fortran code relies on integers to indicate
1657
+ things like the evoltuionary stage of the star as well as
1658
+ key moments in its evolutionary track. If you pass the
1659
+ data frame returned from running
1660
+
1661
+ ```Evolve.evolve```
1662
+
1663
+ you can convert the columns with these integer proxies
1664
+ to their true astrophysical meaning.
1665
+ """
1666
+ kstar_int_to_string_dict = {
1667
+ 0: "Main Sequence (MS), < 0.7 M⊙",
1668
+ 1: "MS, > 0.7 M⊙",
1669
+ 2: "Hertzsprung Gap",
1670
+ 3: "First Giant Branch",
1671
+ 4: "Core Helium Burning",
1672
+ 5: "Early Asymptotic Giant Branch (AGB)",
1673
+ 6: "Thermally Pulsing AGB",
1674
+ 7: "Naked Helium Star MS",
1675
+ 8: "Naked Helium Star Hertzsprung Gap",
1676
+ 9: "Naked Helium Star Giant Branch",
1677
+ 10: "Helium White Dwarf",
1678
+ 11: "Carbon/Oxygen White Dwarf",
1679
+ 12: "Oxygen/Neon White Dwarf",
1680
+ 13: "Neutron Star",
1681
+ 14: "Black Hole",
1682
+ 15: "Massless Remnant",
1683
+ }
1684
+
1685
+ kstar_string_to_int_dict = {v: k for k, v in kstar_int_to_string_dict.items()}
1686
+
1687
+ evolve_type_int_to_string_dict = {
1688
+ 1: "initial state",
1689
+ 2: "kstar change",
1690
+ 3: "begin Roche lobe overflow",
1691
+ 4: "end Roche lobe overlow",
1692
+ 5: "contact",
1693
+ 6: "coalescence",
1694
+ 7: "begin common envelope",
1695
+ 8: "end common envelope",
1696
+ 9: "no remnant leftover",
1697
+ 10: "max evolution time",
1698
+ 11: "binary disruption",
1699
+ 12: "begin symbiotic phase",
1700
+ 13: "end symbiotic phase",
1701
+ 14: "blue straggler",
1702
+ 15: "supernova of primary",
1703
+ 16: "supernova of secondary",
1704
+ 100: "RLOF interpolation timeout error"
1705
+ }
1706
+
1707
+ evolve_type_string_to_int_dict = {
1708
+ v: k for k, v in evolve_type_int_to_string_dict.items()
1709
+ }
1710
+
1711
+ if bpp.kstar_1.dtype in [int, float]:
1712
+ # convert from integer to string
1713
+ bpp["kstar_1"] = bpp["kstar_1"].astype(int)
1714
+ bpp["kstar_1"] = bpp["kstar_1"].apply(lambda x: kstar_int_to_string_dict[x])
1715
+ else:
1716
+ # convert from string to integer
1717
+ bpp["kstar_1"] = bpp["kstar_1"].apply(lambda x: kstar_string_to_int_dict[x])
1718
+
1719
+ if bpp.kstar_2.dtype in [int, float]:
1720
+ # convert from integer to string
1721
+ bpp["kstar_2"] = bpp["kstar_2"].astype(int)
1722
+ bpp["kstar_2"] = bpp["kstar_2"].apply(lambda x: kstar_int_to_string_dict[x])
1723
+ else:
1724
+ # convert from string to integer
1725
+ bpp["kstar_2"] = bpp["kstar_2"].apply(lambda x: kstar_string_to_int_dict[x])
1726
+
1727
+ if bpp.evol_type.dtype in [int, float]:
1728
+ # convert from integer to string
1729
+ bpp["evol_type"] = bpp["evol_type"].astype(int)
1730
+ bpp["evol_type"] = bpp["evol_type"].apply(
1731
+ lambda x: evolve_type_int_to_string_dict[x]
1732
+ )
1733
+ else:
1734
+ # convert from string to integer
1735
+ bpp["evol_type"] = bpp["evol_type"].apply(
1736
+ lambda x: evolve_type_string_to_int_dict[x]
1737
+ )
1738
+
1739
+ return bpp
1740
+
1741
+
1742
+ def parse_inifile(inifile):
1743
+ """Provides a method for parsing the inifile and returning dicts of each section"""
1744
+ if inifile is None:
1745
+ raise ValueError("Please supply an inifile")
1746
+ elif not os.path.isfile(inifile):
1747
+ raise ValueError("inifile supplied does not exist")
1748
+
1749
+ binOps = {
1750
+ ast.Add: operator.add,
1751
+ ast.Sub: operator.sub,
1752
+ ast.Mult: operator.mul,
1753
+ ast.Div: operator.truediv,
1754
+ ast.Mod: operator.mod,
1755
+ }
1756
+
1757
+ def arithmetic_eval(s):
1758
+ """Allows us to control how the strings from the inifile get parses"""
1759
+ node = ast.parse(s, mode="eval")
1760
+
1761
+ def _eval(node):
1762
+ """Different strings receive different evaluation"""
1763
+ if isinstance(node, ast.Expression):
1764
+ return _eval(node.body)
1765
+ elif isinstance(node, ast.Str):
1766
+ return node.s
1767
+ elif isinstance(node, ast.Num):
1768
+ return node.n
1769
+ elif isinstance(node, ast.BinOp):
1770
+ return binOps[type(node.op)](_eval(node.left), _eval(node.right))
1771
+ elif isinstance(node, ast.List):
1772
+ return [_eval(x) for x in node.elts]
1773
+ elif isinstance(node, ast.Name):
1774
+ result = VariableKey(item=node)
1775
+ constants_lookup = {
1776
+ "True": True,
1777
+ "False": False,
1778
+ "None": None,
1779
+ }
1780
+ value = constants_lookup.get(
1781
+ result.name,
1782
+ result,
1783
+ )
1784
+ if type(value) == VariableKey:
1785
+ # return regular string
1786
+ return value.name
1787
+ else:
1788
+ # return special string like True or False
1789
+ return value
1790
+ elif isinstance(node, ast.NameConstant):
1791
+ # None, True, False are nameconstants in python3, but names in 2
1792
+ return node.value
1793
+ else:
1794
+ raise Exception("Unsupported type {}".format(node))
1795
+
1796
+ return _eval(node.body)
1797
+
1798
+ # ---- Create configuration-file-parser object and read parameters file.
1799
+ cp = ConfigParser()
1800
+ cp.optionxform = str
1801
+ cp.read(inifile)
1802
+
1803
+ # ---- Read needed variables from the inifile
1804
+ dictionary = {}
1805
+ for section in cp.sections():
1806
+ # for cosmic we skip any CMC stuff
1807
+ # if "cmc" is a section in the ini file, then we can optionally skip the
1808
+ # COSMIC population sections (or not, if they exist)
1809
+ if section == "cmc":
1810
+ if "rand_seed" not in dictionary.keys():
1811
+ dictionary["rand_seed"] = {}
1812
+ dictionary["rand_seed"]["seed"] = 0
1813
+ if "filters" not in dictionary.keys():
1814
+ dictionary["filters"] = 0
1815
+ if "convergence" not in dictionary.keys():
1816
+ dictionary["convergence"] = 0
1817
+ if "sampling" not in dictionary.keys():
1818
+ dictionary["sampling"] = 0
1819
+ continue
1820
+ dictionary[section] = {}
1821
+ for option in cp.options(section):
1822
+ opt = cp.get(section, option)
1823
+ if "\n" in opt:
1824
+ raise ValueError("We have detected an error in your inifile. A parameter was read in with the following "
1825
+ "value: {0}. Likely, you have an unexpected syntax, such as a space before an parameter/option (i.e. "
1826
+ "the parameter must be flush to the far left of the file".format(opt))
1827
+ try:
1828
+ dictionary[section][option] = arithmetic_eval(opt)
1829
+ except Exception:
1830
+ dictionary[section][option] = json.loads(opt)
1831
+ finally:
1832
+ if option not in dictionary[section].keys():
1833
+ raise ValueError("We have detected an error in your inifile. The folloiwng parameter failed to be read correctly: {0}".format(option))
1834
+
1835
+ BSEDict = dictionary["bse"]
1836
+ seed_int = int(dictionary["rand_seed"]["seed"])
1837
+ filters = dictionary["filters"]
1838
+ convergence = dictionary["convergence"]
1839
+ sampling = dictionary["sampling"]
1840
+
1841
+ return BSEDict, seed_int, filters, convergence, sampling
1842
+
1843
+
1844
+ class VariableKey(object):
1845
+ """
1846
+ A dictionary key which is a variable.
1847
+ @ivar item: The variable AST object.
1848
+ """
1849
+
1850
+ def __init__(self, item):
1851
+ self.name = item.id
1852
+
1853
+ def __eq__(self, compare):
1854
+ return compare.__class__ == self.__class__ and compare.name == self.name
1855
+
1856
+ def __hash__(self):
1857
+ return hash(self.name)