cbhbd 1.0.0__tar.gz

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,52 @@
1
+ name: CI Tests
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - "**"
7
+
8
+ jobs:
9
+ test-cluster:
10
+ name: "Test cluster (no mergers)"
11
+ runs-on: ubuntu-latest
12
+
13
+ steps:
14
+ - name: Checkout repository
15
+ uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.x"
21
+
22
+ - name: Install dependencies
23
+ run: |
24
+ python -m pip install --upgrade pip
25
+ pip install pytest .
26
+ - name: Test cluster
27
+ run: python -m pytest tests/test_cluster.py -v
28
+
29
+ test-mergers:
30
+ name: "Test mergers"
31
+ runs-on: ubuntu-latest
32
+
33
+ steps:
34
+ - name: Checkout repository
35
+ uses: actions/checkout@v4
36
+
37
+ - name: Set up Python
38
+ uses: actions/setup-python@v5
39
+ with:
40
+ python-version: "3.x"
41
+
42
+ - name: Install dependencies
43
+ run: |
44
+ sudo apt-get update
45
+ sudo apt-get install -y libgsl-dev
46
+ python -m pip install --upgrade pip
47
+ pip install pytest .
48
+ pip install surfinBH bhptnrremnant h5py lalsuite
49
+
50
+
51
+ - name: Test mergers
52
+ run: python -m pytest tests/test_merger.py -v
cbhbd-1.0.0/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ runs/*
2
+ temp/*
3
+ *.pdf
4
+ *.png
5
+ *.jpg
6
+ test/*
7
+ __pycache__/
8
+ /cbhbd/gwModel_kick/
cbhbd-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.4
2
+ Name: cbhbd
3
+ Version: 1.0.0
4
+ Summary: A fast code for simulating star cluster and black hole dynamics
5
+ Project-URL: repository, https://github.com/cBHBd/cBHBd
6
+ Author-email: Daniel Marín Pina <daniel.marin@uni-heidelberg.de>, Fabio Antonini <AntoniniF@cardiff.ac.uk>, Fotis Fronimos <fotisfronimos@icc.ub.edu>, Mark Gieles <mgieles@gmail.com>, Nolan Dickson <nolan.dickson@smu.ca>, Vincent Henault-Brunet <Vincent.Henault@smu.ca>
7
+ License-Expression: BSD-3-Clause
8
+ Keywords: bhbdynamics,clusterbh
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: License :: OSI Approved :: BSD License
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Topic :: Scientific/Engineering :: Astronomy
14
+ Requires-Python: >=3.10
15
+ Requires-Dist: astro-ssptools
16
+ Requires-Dist: astropy
17
+ Requires-Dist: numpy
18
+ Requires-Dist: pandas
19
+ Requires-Dist: scipy
20
+ Description-Content-Type: text/markdown
21
+
22
+ # ᴄBHBᴅ
23
+
24
+ A fast code for simulating star clusters and black hole dynamics.
25
+
26
+ ᴄBHBᴅ can be used for
27
+
28
+ 1) the evolution of a star cluster with black holes in a tidal field
29
+ 2) population synthesis of binary black hole mergers in a time-evolving star cluster
30
+
31
+ If you use ᴄBHBᴅ in any publication, please cite it as follows:
32
+ > ᴄBHBᴅ (Antonini & Gieles [2020](https://ui.adsabs.harvard.edu/abs/2020MNRAS.492.2936A/abstract); Antonini et
33
+ > al. [2023](https://ui.adsabs.harvard.edu/abs/2023MNRAS.522..466A/abstract)), with the updates of Fronimos Pouliasis et
34
+ > al. ([2025](https://ui.adsabs.harvard.edu/abs/2026arXiv260528088F/abstract)),
35
+ > Chattopadhyay et al. ([2026](https://ui.adsabs.harvard.edu/abs/2026arXiv260409773C/abstract)), and Marín Pina et al.
36
+ > (in prep)
37
+
38
+ ᴄBHBᴅ stands for the combination of ᴄʟᴜsᴛᴇʀBH (the code for the cluster evolution) and BHBᴅʏɴᴀᴍɪᴄs (the code for the
39
+ black hole mergers).
40
+
41
+ ## Installation
42
+
43
+ The cBHBd package can be installed directly from this repository using pip:
44
+
45
+ ```
46
+ python -m pip install git+https://github.com/cBHBd/cBHBd.git
47
+ ```
48
+
49
+ All required dependencies will be installed automatically.
50
+
51
+ You can also simply download the ᴄBHBᴅ package source from this repository and install or place it into your path:
52
+
53
+ ```
54
+ git clone https://github.com/cBHBd/cBHBd
55
+ ```
56
+
57
+ ## Running ᴄBHBᴅ
58
+
59
+ You can run ᴄBHBᴅ as
60
+
61
+ ```
62
+ from cbhbd.cbhbd import CBHBD
63
+
64
+ c = CBHBD(...)
65
+ ```
66
+
67
+ with the following arguments:
68
+
69
+ - `N`: Initial number of stars _or_ `M0`: Initial cluster mass
70
+ - `rhoh0`: Initial half-mass density [Msun/pc^3] _or_ `rh0`: Initial half-mass radius [pc]
71
+ - `Z`: Metallicity _or_ `FeH`: Metallicity as in [Fe/H]
72
+ - `kwargs`: Additional parameters to override defaults (see code comments in the `CBHBD` class and the `Cluster` class)
73
+
74
+ If you only want to run the cluster evolution, you can use `compute_mergers=False` for efficiency
75
+
76
+ ## Analysing the ᴄBHBᴅ output
77
+
78
+ The output of the simulations is stored in a `CBHBD` object. The cluster properties can be accessed from `c.cluster`.
79
+ See `cluster.py` for the full documentation.
80
+
81
+ ```
82
+ c.cluster.Mst # Total stellar mass in the cluster as function of time
83
+ c.cluster.Mbh # Total BH mass in the cluster as function of time
84
+ c.cluster.rh # Half-mass radius of the cluster as function of time
85
+ ...
86
+ c.cluster.Mst_interp(t) # For convenience, you can also interpolate the cluster properties at any time using the interpolating functions
87
+ ```
88
+
89
+ The merger properties are stored in a `pandas.Dataframe` in `c.mergers`. See `mergers.py` for the full documentation.
90
+
91
+ ```
92
+ c.mergers # The merger data, use print(c.mergers) to see a table of the merger properties
93
+ c.mergers.m1 # The mass of the primary
94
+ c.mergers.merger_type # The type of merger (ejected, gw_capture, ...), see the code for the full documentation
95
+ ...
96
+ c.mergers.to_numpy() # You can also export the merger data to a numpy array
97
+ ```
98
+
cbhbd-1.0.0/README.md ADDED
@@ -0,0 +1,77 @@
1
+ # ᴄBHBᴅ
2
+
3
+ A fast code for simulating star clusters and black hole dynamics.
4
+
5
+ ᴄBHBᴅ can be used for
6
+
7
+ 1) the evolution of a star cluster with black holes in a tidal field
8
+ 2) population synthesis of binary black hole mergers in a time-evolving star cluster
9
+
10
+ If you use ᴄBHBᴅ in any publication, please cite it as follows:
11
+ > ᴄBHBᴅ (Antonini & Gieles [2020](https://ui.adsabs.harvard.edu/abs/2020MNRAS.492.2936A/abstract); Antonini et
12
+ > al. [2023](https://ui.adsabs.harvard.edu/abs/2023MNRAS.522..466A/abstract)), with the updates of Fronimos Pouliasis et
13
+ > al. ([2025](https://ui.adsabs.harvard.edu/abs/2026arXiv260528088F/abstract)),
14
+ > Chattopadhyay et al. ([2026](https://ui.adsabs.harvard.edu/abs/2026arXiv260409773C/abstract)), and Marín Pina et al.
15
+ > (in prep)
16
+
17
+ ᴄBHBᴅ stands for the combination of ᴄʟᴜsᴛᴇʀBH (the code for the cluster evolution) and BHBᴅʏɴᴀᴍɪᴄs (the code for the
18
+ black hole mergers).
19
+
20
+ ## Installation
21
+
22
+ The cBHBd package can be installed directly from this repository using pip:
23
+
24
+ ```
25
+ python -m pip install git+https://github.com/cBHBd/cBHBd.git
26
+ ```
27
+
28
+ All required dependencies will be installed automatically.
29
+
30
+ You can also simply download the ᴄBHBᴅ package source from this repository and install or place it into your path:
31
+
32
+ ```
33
+ git clone https://github.com/cBHBd/cBHBd
34
+ ```
35
+
36
+ ## Running ᴄBHBᴅ
37
+
38
+ You can run ᴄBHBᴅ as
39
+
40
+ ```
41
+ from cbhbd.cbhbd import CBHBD
42
+
43
+ c = CBHBD(...)
44
+ ```
45
+
46
+ with the following arguments:
47
+
48
+ - `N`: Initial number of stars _or_ `M0`: Initial cluster mass
49
+ - `rhoh0`: Initial half-mass density [Msun/pc^3] _or_ `rh0`: Initial half-mass radius [pc]
50
+ - `Z`: Metallicity _or_ `FeH`: Metallicity as in [Fe/H]
51
+ - `kwargs`: Additional parameters to override defaults (see code comments in the `CBHBD` class and the `Cluster` class)
52
+
53
+ If you only want to run the cluster evolution, you can use `compute_mergers=False` for efficiency
54
+
55
+ ## Analysing the ᴄBHBᴅ output
56
+
57
+ The output of the simulations is stored in a `CBHBD` object. The cluster properties can be accessed from `c.cluster`.
58
+ See `cluster.py` for the full documentation.
59
+
60
+ ```
61
+ c.cluster.Mst # Total stellar mass in the cluster as function of time
62
+ c.cluster.Mbh # Total BH mass in the cluster as function of time
63
+ c.cluster.rh # Half-mass radius of the cluster as function of time
64
+ ...
65
+ c.cluster.Mst_interp(t) # For convenience, you can also interpolate the cluster properties at any time using the interpolating functions
66
+ ```
67
+
68
+ The merger properties are stored in a `pandas.Dataframe` in `c.mergers`. See `mergers.py` for the full documentation.
69
+
70
+ ```
71
+ c.mergers # The merger data, use print(c.mergers) to see a table of the merger properties
72
+ c.mergers.m1 # The mass of the primary
73
+ c.mergers.merger_type # The type of merger (ejected, gw_capture, ...), see the code for the full documentation
74
+ ...
75
+ c.mergers.to_numpy() # You can also export the merger data to a numpy array
76
+ ```
77
+
@@ -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
@@ -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