Coverage for pesummary/gw/conversions/tgr.py: 75.6%

168 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-05-02 08:42 +0000

1# Licensed under an MIT style license -- see LICENSE.md 

2 

3from pesummary.utils.utils import logger 

4import numpy as np 

5from scipy.stats import gaussian_kde 

6from scipy.interpolate import interp2d 

7import multiprocessing 

8from pesummary.utils.probability_dict import ProbabilityDict2D 

9 

10__author__ = [ 

11 "Aditya Vijaykumar <aditya.vijaykumar@ligo.org>", 

12 "Charlie Hoy <charlie.hoy@ligo.org>" 

13] 

14 

15 

16def _wrapper_for_multiprocessing_kde(kde, *args): 

17 """Wrapper to evaluate a KDE on multiple cpus 

18 

19 Parameters 

20 ---------- 

21 kde: func 

22 KDE you wish to evaluate 

23 *args: tuple 

24 all args are passed to the KDE 

25 """ 

26 _reshape = (len(args[0]), len(args[1])) 

27 yy, xx = np.meshgrid(args[1], args[0]) 

28 _args = [xx.ravel(), yy.ravel()] 

29 return kde(_args).reshape(_reshape) 

30 

31 

32def _wrapper_for_multiprocessing_interp(interp, *args): 

33 """Wrapper to evaluate an interpolant on multiple cpus 

34 

35 Parameters 

36 ---------- 

37 interp: func 

38 interpolant you wish to use 

39 *args: tuple 

40 all args are passed to the interpolant 

41 """ 

42 return interp(*args) 

43 

44 

45def _imrct_deviation_parameters_integrand_vectorized( 

46 final_mass, 

47 final_spin, 

48 v1, 

49 v2, 

50 P_final_mass_final_spin_i_interp_object, 

51 P_final_mass_final_spin_r_interp_object, 

52 multi_process=1, 

53 wrapper_function_for_multiprocess=None, 

54): 

55 """Compute the integrand of P(delta_final_mass/final_mass_bar, 

56 delta_final_spin/final_spin_bar). 

57 

58 Parameters 

59 ---------- 

60 final_mass: np.ndarray 

61 vector of values of final mass 

62 final_spin: np.ndarray 

63 vector of values of final spin 

64 v1: np.ndarray 

65 array of delta_final_mass/final_mass_bar values 

66 v2: np.ndarray 

67 array of delta_final_spin/final_spin_bar values 

68 P_final_mass_final_spin_i_interp_object: 

69 interpolated P_i(final_mass, final_spin) 

70 P_final_mass_final_spin_r_interp_object: 

71 interpolated P_r(final_mass, final_spin) 

72 multi_process: int 

73 Number of parallel processes. Default: 1 

74 wrapper_function_for_multiprocess: method 

75 Wrapper function for the multiprocessing. Default: None 

76 

77 Returns 

78 ------- 

79 np.array 

80 integrand of P(delta_final_mass/final_mass_bar, 

81 delta_final_spin/final_spin_bar) 

82 """ 

83 final_mass_mat, final_spin_mat = np.meshgrid(final_mass, final_spin) 

84 _abs = np.abs(final_mass_mat * final_spin_mat) 

85 _reshape = (len(v1), len(v1)) 

86 _v2, _v1 = np.meshgrid(v2, v1) 

87 v1, v2 = _v1.ravel(), _v2.ravel() 

88 v1, v2 = v1.reshape(len(v1), 1), v2.reshape(len(v2), 1) 

89 

90 # Create delta_final_mass and delta_final_spin vectors corresponding 

91 # to the given v1 and v2. 

92 

93 # These vectors have to be monotonically increasing in order to 

94 # evaluate the interpolated prob densities. Hence, for v1, v2 < 0, 

95 # flip them, evaluate the prob density (in column or row) and flip it back 

96 

97 # The definition of the delta_* parameters is taken from eq A1 of 

98 # Ghosh et al 2018, arXiv:1704.06784. 

99 

100 delta_final_mass_i = ((1.0 + v1 / 2.0)) * final_mass 

101 delta_final_spin_i = ((1.0 + v2 / 2.0)) * final_spin 

102 delta_final_mass_r = ((1.0 - v1 / 2.0)) * final_mass 

