petra-catalogs 0.1.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.
- petra_catalogs-0.1.0/LICENSE +21 -0
- petra_catalogs-0.1.0/PKG-INFO +18 -0
- petra_catalogs-0.1.0/README.md +2 -0
- petra_catalogs-0.1.0/petra/__init__.py +2 -0
- petra_catalogs-0.1.0/petra/aux_distributions.py +76 -0
- petra_catalogs-0.1.0/petra/cost_matrix.py +54 -0
- petra_catalogs-0.1.0/petra/initialization.py +149 -0
- petra_catalogs-0.1.0/petra/load_samples.py +113 -0
- petra_catalogs-0.1.0/petra/make_catalog.py +54 -0
- petra_catalogs-0.1.0/petra/parametric_fits.py +131 -0
- petra_catalogs-0.1.0/petra/posterior_chain.py +70 -0
- petra_catalogs-0.1.0/petra/relabel.py +146 -0
- petra_catalogs-0.1.0/petra/utils.py +61 -0
- petra_catalogs-0.1.0/pyproject.toml +18 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Aaron D. Johnson, Javier Roulete, Katerina Chatziioannou, Michele Vallisneri, Kyle Gersbach, Chris Trejo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: petra-catalogs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: From LISA global fit to catalogs
|
|
5
|
+
Author: Aaron D. Johnson
|
|
6
|
+
Requires-Python: >=3.11,<4.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Dist: numpy (>=2.2.3,<3.0.0)
|
|
12
|
+
Requires-Dist: pandas (>=2.2.3,<3.0.0)
|
|
13
|
+
Requires-Dist: scipy (>=1.15.2,<2.0.0)
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# petra
|
|
17
|
+
From the LISA global fit to catalogs of single sources
|
|
18
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import List, Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def mv_normal_aux_distribution(sample: np.ndarray,
|
|
6
|
+
aux_parameters: Tuple[List[np.ndarray]],
|
|
7
|
+
source_index) -> np.ndarray:
|
|
8
|
+
"""
|
|
9
|
+
Vectorized: Compute the logpdf of a multivariate normal distribution evaluated at each source in sample.
|
|
10
|
+
Accepts source_index as an int or array-like.
|
|
11
|
+
|
|
12
|
+
Parameters:
|
|
13
|
+
sample: numpy array of shape (num_sources, num_params_per_source)
|
|
14
|
+
aux_parameters: Tuple of lists (means, cov_matrices) where each is of length num_distributions.
|
|
15
|
+
source_index: int or array-like indices specifying which distributions to evaluate.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
If source_index is a scalar, returns an array of shape (num_sources,).
|
|
19
|
+
Otherwise, returns an array of shape (len(source_index), num_sources).
|
|
20
|
+
"""
|
|
21
|
+
means, cov_matrices = aux_parameters
|
|
22
|
+
# Ensure source_index is array-like.
|
|
23
|
+
source_indices = np.atleast_1d(source_index)
|
|
24
|
+
means = np.array(means)[source_indices] # shape: (n, d)
|
|
25
|
+
cov_matrices = np.array(cov_matrices)[source_indices] # shape: (n, d, d)
|
|
26
|
+
|
|
27
|
+
num_sources, d = sample.shape
|
|
28
|
+
# Broadcast sample to match the number of distributions.
|
|
29
|
+
diff = sample[np.newaxis, :, :] - means[:, np.newaxis, :] # shape: (n, num_sources, d)
|
|
30
|
+
inv_cov = np.linalg.inv(cov_matrices) # shape: (n, d, d)
|
|
31
|
+
# Compute log determinant for each covariance matrix.
|
|
32
|
+
_, logdet = np.linalg.slogdet(cov_matrices) # shape: (n,)
|
|
33
|
+
# Compute the Mahalanobis term over distributions and sources.
|
|
34
|
+
mahal = np.einsum('nsi, nij, nsj -> ns', diff, inv_cov, diff) # shape: (n, num_sources)
|
|
35
|
+
logpdf = -0.5 * (d * np.log(2 * np.pi) + logdet[:, None] + mahal) # shape: (n, num_sources)
|
|
36
|
+
|
|
37
|
+
# Return result: squeeze out axis if only one distribution was requested.
|
|
38
|
+
if logpdf.shape[0] == 1:
|
|
39
|
+
return logpdf[0]
|
|
40
|
+
return logpdf
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def uni_normal_aux_distribution_single_parameter(sample: np.ndarray,
|
|
44
|
+
aux_parameters: Tuple[List[np.ndarray]],
|
|
45
|
+
source_index,
|
|
46
|
+
single_parameter: int) -> np.ndarray:
|
|
47
|
+
"""
|
|
48
|
+
Vectorized: Compute the logpdf of a univariate normal distribution on a single parameter for each source.
|
|
49
|
+
Accepts source_index as an int or array-like.
|
|
50
|
+
|
|
51
|
+
Parameters:
|
|
52
|
+
sample: numpy array of shape (num_sources, num_params_per_source)
|
|
53
|
+
aux_parameters: Tuple of lists (means, stds) for the univariate case.
|
|
54
|
+
source_index: int or array-like indices specifying which distributions to evaluate.
|
|
55
|
+
single_parameter: the parameter index to process.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
If source_index is a scalar, returns an array of shape (num_sources,).
|
|
59
|
+
Otherwise, returns an array of shape (len(source_index), num_sources).
|
|
60
|
+
"""
|
|
61
|
+
values = sample[:, single_parameter] # shape: (num_sources,)
|
|
62
|
+
means, stds = aux_parameters
|
|
63
|
+
source_indices = np.atleast_1d(source_index)
|
|
64
|
+
means = np.array(means)[source_indices] # shape: (n,)
|
|
65
|
+
stds = np.array(stds)[source_indices] # shape: (n,)
|
|
66
|
+
|
|
67
|
+
# Compute logpdf in a vectorized way. Broadcasting: (n,1) vs (num_sources,)
|
|
68
|
+
logpdf = (
|
|
69
|
+
-0.5 * np.log(2 * np.pi)
|
|
70
|
+
- np.log(stds)[:, None]
|
|
71
|
+
- 0.5 * (((values - means[:, None]) / stds[:, None]) ** 2)
|
|
72
|
+
) # shape: (n, num_sources)
|
|
73
|
+
|
|
74
|
+
if logpdf.shape[0] == 1:
|
|
75
|
+
return logpdf[0]
|
|
76
|
+
return logpdf
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Callable, List
|
|
3
|
+
from functools import partial
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def create_compute_cost_matrix(aux_distribution: Callable, single_parameter: int = None) -> Callable:
|
|
7
|
+
"""
|
|
8
|
+
Creates the function to compute the cost matrix for a given auxiliary parametric distribution.
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
aux_distribution: The function to compute the cost matrix elements for the auxiliary distribution
|
|
13
|
+
|
|
14
|
+
Return
|
|
15
|
+
------
|
|
16
|
+
compute_cost_matrix: The function to compute the cost matrix
|
|
17
|
+
"""
|
|
18
|
+
if single_parameter is not None:
|
|
19
|
+
aux_distribution = partial(aux_distribution, single_parameter=single_parameter)
|
|
20
|
+
|
|
21
|
+
def compute_cost_matrix(sample: np.ndarray, aux_parameters: List[np.ndarray],
|
|
22
|
+
prob_in_model: np.ndarray, num_distributions: int) -> np.ndarray:
|
|
23
|
+
"""
|
|
24
|
+
Takes in a single sample and a set of parameters for all fitting distributions,
|
|
25
|
+
and returns the cost matrix.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
sample: A numpy array of shape (num_sources, num_params_per_source)
|
|
30
|
+
aux_parameters: A tuple of lists containing the auxiliary distribution parameters
|
|
31
|
+
prob_in_model: A numpy array of shape (num_distributions,) that contains the probability of each source being in the catalog
|
|
32
|
+
num_distributions: The number of distributions to use in the cost matrix
|
|
33
|
+
|
|
34
|
+
Return
|
|
35
|
+
------
|
|
36
|
+
cost_matrix: A numpy array of shape (num_sources, num_distributions)
|
|
37
|
+
"""
|
|
38
|
+
# Precompute logarithms.
|
|
39
|
+
with np.errstate(divide='ignore'): # avoid divide by zero warnings, they are expected when we don't clip prob_in_model
|
|
40
|
+
log_prob = np.log(prob_in_model) # shape: (num_distributions,)
|
|
41
|
+
log_prob_not = np.log1p(-prob_in_model)[np.newaxis, :] # shape: (1, num_distributions)
|
|
42
|
+
|
|
43
|
+
# Vectorize over distribution indices.
|
|
44
|
+
distribution_indices = np.arange(num_distributions)
|
|
45
|
+
# Assume that aux_distribution is vectorized so that it returns an array of shape (num_distributions, num_sources)
|
|
46
|
+
cost_matrix = aux_distribution(sample, aux_parameters, distribution_indices)
|
|
47
|
+
cost_matrix = log_prob[:, np.newaxis] + cost_matrix
|
|
48
|
+
|
|
49
|
+
# Replace any NaN values with log(1 - prob_in_model)
|
|
50
|
+
cost_matrix = np.where(np.isnan(cost_matrix), log_prob_not, cost_matrix)
|
|
51
|
+
|
|
52
|
+
return cost_matrix
|
|
53
|
+
|
|
54
|
+
return compute_cost_matrix
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from petra.posterior_chain import PosteriorChain
|
|
3
|
+
from petra.parametric_fits import uni_normal_fit_single_parameter
|
|
4
|
+
from petra.aux_distributions import uni_normal_aux_distribution_single_parameter
|
|
5
|
+
from petra.relabel import create_relabel_samples
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def relabel_by_sorting(posterior_chain: PosteriorChain, init_parameter_index: int = 0):
|
|
9
|
+
"""
|
|
10
|
+
Relabel sources by simply sorting each row by the chosen parameter.
|
|
11
|
+
|
|
12
|
+
NaNs are replaced by infinity so that they come last, then after sorting
|
|
13
|
+
we revert infinity back to NaN.
|
|
14
|
+
"""
|
|
15
|
+
chain = posterior_chain.get_chain().copy() # shape: (n_samples, n_sources, n_params)
|
|
16
|
+
n_samples, n_sources, _ = chain.shape
|
|
17
|
+
|
|
18
|
+
# Work on the parameter we want to sort on.
|
|
19
|
+
param = chain[:, :, init_parameter_index].copy()
|
|
20
|
+
# Replace NaNs with infinity so they appear at the end when sorting.
|
|
21
|
+
param[np.isnan(param)] = np.inf
|
|
22
|
+
|
|
23
|
+
# For each row, get the permutation that sorts the parameter values.
|
|
24
|
+
ordering = np.argsort(param, axis=1)
|
|
25
|
+
|
|
26
|
+
# Reorder the entire chain.
|
|
27
|
+
reordered_chain = np.take_along_axis(chain, ordering[:, :, np.newaxis], axis=1)
|
|
28
|
+
|
|
29
|
+
# Convert any inf back to NaN in the parameter.
|
|
30
|
+
sorted_param = reordered_chain[:, :, init_parameter_index]
|
|
31
|
+
sorted_param[np.isinf(sorted_param)] = np.nan
|
|
32
|
+
reordered_chain[:, :, init_parameter_index] = sorted_param
|
|
33
|
+
|
|
34
|
+
return PosteriorChain(
|
|
35
|
+
reordered_chain,
|
|
36
|
+
posterior_chain.num_sources,
|
|
37
|
+
posterior_chain.num_params_per_source,
|
|
38
|
+
posterior_chain.trans_dimensional,
|
|
39
|
+
posterior_chain.prob_in_model,
|
|
40
|
+
posterior_chain.cost_dict,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def relabel_by_histogram(posterior_chain: PosteriorChain, init_parameter_index: int = 0):
|
|
45
|
+
"""
|
|
46
|
+
Assign labels to sources based on samples for one parameter,
|
|
47
|
+
ensuring that each original column (source) is used exactly once per row.
|
|
48
|
+
|
|
49
|
+
The algorithm:
|
|
50
|
+
1. Extract the parameter values (for ordering) and replace NaNs with ∞ so they’re
|
|
51
|
+
chosen only after all finite values.
|
|
52
|
+
2. For each ordering column (i.e. for each source position):
|
|
53
|
+
a. Compute a histogram from all remaining finite values (across all rows).
|
|
54
|
+
b. Find the histogram’s mode.
|
|
55
|
+
c. For each row, choose the index (from the not-yet-assigned values) whose value is
|
|
56
|
+
closest to the mode.
|
|
57
|
+
d. Mark that chosen value as “used” by setting it to NaN.
|
|
58
|
+
3. Reorder the full chain according to the computed ordering.
|
|
59
|
+
4. Convert any ∞ values in the sorted parameter back to NaN.
|
|
60
|
+
|
|
61
|
+
Parameters
|
|
62
|
+
----------
|
|
63
|
+
posterior_chain : PosteriorChain
|
|
64
|
+
Contains a 3D array of samples with shape (n_samples, n_sources, n_params).
|
|
65
|
+
init_parameter_index : int, optional
|
|
66
|
+
The parameter index used for sorting (default 0).
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
PosteriorChain
|
|
71
|
+
A new PosteriorChain with sources reordered so that each original column is used once per row.
|
|
72
|
+
"""
|
|
73
|
+
# Get a copy of the full chain (shape: [n_samples, n_sources, n_params])
|
|
74
|
+
chain = posterior_chain.get_chain().copy()
|
|
75
|
+
n_samples, n_sources, _ = chain.shape
|
|
76
|
+
|
|
77
|
+
# Extract the parameter values we are sorting on.
|
|
78
|
+
# Work on a separate copy so that we can mark chosen entries.
|
|
79
|
+
param_values = chain[:, :, init_parameter_index].copy()
|
|
80
|
+
# Replace NaNs with infinity so they are assigned last.
|
|
81
|
+
param_values[np.isnan(param_values)] = np.inf
|
|
82
|
+
|
|
83
|
+
# Prepare an array to store the new ordering (each row should be a permutation of 0..n_sources-1).
|
|
84
|
+
reordered_indices = np.empty((n_samples, n_sources), dtype=int)
|
|
85
|
+
|
|
86
|
+
# Iterate for each new column in the ordering.
|
|
87
|
+
for new_col in range(n_sources):
|
|
88
|
+
# Compute the global histogram mode from all remaining finite values.
|
|
89
|
+
remaining = param_values[np.isfinite(param_values)]
|
|
90
|
+
if remaining.size > 0:
|
|
91
|
+
hist, edges = np.histogram(remaining, bins="auto")
|
|
92
|
+
centers = (edges[:-1] + edges[1:]) / 2
|
|
93
|
+
mode = centers[np.argmax(hist)]
|
|
94
|
+
else:
|
|
95
|
+
# Fallback if no finite values remain (should not occur given our assumptions).
|
|
96
|
+
mode = 0.0
|
|
97
|
+
|
|
98
|
+
# Now, for each row, pick the index (from values not yet chosen) that is closest to the mode.
|
|
99
|
+
for i in range(n_samples):
|
|
100
|
+
row = param_values[i, :]
|
|
101
|
+
# Find the positions that have not been chosen yet (i.e. are not NaN).
|
|
102
|
+
available = np.where(~np.isnan(row))[0]
|
|
103
|
+
if available.size > 0:
|
|
104
|
+
# Compute differences only over available values.
|
|
105
|
+
diffs = np.abs(row[available] - mode)
|
|
106
|
+
best_idx = available[np.argmin(diffs)]
|
|
107
|
+
else:
|
|
108
|
+
# Should not happen; every row originally has at least one finite value.
|
|
109
|
+
best_idx = 0
|
|
110
|
+
# Record the chosen index for this new column.
|
|
111
|
+
reordered_indices[i, new_col] = best_idx
|
|
112
|
+
# Mark the chosen value as used by setting it to NaN.
|
|
113
|
+
param_values[i, best_idx] = np.nan
|
|
114
|
+
|
|
115
|
+
# Reorder the entire chain using the computed ordering.
|
|
116
|
+
# This reorders along the source dimension.
|
|
117
|
+
reordered_chain = np.take_along_axis(chain, reordered_indices[:, :, np.newaxis], axis=1)
|
|
118
|
+
|
|
119
|
+
# In the sorted parameter, convert any ∞ values back to NaN.
|
|
120
|
+
sorted_param = reordered_chain[:, :, init_parameter_index]
|
|
121
|
+
sorted_param[np.isinf(sorted_param)] = np.nan
|
|
122
|
+
reordered_chain[:, :, init_parameter_index] = sorted_param
|
|
123
|
+
|
|
124
|
+
return PosteriorChain(
|
|
125
|
+
reordered_chain,
|
|
126
|
+
posterior_chain.num_sources,
|
|
127
|
+
posterior_chain.num_params_per_source,
|
|
128
|
+
posterior_chain.trans_dimensional,
|
|
129
|
+
posterior_chain.prob_in_model,
|
|
130
|
+
posterior_chain.cost_dict,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def relabel_uni_normal_one_parameter(posterior_chain: PosteriorChain,
|
|
135
|
+
max_num_sources: int = None,
|
|
136
|
+
num_iterations: int = 20,
|
|
137
|
+
init_parameter_index: int = 0,
|
|
138
|
+
eps=1e-2):
|
|
139
|
+
# create single parameter function to relabel samples
|
|
140
|
+
relabel_samples = create_relabel_samples(uni_normal_fit_single_parameter,
|
|
141
|
+
uni_normal_aux_distribution_single_parameter,
|
|
142
|
+
single_parameter=init_parameter_index,
|
|
143
|
+
eps=eps)
|
|
144
|
+
|
|
145
|
+
return relabel_samples(
|
|
146
|
+
posterior_chain,
|
|
147
|
+
max_num_sources=max_num_sources,
|
|
148
|
+
num_iterations=num_iterations
|
|
149
|
+
)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import os
|
|
3
|
+
import glob
|
|
4
|
+
from petra.utils import sort_by_number
|
|
5
|
+
from petra.posterior_chain import PosteriorChain
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def load_samples_product_space(filepath, num_params_per_source, fill_value=np.nan):
|
|
9
|
+
"""
|
|
10
|
+
Load chains from a file and reshape them into a 3D array
|
|
11
|
+
with dimensions (num_samples, num_sources, num_params)
|
|
12
|
+
|
|
13
|
+
Assumes the chain is organized as (param1_source1, param2_source1, ..., paramN_source1, paramN_source2, ...)
|
|
14
|
+
"""
|
|
15
|
+
with open(filepath, "r") as f:
|
|
16
|
+
chain = np.loadtxt(f)
|
|
17
|
+
num_sources_vector = np.rint(chain[:, -5] + 1).astype(int)
|
|
18
|
+
num_sources = np.max(num_sources_vector)
|
|
19
|
+
|
|
20
|
+
only_samples_chain = (
|
|
21
|
+
np.zeros((chain.shape[0], num_sources * num_params_per_source)) + fill_value
|
|
22
|
+
)
|
|
23
|
+
for i in range(chain.shape[0]):
|
|
24
|
+
only_samples_chain[i, : num_sources_vector[i] * num_params_per_source] = chain[
|
|
25
|
+
i, : num_sources_vector[i] * num_params_per_source
|
|
26
|
+
]
|
|
27
|
+
samples = only_samples_chain.reshape(-1, num_sources, num_params_per_source)
|
|
28
|
+
return PosteriorChain(
|
|
29
|
+
samples, num_sources, num_params_per_source, trans_dimensional=True
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_samples_fixed_num_sources(filepath, num_params_per_source, burn=0, thin=1):
|
|
34
|
+
"""
|
|
35
|
+
Load chains from a file and reshape them into a 3D array
|
|
36
|
+
with dimensions (num_samples, num_sources, num_params)
|
|
37
|
+
|
|
38
|
+
Assumes the chain is organized as (param1_source1, param2_source1, ..., paramN_source1, paramN_source2, ...)
|
|
39
|
+
"""
|
|
40
|
+
with open(filepath, "r") as f:
|
|
41
|
+
chain = np.loadtxt(f)
|
|
42
|
+
chain = chain[burn::thin, :-4] # remove metadata columns
|
|
43
|
+
num_sources = (chain.shape[1]) // num_params_per_source
|
|
44
|
+
samples = chain.reshape(-1, num_sources, num_params_per_source)
|
|
45
|
+
return PosteriorChain(samples, num_sources, num_params_per_source)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_samples_ucbmcmc(chain_folder, fill_value=np.nan, burn=0, thin=1, remove_low_numbers=False):
|
|
49
|
+
"""
|
|
50
|
+
Load chains from files and reshape them into a 3D array
|
|
51
|
+
with dimensions (num_samples, num_sources, num_params).
|
|
52
|
+
|
|
53
|
+
Only files with more than 8 samples (i.e. more samples than the number
|
|
54
|
+
of dimensions in the model) are loaded.
|
|
55
|
+
|
|
56
|
+
Assumes the chain is organized as:
|
|
57
|
+
(param1_source1, param2_source1, ...,
|
|
58
|
+
param1_source2, param2_source2, ...,
|
|
59
|
+
...,
|
|
60
|
+
paramN_source1, paramN_source2, ...)
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
# Find and sort all matching filepaths.
|
|
64
|
+
filepaths = sort_by_number(
|
|
65
|
+
glob.glob(os.path.join(chain_folder, "dimension_chain.dat.*"))
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Build a list of valid files that have more samples than the model's 8 dimensions.
|
|
69
|
+
valid_files = []
|
|
70
|
+
for filepath in filepaths:
|
|
71
|
+
# Determine the number of sources from the filename.
|
|
72
|
+
nsources = int(filepath.split(".")[-1])
|
|
73
|
+
if nsources == 0:
|
|
74
|
+
continue # Skip files with no sources
|
|
75
|
+
|
|
76
|
+
# Count the total number of lines in the file.
|
|
77
|
+
with open(filepath, "r") as f:
|
|
78
|
+
total_lines = sum(1 for line in f)
|
|
79
|
+
# Calculate the number of samples by dividing by the number of sources.
|
|
80
|
+
# (Assumes that the file's total line count is an integer multiple of nsources.)
|
|
81
|
+
nsamples = total_lines // nsources
|
|
82
|
+
|
|
83
|
+
# Only accept files with more than 8 samples.
|
|
84
|
+
if not remove_low_numbers:
|
|
85
|
+
valid_files.append((filepath, nsamples, nsources))
|
|
86
|
+
elif remove_low_numbers and nsamples > 8:
|
|
87
|
+
valid_files.append((filepath, nsamples, nsources))
|
|
88
|
+
|
|
89
|
+
if not valid_files:
|
|
90
|
+
raise ValueError("No files with more than 8 samples found.")
|
|
91
|
+
|
|
92
|
+
# Compute the total number of samples and maximum number of sources among valid files.
|
|
93
|
+
total_samples = sum(nsamples for (_, nsamples, _) in valid_files)
|
|
94
|
+
max_sources = max(nsources for (_, _, nsources) in valid_files)
|
|
95
|
+
|
|
96
|
+
# Initialize the array to hold the samples.
|
|
97
|
+
samples = np.full((total_samples, max_sources, 8), fill_value)
|
|
98
|
+
|
|
99
|
+
current_total = 0
|
|
100
|
+
# Loop over valid files and load their data.
|
|
101
|
+
for filepath, nsamples, nsources in valid_files:
|
|
102
|
+
# Load the chain data from the file.
|
|
103
|
+
chain = np.loadtxt(filepath)
|
|
104
|
+
# Reshape and place the data into the samples array.
|
|
105
|
+
samples[current_total: current_total + nsamples, :nsources, :] = chain.reshape((nsamples, nsources, 8))
|
|
106
|
+
current_total += nsamples
|
|
107
|
+
|
|
108
|
+
return PosteriorChain(
|
|
109
|
+
samples[burn::thin],
|
|
110
|
+
max_sources,
|
|
111
|
+
trans_dimensional=True,
|
|
112
|
+
num_params_per_source=8,
|
|
113
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from petra.posterior_chain import PosteriorChain
|
|
2
|
+
from petra.relabel import create_relabel_samples
|
|
3
|
+
from petra.aux_distributions import mv_normal_aux_distribution
|
|
4
|
+
from petra.parametric_fits import mv_normal_fit
|
|
5
|
+
from petra.initialization import relabel_uni_normal_one_parameter, relabel_by_histogram
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def relabel_mv_normal(posterior_chain: PosteriorChain,
|
|
9
|
+
max_num_sources: int = None,
|
|
10
|
+
num_iterations: int = 20,
|
|
11
|
+
eps=1e-2):
|
|
12
|
+
|
|
13
|
+
relabel_samples = create_relabel_samples(mv_normal_fit,
|
|
14
|
+
mv_normal_aux_distribution,
|
|
15
|
+
eps=eps)
|
|
16
|
+
|
|
17
|
+
return relabel_samples(
|
|
18
|
+
posterior_chain,
|
|
19
|
+
max_num_sources=max_num_sources,
|
|
20
|
+
num_iterations=num_iterations
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_catalog_mv_normal(posterior_chain: PosteriorChain,
|
|
25
|
+
max_num_sources: int,
|
|
26
|
+
num_iterations: int = 200,
|
|
27
|
+
initialization_param_index: int = None,
|
|
28
|
+
shuffle_seed: int = None):
|
|
29
|
+
# shuffle the entries
|
|
30
|
+
posterior_chain.randomize_entries(shuffle_seed)
|
|
31
|
+
|
|
32
|
+
# make sure that posterior_chain has the right shape
|
|
33
|
+
if posterior_chain.num_sources > max_num_sources:
|
|
34
|
+
posterior_chain.expand_chain(max_num_sources)
|
|
35
|
+
|
|
36
|
+
# initialize here:
|
|
37
|
+
if initialization_param_index is not None:
|
|
38
|
+
print("Initializing with univariate normal distribution.")
|
|
39
|
+
# initial_posterior_chain = relabel_uni_normal_one_parameter(posterior_chain,
|
|
40
|
+
# max_num_sources=max_num_sources,
|
|
41
|
+
# num_iterations=init_num_iterations,
|
|
42
|
+
# init_parameter_index=initialization_param_index)
|
|
43
|
+
initial_posterior_chain = relabel_by_histogram(posterior_chain,
|
|
44
|
+
init_parameter_index=initialization_param_index)
|
|
45
|
+
else:
|
|
46
|
+
initial_posterior_chain = posterior_chain
|
|
47
|
+
|
|
48
|
+
# relabel using mv normal
|
|
49
|
+
print("Relabeling with multivariate normal distribution.")
|
|
50
|
+
relabeled_chain = relabel_mv_normal(initial_posterior_chain,
|
|
51
|
+
max_num_sources=max_num_sources,
|
|
52
|
+
num_iterations=num_iterations)
|
|
53
|
+
|
|
54
|
+
return relabeled_chain
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
import numpy as np
|
|
3
|
+
from functools import partial
|
|
4
|
+
from typing import Callable
|
|
5
|
+
from petra.utils import find_prob_in_model
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def create_parametric_fit(fit_function, single_parameter=None):
|
|
9
|
+
"""
|
|
10
|
+
Create a function to fit a parametric distribution to the chain of samples. Extensible to any parametric distribution.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
fit_function: Callable function with the signature (chain, max_num_sources, single_parameter (optional))
|
|
15
|
+
The function to fit the parametric distribution to the chain of samples
|
|
16
|
+
|
|
17
|
+
Return
|
|
18
|
+
------
|
|
19
|
+
parametric_fit: Callable function with the signature (chain, max_num_sources)
|
|
20
|
+
The function to fit the parametric distribution to the chain of samples
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
if single_parameter is not None:
|
|
24
|
+
fit_function = partial(fit_function, fit_parameter=single_parameter)
|
|
25
|
+
|
|
26
|
+
def parametric_fit(chain, max_num_sources):
|
|
27
|
+
"""
|
|
28
|
+
Fit a parametric distribution to the chain of samples .
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
chain: A numpy array of shape (num_samples, num_entries, num_params_per_source)
|
|
33
|
+
max_num_sources: The maximum number of sources to consider in the catalog
|
|
34
|
+
|
|
35
|
+
Return
|
|
36
|
+
------
|
|
37
|
+
fit: The fit to the chain of samples
|
|
38
|
+
"""
|
|
39
|
+
return fit_function(chain, max_num_sources)
|
|
40
|
+
|
|
41
|
+
return parametric_fit
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def mv_normal_fit(chain, max_num_sources):
|
|
45
|
+
"""
|
|
46
|
+
Get a MV Gaussian fit (mu, cov) to use in the cost matrix. This function takes in a chain of samples and returns the
|
|
47
|
+
mean and covariance matrix. NaN values are removed by pandas before the calculation.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
chain: A numpy array of shape (num_samples, num_entries, num_params_per_source)
|
|
52
|
+
max_num_sources: The maximum number of sources to consider in the catalog
|
|
53
|
+
|
|
54
|
+
Return
|
|
55
|
+
-------
|
|
56
|
+
means: A list of length max_num_sources of numpy arrays of shape (num_params_per_source,)
|
|
57
|
+
cov_matrices: A list of length max_num_sources of numpy arrays of shape (num_params_per_source, num_params_per_source)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
means = []
|
|
61
|
+
cov_matrices = []
|
|
62
|
+
for source in range(max_num_sources):
|
|
63
|
+
sample_i = chain[:, source, :] # shape: (num_samples, num_params)
|
|
64
|
+
valid = ~np.isnan(sample_i).any(axis=1)
|
|
65
|
+
valid_samples = sample_i[valid]
|
|
66
|
+
if valid_samples.shape[0] < 8:
|
|
67
|
+
print('Fewer than 8 values in source index {}. Appending a standard normal distribution.'.format(source))
|
|
68
|
+
means.append(np.zeros(chain.shape[2]))
|
|
69
|
+
cov_matrices.append(np.identity(chain.shape[2]))
|
|
70
|
+
continue
|
|
71
|
+
df = pd.DataFrame(chain[:, source, :])
|
|
72
|
+
means.append(np.array(df.dropna().mean()))
|
|
73
|
+
cov_matrices.append(np.array(df.dropna().cov()))
|
|
74
|
+
return np.array(means), np.array(cov_matrices)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def uni_normal_fit_single_parameter(chain, max_num_sources, fit_parameter):
|
|
78
|
+
"""
|
|
79
|
+
Get a univariate Gaussian fit (mu, std) to use in the cost matrix. This function takes in a chain of samples and returns the
|
|
80
|
+
mean and standard deviation. NaN values are removed by pandas before the calculation.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
chain: A numpy array of shape (num_samples, num_entries, num_params_per_source)
|
|
85
|
+
max_num_sources: The maximum number of sources to consider in the catalog
|
|
86
|
+
fit_parameter: The index of the parameter to fit
|
|
87
|
+
|
|
88
|
+
Return
|
|
89
|
+
------
|
|
90
|
+
means: A list of length max_num_sources of numpy arrays of shape (num_params_per_source,)
|
|
91
|
+
stds: A list of length max_num_sources of numpy arrays of shape (num_params_per_source,)
|
|
92
|
+
"""
|
|
93
|
+
means = []
|
|
94
|
+
stds = []
|
|
95
|
+
for source in range(max_num_sources):
|
|
96
|
+
sample_i = chain[:, source, fit_parameter] # shape: (num_samples, num_params)
|
|
97
|
+
valid = np.where(~np.isnan(sample_i))
|
|
98
|
+
valid_samples = sample_i[valid]
|
|
99
|
+
if valid_samples.shape[0] < 8:
|
|
100
|
+
print('Fewer than 8 values in source index {}. Appending a standard normal distribution.'.format(source))
|
|
101
|
+
means.append(0)
|
|
102
|
+
stds.append(1)
|
|
103
|
+
continue
|
|
104
|
+
else:
|
|
105
|
+
df = pd.DataFrame(chain[:, source, fit_parameter])
|
|
106
|
+
mean = np.nanmean(df)
|
|
107
|
+
std = np.nanstd(df)
|
|
108
|
+
means.append(mean)
|
|
109
|
+
stds.append(std)
|
|
110
|
+
return np.array(means), np.array(stds)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def update_parametric_fit_and_prob_in_model(posterior_chain, max_num_sources, parametric_fit_function: Callable, eps=1e-2):
|
|
114
|
+
"""
|
|
115
|
+
Update the parametric fit and the probability of each source being in the model.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
chain: A numpy array of shape (num_samples, num_entries, num_params_per_source)
|
|
120
|
+
max_num_sources: The maximum number of sources to consider in the catalog
|
|
121
|
+
fit_function: A function that fits a parametric distribution to the chain of samples
|
|
122
|
+
prob_in_model: A numpy array of shape (max_num_sources,) that contains the probability of each source being in the model
|
|
123
|
+
|
|
124
|
+
Return
|
|
125
|
+
------
|
|
126
|
+
parametric_fit: The fit to the chain of samples
|
|
127
|
+
prob_in_model: A numpy array of shape (max_num_sources,)
|
|
128
|
+
"""
|
|
129
|
+
aux_params = parametric_fit_function(posterior_chain.get_chain(), max_num_sources)
|
|
130
|
+
prob_in_model = find_prob_in_model(posterior_chain.get_chain(), max_num_sources, eps=eps)
|
|
131
|
+
return aux_params, prob_in_model
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class PosteriorChain:
|
|
7
|
+
"""
|
|
8
|
+
A dataclass to store the chains, number of sources, and number of parameters per source
|
|
9
|
+
|
|
10
|
+
Parameters
|
|
11
|
+
----------
|
|
12
|
+
chain: np.ndarray
|
|
13
|
+
The chain of samples
|
|
14
|
+
num_sources: int
|
|
15
|
+
The number of sources
|
|
16
|
+
num_params_per_source: int
|
|
17
|
+
The number of parameters per source
|
|
18
|
+
trans_dimensional: bool
|
|
19
|
+
Whether the chain has variable number of sources
|
|
20
|
+
prob_in_model: ndarray
|
|
21
|
+
Probability of a source being in the model
|
|
22
|
+
cost_dict: dict
|
|
23
|
+
A dictionary of total cost for the labeling
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
chain: np.ndarray
|
|
27
|
+
num_sources: int
|
|
28
|
+
num_params_per_source: int
|
|
29
|
+
trans_dimensional: bool = False
|
|
30
|
+
prob_in_model: np.ndarray = None
|
|
31
|
+
cost_dict: dict = None
|
|
32
|
+
|
|
33
|
+
def __post_init__(self):
|
|
34
|
+
if self.cost_dict is None:
|
|
35
|
+
self.cost_dict = {}
|
|
36
|
+
self.chain = self.chain.reshape(-1, self.num_sources, self.num_params_per_source)
|
|
37
|
+
|
|
38
|
+
def __repr__(self):
|
|
39
|
+
return self.chain.view().__repr__()
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def shape(self):
|
|
43
|
+
return self.chain.shape
|
|
44
|
+
|
|
45
|
+
def __getitem__(self, index):
|
|
46
|
+
return self.chain[index]
|
|
47
|
+
|
|
48
|
+
def __setitem__(self, index, value):
|
|
49
|
+
self.chain[index] = value
|
|
50
|
+
|
|
51
|
+
def get_chain(self, burn=0, thin=1):
|
|
52
|
+
return self.chain[burn::thin]
|
|
53
|
+
|
|
54
|
+
def expand_chain(self, max_num_sources):
|
|
55
|
+
"""
|
|
56
|
+
Expand the second dimension of the chain to the maximum number of sources.
|
|
57
|
+
"""
|
|
58
|
+
expanded_chain = np.zeros((self.chain.shape[0], max_num_sources, self.num_params_per_source)) + np.nan
|
|
59
|
+
expanded_chain[:, :self.num_sources, :] = self.chain
|
|
60
|
+
return PosteriorChain(expanded_chain, max_num_sources, self.num_params_per_source, True, self.prob_in_model, self.cost_dict)
|
|
61
|
+
|
|
62
|
+
def randomize_entries(self, seed=None):
|
|
63
|
+
"""
|
|
64
|
+
Randomize the entries (second dimension) of the chain without repetition.
|
|
65
|
+
"""
|
|
66
|
+
# Shuffle along axis=1 (the second dimension) for each index in the first dimension
|
|
67
|
+
if seed is not None:
|
|
68
|
+
np.random.seed(seed)
|
|
69
|
+
for i in range(self.chain.shape[0]):
|
|
70
|
+
np.random.shuffle(self.chain[i])
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from scipy import optimize
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from petra.utils import fill_missing_indices
|
|
6
|
+
from petra.posterior_chain import PosteriorChain
|
|
7
|
+
from petra.parametric_fits import update_parametric_fit_and_prob_in_model, create_parametric_fit
|
|
8
|
+
from petra.cost_matrix import create_compute_cost_matrix
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def relabel_samples_one_iteration(chain, aux_parameters, prob_in_model, max_num_sources, compute_cost_matrix: Callable):
|
|
12
|
+
"""
|
|
13
|
+
Takes in a chain of samples and a MV Gaussian fit and returns the sorted chain
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
chain: A numpy array of shape (num_chains, num_sources, num_params_per_source)
|
|
18
|
+
parameters: A set of parameters that fit the aux distribution to the chain
|
|
19
|
+
prob_in_model: A numpy array of shape (max_num_sources,) that contains the probability of each source being in the model
|
|
20
|
+
max_num_sources: The maximum number of sources to consider in the catalog (can be less than, equal to, or greater than the number of entries in the chain)
|
|
21
|
+
|
|
22
|
+
Return
|
|
23
|
+
------
|
|
24
|
+
relabeled_chain: A numpy array of shape (num_chains, num_sources, num_params_per_source)
|
|
25
|
+
cost: Cost of the optimal assignment
|
|
26
|
+
"""
|
|
27
|
+
# TODO(Aaron): Parallelize this for loop over samples
|
|
28
|
+
relabeled_list = []
|
|
29
|
+
total_cost = 0
|
|
30
|
+
for sample in chain:
|
|
31
|
+
cost_matrix = compute_cost_matrix(sample, aux_parameters, prob_in_model, max_num_sources)
|
|
32
|
+
total_entries = max(len(sample), max_num_sources) # pick the larger of number of aux distributions or sample entries
|
|
33
|
+
row_ind, col_ind = optimize.linear_sum_assignment(cost_matrix, maximize=True) # solve the linear sum assignment problem
|
|
34
|
+
total_cost -= cost_matrix[row_ind, col_ind].sum()
|
|
35
|
+
# TODO(Aaron): Is the following line still necessary? It shouldn't change the result either way.
|
|
36
|
+
relabeled_list.append(fill_missing_indices(total_entries, col_ind)) # sum assignment only outputs 1 number for each labeling distribution input
|
|
37
|
+
relabeled_array = np.array(relabeled_list)
|
|
38
|
+
|
|
39
|
+
relabeled_samples = np.array([row[m] for row, m in zip(chain, relabeled_array)]) # reorder the original samples
|
|
40
|
+
total_cost /= len(chain)
|
|
41
|
+
return relabeled_samples, total_cost
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def relabel_posterior_chain_one_iteration(posterior_chain: PosteriorChain, aux_parameters, prob_in_model, max_num_sources, compute_cost_matrix: Callable):
|
|
45
|
+
"""
|
|
46
|
+
Takes in a chain of samples and a MV Gaussian fit and returns the sorted chain
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
chain: A PosteriorChain object
|
|
51
|
+
parameters: A set of parameters that fit the aux distribution to the chain
|
|
52
|
+
prob_in_model: A numpy array of shape (max_num_sources,) that contains the probability of each source being in the model
|
|
53
|
+
max_num_sources: The maximum number of sources to consider in the catalog (can be less than, equal to, or greater than the number of entries in the chain)
|
|
54
|
+
|
|
55
|
+
Return
|
|
56
|
+
------
|
|
57
|
+
relabeled_chain: A PosteriorChain object
|
|
58
|
+
cost: Cost of the optimal assignment
|
|
59
|
+
"""
|
|
60
|
+
cost_dict = posterior_chain.cost_dict
|
|
61
|
+
relabeled_chain, total_cost = relabel_samples_one_iteration(posterior_chain.get_chain(), aux_parameters, prob_in_model, max_num_sources, compute_cost_matrix)
|
|
62
|
+
if cost_dict is None:
|
|
63
|
+
cost_dict = {}
|
|
64
|
+
cost_dict[max_num_sources] = total_cost
|
|
65
|
+
return PosteriorChain(relabeled_chain, posterior_chain.num_sources, posterior_chain.num_params_per_source, prob_in_model=prob_in_model, cost_dict=cost_dict)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def create_relabel_samples(parametric_fit_function: Callable,
|
|
69
|
+
aux_distribution: Callable,
|
|
70
|
+
single_parameter: int = None,
|
|
71
|
+
eps: float = 1e-2):
|
|
72
|
+
|
|
73
|
+
# make all the necessary pieces
|
|
74
|
+
param_fit = create_parametric_fit(parametric_fit_function, single_parameter=single_parameter)
|
|
75
|
+
compute_cost_matrix = create_compute_cost_matrix(aux_distribution, single_parameter=single_parameter)
|
|
76
|
+
|
|
77
|
+
def relabel_samples(posterior_chain: PosteriorChain,
|
|
78
|
+
max_num_sources: int = None,
|
|
79
|
+
num_iterations: int = 200):
|
|
80
|
+
"""
|
|
81
|
+
Takes in a posterior_chain and relabels the samples using a multivariate normal auxiliary fitting distribution
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
posterior_chain: A PosteriorChain object
|
|
86
|
+
max_num_sources: The maximum number of sources to consider in the catalog (can be less than, equal to, or greater than the number of entries in the chain)
|
|
87
|
+
num_iterations: How many tries to use before stopping the algorithm
|
|
88
|
+
init_parameter_index: Which parameter index to use in the initialization procedures
|
|
89
|
+
shuffle_entries: Whether to shuffle the entries in the chain at the start
|
|
90
|
+
shuffle_seed: Seed for the random number generator
|
|
91
|
+
|
|
92
|
+
Return
|
|
93
|
+
------
|
|
94
|
+
relabeled_chain: A PosteriorChain object
|
|
95
|
+
cost: Cost of the optimal assignment
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# if shuffle_entries:
|
|
99
|
+
# posterior_chain.randomize_entries(shuffle_seed)
|
|
100
|
+
if max_num_sources is None:
|
|
101
|
+
max_num_sources = posterior_chain.num_sources
|
|
102
|
+
if max_num_sources > posterior_chain.num_sources:
|
|
103
|
+
# increase the width of the posterior chain to the maximum number of sources
|
|
104
|
+
posterior_chain = posterior_chain.expand_chain(max_num_sources)
|
|
105
|
+
if max_num_sources < posterior_chain.num_sources:
|
|
106
|
+
raise ValueError("The maximum number of sources cannot be less than the number of entries in the chain.")
|
|
107
|
+
|
|
108
|
+
print()
|
|
109
|
+
print(f"Sorting the posterior chain:\n\tMaximum number of iterations: {num_iterations}\n\tMaximum number of source labels:{max_num_sources}\n")
|
|
110
|
+
|
|
111
|
+
# set up the for loop
|
|
112
|
+
old_posterior_chain = posterior_chain
|
|
113
|
+
old_parametric_fit, old_prob_in_model = update_parametric_fit_and_prob_in_model(posterior_chain, max_num_sources, param_fit, eps=eps)
|
|
114
|
+
old_cost_of_assignment = 0
|
|
115
|
+
|
|
116
|
+
for iteration in range(num_iterations):
|
|
117
|
+
# get the new values
|
|
118
|
+
new_posterior_chain = relabel_posterior_chain_one_iteration(old_posterior_chain, old_parametric_fit, old_prob_in_model, max_num_sources, compute_cost_matrix)
|
|
119
|
+
new_parametric_fit, new_prob_in_model = update_parametric_fit_and_prob_in_model(new_posterior_chain, max_num_sources, param_fit, eps=eps)
|
|
120
|
+
new_cost_of_assignment = new_posterior_chain.cost_dict[max_num_sources]
|
|
121
|
+
|
|
122
|
+
# print out the results
|
|
123
|
+
delta_cost_of_assignment = new_cost_of_assignment - old_cost_of_assignment
|
|
124
|
+
print(f"Iteration {iteration + 1}: Difference in cost of assignment is {delta_cost_of_assignment} with total cost of {new_cost_of_assignment}.")
|
|
125
|
+
print(f"\tProbabilities in model: {new_prob_in_model}")
|
|
126
|
+
|
|
127
|
+
# break if converged
|
|
128
|
+
if (delta_cost_of_assignment == 0):
|
|
129
|
+
print(f"Stopped after {iteration + 1} iterations because the cost didn't change from the previous iteration.")
|
|
130
|
+
new_parametric_fit = old_parametric_fit
|
|
131
|
+
new_prob_in_model = old_prob_in_model
|
|
132
|
+
new_posterior_chain = old_posterior_chain
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
# update the old values to the new values
|
|
136
|
+
old_posterior_chain = new_posterior_chain
|
|
137
|
+
old_parametric_fit = new_parametric_fit
|
|
138
|
+
old_prob_in_model = new_prob_in_model
|
|
139
|
+
old_cost_of_assignment = new_cost_of_assignment
|
|
140
|
+
|
|
141
|
+
if iteration == num_iterations - 1:
|
|
142
|
+
print(f"Final cost of assignment: {new_cost_of_assignment} after the maximum number ({num_iterations}) of iterations.")
|
|
143
|
+
|
|
144
|
+
return new_posterior_chain
|
|
145
|
+
|
|
146
|
+
return relabel_samples
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def find_prob_in_model(chain, max_num_sources, eps=1e-2):
|
|
5
|
+
"""
|
|
6
|
+
Find the number of samples for each entry in the chain and divide by the total number of samples.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
chains: A numpy array of shape (num_samples, num_entries, num_params_per_source)
|
|
11
|
+
max_num_sources: The maximum number of sources to consider in the catalog
|
|
12
|
+
eps: A small number to avoid division by zero
|
|
13
|
+
|
|
14
|
+
Return
|
|
15
|
+
------
|
|
16
|
+
prob_in_model: A numpy array of shape (max_num_sources,)
|
|
17
|
+
"""
|
|
18
|
+
num_samples = chain.shape[0]
|
|
19
|
+
prob_in_model = np.zeros(max_num_sources)
|
|
20
|
+
for i in range(max_num_sources):
|
|
21
|
+
prob_in_model[i] = np.sum(~np.isnan(chain[:, i, 0])) / num_samples
|
|
22
|
+
# clip the probabilities to avoid division by zero in np.log
|
|
23
|
+
prob_in_model = np.clip(prob_in_model, eps, 1 - eps)
|
|
24
|
+
return prob_in_model
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fill_missing_indices(total_sources, given_indices):
|
|
28
|
+
# Step 1: Generate a list of all indices from 0 to total_sources - 1
|
|
29
|
+
all_indices = np.arange(total_sources)
|
|
30
|
+
# Step 2: Convert given_indices to a set for faster operations
|
|
31
|
+
given_indices_set = set(given_indices)
|
|
32
|
+
# Step 3: Filter out the given indices from all_indices to get missing indices
|
|
33
|
+
missing_indices = [index for index in all_indices if index not in given_indices_set]
|
|
34
|
+
# Step 4: Combine the given indices with the missing indices
|
|
35
|
+
filled_indices = list(given_indices) + missing_indices
|
|
36
|
+
|
|
37
|
+
return np.array(filled_indices)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def count_swaps(arr):
|
|
41
|
+
"""
|
|
42
|
+
Counts the number of swaps needed to sort an array.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
arr (np.array): The input array.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
int: The number of swaps.
|
|
49
|
+
"""
|
|
50
|
+
sorted_arr = np.sort(arr)
|
|
51
|
+
swaps = np.sum(arr != sorted_arr)
|
|
52
|
+
return swaps // 2
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def sort_by_number(filenames):
|
|
56
|
+
# Extract the number after the dot and convert it to an integer
|
|
57
|
+
def extract_number(filename):
|
|
58
|
+
return int(filename.split(".")[-1])
|
|
59
|
+
|
|
60
|
+
# Sort the filenames using the extracted number
|
|
61
|
+
return sorted(filenames, key=extract_number)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "petra-catalogs"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "From LISA global fit to catalogs"
|
|
5
|
+
authors = ["Aaron D. Johnson", "Javier Roulete", "Katerina Chatziioannou", "Michele Vallisneri", "Kyle Gersbach", "Chris Trejo"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{ include = "petra"}]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.11"
|
|
11
|
+
numpy = "^2.2.3"
|
|
12
|
+
scipy = "^1.15.2"
|
|
13
|
+
pandas = "^2.2.3"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["poetry-core"]
|
|
18
|
+
build-backend = "poetry.core.masonry.api"
|