Coverage for bilby/core/sampler/dynesty_utils.py: 90%

305 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-06 04:57 +0000

1import warnings 

2 

3import numpy as np 

4from dynesty.nestedsamplers import MultiEllipsoidSampler, UnitCubeSampler 

5from dynesty.utils import apply_reflect, get_random_generator 

6 

7from ...bilby_mcmc.chain import calculate_tau 

8from ..utils.log import logger 

9from .base_sampler import _SamplingContainer 

10 

11 

12class LivePointSampler(UnitCubeSampler): 

13 """ 

14 Modified version of dynesty UnitCubeSampler that adapts the MCMC 

15 length in addition to the proposal scale, this corresponds to 

16 :code:`bound=live`. 

17 

18 In order to support live-point based proposals, e.g., differential 

19 evolution (:code:`diff`), the live points are added to the 

20 :code:`kwargs` passed to the evolve method. 

21 

22 Note that this does not perform ellipsoid clustering as with the 

23 :code:`bound=multi` option, if ellipsoid-based proposals are used, e.g., 

24 :code:`volumetric`, consider using the 

25 :code:`MultiEllipsoidLivePointSampler` (:code:`sample=live-multi`). 

26 """ 

27 

28 rebuild = False 

29 

30 def update_user(self, blob, update=True): 

31 """ 

32 Update the proposal parameters based on the number of accepted steps 

33 and MCMC chain length. 

34 

35 There are a number of logical checks performed: 

36 - if the ACT tracking rwalk method is being used and any parallel 

37 process has an empty cache, set the :code:`rebuild` flag to force 

38 the cache to rebuild at the next call. This improves the efficiency 

39 when using parallelisation. 

40 - update the :code:`walks` parameter to asymptotically approach the 

41 desired number of accepted steps for the :code:`FixedRWalk` proposal. 

42 - update the ellipsoid scale if the ellipsoid proposals are being used. 

43 """ 

44 # do we need to trigger rebuilding the cache 

45 if blob.get("remaining", 0) == 1: 

46 self.rebuild = True 

47 if update: 

48 self.kwargs["rebuild"] = self.rebuild 

49 self.rebuild = False 

50 

51 # update walks to match target naccept 

52 accept_prob = max(0.5, blob["accept"]) / self.kwargs["walks"] 

53 delay = self.nlive // 10 - 1 

54 n_target = getattr(_SamplingContainer, "naccept", 60) 

55 self.walks = (self.walks * delay + n_target / accept_prob) / (delay + 1) 

56 self.kwargs["walks"] = min(int(np.ceil(self.walks)), _SamplingContainer.maxmcmc) 

57 

58 self.scale = blob["accept"] 

59 

60 update_rwalk = update_user 

61 

62 def propose_live(self, *args): 

63 """ 

64 We need to make sure the live points are passed to the proposal 

65 function if we are using live point-based proposals. 

66 """ 

67 self.kwargs["nlive"] = self.nlive 

68 self.kwargs["live"] = self.live_u 

69 i = self.rstate.integers(self.nlive) 

70 u = self.live_u[i, :] 

71 return u, np.identity(self.ncdim) 

72 

73 

74class MultiEllipsoidLivePointSampler(MultiEllipsoidSampler): 

75 """ 

76 Modified version of dynesty MultiEllipsoidSampler that adapts the MCMC 

77 length in addition to the proposal scale, this corresponds to 

78 :code:`bound=live-multi`. 

79 

80 Additionally, in order to support live point-based proposals, e.g., 

81 differential evolution (:code:`diff`), the live points are added to the 

82 :code:`kwargs` passed to the evolve method. 

83 

84 When just using the :code:`diff` proposal method, consider using the 

85 :code:`LivePointSampler` (:code:`sample=live`). 

86 """ 

87 

88 rebuild = False 

89 

90 def update_user(self, blob, update=True): 

91 LivePointSampler.update_user(self, blob=blob, update=update) 

