cbhbd 1.0.0__py3-none-any.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.
cbhbd/__init__.py ADDED
@@ -0,0 +1,8 @@
1
+ '''
2
+ cBHBd
3
+ A fast code for simulating star cluster and black hole dynamics.
4
+ '''
5
+ from . import cluster
6
+ from . import mergers
7
+ from . import remnant
8
+ from . import cbhbd
cbhbd/cbhbd.py ADDED
@@ -0,0 +1,159 @@
1
+ import numpy as np
2
+ from . import cluster
3
+ from . import mergers
4
+ from . import remnant
5
+
6
+
7
+ class MergerOutcome:
8
+ # Define all possible merger types
9
+
10
+ InClusterInspiral = "incluster_inspiral"
11
+ GWCaptureBS = "gw_capture_bs"
12
+ GWCaptureBB = "gw_capture_bb"
13
+ Ejected = "ejected"
14
+ GWCaptureDirect = "gw_capture_direct"
15
+
16
+ @staticmethod
17
+ def get_outcomes():
18
+ return [MergerOutcome.InClusterInspiral,
19
+ MergerOutcome.GWCaptureBS,
20
+ MergerOutcome.GWCaptureBB,
21
+ MergerOutcome.Ejected,
22
+ MergerOutcome.GWCaptureDirect]
23
+
24
+
25
+ class CBHBD:
26
+ remnant_models = {k: None for k in remnant.available_remnant_models.keys()}
27
+
28
+ def __init__(self, **kwargs):
29
+ self._run_mergers = mergers._run_mergers
30
+ self._get_IMBH_data = mergers._get_IMBH_data
31
+
32
+ self.tend = 13.8e3 # [Myr] Requested final time for integration. Actual final time can be smaller if e.g. the cluster dissolves
33
+ self.M0 = None # [Msun] Initial mass of the cluster.
34
+ self.N = None # Initial number of stars in the cluster
35
+ self.rh0 = None # [pc] Initial half-mass radius
36
+ self.rhoh0 = None # [Msun/pc^3] Initial density within the half-mass radius
37
+ self.rg = 8 # [kpc] Galactocentric distance of the cluster. For eccentric orbits, consider it as a(1 - e) where a is the semi-major axis, e is eccentricity.
38
+ self.Z = None # Metallicity of the cluster, where Z=0.02 is solar metallicity
39
+ self.FeH = None # Metallicity of the cluster, where FeH=0 is solar metallicity
40
+
41
+ self.Zsolar = 0.02 # Solar metallicity.
42
+
43
+ self.verbose = True # If True, print extra output.
44
+ self.seed = None # Seed for random number generator. Use None to get a random seed.
45
+ self.remnant_model = "RemnantModelVarma19Islam23" # GW remnant model, see remnant.py for available options.
46
+ self.compute_mergers = True # If true, compute the mergers. If false, only compute the cluster model.
47
+ self.ifmr = "sevn-rapid" # IFMR relation to be used if ssp is selected.
48
+
49
+ self.dtout = None # [Myr] Time step for integration. If None, computations are faster.
50
+ self.dense_output = True # Condition for solve_ivp to give continuous solutions. Facilitates interpolation. Can be used instead of specifying t_eval. Necessary if compute_mergers==True.
51
+ self.BH = True # Condition to work with BHs in clusterBH.
52
+ self.ssp = True # Condition to use the SSP tools to extract the BHMF at any moment. Default option uses such tools.
53
+ self.kick = True # Condition to include natal kicks. Affects the BH population obtained. Default option considers kicks.
54
+
55
+ self.output_mergers = False # A Boolean parameter to save the mergers to a file
56
+ self.outfile_mergers = "mergers.txt" # File to save the mergers, if needed.
57
+
58
+ # The user can either specify the initial average mass beforehand, or it is extracted from the IMF.
59
+ # If m0 is not None, the IMF must be well defined so that it matches.
60
+ self.m0 = None # [Msun] Average mass obtained for this particular IMF.
61
+
62
+ # Pass in lists for a_slopes, m_breaks and nbins. Any number of IMF intervals, based on size of lists. m_breaks must be size N + 1.
63
+
64
+ # Slopes of mass function.
65
+ self.a_slopes = [-1.3, -2.3, -2.3]
66
+ self.a_kroupa = self.a_slopes.copy() # In case the user inserts a different set of slopes (and mass breaks), the stellar mass loss rate ν (and time tsev) is different. If ssp is True, this is acoounted, if not, bottom-light IMFs are studied with sev_tune=True.
67
+
68
+ # Mass function break masses.
69
+ self.m_breaks = [0.08, 0.5, 1, 150]
70
+ self.m_kroupa = self.m_breaks.copy() # The user can insert different masses. The slopes and mass breaks of the Kroupa should not change.
71
+
72
+ # Number of bins per interval of the mass function.
73
+ self.nbins = [5, 5, 20]
74
+
75
+ # Valid input parameters. Do not define more input params after this line.
76
+ self.cbhbd_params = [attr for attr in self.__dict__.keys() if attr[0] != "_"]
77
+
78
+ # Read the user's input parameters
79
+ if kwargs is not None:
80
+ for key, value in kwargs.items():
81
+ setattr(self, key, value)
82
+
83
+ assert self.remnant_model in remnant.available_remnant_models, f"Remnant model {self.remnant_model} not available, please choose from {remnant.available_remnant_models.keys()}"
84
+ self._construct_input_params()
85
+
86
+ # Add the unchanged defaults to kwargs, to then pass it to ClusterBH
87
+ for attr, value in self.__dict__.items():
88
+ if attr[0] != "_":
89
+ kwargs[attr] = value
90
+
91
+ # Derived variables
92
+ self.mmin = self.m_breaks[0]
93
+ self.mmax = self.m_breaks[-1]
94
+
95
+ # Output
96
+ self.cluster, self.mergers = None, None
97
+ self.mIMBH, self.chiIMBH, self.genIMBH = None, None, None # Properties of the most massive BH in the cluster.
98
+ self.mIMBHej, self.chiIMBHej, self.genIMBHej = None, None, None # Properties of the most massive BH ejected from the cluster.
99
+ self.bhv = None
100
+
101
+ self._run(**kwargs)
102
+
103
+ def _construct_input_params(self):
104
+ # Construct the mass and IMF
105
+ if (self.M0 is None) == (self.N is None):
106
+ raise ValueError("Please provide either M0 or N. "
107
+ "To change the mean mass, modify the IMF breaks and limits (m_break) or slopes (a_slope)")
108
+ if self.m0 is None:
109
+ self.m0 = cluster.initial_average_mass(self.a_slopes, self.m_breaks)
110
+ if self.M0 is None:
111
+ self.M0 = self.m0 * self.N
112
+ else:
113
+ self.N = self.M0 / self.m0
114
+
115
+ # Construct the half-mass radius and density
116
+ if (self.rh0 is None) == (self.rhoh0 is None):
117
+ raise ValueError("Please provide either rh0 or rhoh0.")
118
+ if self.rhoh0 is None:
119
+ self.rhoh0 = 3 * self.M0 / (8 * np.pi * self.rh0 ** 3)
120
+ else:
121
+ self.rh0 = (3 * self.M0 / (8 * np.pi * self.rhoh0)) ** (1 / 3)
122
+
123
+ # Construct the metallicity
124
+ if (self.Z is None) == (self.FeH is None):
125
+ raise ValueError("Please provide either Z or FeH.")
126
+ if self.Z is None:
127
+ self.Z = self.Zsolar * 10 ** self.FeH
128
+ else:
129
+ self.FeH = np.log10(self.Z / self.Zsolar)
130
+
131
+ # General sanity checks
132
+ assert self.rh0 > 0
133
+ assert self.rhoh0 > 0
134
+ assert self.M0 > 0
135
+ assert self.N > 0
136
+ if self.compute_mergers:
137
+ assert self.ssp, "Mergers are only available when SSP is enabled"
138
+ assert self.BH, "Mergers are only available when BHs are enabled"
139
+ assert self.dense_output, "Mergers are only available when dense_output is enabled"
140
+
141
+ def _run(self, **kwargs):
142
+ self.cluster = cluster.Cluster(**kwargs)
143
+
144
+ if self.compute_mergers:
145
+ try:
146
+ self.mergers = self._run_mergers(self)
147
+ self._get_IMBH_data(self)
148
+ if self.output_mergers:
149
+ self.mergers.to_csv(self.outfile_mergers, header=True, index=False)
150
+ except Exception as err:
151
+ print("Error in computing the mergers in model with", flush=True)
152
+ print(f"\t Mass = {self.M0} M_sun", flush=True)
153
+ print(f"\t Metallicity = {self.Z}", flush=True)
154
+ print(f"\t Final time = {self.tend} Myr", flush=True)
155
+ print(f"\t Initial density = {self.rhoh0:.3g} M_sun/pc^3", flush=True)
156
+ print(f"\t IFMR model = {self.ifmr}", flush=True)
157
+ print(f"\t Seed = {self.seed}", flush=True)
158
+ print(err, flush=True)
159
+ raise err