103 delta_final_spin_r = ((1.0 - v2 / 2.0)) * final_spin 

104 

105 for num in range(len(v1)): 

106 if (1.0 + v1[num] / 2.0) < 0.0: 

107 delta_final_mass_i[num] = np.flipud(delta_final_mass_i[num]) 

108 if (1.0 + v2[num] / 2.0) < 0.0: 

109 delta_final_spin_i[num] = np.flipud(delta_final_spin_i[num]) 

110 if (1.0 - v1[num] / 2.0) < 0.0: 

111 delta_final_mass_r[num] = np.flipud(delta_final_mass_r[num]) 

112 if (1.0 - v2[num] / 2.0) < 0.0: 

113 delta_final_spin_r[num] = np.flipud(delta_final_spin_r[num]) 

114 

115 if multi_process > 1: 

116 with multiprocessing.Pool(multi_process) as pool: 

117 _length = len(delta_final_mass_i) 

118 _P_i = pool.starmap( 

119 wrapper_function_for_multiprocess, 

120 zip( 

121 [P_final_mass_final_spin_i_interp_object] * _length, 

122 delta_final_mass_i, 

123 delta_final_spin_i, 

124 ), 

125 ) 

126 _P_r = pool.starmap( 

127 wrapper_function_for_multiprocess, 

128 zip( 

129 [P_final_mass_final_spin_r_interp_object] * _length, 

130 delta_final_mass_r, 

131 delta_final_spin_r, 

132 ), 

133 ) 

134 P_i = np.array([i for i in _P_i]) 

135 P_r = np.array([i for i in _P_r]) 

136 else: 

137 P_i, P_r = [], [] 

138 for num in range(len(delta_final_mass_i)): 

139 P_i.append( 

140 wrapper_function_for_multiprocess( 

141 P_final_mass_final_spin_i_interp_object, 

142 delta_final_mass_i[num], 

143 delta_final_spin_i[num], 

144 ) 

145 ) 

146 P_r.append( 

147 wrapper_function_for_multiprocess( 

148 P_final_mass_final_spin_r_interp_object, 

149 delta_final_mass_r[num], 

150 delta_final_spin_r[num], 

151 ) 

152 ) 

153 

154 for num in range(len(v1)): 

155 if (1.0 + v1[num] / 2.0) < 0.0: 

156 P_i[num] = np.fliplr(P_i[num]) 

157 if (1.0 + v2[num] / 2.0) < 0.0: 

158 P_i[num] = np.flipud(P_i[num]) 

159 if (1.0 - v1[num] / 2.0) < 0.0: 

160 P_r[num] = np.fliplr(P_r[num]) 

161 if (1.0 - v2[num] / 2.0) < 0.0: 

162 P_r[num] = np.flipud(P_r[num]) 

163 

164 # The integration is performed according to eq A2 of Ghosh et al, 

165 # arXiv:1704.06784 

166 

167 _prod = np.array( 

168 [np.sum(_P_i * _P_r * _abs) for _P_i, _P_r in zip(P_i, P_r)] 

169 ).reshape(_reshape) 

170 return _prod 

171 

172 

173def _apply_args_and_kwargs(function, args, kwargs): 

174 """Apply a tuple of args and a dictionary of kwargs to a function 

175 

176 Parameters 

177 ---------- 

178 function: func 

179 function you wish to use 

180 args: tuple 

181 all args passed to function 

182 kwargs: dict 

183 all kwargs passed to function 

184 """ 

185 return function(*args, **kwargs) 

186 

187 

188def _imrct_deviation_parameters_integrand_series( 

189 final_mass, 

190 final_spin, 

191 v1, 

192 v2, 

193 P_final_mass_final_spin_i_interp_object, 

194 P_final_mass_final_spin_r_interp_object, 

195 multi_process=1, 

196 **kwargs 

197): 

198 """ 

199 Creates the over the deviation parameter space. 

200 

201 Parameters 

202 ---------- 

203 final_mass: np.ndarray 

204 vector of values of final mass 

205 final_spin: np.ndarray 

206 vector of values of final spin 

207 v1: np.ndarray 

208 array of delta_final_mass/final_mass_bar values 

209 v2: np.ndarray 

210 array of delta_final_spin/final_spin_bar values 

211 P_final_mass_final_spin_i_interp_object: 

212 interpolated P_i(final_mass, final_spin) 

213 P_final_massfinal_spin_r_interp_object: 

214 interpolated P_r(final_mass, final_spin) 

215 """ 