92 super(MultiEllipsoidLivePointSampler, self).update_rwalk( 

93 blob=blob, update=update 

94 ) 

95 

96 update_rwalk = update_user 

97 

98 def propose_live(self, *args): 

99 """ 

100 We need to make sure the live points are passed to the proposal 

101 function if we are using ensemble proposals. 

102 """ 

103 self.kwargs["nlive"] = self.nlive 

104 self.kwargs["live"] = self.live_u 

105 return super(MultiEllipsoidLivePointSampler, self).propose_live(*args) 

106 

107 

108class FixedRWalk: 

109 """ 

110 Run the MCMC walk for a fixed length. This is nearly equivalent to 

111 :code:`bilby.sampling.sample_rwalk` except that different proposal 

112 distributions can be used. 

113 """ 

114 

115 def __call__(self, args): 

116 current_u = args.u 

117 naccept = 0 

118 ncall = 0 

119 

120 periodic = args.kwargs["periodic"] 

121 reflective = args.kwargs["reflective"] 

122 boundary_kwargs = dict(periodic=periodic, reflective=reflective) 

123 

124 proposals, common_kwargs, proposal_kwargs = _get_proposal_kwargs(args) 

125 walks = len(proposals) 

126 

127 accepted = list() 

128 

129 for prop in proposals: 

130 u_prop = proposal_funcs[prop]( 

131 u=current_u, **common_kwargs, **proposal_kwargs[prop] 

132 ) 

133 u_prop = apply_boundaries_(u_prop=u_prop, **boundary_kwargs) 

134 if u_prop is None: 

135 accepted.append(0) 

136 continue 

137 

138 v_prop = args.prior_transform(u_prop) 

139 logl_prop = args.loglikelihood(v_prop) 

140 ncall += 1 

141 

142 if logl_prop > args.loglstar: 

143 current_u = u_prop 

144 current_v = v_prop 

145 logl = logl_prop 

146 naccept += 1 

147 accepted.append(1) 

148 else: 

149 accepted.append(0) 

150 

151 if naccept == 0: 

152 logger.debug( 

153 "Unable to find a new point using walk: returning a random point. " 

154 "If this warning occurs often, increase naccept." 

155 ) 

156 # Technically we can find out the likelihood value 

157 # stored somewhere 

158 # But I'm currently recomputing it 

159 current_u = common_kwargs["rstate"].uniform(0, 1, len(current_u)) 

160 current_v = args.prior_transform(current_u) 

161 logl = args.loglikelihood(current_v) 

162 

163 blob = { 

164 "accept": naccept, 

165 "reject": walks - naccept, 

166 "scale": args.scale, 

167 } 

168 

169 return current_u, current_v, logl, ncall, blob 

170 

171 

172class ACTTrackingRWalk: 

173 """ 

174 Run the MCMC sampler for many iterations in order to reliably estimate 

175 the autocorrelation time. 

176 

177 This builds a cache of :code:`nact / thin` points consistent with the 

178 likelihood constraint. 

179 While this approach is the most robust, it is not well optimized for 

180 parallelised sampling as the length of the MCMC will be different for each 

181 parallel process. 

182 """ 

183 

184 # the _cache is a class level variable to avoid being forgotten at every 

185 # iteration when using multiprocessing 

186 _cache = list() 

187 

188 def __init__(self): 

189 self.act = 1 

190 self.thin = getattr(_SamplingContainer, "nact", 2) 

191 self.maxmcmc = getattr(_SamplingContainer, "maxmcmc", 5000) * 50 

192 

193 def __call__(self, args): 

194 self.args = args 

195 if args.kwargs.get("rebuild", False): 

196 logger.debug("Force rebuilding cache") 

197 self.build_cache() 

198 while self.cache[0][2] < args.loglstar: 

199 self.cache.pop(0) 

200 current_u, current_v, logl, ncall, blob = self.cache.pop(0) 

