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.
- cosmic/.dylibs/libgcc_s.1.1.dylib +0 -0
- cosmic/.dylibs/libgfortran.5.dylib +0 -0
- cosmic/.dylibs/libquadmath.0.dylib +0 -0
- cosmic/Match.py +191 -0
- cosmic/__init__.py +32 -0
- cosmic/_commit_hash.py +1 -0
- cosmic/_evolvebin.cpython-313-darwin.so +0 -0
- cosmic/_version.py +1 -0
- cosmic/bse_utils/__init__.py +18 -0
- cosmic/bse_utils/zcnsts.py +570 -0
- cosmic/bse_utils/zdata.py +596 -0
- cosmic/checkstate.py +128 -0
- cosmic/data/cosmic-settings.json +1635 -0
- cosmic/evolve.py +607 -0
- cosmic/filter.py +214 -0
- cosmic/get_commit_hash.py +15 -0
- cosmic/output.py +466 -0
- cosmic/plotting.py +680 -0
- cosmic/sample/__init__.py +26 -0
- cosmic/sample/cmc/__init__.py +18 -0
- cosmic/sample/cmc/elson.py +411 -0
- cosmic/sample/cmc/king.py +260 -0
- cosmic/sample/initialbinarytable.py +251 -0
- cosmic/sample/initialcmctable.py +449 -0
- cosmic/sample/sampler/__init__.py +25 -0
- cosmic/sample/sampler/cmc.py +418 -0
- cosmic/sample/sampler/independent.py +1252 -0
- cosmic/sample/sampler/multidim.py +882 -0
- cosmic/sample/sampler/sampler.py +130 -0
- cosmic/test_evolve.py +108 -0
- cosmic/test_match.py +30 -0
- cosmic/test_sample.py +580 -0
- cosmic/test_utils.py +198 -0
- cosmic/utils.py +1574 -0
- cosmic_popsynth-3.6.2.data/scripts/cosmic-pop +544 -0
- cosmic_popsynth-3.6.2.dist-info/METADATA +55 -0
- cosmic_popsynth-3.6.2.dist-info/RECORD +38 -0
- cosmic_popsynth-3.6.2.dist-info/WHEEL +6 -0
cosmic/filter.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Copyright (C) Duncan Macleod (2017-2020)
|
|
3
|
+
#
|
|
4
|
+
# This file is part of GWpy.
|
|
5
|
+
#
|
|
6
|
+
# GWpy 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
|
+
# GWpy 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 GWpy. If not, see <http://www.gnu.org/licenses/>.
|
|
18
|
+
|
|
19
|
+
"""Utilies for filtering a `Table` using column slice definitions
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import operator
|
|
23
|
+
import token
|
|
24
|
+
import re
|
|
25
|
+
from tokenize import generate_tokens
|
|
26
|
+
from collections import OrderedDict
|
|
27
|
+
|
|
28
|
+
from six import string_types
|
|
29
|
+
from six.moves import StringIO
|
|
30
|
+
|
|
31
|
+
__author__ = "Duncan Macleod <duncan.macleod@ligo.org>"
|
|
32
|
+
|
|
33
|
+
OPERATORS = OrderedDict(
|
|
34
|
+
[
|
|
35
|
+
("<", operator.lt),
|
|
36
|
+
("<=", operator.le),
|
|
37
|
+
("=", operator.eq),
|
|
38
|
+
("==", operator.eq),
|
|
39
|
+
(">=", operator.ge),
|
|
40
|
+
(">", operator.gt),
|
|
41
|
+
("!=", operator.ne),
|
|
42
|
+
]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
OPERATORS_INV = OrderedDict(
|
|
46
|
+
[
|
|
47
|
+
("<=", operator.ge),
|
|
48
|
+
("<", operator.gt),
|
|
49
|
+
(">", operator.lt),
|
|
50
|
+
(">=", operator.le),
|
|
51
|
+
]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
QUOTE_REGEX = re.compile(r"^[\s\"\']+|[\s\"\']+$")
|
|
55
|
+
DELIM_REGEX = re.compile(r"(and|&+)", re.I)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# -- filter parsing -----------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _float_or_str(value):
|
|
62
|
+
"""Internal method to attempt `float(value)` handling a `ValueError`"""
|
|
63
|
+
# remove any surrounding quotes
|
|
64
|
+
value = QUOTE_REGEX.sub("", value)
|
|
65
|
+
try: # attempt `float()` conversion
|
|
66
|
+
return float(value)
|
|
67
|
+
except ValueError: # just return the input
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def parse_operator(mathstr):
|
|
72
|
+
"""Parse a `str` as a function from the `operator` module
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
mathstr : `str`
|
|
77
|
+
a `str` representing a mathematical operator
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
op : `func`
|
|
82
|
+
a callable `operator` module function
|
|
83
|
+
|
|
84
|
+
Raises
|
|
85
|
+
------
|
|
86
|
+
KeyError
|
|
87
|
+
if input `str` cannot be mapped to an `operator` function
|
|
88
|
+
|
|
89
|
+
Examples
|
|
90
|
+
--------
|
|
91
|
+
>>> parse_operator('>')
|
|
92
|
+
<built-in function gt>
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
return OPERATORS[mathstr]
|
|
96
|
+
except KeyError as exc:
|
|
97
|
+
exc.args = ("Unrecognised operator %r" % mathstr,)
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parse_column_filter(definition):
|
|
102
|
+
"""Parse a `str` of the form 'column>50'
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
definition : `str`
|
|
107
|
+
a column filter definition of the form ``<name><operator><threshold>``
|
|
108
|
+
or ``<threshold><operator><name><operator><threshold>``, e.g.
|
|
109
|
+
``frequency >= 10``, or ``50 < snr < 100``
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
filters : `list` of `tuple`
|
|
114
|
+
a `list` of filter 3-`tuple`s, where each `tuple` contains the
|
|
115
|
+
following elements:
|
|
116
|
+
|
|
117
|
+
- ``column`` (`str`) - the name of the column on which to operate
|
|
118
|
+
- ``operator`` (`callable`) - the operator to call when evaluating
|
|
119
|
+
the filter
|
|
120
|
+
- ``operand`` (`anything`) - the argument to the operator function
|
|
121
|
+
|
|
122
|
+
Raises
|
|
123
|
+
------
|
|
124
|
+
ValueError
|
|
125
|
+
if the filter definition cannot be parsed
|
|
126
|
+
|
|
127
|
+
KeyError
|
|
128
|
+
if any parsed operator string cannnot be mapped to a function from
|
|
129
|
+
the `operator` module
|
|
130
|
+
|
|
131
|
+
Notes
|
|
132
|
+
-----
|
|
133
|
+
Strings that contain non-alphanumeric characters (e.g. hyphen `-`) should
|
|
134
|
+
be quoted inside the filter definition, to prevent such characters
|
|
135
|
+
being interpreted as operators, e.g. ``channel = X1:TEST`` should always
|
|
136
|
+
be passed as ``channel = "X1:TEST"``.
|
|
137
|
+
|
|
138
|
+
Examples
|
|
139
|
+
--------
|
|
140
|
+
>>> parse_column_filter("frequency>10")
|
|
141
|
+
[('frequency', <function operator.gt>, 10.)]
|
|
142
|
+
>>> parse_column_filter("50 < snr < 100")
|
|
143
|
+
[('snr', <function operator.gt>, 50.), ('snr', <function operator.lt>, 100.)]
|
|
144
|
+
>>> parse_column_filter("channel = "H1:TEST")
|
|
145
|
+
[('channel', <function operator.eq>, 'H1:TEST')]
|
|
146
|
+
""" # noqa
|
|
147
|
+
# parse definition into parts (skipping null tokens)
|
|
148
|
+
parts = list(generate_tokens(StringIO(definition.strip()).readline))
|
|
149
|
+
while parts[-1][0] in (token.ENDMARKER, token.NEWLINE):
|
|
150
|
+
parts = parts[:-1]
|
|
151
|
+
|
|
152
|
+
# parse simple definition: e.g: snr > 5
|
|
153
|
+
if len(parts) == 3:
|
|
154
|
+
a, b, c = parts # pylint: disable=invalid-name
|
|
155
|
+
if a[0] in [token.NAME, token.STRING]: # string comparison
|
|
156
|
+
name = QUOTE_REGEX.sub("", a[1])
|
|
157
|
+
oprtr = OPERATORS[b[1]]
|
|
158
|
+
value = _float_or_str(c[1])
|
|
159
|
+
return [(name, oprtr, value)]
|
|
160
|
+
elif b[0] in [token.NAME, token.STRING]:
|
|
161
|
+
name = QUOTE_REGEX.sub("", b[1])
|
|
162
|
+
oprtr = OPERATORS_INV[b[1]]
|
|
163
|
+
value = _float_or_str(a[1])
|
|
164
|
+
return [(name, oprtr, value)]
|
|
165
|
+
|
|
166
|
+
# parse between definition: e.g: 5 < snr < 10
|
|
167
|
+
elif len(parts) == 5:
|
|
168
|
+
a, b, c, d, e = list(zip(*parts))[1] # pylint: disable=invalid-name
|
|
169
|
+
name = QUOTE_REGEX.sub("", c)
|
|
170
|
+
return [
|
|
171
|
+
(name, OPERATORS_INV[b], _float_or_str(a)),
|
|
172
|
+
(name, OPERATORS[d], _float_or_str(e)),
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
raise ValueError("Cannot parse filter definition from %r" % definition)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def parse_column_filters(*definitions):
|
|
179
|
+
"""Parse multiple compound column filter definitions
|
|
180
|
+
|
|
181
|
+
Examples
|
|
182
|
+
--------
|
|
183
|
+
>>> parse_column_filters('snr > 10', 'frequency < 1000')
|
|
184
|
+
[('snr', <function operator.gt>, 10.), ('frequency', <function operator.lt>, 1000.)]
|
|
185
|
+
>>> parse_column_filters('snr > 10 && frequency < 1000')
|
|
186
|
+
[('snr', <function operator.gt>, 10.), ('frequency', <function operator.lt>, 1000.)]
|
|
187
|
+
""" # noqa: E501
|
|
188
|
+
fltrs = []
|
|
189
|
+
for def_ in _flatten(definitions):
|
|
190
|
+
if is_filter_tuple(def_):
|
|
191
|
+
fltrs.append(def_)
|
|
192
|
+
else:
|
|
193
|
+
for splitdef in DELIM_REGEX.split(def_)[::2]:
|
|
194
|
+
fltrs.extend(parse_column_filter(splitdef))
|
|
195
|
+
return fltrs
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _flatten(container):
|
|
199
|
+
"""Flatten arbitrary nested list of filters into a 1-D list"""
|
|
200
|
+
if isinstance(container, string_types):
|
|
201
|
+
container = [container]
|
|
202
|
+
for elem in container:
|
|
203
|
+
if isinstance(elem, string_types) or is_filter_tuple(elem):
|
|
204
|
+
yield elem
|
|
205
|
+
else:
|
|
206
|
+
for elem2 in _flatten(elem):
|
|
207
|
+
yield elem2
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def is_filter_tuple(tup):
|
|
211
|
+
"""Return whether a `tuple` matches the format for a column filter"""
|
|
212
|
+
return isinstance(tup, (tuple, list)) and (
|
|
213
|
+
len(tup) == 3 and isinstance(tup[0], string_types) and callable(tup[1])
|
|
214
|
+
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
def get_commit_hash():
|
|
4
|
+
# Run git command to get the latest commit hash
|
|
5
|
+
result = subprocess.run(['git', 'rev-parse', 'HEAD'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
6
|
+
commit_hash = result.stdout.decode('utf-8').strip()
|
|
7
|
+
return commit_hash
|
|
8
|
+
|
|
9
|
+
def write_commit_hash_to_file(commit_hash):
|
|
10
|
+
with open('./src/cosmic/_commit_hash.py', 'w') as f:
|
|
11
|
+
f.write(f'COMMIT_HASH = "{commit_hash}"\n')
|
|
12
|
+
|
|
13
|
+
if __name__ == "__main__":
|
|
14
|
+
commit_hash = get_commit_hash()
|
|
15
|
+
write_commit_hash_to_file(commit_hash)
|
cosmic/output.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import h5py as h5
|
|
4
|
+
from cosmic.evolve import Evolve
|
|
5
|
+
from cosmic._version import __version__
|
|
6
|
+
from cosmic.plotting import plot_binary_evol
|
|
7
|
+
import matplotlib.pyplot as plt
|
|
8
|
+
from matplotlib.colors import ListedColormap, BoundaryNorm
|
|
9
|
+
import numpy as np
|
|
10
|
+
import warnings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = ['COSMICOutput', 'COSMICPopOutput', 'save_initC', 'load_initC']
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
kstar_translator = [
|
|
17
|
+
{'long': 'Main Sequence (Low mass)', 'short': 'MS < 0.7', 'colour': (0.996078, 0.843476, 0.469158, 1.0)},
|
|
18
|
+
{'long': 'Main Sequence', 'short': 'MS', 'colour': (0.996078, 0.843476, 0.469158, 1.0)},
|
|
19
|
+
{'long': 'Hertzsprung Gap', 'short': 'HG', 'colour': (0.939608, 0.471373, 0.094902, 1.0)},
|
|
20
|
+
{'long': 'First Giant Branch', 'short': 'FGB', 'colour': (0.716186, 0.833203, 0.916155, 1.0)},
|
|
21
|
+
{'long': 'Core Helium Burning', 'short': 'CHeB', 'colour': (0.29098, 0.59451, 0.78902, 1.0)},
|
|
22
|
+
{'long': 'Early AGB', 'short': 'EAGB', 'colour': (0.294902, 0.690196, 0.384314, 1.0)},
|
|
23
|
+
{'long': 'Thermally Pulsing AGB', 'short': 'TPAGB',
|
|
24
|
+
'colour': (0.723122, 0.889612, 0.697178, 1.0)},
|
|
25
|
+
{'long': 'Helium Main Sequence', 'short': 'HeMS', 'colour': (0.254627, 0.013882, 0.615419, 1.0)},
|
|
26
|
+
{'long': 'Helium Hertsprung Gap', 'short': 'HeHG', 'colour': (0.562738, 0.051545, 0.641509, 1.0)},
|
|
27
|
+
{'long': 'Helium Giant Branch', 'short': 'HeGB', 'colour': (0.798216, 0.280197, 0.469538, 1.0)},
|
|
28
|
+
{'long': 'Helium White Dwarf', 'short': 'HeWD', 'colour': (0.368166, 0.232828, 0.148275, 1.0)},
|
|
29
|
+
{'long': 'Carbon/Oxygen White Dwarf', 'short': 'COWD', 'colour': (0.620069, 0.392132, 0.249725, 1.0)},
|
|
30
|
+
{'long': 'Oxygen/Neon White Dwarf', 'short': 'ONeWD', 'colour': (0.867128, 0.548372, 0.349225, 1.0)},
|
|
31
|
+
{'long': 'Neutron Star', 'short': 'NS', 'colour': (0.501961, 0.501961, 0.501961, 1.0)},
|
|
32
|
+
{'long': 'Black Hole', 'short': 'BH', 'colour': (0.0, 0.0, 0.0, 1.0)},
|
|
33
|
+
{'long': 'Massless Remnant', 'short': 'MR', 'colour': "white"},
|
|
34
|
+
{'long': 'Chemically Homogeneous', 'short': 'CHE', 'colour': (0.647059, 0.164706, 0.164706, 1.0)}
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class COSMICOutput:
|
|
39
|
+
def __init__(self, bpp=None, bcm=None, initC=None, kick_info=None, file=None, label=None,
|
|
40
|
+
file_key_suffix=''):
|
|
41
|
+
"""Container for COSMIC output data components.
|
|
42
|
+
|
|
43
|
+
Can be initialized either from data components directly or by loading from an HDF5 file.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
bpp : `pandas.DataFrame`, optional
|
|
48
|
+
Important evolution timestep table, by default None
|
|
49
|
+
bcm : `pandas.DataFrame`, optional
|
|
50
|
+
User-defined timestep table, by default None
|
|
51
|
+
initC : `pandas.DataFrame`, optional
|
|
52
|
+
Initial conditions table, by default None
|
|
53
|
+
kick_info : `pandas.DataFrame`, optional
|
|
54
|
+
Natal kick information table, by default None
|
|
55
|
+
file : `str`, optional
|
|
56
|
+
Filename/path to HDF5 file to load data from, by default None
|
|
57
|
+
label : `str`, optional
|
|
58
|
+
Optional label for the output instance, by default None
|
|
59
|
+
file_key_suffix : `str`, optional
|
|
60
|
+
Suffix to append to dataset keys when loading from file, by default ''. E.g. if set to '_singles',
|
|
61
|
+
datasets 'bpp_singles', 'bcm_singles', etc. will be loaded as bpp, bcm, etc.
|
|
62
|
+
|
|
63
|
+
Raises
|
|
64
|
+
------
|
|
65
|
+
ValueError
|
|
66
|
+
If neither file nor all data components are provided.
|
|
67
|
+
"""
|
|
68
|
+
# require that either file is given or all data components are given
|
|
69
|
+
if file is None and (bpp is None or bcm is None or initC is None or kick_info is None):
|
|
70
|
+
raise ValueError("Either file or all data components (bpp, bcm, initC, kick_info) must be provided.")
|
|
71
|
+
if file is not None:
|
|
72
|
+
self.bpp = pd.read_hdf(file, key=f'bpp{file_key_suffix}')
|
|
73
|
+
self.bcm = pd.read_hdf(file, key=f'bcm{file_key_suffix}')
|
|
74
|
+
self.initC = load_initC(file, key=f'initC{file_key_suffix}',
|
|
75
|
+
settings_key=f'initC_{file_key_suffix}_settings')
|
|
76
|
+
self.kick_info = pd.read_hdf(file, key=f'kick_info{file_key_suffix}')
|
|
77
|
+
with h5.File(file, 'r') as f:
|
|
78
|
+
file_version = f.attrs.get('COSMIC_version', 'unknown')
|
|
79
|
+
label = f.attrs.get('label', '')
|
|
80
|
+
self.label = label if label != '' else None
|
|
81
|
+
if file_version != __version__:
|
|
82
|
+
warnings.warn(f"You have loaded COSMICOutput from a file that was run using COSMIC version {file_version}, "
|
|
83
|
+
f"but the current version is {__version__}. "
|
|
84
|
+
"There may be compatibility issues, or differences in output when rerunning, be sure to check the changelog.", UserWarning)
|
|
85
|
+
else:
|
|
86
|
+
self.bpp = bpp
|
|
87
|
+
self.bcm = bcm
|
|
88
|
+
self.initC = initC
|
|
89
|
+
self.kick_info = kick_info
|
|
90
|
+
self.label = label if label is not None else None
|
|
91
|
+
|
|
92
|
+
def __len__(self):
|
|
93
|
+
return len(self.initC)
|
|
94
|
+
|
|
95
|
+
def __repr__(self):
|
|
96
|
+
return f'<COSMICOutput{" - " + self.label if self.label is not None else ""}: {len(self)} {"binaries" if len(self) != 1 else "binary"}>'
|
|
97
|
+
|
|
98
|
+
def __getitem__(self, key):
|
|
99
|
+
"""Subselect binaries by bin_num across all data components.
|
|
100
|
+
Keys can be integers or lists/arrays of integers or slices.
|
|
101
|
+
If the key is an array of bools, mask initC to get the corresponding bin_nums."""
|
|
102
|
+
# convert key to list of bin_nums, regardless of input type
|
|
103
|
+
if isinstance(key, int):
|
|
104
|
+
key = [key]
|
|
105
|
+
elif isinstance(key, slice):
|
|
106
|
+
key = self.initC['bin_num'].iloc[key].tolist()
|
|
107
|
+
elif isinstance(key, (pd.Series, list, np.ndarray)) and len(key) == len(self.initC) and isinstance(key[0], (bool, np.bool_)):
|
|
108
|
+
if not key.any():
|
|
109
|
+
raise IndexError("Boolean mask resulted in zero selected binaries.")
|
|
110
|
+
key = self.initC['bin_num'][key].tolist()
|
|
111
|
+
# otherwise, reject invalid types
|
|
112
|
+
elif not isinstance(key, (list, np.ndarray, pd.Series)):
|
|
113
|
+
raise TypeError("Key must be an int, slice, list/array of ints, or boolean mask.")
|
|
114
|
+
|
|
115
|
+
bpp_subset = self.bpp[self.bpp['bin_num'].isin(key)]
|
|
116
|
+
bcm_subset = self.bcm[self.bcm['bin_num'].isin(key)]
|
|
117
|
+
initC_subset = self.initC[self.initC['bin_num'].isin(key)]
|
|
118
|
+
kick_info_subset = self.kick_info[self.kick_info['bin_num'].isin(key)]
|
|
119
|
+
return COSMICOutput(bpp=bpp_subset, bcm=bcm_subset, initC=initC_subset, kick_info=kick_info_subset, label=self.label)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def final_bpp(self):
|
|
123
|
+
"""Get the final timestep for each binary from the bpp table.
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
final_bpp : `pandas.DataFrame`
|
|
128
|
+
DataFrame containing only the final timestep for each binary.
|
|
129
|
+
"""
|
|
130
|
+
return self.bpp.drop_duplicates(subset='bin_num', keep='last')
|
|
131
|
+
|
|
132
|
+
def save(self, output_file):
|
|
133
|
+
"""Save all data components to an HDF5 file
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
output_file : `str`
|
|
138
|
+
Filename/path to the HDF5 file
|
|
139
|
+
"""
|
|
140
|
+
self.bpp.to_hdf(output_file, key='bpp')
|
|
141
|
+
self.bcm.to_hdf(output_file, key='bcm')
|
|
142
|
+
save_initC(output_file, self.initC, key='initC', settings_key='initC_settings')
|
|
143
|
+
self.kick_info.to_hdf(output_file, key='kick_info')
|
|
144
|
+
with h5.File(output_file, 'a') as f:
|
|
145
|
+
f.attrs['COSMIC_version'] = __version__
|
|
146
|
+
f.attrs['label'] = self.label if self.label is not None else ''
|
|
147
|
+
|
|
148
|
+
def rerun_with_settings(self, new_settings, reset_kicks=False, inplace=False):
|
|
149
|
+
"""Rerun the simulation with new settings.
|
|
150
|
+
|
|
151
|
+
Parameters
|
|
152
|
+
----------
|
|
153
|
+
new_settings : `dict`
|
|
154
|
+
Dictionary of new settings to apply. Any setting not included will retain its original value.
|
|
155
|
+
reset_kicks : `bool`, optional
|
|
156
|
+
If True, reset natal kicks to be randomly sampled again.
|
|
157
|
+
If False, retain original kicks. By default False.
|
|
158
|
+
(You may want to reset the kicks if changing settings that affect remnant masses or
|
|
159
|
+
kick distribution.)
|
|
160
|
+
inplace : `bool`, optional
|
|
161
|
+
If True, update the current instance. If False, return a new instance. By default False.
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
new_output : `COSMICOutput`
|
|
166
|
+
New COSMICOutput instance with updated simulation results (only if inplace is False).
|
|
167
|
+
"""
|
|
168
|
+
# merge new settings with existing initC
|
|
169
|
+
updated_initC = self.initC.copy()
|
|
170
|
+
for key, value in new_settings.items():
|
|
171
|
+
if key in updated_initC.columns:
|
|
172
|
+
updated_initC[key] = value
|
|
173
|
+
else:
|
|
174
|
+
raise KeyError(f"Setting '{key}' not found in initC columns.")
|
|
175
|
+
|
|
176
|
+
# reset kicks if requested
|
|
177
|
+
if reset_kicks:
|
|
178
|
+
kick_cols = ["natal_kick_1", "natal_kick_2", "phi_1", "phi_2", "theta_1", "theta_2",
|
|
179
|
+
"mean_anomaly_1", "mean_anomaly_2"]
|
|
180
|
+
for col in kick_cols:
|
|
181
|
+
updated_initC[col] = -100.0
|
|
182
|
+
elif 'kickflag' in new_settings or 'remnantflag' in new_settings:
|
|
183
|
+
warnings.warn(
|
|
184
|
+
"You have changed 'kickflag' or 'remnantflag' without resetting kicks. "
|
|
185
|
+
"This may lead to inconsistent results if the kick distribution or remnant masses have changed. "
|
|
186
|
+
"Consider setting reset_kicks=True.", UserWarning
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# re-run the simulation
|
|
190
|
+
new_bpp, new_bcm, new_initC, new_kick_info = Evolve.evolve(initialbinarytable=updated_initC)
|
|
191
|
+
|
|
192
|
+
if inplace:
|
|
193
|
+
self.bpp = new_bpp
|
|
194
|
+
self.bcm = new_bcm
|
|
195
|
+
self.initC = new_initC
|
|
196
|
+
self.kick_info = new_kick_info
|
|
197
|
+
else:
|
|
198
|
+
return COSMICOutput(bpp=new_bpp, bcm=new_bcm, initC=new_initC, kick_info=new_kick_info)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def plot_detailed_evolution(self, bin_num, show=True, **kwargs):
|
|
202
|
+
"""Plot detailed evolution for a specific binary.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
bin_num : `int`
|
|
207
|
+
Index of the binary to plot.
|
|
208
|
+
**kwargs :
|
|
209
|
+
Additional keyword arguments passed to the plotting function (plotting.plot_binary_evol).
|
|
210
|
+
"""
|
|
211
|
+
# check the bin_num is in the bcm
|
|
212
|
+
if bin_num not in self.bcm['bin_num'].values:
|
|
213
|
+
raise ValueError(f"bin_num {bin_num} not found in bcm table.")
|
|
214
|
+
|
|
215
|
+
# warn if bcm has only two entries for this binary
|
|
216
|
+
bcm_subset = self.bcm[self.bcm['bin_num'] == bin_num]
|
|
217
|
+
if len(bcm_subset) <= 2:
|
|
218
|
+
warnings.warn(
|
|
219
|
+
f"bcm table for bin_num {bin_num} has only {len(bcm_subset)} entries. Detailed evolution "
|
|
220
|
+
"plot may be uninformative. You should set dtp, or timestep_conditions, to increase the "
|
|
221
|
+
"number of timesteps in the bcm table.", UserWarning
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
if "ktype_kwargs" not in kwargs:
|
|
225
|
+
kwargs["ktype_kwargs"] = {'k_type_colors': [kstar_translator[k]["colour"] for k in range(len(kstar_translator))]}
|
|
226
|
+
fig = plot_binary_evol(self.bcm.loc[bin_num], **kwargs)
|
|
227
|
+
if show:
|
|
228
|
+
plt.show()
|
|
229
|
+
return fig
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def plot_distribution(self, x_col, y_col=None, c_col=None, when='final',
|
|
233
|
+
fig=None, ax=None, show=True,
|
|
234
|
+
xlabel='auto', ylabel='auto', clabel='auto', **kwargs):
|
|
235
|
+
"""Plot distribution of binaries in specified columns.
|
|
236
|
+
|
|
237
|
+
Plots can be histograms (if only x_col is given) or scatter plots (if both x_col and y_col are given).
|
|
238
|
+
Optionally, colour coding can be applied using c_col.
|
|
239
|
+
|
|
240
|
+
Parameters
|
|
241
|
+
----------
|
|
242
|
+
x_col : `str`
|
|
243
|
+
Column name for x-axis.
|
|
244
|
+
y_col : `str`, optional
|
|
245
|
+
Column name for y-axis. If None, a histogram will be plotted. By default None.
|
|
246
|
+
c_col : `str`, optional
|
|
247
|
+
Column name for colour coding. By default None.
|
|
248
|
+
when : `str`, optional
|
|
249
|
+
When to take the values from: 'initial' or 'final'. By default 'final'.
|
|
250
|
+
fig : `matplotlib.figure.Figure`, optional
|
|
251
|
+
Figure to plot on. If None, a new figure is created. By default None.
|
|
252
|
+
ax : `matplotlib.axes.Axes`, optional
|
|
253
|
+
Axes to plot on. If None, new axes are created. By default None.
|
|
254
|
+
show : `bool`, optional
|
|
255
|
+
If True, display the plot immediately. By default True.
|
|
256
|
+
xlabel : `str`, optional
|
|
257
|
+
Label for x-axis. If 'auto', uses the column name. By default 'auto'.
|
|
258
|
+
ylabel : `str`, optional
|
|
259
|
+
Label for y-axis. If 'auto', uses the column name or 'Count' for histogram. By default 'auto'.
|
|
260
|
+
clabel : `str`, optional
|
|
261
|
+
Label for colorbar. If 'auto', uses the column name. By default 'auto
|
|
262
|
+
**kwargs :
|
|
263
|
+
Additional keyword arguments passed to the plotting function.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
fig : `matplotlib.figure.Figure`
|
|
268
|
+
The figure containing the plot.
|
|
269
|
+
ax : `matplotlib.axes.Axes`
|
|
270
|
+
The axes containing the plot.
|
|
271
|
+
"""
|
|
272
|
+
if fig is None or ax is None:
|
|
273
|
+
fig, ax = plt.subplots()
|
|
274
|
+
|
|
275
|
+
if when == 'initial':
|
|
276
|
+
data = self.initC
|
|
277
|
+
elif when == 'final':
|
|
278
|
+
data = self.bpp.drop_duplicates(subset='bin_num', keep='last')
|
|
279
|
+
else:
|
|
280
|
+
raise ValueError("Parameter 'when' must be either 'initial' or 'final'.")
|
|
281
|
+
|
|
282
|
+
if xlabel == 'auto':
|
|
283
|
+
xlabel = x_col
|
|
284
|
+
if ylabel == 'auto':
|
|
285
|
+
ylabel = y_col if y_col is not None else 'Count'
|
|
286
|
+
if clabel == 'auto' and c_col is not None:
|
|
287
|
+
clabel = c_col
|
|
288
|
+
|
|
289
|
+
if y_col is None:
|
|
290
|
+
# histogram
|
|
291
|
+
ax.hist(data[x_col], bins=kwargs.get('bins', "fd"),
|
|
292
|
+
color=kwargs.get('color', "tab:blue"), **kwargs)
|
|
293
|
+
ax.set(
|
|
294
|
+
xlabel=xlabel,
|
|
295
|
+
ylabel=ylabel,
|
|
296
|
+
)
|
|
297
|
+
else:
|
|
298
|
+
# scatter plot
|
|
299
|
+
c = data[c_col] if c_col is not None else kwargs.get('color', None)
|
|
300
|
+
if c_col == 'kstar_1' or c_col == 'kstar_2':
|
|
301
|
+
c = data[c_col].map(lambda k: kstar_translator[k]['colour'])
|
|
302
|
+
sc = ax.scatter(data[x_col], data[y_col],
|
|
303
|
+
c=c,
|
|
304
|
+
**kwargs)
|
|
305
|
+
ax.set(
|
|
306
|
+
xlabel=xlabel,
|
|
307
|
+
ylabel=ylabel,
|
|
308
|
+
)
|
|
309
|
+
if c_col is not None and c_col not in ['kstar_1', 'kstar_2']:
|
|
310
|
+
cbar = fig.colorbar(sc, ax=ax)
|
|
311
|
+
cbar.set_label(clabel)
|
|
312
|
+
elif c_col is not None:
|
|
313
|
+
# extract colours and labels
|
|
314
|
+
colours = [entry["colour"] for entry in kstar_translator[1:-2]]
|
|
315
|
+
labels = [entry["short"] for entry in kstar_translator[1:-2]]
|
|
316
|
+
|
|
317
|
+
# create colormap
|
|
318
|
+
cmap = ListedColormap(colours)
|
|
319
|
+
bounds = np.arange(len(colours) + 1)
|
|
320
|
+
norm = BoundaryNorm(bounds, cmap.N)
|
|
321
|
+
|
|
322
|
+
cb = plt.colorbar(
|
|
323
|
+
mappable=plt.cm.ScalarMappable(norm=norm, cmap=cmap),
|
|
324
|
+
ticks=np.arange(len(colours)) + 0.5,
|
|
325
|
+
boundaries=bounds,
|
|
326
|
+
ax=ax
|
|
327
|
+
)
|
|
328
|
+
cb.ax.set_yticklabels(labels)
|
|
329
|
+
cb.set_label(clabel)
|
|
330
|
+
|
|
331
|
+
if show:
|
|
332
|
+
plt.show()
|
|
333
|
+
return fig, ax
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
class COSMICPopOutput():
|
|
337
|
+
def __init__(self, file, label=None):
|
|
338
|
+
# read in convergence tables and totals
|
|
339
|
+
keys = ['conv', 'idx', 'match', 'mass_binaries', 'mass_singles',
|
|
340
|
+
'n_binaries', 'n_singles', 'mass_stars', 'n_stars']
|
|
341
|
+
for key in keys:
|
|
342
|
+
setattr(self, key, pd.read_hdf(file, key=key))
|
|
343
|
+
|
|
344
|
+
# load config back from JSON storage
|
|
345
|
+
with h5.File(file, 'r') as f:
|
|
346
|
+
self.config = json.loads(f['config'][()])
|
|
347
|
+
|
|
348
|
+
# create a COSMICOutput for the binaries, and optionally for the singles
|
|
349
|
+
self.output = COSMICOutput(file=file, label=label + ' [binaries]' if label is not None else None)
|
|
350
|
+
singles = "keep_singles" in self.config["sampling"] and self.config["sampling"]["keep_singles"]
|
|
351
|
+
self.singles_output = COSMICOutput(
|
|
352
|
+
file=file, label=label + ' [singles]' if label is not None else None,
|
|
353
|
+
file_key_suffix='_singles'
|
|
354
|
+
) if singles else None
|
|
355
|
+
self.label = label
|
|
356
|
+
|
|
357
|
+
def __repr__(self):
|
|
358
|
+
r = f'<COSMICPopOutput{" - " + self.label if self.label is not None else ""}: {len(self.output)} binaries>'
|
|
359
|
+
if self.singles_output is not None:
|
|
360
|
+
r = r[:-1] + f', {len(self.singles_output)} singles>'
|
|
361
|
+
return r
|
|
362
|
+
|
|
363
|
+
def __len__(self):
|
|
364
|
+
return len(self.conv)
|
|
365
|
+
|
|
366
|
+
def to_combined_output(self):
|
|
367
|
+
"""Combine binaries and singles into a single COSMICOutput instance.
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
combined_output : `COSMICOutput`
|
|
372
|
+
COSMICOutput instance containing both binaries and singles.
|
|
373
|
+
|
|
374
|
+
Raises
|
|
375
|
+
------
|
|
376
|
+
ValueError
|
|
377
|
+
If singles output is not available.
|
|
378
|
+
"""
|
|
379
|
+
if self.singles_output is None:
|
|
380
|
+
raise ValueError("Singles output is not available in this COSMICPopOutput instance.")
|
|
381
|
+
|
|
382
|
+
bpp = pd.concat([self.output.bpp, self.singles_output.bpp], ignore_index=True)
|
|
383
|
+
bcm = pd.concat([self.output.bcm, self.singles_output.bcm], ignore_index=True)
|
|
384
|
+
initC = pd.concat([self.output.initC, self.singles_output.initC], ignore_index=True)
|
|
385
|
+
kick_info = pd.concat([self.output.kick_info, self.singles_output.kick_info], ignore_index=True)
|
|
386
|
+
|
|
387
|
+
return COSMICOutput(
|
|
388
|
+
bpp=bpp,
|
|
389
|
+
bcm=bcm,
|
|
390
|
+
initC=initC,
|
|
391
|
+
kick_info=kick_info,
|
|
392
|
+
label=self.label + ' [combined]' if self.label is not None else None
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def save_initC(filename, initC, key="initC", settings_key="initC_settings", force_save_all=False):
|
|
397
|
+
"""Save an initC table to an HDF5 file.
|
|
398
|
+
|
|
399
|
+
Any column where every binary has the same value (setting) is saved separately with only a single copy
|
|
400
|
+
to save space.
|
|
401
|
+
|
|
402
|
+
This will take slightly longer (a few seconds instead of 1 second) to run but will save you around
|
|
403
|
+
a kilobyte per binary, which adds up!
|
|
404
|
+
|
|
405
|
+
Parameters
|
|
406
|
+
----------
|
|
407
|
+
filename : `str`
|
|
408
|
+
Filename/path to the HDF5 file
|
|
409
|
+
initC : `pandas.DataFrame`
|
|
410
|
+
Initial conditions table
|
|
411
|
+
key : `str`, optional
|
|
412
|
+
Dataset key to use for main table, by default "initC"
|
|
413
|
+
settings_key : `str`, optional
|
|
414
|
+
Dataset key to use for settings table, by default "initC_settings"
|
|
415
|
+
force_save_all : `bool`, optional
|
|
416
|
+
If true, force all settings columns to be saved in the main table, by default False
|
|
417
|
+
"""
|
|
418
|
+
|
|
419
|
+
# for each column, check if all values are the same
|
|
420
|
+
uniques = initC.nunique(axis=0)
|
|
421
|
+
compress_cols = [col for col in initC.columns if uniques[col] == 1]
|
|
422
|
+
|
|
423
|
+
if len(compress_cols) == 0 or force_save_all:
|
|
424
|
+
# nothing to compress, just save the whole table
|
|
425
|
+
initC.to_hdf(filename, key=key)
|
|
426
|
+
else:
|
|
427
|
+
# save the main table without the compressed columns
|
|
428
|
+
initC.drop(columns=compress_cols).to_hdf(filename, key=key)
|
|
429
|
+
|
|
430
|
+
# save the compressed columns separately
|
|
431
|
+
settings_df = pd.DataFrame([{col: initC[col].iloc[0] for col in compress_cols}])
|
|
432
|
+
settings_df.to_hdf(filename, key=settings_key)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def load_initC(filename, key="initC", settings_key="initC_settings"):
|
|
436
|
+
"""Load an initC table from an HDF5 file.
|
|
437
|
+
|
|
438
|
+
If settings were saved separately, they are merged back into the main table.
|
|
439
|
+
|
|
440
|
+
Parameters
|
|
441
|
+
----------
|
|
442
|
+
filename : `str`
|
|
443
|
+
Filename/path to the HDF5 file
|
|
444
|
+
key : `str`, optional
|
|
445
|
+
Dataset key to use for main table, by default "initC"
|
|
446
|
+
settings_key : `str`, optional
|
|
447
|
+
Dataset key to use for settings table, by default "initC_settings"
|
|
448
|
+
|
|
449
|
+
Returns
|
|
450
|
+
-------
|
|
451
|
+
initC : `pandas.DataFrame`
|
|
452
|
+
Initial conditions table
|
|
453
|
+
"""
|
|
454
|
+
|
|
455
|
+
with h5.File(filename, 'r') as f:
|
|
456
|
+
has_settings = settings_key in f.keys()
|
|
457
|
+
|
|
458
|
+
initC = pd.read_hdf(filename, key=key)
|
|
459
|
+
|
|
460
|
+
if has_settings:
|
|
461
|
+
settings_df = pd.read_hdf(filename, key=settings_key)
|
|
462
|
+
initC.loc[:, settings_df.columns] = settings_df.values[0]
|
|
463
|
+
|
|
464
|
+
return initC
|
|
465
|
+
|
|
466
|
+
|