216 P = np.zeros(shape=(len(v1), len(v2))) 

217 if multi_process == 1: 

218 logger.debug("Performing calculation on a single cpu. This may take some time") 

219 for i, _v2 in enumerate(v2): 

220 for j, _v1 in enumerate(v1): 

221 P[i, j] = _imrct_deviation_parameters_integrand_vectorized( 

222 final_mass, 

223 final_spin, 

224 [_v1], 

225 [_v2], 

226 P_final_mass_final_spin_i_interp_object, 

227 P_final_mass_final_spin_r_interp_object, 

228 multi_process=1, 

229 **kwargs 

230 ) 

231 else: 

232 logger.debug("Splitting the calculation across {} cpus".format(multi_process)) 

233 _v1, _v2 = np.meshgrid(v1, v2) 

234 _v1, _v2 = _v1.ravel(), _v2.ravel() 

235 with multiprocessing.Pool(multi_process) as pool: 

236 args = [ 

237 [final_mass] * len(_v1), 

238 [final_spin] * len(_v1), 

239 np.atleast_2d(_v1).T.tolist(), 

240 np.atleast_2d(_v2).T.tolist(), 

241 [P_final_mass_final_spin_i_interp_object] * len(_v1), 

242 [P_final_mass_final_spin_r_interp_object] * len(_v1), 

243 ] 

244 kwargs["multi_process"] = 1 

245 _args = np.array(args, dtype=object).T 

246 _P = pool.starmap( 

247 _apply_args_and_kwargs, 

248 zip( 

249 [_imrct_deviation_parameters_integrand_vectorized] * len(_args), 

250 _args, 

251 [kwargs] * len(_args), 

252 ), 

253 ) 

254 P = np.array([i[0] for i in _P]).reshape(len(v1), len(v2)) 

255 return P 

256 

257 

258def imrct_deviation_parameters_integrand(*args, vectorize=False, **kwargs): 

259 """Compute the final mass and final spin deviation parameters 

260 

261 Parameters 

262 ---------- 

263 *args: tuple 

264 all args passed to either 

265 _imrct_deviation_parameters_integrand_vectorized or 

266 _imrct_deviation_parameters_integrand_series 

267 vectorize: bool 

268 Vectorize the calculation. Note that vectorize=True uses up a lot 

269 of memory 

270 kwargs: dict, optional 

271 all kwargs passed to either 

272 _imrct_deviation_parameters_integrand_vectorized or 

273 _imrct_deviation_parameters_integrand_series 

274 """ 

275 if vectorize: 

276 return _imrct_deviation_parameters_integrand_vectorized(*args, **kwargs) 

277 return _imrct_deviation_parameters_integrand_series(*args, **kwargs) 

278 

279 

280def imrct_deviation_parameters_from_final_mass_final_spin( 

281 final_mass_inspiral, 

282 final_spin_inspiral, 

283 final_mass_postinspiral, 

284 final_spin_postinspiral, 

285 N_bins=101, 

286 final_mass_deviation_lim=1, 

287 final_spin_deviation_lim=1, 

288 multi_process=1, 

289 use_kde=False, 

290 kde=gaussian_kde, 

291 kde_kwargs=dict(), 

292 interp_method=interp2d, 

293 interp_kwargs=dict(fill_value=0.0, bounds_error=False), 

294 vectorize=False, 

295): 