201 blob["remaining"] = len(self.cache) 

202 return current_u, current_v, logl, ncall, blob 

203 

204 @property 

205 def cache(self): 

206 if len(self._cache) == 0: 

207 self.build_cache() 

208 else: 

209 logger.debug(f"Not rebuilding cache, remaining size {len(self._cache)}") 

210 return self._cache 

211 

212 def build_cache(self): 

213 args = self.args 

214 # Bounds 

215 periodic = args.kwargs.get("periodic", None) 

216 reflective = args.kwargs.get("reflective", None) 

217 boundary_kwargs = dict(periodic=periodic, reflective=reflective) 

218 

219 proposals, common_kwargs, proposal_kwargs = _get_proposal_kwargs(args) 

220 

221 # Setup 

222 current_u = args.u 

223 check_interval = int(np.ceil(self.act)) 

224 target_nact = 50 

225 next_check = check_interval 

226 n_checks = 0 

227 

228 # Initialize internal variables 

229 current_v = args.prior_transform(np.array(current_u)) 

230 logl = args.loglikelihood(np.array(current_v)) 

231 accept = 0 

232 reject = 0 

233 nfail = 0 

234 ncall = 0 

235 act = np.inf 

236 u_list = list() 

237 v_list = list() 

238 logl_list = list() 

239 most_failures = 0 

240 current_failures = 0 

241 

242 iteration = 0 

243 while iteration < min(target_nact * act, self.maxmcmc): 

244 iteration += 1 

245 

246 prop = proposals[iteration % len(proposals)] 

247 u_prop = proposal_funcs[prop]( 

248 u=current_u, **common_kwargs, **proposal_kwargs[prop] 

249 ) 

250 u_prop = apply_boundaries_(u_prop=u_prop, **boundary_kwargs) 

251 success = False 

252 if u_prop is not None: 

253 v_prop = args.prior_transform(np.array(u_prop)) 

254 logl_prop = args.loglikelihood(np.array(v_prop)) 

255 ncall += 1 

256 if logl_prop > args.loglstar: 

257 success = True 

258 current_u = u_prop 

259 current_v = v_prop 

260 logl = logl_prop 

261 

262 u_list.append(current_u) 

263 v_list.append(current_v) 

264 logl_list.append(logl) 

265 

266 if success: 

267 accept += 1 

268 most_failures = max(most_failures, current_failures) 

269 current_failures = 0 

270 else: 

271 nfail += 1 

272 current_failures += 1 

273 

274 # If we've taken the minimum number of steps, calculate the ACT 

275 if iteration > next_check and accept > target_nact: 

276 n_checks += 1 

277 most_failures = max(most_failures, current_failures) 

278 act = self._calculate_act( 

279 accept=accept, 

280 iteration=iteration, 

281 samples=np.array(u_list), 

282 most_failures=most_failures, 

283 ) 

284 to_next_update = (act * target_nact - iteration) // 2 