296 """Compute the IMR Consistency Test deviation parameters. 

297 Code borrows from the implementation in lalsuite: 

298 https://git.ligo.org/lscsoft/lalsuite/-/blob/master/lalinference/python/lalinference/imrtgr/imrtgrutils.py 

299 and 

300 https://git.ligo.org/lscsoft/lalsuite/-/blob/master/lalinference/bin/imrtgr_imr_consistency_test.py 

301 

302 Parameters 

303 ---------- 

304 final_mass_inspiral: np.ndarray 

305 values of final mass calculated from the inspiral part 

306 final_spin_inspiral: np.ndarray 

307 values of final spin calculated from the inspiral part 

308 final_mass_postinspiral: np.ndarray 

309 values of final mass calculated from the post-inspiral part 

310 final_spin_postinspiral: np.ndarray 

311 values of final spin calculated from the post-inspiral part 

312 final_mass_deviation_lim: float 

313 Maximum absolute value of the final mass deviation parameter. Default 1. 

314 final_spin_deviation_lim: float 

315 Maximum absolute value of the final spin deviation parameter. Default 1. 

316 N_bins: int, optional 

317 Number of equally spaced bins between [-final_mass_deviation_lim, 

318 final_mass_deviation_lim] and [-final_spin_deviation_lim, 

319 final_spin_deviation_lim]. Default is 101. 

320 multi_process: int 

321 Number of parallel processes. Default is 1. 

322 use_kde: bool 

323 If `True`, uses kde instead of interpolation. Default is False. 

324 kde: method 

325 KDE method to use. Default is scipy.stats.gaussian_kde 

326 kde_kwargs: dict 

327 Arguments to be passed to the KDE method 

328 interp_method: method 

329 Interpolation method to use. Default is scipy.interpolate.interp2d 

330 interp_kwargs: dict, optional 

331 Arguments to be passed to the interpolation method 

332 Default is `dict(fill_value=0.0, bounds_error=False)` 

333 vectorize: bool 

334 if True, use vectorized imrct_deviation_parameters_integrand 

335 function. This is quicker but does consume more memory. Default: False 

336 

337 Returns 

338 ------- 

339 imrct_deviations: ProbabilityDict2d 

340 Contains the 2d pdf of the IMRCT deviation parameters 

341 """ 

342 # Find the maximum values 

343 final_mass_lim = np.max(np.append(final_mass_inspiral, final_mass_postinspiral)) 

344 final_spin_lim = np.max( 

345 np.abs(np.append(final_spin_inspiral, final_spin_postinspiral)) 

346 ) 

347 

348 # bin the data 

349 final_mass_bins = np.linspace(-final_mass_lim, final_mass_lim, N_bins) 

350 diff_final_mass = final_mass_bins[1] - final_mass_bins[0] 

351 final_spin_bins = np.linspace(-final_spin_lim, final_spin_lim, N_bins) 

352 diff_final_spin = final_spin_bins[1] - final_spin_bins[0] 

353 final_mass_intp = (final_mass_bins[:-1] + final_mass_bins[1:]) * 0.5 

354 final_spin_intp = (final_spin_bins[:-1] + final_spin_bins[1:]) * 0.5 

355 if use_kde: 

356 logger.debug("Using KDE to interpolate data") 

357 # kde the samples for final mass and final spin 

358 final_mass_intp = np.append( 

359 final_mass_intp, final_mass_bins[-1] + diff_final_mass 

360 ) 

361 final_spin_intp = np.append( 

362 final_spin_intp, final_spin_bins[-1] + diff_final_spin 

363 ) 

364 inspiral_interp = kde( 

365 np.array([final_mass_inspiral, final_spin_inspiral]), **kde_kwargs 

366 ) 

367 postinspiral_interp = kde( 

368 np.array([final_mass_postinspiral, final_spin_postinspiral]), **kde_kwargs 

369 ) 

370 _wrapper_function = _wrapper_for_multiprocessing_kde 

371 else: 

372 logger.debug("Interpolating 2d histogram data") 

373 _inspiral_2d_histogram, _, _ = np.histogram2d( 

374 final_mass_inspiral, 

375 final_spin_inspiral, 

376 bins=(final_mass_bins, final_spin_bins), 

377 density=True, 

378 ) 

379 _postinspiral_2d_histogram, _, _ = np.histogram2d( 

380 final_mass_postinspiral, 

381 final_spin_postinspiral, 

382 bins=(final_mass_bins, final_spin_bins), 

383 density=True, 

384 ) 

385 # transpose density to go from (X,Y) indexing returned by 

386 # np.histogram2d() to array (i,j) indexing for further computations. 

387 # From now onwards, different rows (i) correspond to different values 

388 # of final mass and different columns (j) correspond to different 