285 to_next_update = max(to_next_update, iteration // 100) 

286 next_check += to_next_update 

287 logger.debug( 

288 f"it: {iteration}, accept: {accept}, reject: {reject}, " 

289 f"fail: {nfail}, act: {act:.2f}, nact: {iteration / act:.2f} " 

290 ) 

291 elif iteration > next_check: 

292 next_check += check_interval 

293 

294 most_failures = max(most_failures, current_failures) 

295 self.act = self._calculate_act( 

296 accept=accept, 

297 iteration=iteration, 

298 samples=np.array(u_list), 

299 most_failures=most_failures, 

300 ) 

301 reject += nfail 

302 blob = {"accept": accept, "reject": reject, "scale": args.scale} 

303 iact = int(np.ceil(self.act)) 

304 thin = self.thin * iact 

305 

306 if accept == 0: 

307 logger.debug( 

308 "Unable to find a new point using walk: returning a random point" 

309 ) 

310 u = common_kwargs["rstate"].uniform(size=len(current_u)) 

311 v = args.prior_transform(u) 

312 logl = args.loglikelihood(v) 

313 self._cache.append((u, v, logl, ncall, blob)) 

314 elif not np.isfinite(act): 

315 logger.warning( 

316 "Unable to find a new point using walk: try increasing maxmcmc" 

317 ) 

318 self._cache.append((current_u, current_v, logl, ncall, blob)) 

319 elif (self.thin == -1) or (len(u_list) <= thin): 

320 self._cache.append((current_u, current_v, logl, ncall, blob)) 

321 else: 

322 u_list = u_list[thin::thin] 

323 v_list = v_list[thin::thin] 

324 logl_list = logl_list[thin::thin] 

325 n_found = len(u_list) 

326 accept = max(accept // n_found, 1) 

327 reject //= n_found 

328 nfail //= n_found 

329 ncall_list = [ncall // n_found] * n_found 

330 blob_list = [ 

331 dict(accept=accept, reject=reject, fail=nfail, scale=args.scale) 

332 ] * n_found 

333 self._cache.extend(zip(u_list, v_list, logl_list, ncall_list, blob_list)) 

334 logger.debug( 

335 f"act: {self.act:.2f}, max failures: {most_failures}, thin: {thin}, " 

336 f"iteration: {iteration}, n_found: {n_found}" 

337 ) 

338 logger.debug( 

339 f"Finished building cache with length {len(self._cache)} after " 

340 f"{iteration} iterations with {ncall} likelihood calls and ACT={self.act:.2f}" 

341 ) 

342 

343 @staticmethod 

344 def _calculate_act(accept, iteration, samples, most_failures): 

345 """ 

346 Take the maximum of three ACT estimates, leading to a conservative estimate: 

347 

348 - a full ACT estimate as done in :code:`bilby_mcmc`. This is almost always the 

349 longest estimator, and is the most computationally expensive. The other 

350 methods mostly catch cases where the estimated ACT is very small. 

351 - the naive ACT used for the acceptance tracking walk. 

352 - the most failed proposals between any pair of accepted steps. This is a strong 

353 lower bound, because we know that if we thin by less than this, there will be 

354 duplicate points. 

355 """ 

356 if accept > 0: 

357 naive_act = 2 / accept * iteration - 1 

358 else: 

359 return np.inf 

360 return max(calculate_tau(samples), naive_act, most_failures) 

361 

362 

363class AcceptanceTrackingRWalk: 

364 """ 

365 This is a modified version of dynesty.sampling.sample_rwalk that runs the 

366 MCMC random walk for a user-specified number of a crude approximation to 

367 the autocorrelation time. 

368 

369 This is the proposal method used by default for :code:`Bilby<2` and 

370 corresponds to specifying :code:`sample="rwalk"` 

371 """ 

372 

373 # to retain state between calls to pool.Map, this needs to be a class 

374 # level attribute 

375 old_act = None 

376 

377 def __init__(self, old_act=None): 

378 self.maxmcmc = getattr(_SamplingContainer, "maxmcmc", 5000) 

379 self.nact = getattr(_SamplingContainer, "nact", 40) 

380 

381 def __call__(self, args): 

382 rstate = get_random_generator(args.rseed) 

383 

384 periodic = args.kwargs.get("periodic", None) 

385 reflective = args.kwargs.get("reflective", None) 

386 boundary_kwargs = dict(periodic=periodic, reflective=reflective) 

387 

388 u = args.u 

389 nlive = args.kwargs.get("nlive", args.kwargs.get("walks", 100)) 

390 

391 proposals, common_kwargs, proposal_kwargs = _get_proposal_kwargs(args) 

392 

393 accept = 0 

394 reject = 0 

395 nfail = 0 

396 act = np.inf 

397 

398 iteration = 0 

399 while iteration < self.nact * act: 

400 iteration += 1 

401 

402 prop = proposals[iteration % len(proposals)] 

403 u_prop = proposal_funcs[prop](u, **common_kwargs, **proposal_kwargs[prop]) 

404 u_prop = apply_boundaries_(u_prop, **boundary_kwargs) 

405 

406 if u_prop is None: 

407 nfail += 1 

408 continue 

409 

410 # Check proposed point. 

411 v_prop = args.prior_transform(np.array(u_prop)) 

412 logl_prop = args.loglikelihood(np.array(v_prop)) 

413 if logl_prop > args.loglstar: 

414 u = u_prop 

415 v = v_prop 

416 logl = logl_prop 

417 accept += 1 

418 else: 

419 reject += 1 

420 

421 # If we've taken the minimum number of steps, calculate the ACT 

422 if iteration > self.nact: 

423 act = self.estimate_nmcmc( 

424 accept_ratio=accept / (accept + reject + nfail), 

425 safety=1, 

426 tau=nlive, 

427 ) 

428 

429 # If we've taken too many likelihood evaluations then break 

430 if accept + reject > self.maxmcmc: 

431 warnings.warn( 

432 f"Hit maximum number of walks {self.maxmcmc} with accept={accept}," 

433 f" reject={reject}, and nfail={nfail} try increasing maxmcmc" 

434 ) 

435 break 

436 

437 if not (np.isfinite(act) and accept > 0): 

438 logger.debug( 

439 "Unable to find a new point using walk: returning a random point" 

440 ) 

441 u = rstate.uniform(size=len(u)) 

442 v = args.prior_transform(u) 

443 logl = args.loglikelihood(v) 

444 

445 blob = {"accept": accept, "reject": reject + nfail, "scale": args.scale} 

446 AcceptanceTrackingRWalk.old_act = act 

447 

448 ncall = accept + reject 

449 return u, v, logl, ncall, blob 

450 

451 def estimate_nmcmc(self, accept_ratio, safety=5, tau=None): 

452 """Estimate autocorrelation length of chain using acceptance fraction 

453 

454 Using ACL = (2/acc) - 1 multiplied by a safety margin. Code adapted from: 

455 

456 - https://github.com/farr/Ensemble.jl 

457 - https://github.com/johnveitch/cpnest/blob/master/cpnest/sampler.py 

458 

459 Parameters 

460 ========== 

461 accept_ratio: float [0, 1] 

462 Ratio of the number of accepted points to the total number of points 

463 old_act: int 

464 The ACT of the last iteration 

465 maxmcmc: int 

466 The maximum length of the MCMC chain to use 

467 safety: int 

468 A safety factor applied in the calculation 

469 tau: int (optional) 

470 The ACT, if given, otherwise estimated. 

471 

472 Notes 

473 ===== 

474 This method does not compute a reliable estimate of the autocorrelation 

475 length for our proposal distributions. 

476 """ 

477 if tau is None: 

478 tau = self.maxmcmc / safety 

479 

480 if accept_ratio == 0.0: 

481 if self.old_act is None: 

482 Nmcmc_exact = np.inf 

483 else: 

484 Nmcmc_exact = (1 + 1 / tau) * self.old_act 

485 else: 

486 estimated_act = 2 / accept_ratio - 1 

487 Nmcmc_exact = safety * estimated_act 

488 if self.old_act is not None: 

489 Nmcmc_exact = (1 - 1 / tau) * self.old_act + Nmcmc_exact / tau 

490 Nmcmc_exact = float(min(Nmcmc_exact, self.maxmcmc)) 

491 return max(safety, Nmcmc_exact) 

492 

493 

494def _get_proposal_kwargs(args): 

495 """ 

496 Resolve the proposal cycle from the provided keyword arguments. 

497 

498 The steps involved are: 

499 

500 - extract the requested proposal types from the :code:`_SamplingContainer`. 

501 If none are specified, only differential evolution will be used. 

502 - differential evolution requires the live points to be passed. If they are 

503 not present, raise an error. 

504 - if a dictionary, e.g., :code:`dict(diff=5, volumetric=1)` is specified, 

505 the keys will be used weighted by the values, e.g., 5:1 differential 

506 evolution to volumetric. 

507 - each proposal needs different keyword arguments, see the specific functions 

508 for what requires which. 

509 

510 

511 Parameters 

512 ========== 

513 args: dynesty.sampler.SamplerArgument 

514 Object that carries around various pieces of information about the 

515 analysis. 

516 """ 

517 rstate = get_random_generator(args.rseed) 

518 walks = args.kwargs.get("walks", 100) 

519 current_u = args.u 

520 n_cluster = args.axes.shape[0] 

521 

522 proposals = getattr(_SamplingContainer, "proposals", None) 

523 if proposals is None: 

524 proposals = ["diff"] 

525 if "diff" in proposals: 

526 live = args.kwargs.get("live", None) 

527 if live is None: 

528 raise ValueError( 

529 "Live points not passed for differential evolution, specify " 

530 "bound='live' to use differential evolution proposals." 

531 ) 

532 live = np.unique(live, axis=0) 

533 matches = np.where(np.equal(current_u, live).all(axis=1))[0] 

534 np.delete(live, matches, 0) 

535 

536 if isinstance(proposals, (list, set, tuple)): 

537 proposals = rstate.choice(proposals, int(walks)) 

538 elif isinstance(proposals, dict): 

539 props, weights = zip(*proposals.items()) 

540 weights = np.array(weights) / sum(weights) 

541 proposals = rstate.choice(list(props), int(walks), p=weights) 

542 

543 common_kwargs = dict( 

544 n=len(current_u), 

545 n_cluster=n_cluster, 

546 rstate=rstate, 

547 ) 

548 proposal_kwargs = dict() 

549 if "diff" in proposals: 

550 proposal_kwargs["diff"] = dict( 

551 live=live[:, :n_cluster], 

552 mix=0.5, 

553 scale=2.38 / (2 * n_cluster) ** 0.5, 

554 ) 

555 if "volumetric" in proposals: 

556 proposal_kwargs["volumetric"] = dict( 

557 axes=args.axes, 

558 scale=args.scale, 

559 ) 

560 return proposals, common_kwargs, proposal_kwargs 

561 

562 

563def propose_differetial_evolution( 

564 u, 

565 live, 

566 n, 

567 n_cluster, 

568 rstate, 

569 mix=0.5, 

570 scale=1, 

571): 

572 r""" 

573 Propose a new point using ensemble differential evolution 

574 (`ter Braak + (2006) <https://doi.org/10.1007/s11222-006-8769-1>`_). 

575 

576 .. math:: 

577 

578 u_{\rm prop} = u + \gamma (v_{a} - v_{b}) 

579 

580 We consider two choices for :math:`\gamma`: weighted by :code:`mix`. 

581 

582 - :math:`\gamma = 1`: this is a mode-hopping mode for efficiently 

583 exploring multi-modal spaces 

584 - :math:`\gamma \sim \Gamma\left(\gamma; k=4, \theta=\frac{\kappa}{4}\right)` 

585 

586 Here :math:`\kappa = 2.38 / \sqrt{2 n}` unless specified by the user and 

587 we scale by a random draw from a Gamma distribution. The specific 

588 distribution was chosen somewhat arbitrarily to have mean and mode close to 

589 :math:`\kappa` and give good acceptance and autocorrelation times on a subset 

590 of problems. 

591 

592 Parameters 

593 ---------- 

594 u: np.ndarray 

595 The current point. 

596 live: np.ndarray 

597 The ensemble of live points to select :math:`v` from. 

598 n: int 

599 The number of dimensions being explored 

600 n_cluster: int 

601 The number of dimensions to run the differential evolution over, the 

602 first :code:`n_cluster` dimensions are used. The rest are randomly 

603 sampled from the prior. 

604 rstate: np.random.RandomState 

605 The random state to use to generate random numbers. 

606 mix: float 

607 The fraction of proposed points that should follow the specified scale 

608 rather than mode hopping. :code:`default=0.5` 

609 scale: float 

610 The amount to scale the difference vector by. 

611 :code:`default = 2.38 / (2 * n_cluster)**0.5)` 

612 

613 Returns 

614 ------- 

615 u_prop: np.ndarray 

616 The proposed point. 

617 """ 

618 delta = np.diff(rstate.choice(live, 2, replace=False), axis=0)[0] 

619 if rstate.uniform(0, 1) < mix: 

620 if scale is None: 

621 scale = 2.38 / (2 * n_cluster) ** 0.5 

622 scale *= rstate.gamma(4, 0.25) 

623 else: 

624 scale = 1 

625 u_prop = u.copy() 

626 u_prop[:n_cluster] += delta * scale 

627 u_prop[n_cluster:] = rstate.uniform(0, 1, n - n_cluster) 

628 return u_prop 

629 

630 

631def propose_volumetric( 

632 u, 

633 axes, 

634 scale, 

635 n, 

636 n_cluster, 

637 rstate, 

638): 

639 """ 

640 Propose a new point using the default :code:`dynesty` proposal. 

641 

642 The new difference vector is scaled by a vector isotropically drawn 

643 from an ellipsoid centered on zero with covariance given by the 

644 provided axis. Note that the magnitude of this proposal is heavily 

645 skewed to the size of the ellipsoid. 

646 

647 Parameters 

648 ---------- 

649 u: np.ndarray 

650 The current point. 

651 n: int 

652 The number of dimensions being explored. 

653 scale: float 

654 The amount to scale the proposed point by. 

655 n_cluster: int 

656 The number of dimensions to run the differential evolution over, the 

657 first :code:`n_cluster` dimensions are used. The rest are randomly 

658 sampled from the prior. 

659 rstate: np.random.RandomState 

660 The random state to use to generate random numbers. 

661 

662 Returns 

663 ------- 

664 u_prop: np.ndarray 

665 The proposed point. 

666 """ 

667 # Propose a direction on the unit n-sphere. 

668 drhat = rstate.normal(0, 1, n_cluster) 

669 drhat /= np.linalg.norm(drhat) 

670 

671 # Scale based on dimensionality. 

672 dr = drhat * rstate.uniform(0, 1) ** (1.0 / n_cluster) 

673 

674 # Transform to proposal distribution. 

675 delta = scale * np.dot(axes, dr) 

676 u_prop = u.copy() 

677 u_prop[:n_cluster] += delta 

678 u_prop[n_cluster:] = rstate.uniform(0, 1, n - n_cluster) 

679 return u_prop 

680 

681 

682def apply_boundaries_(u_prop, periodic, reflective): 

683 """ 

684 Apply the periodic and reflective boundaries and test if we are inside the 

685 unit cube. 

686 

687 Parameters 

688 ---------- 

689 u_prop: np.ndarray 

690 The proposed point in the unit hypercube space. 

691 periodic: np.ndarray 

692 Indices of the parameters with periodic boundaries. 

693 reflective: np.ndarray 

694 Indices of the parameters with reflective boundaries. 

695 

696 Returns 

697 ======= 

698 [np.ndarray, None]: 

699 Either the remapped proposed point, or None if the proposed point 

700 lies outside the unit cube. 

701 """ 

702 # Wrap periodic parameters 

703 if periodic is not None: 

704 u_prop[periodic] = np.mod(u_prop[periodic], 1) 

705 

706 # Reflect 

707 if reflective is not None: 

708 u_prop[reflective] = apply_reflect(u_prop[reflective]) 

709 

710 if u_prop.min() < 0 or u_prop.max() > 1: 

711 return None 

712 else: 

713 return u_prop 

714 

715 

716proposal_funcs = dict(diff=propose_differetial_evolution, volumetric=propose_volumetric)