389 # values of final_spin 

390 _inspiral_2d_histogram = _inspiral_2d_histogram.T 

391 _postinspiral_2d_histogram = _postinspiral_2d_histogram.T 

392 inspiral_interp = interp_method( 

393 final_mass_intp, final_spin_intp, _inspiral_2d_histogram, **interp_kwargs 

394 ) 

395 postinspiral_interp = interp_method( 

396 final_mass_intp, final_spin_intp, _postinspiral_2d_histogram, **interp_kwargs 

397 ) 

398 _wrapper_function = _wrapper_for_multiprocessing_interp 

399 

400 final_mass_deviation_vec = np.linspace( 

401 -final_mass_deviation_lim, final_mass_deviation_lim, N_bins 

402 ) 

403 final_spin_deviation_vec = np.linspace( 

404 -final_spin_deviation_lim, final_spin_deviation_lim, N_bins 

405 ) 

406 

407 diff_final_mass_deviation = final_mass_deviation_vec[1] - final_mass_deviation_vec[0] 

408 diff_final_spin_deviation = final_spin_deviation_vec[1] - final_spin_deviation_vec[0] 

409 

410 P_final_mass_deviation_final_spin_deviation = imrct_deviation_parameters_integrand( 

411 final_mass_intp, 

412 final_spin_intp, 

413 final_mass_deviation_vec, 

414 final_spin_deviation_vec, 

415 inspiral_interp, 

416 postinspiral_interp, 

417 multi_process=multi_process, 

418 vectorize=vectorize, 

419 wrapper_function_for_multiprocess=_wrapper_function, 

420 ) 

421 

422 imrct_deviations = ProbabilityDict2D( 

423 { 

424 "final_mass_final_spin_deviations": [ 

425 final_mass_deviation_vec, 

426 final_spin_deviation_vec, 

427 P_final_mass_deviation_final_spin_deviation 

428 / np.sum(P_final_mass_deviation_final_spin_deviation), 

429 ] 

430 } 

431 ) 

432 return imrct_deviations 

433 

434 

435def generate_imrct_deviation_parameters( 

436 samples, evolve_spins_forward=True, inspiral_string="inspiral", 

437 postinspiral_string="postinspiral", approximant=None, f_low=None, 

438 return_samples_used=False, **kwargs 

439): 

440 """Generate deviation parameter pdfs for the IMR Consistency Test 

441 

442 Parameters 

443 ---------- 

444 samples: MultiAnalysisSamplesDict 

445 Dictionary containing inspiral and postinspiral samples 

446 evolve_spins_forward: bool 

447 If `True`, evolve spins to the ISCO frequency. Default: True. 

448 inspiral_string: string 

449 Identifier for the inspiral samples 

450 postinspiral_string: string 

451 Identifier for the post-inspiral samples 

452 approximant: dict, optional 

453 The approximant used for the inspiral and postinspiral analyses. 

454 Keys of the dictionary must be the same as the inspiral_string and 

455 postinspiral_string. Default None 

456 f_low: dict, optional 

457 The low frequency cut-off used for the inspiral and postinspiral 

458 analyses. Keys of the dictionary must be the same as the inspiral_string 

459 and postinspiral_string. Default None 

460 return_samples_used: Bool, optional 

461 if True, return the samples which were used to generate the IMRCT deviation 

462 parameters. These samples will match the input but may include remnant 

463 samples if they were not previously present 

464 kwargs: dict, optional 

465 Keywords to be passed to imrct_deviation_parameters_from_final_mass_final_spin 

466 

467 Returns 

468 ------- 

469 imrct_deviations: ProbabilityDict2d 

470 2d pdf of the IMRCT deviation parameters 

471 data: dict 

472 Metadata 

473 """ 

474 import time 

475 

476 remnant_condition = lambda _dictionary, _suffix: all( 

477 "{}{}".format(param, _suffix) not in _dictionary.keys() for 

478 param in ["final_mass", "final_spin"] 

479 ) 

480 evolved = np.ones(2, dtype=bool) 

481 suffix = [""] 

482 evolve_spins = ["ISCO"] 

483 if not evolve_spins_forward: 

484 suffix = ["_non_evolved"] + suffix 

485 evolve_spins = [False, False] 

486 fits_data = {} 

487 for idx, (key, sample) in enumerate(samples.items()): 

488 zipped = zip(suffix, evolve_spins) 

489 for num, (_suffix, _evolve_spins) in enumerate(zipped): 

490 cond = remnant_condition(sample, _suffix) 

491 _found_msg = ( 

492 "Found {} remnant properties in the posterior table " 

493 "for {}. Using these for calculation." 

494 ) 

495 if not cond: 

496 logger.info( 

497 _found_msg.format( 

498 "evolved" if not len(_suffix) else "non-evolved", 

499 key 

500 ) 

501 ) 

502 if len(_suffix): 

503 evolved[idx] = False 

504 break 

505 elif not remnant_condition(sample, ""): 

506 logger.info(_found_msg.format("evolved", key)) 

507 evolved[idx] = True 

508 break 

509 else: 

510 logger.warning( 

511 "{} remnant properties not found in the posterior " 

512 "table for {}. Trying to calculate them.".format( 

513 "Evolved" if not len(_suffix) else "Non-evolved", 

514 key 

515 ) 

516 ) 

517 returned_extra_kwargs = sample.generate_all_posterior_samples( 

518 evolve_spins=_evolve_spins, return_kwargs=True, 

519 approximant=( 

520 approximant[key] if approximant is not None else None 

521 ), f_low=f_low[key] if f_low is not None else None 

522 ) 

523 _cond = remnant_condition(sample, _suffix) 

524 if not _cond: 

525 logger.info( 

526 "{} remnant properties generated. Using these " 

527 "samples for calculation".format( 

528 "Evolved" if not len(_suffix) else "Non-evolved" 

529 ) 

530 ) 

531 for fit in ["final_mass_NR_fits", "final_spin_NR_fits"]: 

532 fits_data["{} {}".format(key, fit)] = returned_extra_kwargs[ 

533 "meta_data" 

534 ][fit] 

535 if len(_suffix): 

536 evolved[idx] = False 

537 break 

538 

539 if num == 1: 

540 raise ValueError( 

541 "Unable to compute the remnant properties" 

542 ) 

543 if not all(_evolved == evolved[0] for _evolved in evolved): 

544 keys = list(samples.keys()) 

545 _inspiral_index = keys.index(inspiral_string) 

546 _postinspiral_index = keys.index(postinspiral_string) 

547 raise ValueError( 

548 "Using {} remnant properties for the inspiral and {} remnant " 

549 "properties for the postinspiral. This must be the same for " 

550 "the calculation".format( 

551 "evolved" if evolved[_inspiral_index] else "non-evolved", 

552 "non-evolved" if not evolved[_postinspiral_index] else "evolved" 

553 ) 

554 ) 

555 samples_string = "final_{}" 

556 if not evolved[0]: 

557 samples_string += "_non_evolved" 

558 

559 logger.info("Calculating IMRCT deviation parameters and GR Quantile") 

560 t0 = time.time() 

561 imrct_deviations = imrct_deviation_parameters_from_final_mass_final_spin( 

562 samples[inspiral_string][samples_string.format("mass")], 

563 samples[inspiral_string][samples_string.format("spin")], 

564 samples[postinspiral_string][samples_string.format("mass")], 

565 samples[postinspiral_string][samples_string.format("spin")], 

566 **kwargs, 

567 ) 

568 gr_quantile = ( 

569 imrct_deviations[ 

570 "final_mass_final_spin_deviations" 

571 ].minimum_encompassing_contour_level(0.0, 0.0) 

572 * 100 

573 ) 

574 t1 = time.time() 

575 data = kwargs.copy() 

576 data["evolve_spins"] = evolved 

577 data["Time (seconds)"] = round(t1 - t0, 2) 

578 data["GR Quantile (%)"] = gr_quantile[0] 

579 data.update(fits_data) 

580 logger.info( 

581 "Calculation Finished in {} seconds. GR Quantile is {} %.".format( 

582 data["Time (seconds)"], round(data["GR Quantile (%)"], 2) 

583 ) 

584 ) 

585 if return_samples_used: 

586 return imrct_deviations, data, evolved[0], samples 

587 return imrct_deviations, data, evolved[0]