Coverage for metadetect / fitting.py: 4%

630 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-09 01:50 -0800

1import logging 

2import copy 

3 

4import numpy as np 

5 

6import ngmix 

7import ngmix.prepsfmom 

8from ngmix.gexceptions import ( 

9 BootPSFFailure, PSFFluxFailure, 

10) 

11from ngmix.moments import make_mom_result, fwhm_to_T 

12from ngmix.bootstrap import bootstrap 

13from ngmix.runners import Runner, PSFRunner 

14from ngmix.guessers import SimplePSFGuesser 

15from ngmix.fitting import Fitter 

16from packaging.version import parse 

17 

18from .util import Namer 

19from . import procflags 

20 

21MAX_NUM_SHEAR_BANDS = 6 

22 

23logger = logging.getLogger(__name__) 

24 

25if parse(ngmix.__version__) < parse("2.1.0"): 25 ↛ 26line 25 didn't jump to line 26 because the condition on line 25 was never true

26 MOMNAME = "mom" 

27else: 

28 MOMNAME = "sums" 

29 

30 

31def fit_mbobs_gauss( 

32 *, 

33 mbobs, 

34 bmask_flags, 

35 rng, 

36 shear_bands=None, 

37 obj_runner=None, 

38 psf_runner=None, 

39 coadd=False, 

40): 

41 """Fit a multiband obs using a Gaussian fit. 

42 

43 Parameters 

44 ---------- 

45 mbobs : ngmix.MultiBandObsList 

46 The observation to use for shear measurement. 

47 bmask_flags : int 

48 Observations with these bits set in the bmask are not fit. 

49 rng : np.random.RandomState 

50 Random state for fitting. 

51 shear_bands : list of int, optional 

52 A list of indices into each mbobs that denotes which band is used for shear. 

53 Default is to use all bands. 

54 obj_runner : ngmix.runners.Runner, optional 

55 If not None, the result of `get_gauss_obj_runner` is suggested. If None, 

56 `get_gauss_obj_runner` is called. 

57 psf_runner : ngmix.runners.PSFRunner, optional 

58 If not None, the result of `get_gauss_psf_runner` is suggested. If None, 

59 `get_gauss_psf_runner` is called. 

60 coadd : bool, optional 

61 If True, coadd the mbobs over all bands and then fit. Default is False. 

62 

63 Returns 

64 ------- 

65 res : np.ndarray 

66 A structured array of the fitting results. 

67 """ 

68 if shear_bands is None: 

69 shear_bands = list(range(len(mbobs))) 

70 

71 res = get_wavg_output_struct(len(mbobs), "gauss", shear_bands=shear_bands) 

72 

73 flags = 0 

74 for obslist in mbobs: 

75 if len(obslist) == 0: 

76 flags |= procflags.MISSING_BAND 

77 continue 

78 

79 if len(obslist) > 1: 

80 flags |= procflags.INCONSISTENT_BANDS 

81 continue 

82 

83 for obs in obslist: 

84 if not np.any(obs.weight > 0): 

85 flags |= procflags.ZERO_WEIGHTS 

86 

87 if np.any((obs.bmask & bmask_flags) != 0): 

88 flags |= procflags.EDGE_HIT 

89 

90 if any(s >= len(mbobs) for s in shear_bands): 

91 flags |= procflags.INCONSISTENT_BANDS 

92 

93 if coadd: 

94 if flags == 0: 

95 # first we coadd the shear bands 

96 coadd_obs, coadd_flags = make_coadd_obs(mbobs, shear_bands=shear_bands) 

97 flags |= coadd_flags 

98 

99 if flags == 0: 

100 shear_mbobs = ngmix.observation.get_mb_obs(coadd_obs) 

101 else: 

102 if flags == 0: 

103 shear_mbobs = ngmix.MultiBandObsList() 

104 for band in shear_bands: 

105 shear_mbobs.append(mbobs[band]) 

106 

107 if flags == 0: 

108 try: 

109 ores = bootstrap( 

110 shear_mbobs, 

111 get_gauss_obj_runner( 

112 rng, 

113 len(shear_mbobs), 

114 shear_mbobs[0][0].jacobian.get_scale(), 

115 ) 

116 if obj_runner is None else obj_runner, 

117 psf_runner=( 

118 get_gauss_psf_runner(rng) 

119 if psf_runner is None 

120 else psf_runner 

121 ), 

122 ) 

123 except BootPSFFailure: 

124 flags |= procflags.PSF_FAILURE 

125 except PSFFluxFailure: 

126 flags |= procflags.PSF_FAILURE 

127 except Exception: 

128 flags |= procflags.OBJ_FAILURE 

129 flags |= procflags.PSF_FAILURE 

130 

131 if flags == 0: 

132 res["gauss_obj_flags"] = ores["flags"] 

133 res["gauss_T_flags"] = ores["flags"] 

134 

135 if len(res["gauss_band_flux_flags"].shape) == 2: 

136 res["gauss_band_flux_flags"][0, shear_bands] = ores["flags"] 

137 else: 

138 res["gauss_band_flux_flags"] = ores["flags"] 

139 

140 if ores["flags"] == 0: 

141 res["gauss_s2n"] = ores["s2n"] 

142 res["gauss_g"] = ores["g"] 

143 res["gauss_g_cov"] = ores["g_cov"] 

144 res["gauss_T"] = ores["T"] 

145 res["gauss_T_err"] = ores["T_err"] 

146 

147 if len(res["gauss_band_flux_flags"].shape) == 2: 

148 res["gauss_band_flux"][0, shear_bands] = ores["flux"] 

149 res["gauss_band_flux_err"][0, shear_bands] = ores["flux_err"] 

150 else: 

151 res["gauss_band_flux"] = ores["flux"] 

152 res["gauss_band_flux_err"] = ores["flux_err"] 

153 

154 if coadd: 

155 pres = shear_mbobs[0][0].psf.meta["result"] 

156 res["gauss_psf_flags"] = pres["flags"] 

157 if res["gauss_psf_flags"] == 0: 

158 res["gauss_psf_T"] = pres["T"] 

159 res["gauss_psf_g"] = _extract_g(pres) 

160 if ores["flags"] == 0: 

161 res["gauss_T_ratio"] = res["gauss_T"] / res["gauss_psf_T"] 

162 else: 

163 pflags = 0 

164 psf_g_sum = np.zeros(2) 

165 psf_T_sum = 0.0 

166 wgt_sum = 0.0 

167 for obslist in shear_mbobs: 

168 for obs in obslist: 

169 pflags |= obs.psf.meta["result"]["flags"] 

170 if obs.psf.meta["result"]["flags"] == 0: 

171 msk = obs.weight > 0 

172 if not np.any(msk): 

173 pflags |= procflags.ZERO_WEIGHTS 

174 else: 

175 _wgt = np.median(obs.weight[msk]) 

176 psf_T_sum += obs.psf.meta["result"]["T"] * _wgt 

177 g = _extract_g(obs.psf.meta["result"]) 

178 psf_g_sum += ( 

179 g 

180 * _wgt 

181 * obs.psf.meta["result"]["T"] 

182 ) 

183 wgt_sum += _wgt 

184 

185 res["gauss_psf_flags"] = pflags 

186 if res["gauss_psf_flags"] == 0: 

187 res["gauss_psf_T"] = psf_T_sum / wgt_sum 

188 res["gauss_psf_g"] = psf_g_sum / psf_T_sum 

189 if ores["flags"] == 0: 

190 res["gauss_T_ratio"] = res["gauss_T"] / res["gauss_psf_T"] 

191 

192 res["gauss_flags"] = res["gauss_obj_flags"] | res["gauss_psf_flags"] 

193 else: 

194 res["gauss_flags"] |= flags 

195 

196 return res 

197 

198 

199def _extract_g(res): 

200 """ 

201 We may have fit the PSF with adaptive moments, so check for 

202 'e' as well 

203 """ 

204 if 'g' in res: 

205 g = res['g'] 

206 elif 'e' in res: 

207 e1, e2 = res['e'] 

208 g1, g2 = ngmix.shape.e1e2_to_g1g2(e1, e2) 

209 g = np.array([g1, g2]) 

210 return g 

211 

212 

213def get_gauss_psf_runner(rng): 

214 psf_guesser = SimplePSFGuesser( 

215 rng=rng, 

216 guess_from_moms=True, 

217 ) 

218 psf_fitter = Fitter( 

219 model="gauss", 

220 fit_pars={ 

221 "maxfev": 2000, 

222 "xtol": 1.0e-5, 

223 "ftol": 1.0e-5, 

224 }, 

225 ) 

226 psf_runner = PSFRunner( 

227 fitter=psf_fitter, 

228 guesser=psf_guesser, 

229 ntry=2, 

230 ) 

231 return psf_runner 

232 

233 

234def get_gauss_obj_runner(rng, nband, scale): 

235 prior = _make_ml_prior(rng, scale, nband) 

236 

237 fitter = ngmix.fitting.Fitter( 

238 model='gauss', 

239 prior=prior, 

240 fit_pars={ 

241 "maxfev": 2000, 

242 "xtol": 5.0e-5, 

243 "ftol": 5.0e-5, 

244 }, 

245 ) 

246 guesser = ngmix.guessers.TPSFFluxGuesser( 

247 rng=rng, 

248 T=0.25, 

249 prior=prior, 

250 ) 

251 runner = Runner( 

252 fitter=fitter, 

253 guesser=guesser, 

254 ntry=2, 

255 ) 

256 

257 return runner 

258 

259 

260def _make_ml_prior(rng, scale, nband): 

261 """make the prior for the fitter. 

262 

263 Parameters 

264 ---------- 

265 rng: np.random.RandomState 

266 The random number generator 

267 scale: float 

268 Pixel scale 

269 nband: int 

270 number of bands 

271 """ 

272 g_prior = ngmix.priors.GPriorBA(sigma=0.3, rng=rng) 

273 cen_prior = ngmix.priors.CenPrior( 

274 cen1=0, cen2=0, sigma1=scale, sigma2=scale, rng=rng, 

275 ) 

276 T_prior = ngmix.priors.TwoSidedErf( 

277 minval=-10.0, 

278 width_at_min=0.03, 

279 maxval=1.0e6, 

280 width_at_max=1.0e5, 

281 rng=rng, 

282 ) 

283 F_prior = ngmix.priors.TwoSidedErf( 

284 minval=-1.0e4, 

285 width_at_min=1.0, 

286 maxval=1.0e9, 

287 width_at_max=0.25e8, 

288 rng=rng, 

289 ) 

290 F_prior = [F_prior] * nband 

291 

292 prior = ngmix.joint_prior.PriorSimpleSep( 

293 cen_prior=cen_prior, 

294 g_prior=g_prior, 

295 T_prior=T_prior, 

296 F_prior=F_prior, 

297 ) 

298 

299 return prior 

300 

301 

302def fit_mbobs_list_joint( 

303 *, mbobs_list, fitter_name, bmask_flags, rng, shear_bands=None, 

304 symmetrize=True, coadd=False, 

305): 

306 """Fit the ojects in a list of ngmix.MultiBandObsList using a joint fitter. 

307 

308 The fitter is run per object on the bands used for shear. 

309 

310 Parameters 

311 ---------- 

312 mbobs_list : a list of ngmix.MultiBandObsList 

313 The observations to use for shear measurement. 

314 fitter_name : str 

315 The name of the fitter to use. 

316 bmask_flags : int 

317 Observations with these bits set in the bmask are not fit. 

318 shear_bands : list of int, optional 

319 A list of indices into each mbobs that denotes which band is used for shear. 

320 Default is to use all bands. 

321 rng : np.random.RandomState 

322 Random state for fitting. 

323 symmetrize : bool, optional 

324 If True, apply 4-fold symmetry to the mask+weight map. Default is True. 

325 Ignored for gaussian fits. 

326 coadd : bool, optional 

327 If True, coadd the mbobs over all bands and then fit. Default is False. 

328 Ignored for adaptive moments which always coadds. 

329 

330 Returns 

331 ------- 

332 res : np.ndarray 

333 A structured array of the fitting results. 

334 """ 

335 if fitter_name in ["am", "admom"]: 

336 fit_func = fit_mbobs_admom 

337 kwargs = {"runner": get_admom_runner(rng), 'symmetrize': symmetrize} 

338 elif fitter_name == "gauss": 

339 fit_func = fit_mbobs_gauss 

340 kwargs = {"coadd": coadd} 

341 if len(mbobs_list) > 0 and len(mbobs_list[0]) > 0 and len(mbobs_list[0][0]) > 0: 

342 scale = mbobs_list[0][0][0].jacobian.get_scale() 

343 if coadd: 

344 nband = 1 

345 else: 

346 if shear_bands is None: 

347 nband = len(mbobs_list[0]) 

348 else: 

349 nband = len(shear_bands) 

350 kwargs["obj_runner"] = get_gauss_obj_runner(rng, nband, scale) 

351 kwargs["psf_runner"] = get_gauss_psf_runner(rng) 

352 else: 

353 raise RuntimeError("Joint fitter '%s' not recognized!" % fitter_name) 

354 

355 res = [] 

356 for i, mbobs in enumerate(mbobs_list): 

357 _res = fit_func( 

358 mbobs=mbobs, 

359 bmask_flags=bmask_flags, 

360 shear_bands=shear_bands, 

361 rng=rng, 

362 **kwargs, 

363 ) 

364 res.append(_res) 

365 

366 if len(res) > 0: 

367 return np.hstack(res) 

368 else: 

369 return None 

370 

371 

372def get_admom_runner(rng): 

373 fitter = ngmix.admom.AdmomFitter(rng=rng) 

374 guesser = ngmix.guessers.GMixPSFGuesser( 

375 rng=rng, ngauss=1, guess_from_moms=True, 

376 ) 

377 return ngmix.runners.Runner( 

378 fitter=fitter, guesser=guesser, 

379 ntry=2, 

380 ) 

381 

382 

383def fit_mbobs_admom( 

384 *, 

385 mbobs, 

386 bmask_flags, 

387 rng, 

388 shear_bands=None, 

389 runner=None, 

390 symmetrize=True, 

391): 

392 """Fit a multiband obs using adaptive moments. 

393 

394 This function forms a coadd of the shear bands and then runs 

395 adaptive moments on the coadd. 

396 

397 Parameters 

398 ---------- 

399 mbobs : ngmix.MultiBandObsList 

400 The observation to use for shear measurement. 

401 bmask_flags : int 

402 Observations with these bits set in the bmask are not fit. 

403 rng : np.random.RandomState 

404 Random state for fitting. 

405 shear_bands : list of int, optional 

406 A list of indices into each mbobs that denotes which band is used for shear. 

407 Default is to use all bands. 

408 runner : ngmix.runners.Runner, optional 

409 If not None, the result of `get_admom_runner` is suggested. If None, 

410 `get_admom_runner` is called. 

411 symmetrize : bool, optional 

412 If True, apply 4-fold symmetry to the mask+weight map. Default is True. 

413 

414 Returns 

415 ------- 

416 res : np.ndarray 

417 A structured array of the fitting results. 

418 """ 

419 runner = get_admom_runner(rng) if runner is None else runner 

420 nband = len(mbobs) 

421 if shear_bands is None: 

422 shear_bands = list(range(len(mbobs))) 

423 res = get_wavg_output_struct(nband, "am", shear_bands=shear_bands) 

424 

425 flags = 0 

426 for obslist in mbobs: 

427 if len(obslist) == 0: 

428 flags |= procflags.MISSING_BAND 

429 continue 

430 

431 if len(obslist) > 1: 

432 flags |= procflags.INCONSISTENT_BANDS 

433 continue 

434 

435 for obs in obslist: 

436 if not np.any(obs.weight > 0): 

437 flags |= procflags.ZERO_WEIGHTS 

438 

439 if np.any((obs.bmask & bmask_flags) != 0): 

440 flags |= procflags.EDGE_HIT 

441 

442 if flags == 0: 

443 # first we coadd the shear bands 

444 coadd_obs, coadd_flags = make_coadd_obs(mbobs, shear_bands=shear_bands) 

445 flags |= coadd_flags 

446 

447 if flags == 0: 

448 # then fit the PSF 

449 try: 

450 pres = runner.go(coadd_obs.psf) 

451 except Exception: 

452 flags |= procflags.PSF_FAILURE 

453 else: 

454 res["am_psf_flags"] = pres["flags"] 

455 if pres["flags"] == 0: 

456 res["am_psf_g"] = pres["e"] 

457 res["am_psf_T"] = pres["T"] 

458 

459 if flags == 0: 

460 # then fit the object 

461 if symmetrize: 

462 sym_coadd_obs = symmetrize_obs_weights(coadd_obs) 

463 else: 

464 sym_coadd_obs = coadd_obs 

465 try: 

466 gres = runner.go(sym_coadd_obs) 

467 except Exception: 

468 flags |= procflags.OBJ_FAILURE 

469 # replace no attempt 

470 res["am_flags"] = flags 

471 else: 

472 res["am_T_flags"] = gres["T_flags"] 

473 if gres["T_flags"] == 0: 

474 res["am_T"] = gres["T"] 

475 res["am_T_err"] = gres["T_err"] 

476 if pres["flags"] == 0: 

477 res["am_T_ratio"] = res["am_T"] / res["am_psf_T"] 

478 

479 res["am_obj_flags"] = gres["flags"] 

480 if gres["flags"] == 0: 

481 res["am_s2n"] = gres["s2n"] 

482 res["am_g"] = gres["e"] 

483 res["am_g_cov"] = gres["e_cov"] 

484 

485 # this replaces the flags so they are zero and unsets the default of 

486 # no attempt 

487 res["am_flags"] = (res["am_psf_flags"] | res["am_obj_flags"]) 

488 

489 else: 

490 # this branch ensures noattempt remains set 

491 res["am_flags"] |= flags 

492 

493 return res 

494 

495 

496def make_coadd_obs(mbobs, shear_bands=None): 

497 """Coadd the observations in an ngmix mbobs assuming they all have the same 

498 shaped images and same Jacobians. 

499 

500 Parameters 

501 ---------- 

502 mbobs : ngmix.MultibandObsList 

503 The observation to use for shear measurement. 

504 shear_bands : list of int, optional 

505 A list of indices into each mbobs that denotes which band is used for shear. 

506 Default is to use all bands. 

507 

508 Returns 

509 ------- 

510 coadd_obs : ngmix.Observation 

511 The coadded observation with the PSF set. 

512 flags : int 

513 Any flags for errors in making the coadd obs. 

514 """ 

515 nbands = len(mbobs) 

516 flags = 0 

517 if shear_bands is None: 

518 shear_bands = list(range(nbands)) 

519 

520 # if we have one input band, then simply 

521 # return if if the data is consistent 

522 if nbands == 1: 

523 if shear_bands[0] == 0: 

524 return mbobs[0][0], flags 

525 else: 

526 flags |= procflags.INCONSISTENT_BANDS 

527 return None, flags 

528 

529 # if we ask for a single band for shear, then return that band 

530 # if the data is consistent 

531 if len(shear_bands) == 1: 

532 if shear_bands[0] < nbands: 

533 return mbobs[shear_bands[0]][0], flags 

534 else: 

535 flags |= procflags.INCONSISTENT_BANDS 

536 return None, flags 

537 

538 # at this point, we are coadding more than one band 

539 # let's check for consistent data 

540 

541 # we can't use shear bands we do not have 

542 if any(s >= nbands for s in shear_bands): 

543 flags |= procflags.INCONSISTENT_BANDS 

544 return None, flags 

545 

546 # all of the image shapes and jacobians have to be the same 

547 fobs = mbobs[shear_bands[0]][0] 

548 for shear_band in shear_bands[1:]: 

549 obs = mbobs[shear_band][0] 

550 if ( 

551 (repr(fobs.jacobian) != repr(obs.jacobian)) 

552 or (fobs.image.shape != obs.image.shape) 

553 or (not fobs.has_psf()) 

554 or (not obs.has_psf()) 

555 or (repr(fobs.psf.jacobian) != repr(obs.psf.jacobian)) 

556 or (fobs.psf.image.shape != obs.psf.image.shape) 

557 ): 

558 flags |= procflags.INCONSISTENT_BANDS 

559 return None, flags 

560 

561 # if we get here, we need to coadd 

562 # first make the weights 

563 wgts = np.zeros(len(shear_bands)) 

564 for i, band in enumerate(shear_bands): 

565 obs = mbobs[band][0] 

566 msk = obs.weight > 0 

567 if not np.any(msk): 

568 flags |= procflags.ZERO_WEIGHTS 

569 # let's not waste time since we are missing a band 

570 return None, flags 

571 else: 

572 wgts[i] = np.median(obs.weight[msk]) 

573 wgts /= np.sum(wgts) 

574 

575 # finish coadding 

576 image = np.zeros_like(mbobs[shear_bands[0]][0].image) 

577 weight = np.zeros_like(mbobs[shear_bands[0]][0].weight) 

578 mfrac = np.zeros_like(mbobs[shear_bands[0]][0].image) 

579 nmfrac = 0 

580 noise = np.zeros_like(mbobs[shear_bands[0]][0].image) 

581 nnoise = 0 

582 bmask = np.zeros_like(mbobs[shear_bands[0]][0].image, dtype=np.int32) 

583 nbmask = 0 

584 ormask = np.zeros_like(mbobs[shear_bands[0]][0].image, dtype=np.int32) 

585 normask = 0 

586 meta = {} 

587 psf_meta = {} 

588 psf_image = np.zeros_like(mbobs[shear_bands[0]][0].psf.image) 

589 

590 for i, shear_band in enumerate(shear_bands): 

591 obs = mbobs[shear_band][0] 

592 wgt = wgts[i] 

593 

594 meta.update(obs.meta) 

595 meta.update(mbobs[shear_band].meta) 

596 psf_meta.update(obs.psf.meta) 

597 

598 psf_image += (wgt * obs.psf.image) 

599 

600 image += (wgt * obs.image) 

601 weight += (wgt**2 / obs.weight) 

602 

603 if obs.has_mfrac(): 

604 mfrac += (wgt * obs.mfrac) 

605 nmfrac += 1 

606 

607 if obs.has_noise(): 

608 noise += (wgt * obs.noise) 

609 nnoise += 1 

610 

611 if obs.has_bmask(): 

612 bmask |= obs.bmask 

613 nbmask += 1 

614 

615 if obs.has_ormask(): 

616 ormask |= obs.ormask 

617 normask += 1 

618 

619 weight = 1.0 / weight 

620 msk = ~np.isfinite(weight) 

621 if np.any(msk): 

622 weight[msk] = 0 

623 

624 if np.all(weight == 0): 

625 flags |= procflags.ZERO_WEIGHTS 

626 return None, flags 

627 

628 for var in [nmfrac, nnoise, nbmask, normask]: 

629 if var not in [0, len(shear_bands)]: 

630 flags |= procflags.INCONSISTENT_BANDS 

631 return None, flags 

632 

633 meta.update(mbobs.meta) 

634 

635 # set non-zero weights for PSF 

636 psf_nse = np.sqrt(np.sum(psf_image**2)) / 1000.0 

637 psf_weight = psf_image*0 + 1.0/psf_nse**2 

638 

639 kwargs = { 

640 "weight": weight, 

641 "jacobian": mbobs[shear_bands[0]][0].jacobian, 

642 "psf": ngmix.Observation( 

643 psf_image, 

644 jacobian=mbobs[shear_bands[0]][0].psf.jacobian, 

645 meta=psf_meta, 

646 weight=psf_weight, 

647 ), 

648 "meta": meta, 

649 } 

650 if nmfrac > 0: 

651 kwargs["mfrac"] = mfrac 

652 if nnoise > 0: 

653 kwargs["noise"] = noise 

654 if nbmask > 0: 

655 kwargs["bmask"] = bmask 

656 if normask > 0: 

657 kwargs["ormask"] = ormask 

658 

659 cobs = ngmix.Observation(image, **kwargs) 

660 

661 return cobs, flags 

662 

663 

664def get_coellip_ngauss(name): 

665 ngauss = int(name[7:]) 

666 return ngauss 

667 

668 

669def fit_all_psfs(mbobs, rng): 

670 """ 

671 measure all psfs in the input observations and store the results 

672 in the meta dictionary, and possibly as a gmix for model fits 

673 

674 Parameters 

675 ---------- 

676 mbobs: ngmix.MultiBandObsList 

677 The observations to fit 

678 psf_conf: dict 

679 Config for the measurements/fitting 

680 rng: np.random.RandomState 

681 The random number generator, used for guessers 

682 """ 

683 fitter = ngmix.admom.AdmomFitter(rng=rng) 

684 guesser = ngmix.guessers.GMixPSFGuesser( 

685 rng=rng, ngauss=1, guess_from_moms=True, 

686 ) 

687 

688 runner = ngmix.runners.PSFRunner( 

689 fitter=fitter, guesser=guesser, ntry=10, 

690 ) 

691 

692 for obslist in mbobs: 

693 assert len(obslist) == 1, 'metadetect is not multi-epoch' 

694 

695 obs = obslist[0] 

696 runner.go(obs=obs) 

697 

698 flags = obs.psf.meta['result']['flags'] 

699 if flags != 0: 

700 raise BootPSFFailure("failed to measure psfs: %s" % flags) 

701 

702 

703def fit_mbobs_list_wavg( 

704 *, mbobs_list, fitter, bmask_flags, shear_bands=None, fwhm_reg=0, 

705 symmetrize=True, 

706): 

707 """Fit the ojects in a list of ngmix.MultiBandObsList using a weighted average 

708 over bands. 

709 

710 The fitter is run per-band and then results are combined across bands 

711 via a weighted average. 

712 

713 Parameters 

714 ---------- 

715 mbobs_list : a list of ngmix.MultiBandObsList 

716 The observations to use for shear measurement. 

717 fitter : ngmix fitter 

718 The fitter to use per band per MultiBandObsList. 

719 bmask_flags : int 

720 Observations with these bits set in the bmask are not fit. 

721 shear_bands : list of int, optional 

722 A list of indices into each mbobs that denotes which band is used for shear. 

723 Default is to use all bands. 

724 fwhm_reg : float, optional 

725 This value is converted to T and used to regularize the shapes via 

726 

727 e_{1,2} = M_{1,2}/(T + T_reg) 

728 

729 For Gaussians, this relationship is equivalent to smoothing by a round 

730 Gaussian with FWHM `fwhm_reg`. 

731 symmetrize : bool, optional 

732 If True, apply 4-fold symmetry to the mask+weight map. Default is True. 

733 

734 Returns 

735 ------- 

736 res : np.ndarray 

737 A structured array of the fitting results. 

738 """ 

739 res = [] 

740 for i, mbobs in enumerate(mbobs_list): 

741 

742 _res = fit_mbobs_wavg( 

743 mbobs=mbobs, 

744 fitter=fitter, 

745 bmask_flags=bmask_flags, 

746 shear_bands=shear_bands, 

747 fwhm_reg=fwhm_reg, 

748 symmetrize=symmetrize, 

749 ) 

750 res.append(_res) 

751 

752 if len(res) > 0: 

753 return np.hstack(res) 

754 else: 

755 return None 

756 

757 

758def fit_mbobs_wavg( 

759 *, 

760 mbobs, 

761 fitter, 

762 bmask_flags, 

763 shear_bands=None, 

764 fwhm_reg=0, 

765 symmetrize=True, 

766): 

767 """Fit the object in the ngmix.MultiBandObsList using a weighted average 

768 over bands. 

769 

770 The fitter is run per-band and then results are combined across bands 

771 via a weighted average. 

772 

773 Parameters 

774 ---------- 

775 mbobs : ngmix.MultiBandObsList 

776 The observation to use for shear measurement. 

777 fitter : ngmix fitter 

778 The fitter to use per band. 

779 bmask_flags : int 

780 Observations with these bits set in the bmask are not fit. 

781 shear_bands : list of int, optional 

782 A list of indices into each mbobs that denotes which band is used for shear. 

783 Default is to use all bands. 

784 fwhm_reg : float, optional 

785 This value is converted to T and used to regularize the shapes via 

786 

787 e_{1,2} = M_{1,2}/(T + T_reg) 

788 

789 For Gaussians, this relationship is equivalent to smoothing by a round 

790 Gaussian with FWHM `fwhm_reg`. 

791 symmetrize : bool, optional 

792 If True, apply 4-fold symmetry to the mask+weight map. Default is True. 

793 

794 Returns 

795 ------- 

796 res : np.ndarray 

797 A structured array of the fitting results. 

798 """ 

799 nband = len(mbobs) 

800 all_res = [] 

801 all_psf_res = [] 

802 all_is_shear_band = [] 

803 all_wgts = [] 

804 all_flags = [] 

805 

806 if shear_bands is None: 

807 shear_bands = list(range(nband)) 

808 

809 if fitter.kind == 'am': 

810 assert len(mbobs) == 1, 'Use only one band for adaptive moments' 

811 

812 for band in range(nband): 

813 all_is_shear_band.append(True if band in shear_bands else False) 

814 fres = _fit_obslist( 

815 obslist=mbobs[band], 

816 fitter=fitter, 

817 bmask_flags=bmask_flags, 

818 symmetrize=symmetrize, 

819 ) 

820 all_wgts.append(fres["wgt"]) 

821 all_res.append(fres["obj_res"]) 

822 all_psf_res.append(fres["psf_res"]) 

823 all_flags.append(fres["flags"]) 

824 

825 return _combine_fit_results_wavg( 

826 all_res=all_res, 

827 all_psf_res=all_psf_res, 

828 all_is_shear_band=all_is_shear_band, 

829 all_wgts=all_wgts, 

830 model=fitter.kind, 

831 all_flags=all_flags, 

832 shear_bands=shear_bands, 

833 fwhm_reg=fwhm_reg, 

834 ) 

835 

836 

837def _fit_obslist( 

838 *, 

839 obslist, 

840 fitter, 

841 bmask_flags, 

842 symmetrize, 

843): 

844 if len(obslist) == 0: 

845 # we will flag this later 

846 res = {} 

847 res["flags"] = 0 

848 res["flags"] |= procflags.MISSING_BAND 

849 res["wgt"] = 0 

850 res["obj_res"] = None 

851 res["psf_res"] = None 

852 return res 

853 else: 

854 return _fit_obs( 

855 obs=obslist[0], 

856 fitter=fitter, 

857 bmask_flags=bmask_flags, 

858 symmetrize=symmetrize, 

859 ) 

860 

861 

862def _fit_obs( 

863 *, 

864 obs, 

865 fitter, 

866 bmask_flags, 

867 symmetrize, 

868): 

869 if isinstance(fitter, ngmix.prepsfmom.PrePSFMom): 

870 psf_go_kwargs = {"no_psf": True} 

871 else: 

872 psf_go_kwargs = {} 

873 

874 res = {} 

875 flags = 0 

876 

877 if symmetrize: 

878 obs = symmetrize_obs_weights(obs) 

879 

880 if not np.any(obs.weight > 0): 

881 flags |= procflags.ZERO_WEIGHTS 

882 

883 if np.any((obs.bmask & bmask_flags) != 0): 

884 flags |= procflags.EDGE_HIT 

885 

886 if flags != 0: 

887 # we will flag this later 

888 res["flags"] = flags 

889 res["wgt"] = 0 

890 res["obj_res"] = None 

891 res["psf_res"] = None 

892 else: 

893 res["flags"] = flags 

894 res["wgt"] = np.median(obs.weight[obs.weight > 0]) 

895 res["obj_res"] = fitter.go(obs) 

896 res["psf_res"] = fitter.go(obs.psf, **psf_go_kwargs) 

897 

898 if fitter.kind == "am" and MOMNAME == "mom": 

899 res["obj_res"]["mom"] = res["obj_res"]["sums"] 

900 res["obj_res"]["mom_cov"] = res["obj_res"]["sums_cov"] 

901 res["psf_res"]["mom"] = res["psf_res"]["sums"] 

902 res["psf_res"]["mom_cov"] = res["psf_res"]["sums_cov"] 

903 

904 if res["obj_res"]["flags"] != 0: 

905 logger.debug("per band fitter failed: %s" % res["obj_res"]['flagstr']) 

906 

907 if res["psf_res"]["flags"] != 0: 

908 logger.debug("per band psf fitter failed: %s" % res["psf_res"]['flagstr']) 

909 

910 return res 

911 

912 

913def _sum_bands_wavg( 

914 *, all_res, all_is_shear_band, all_wgts, all_flags, all_wgt_res, 

915): 

916 """A function to sum all of the moments across different bands for combined moments 

917 estimates. 

918 

919 This function sums the moments together using the entires from `all_wgts`. 

920 

921 raw_mom = \\sum_i all_wgts[i] * all_res[i]["sums"] 

922 

923 If `all_wgt_res` is given, then the weights are adjust to include the ratio of the 

924 flux moments between `all_wgt_res` and `all_res`: 

925 

926 all_wgts[i] -> all_wgts[i] * all_wgt_res[i]["sums"][5] / all_res[i]["sums"][5] 

927 

928 If `mom_norm` is in the results dicts in `all_res` and `all_wgt_res`, then the 

929 moments fields `mom` are divided by this normalization before being averaged. 

930 

931 The fluxes are summed without any additional weighting beyond the entires in 

932 `all_wgts` 

933 

934 flux = \\sum_i all_wgts[i] * all_res[i]["flux"] 

935 

936 

937 Similar sums are carried out for the the moments covariance and the flux variance 

938 with the approprtiate weights squared. 

939 

940 Parameters 

941 ---------- 

942 all_res : list of dicts 

943 List of the moments fit results from each band. 

944 all_is_shear_band : list of bool 

945 List of bools indicating if a given band is to be summed into the outputs and 

946 thus used for shear. 

947 all_wgts : list of floats 

948 List of floats giving the total weight for each band. 

949 all_flags : list of ints 

950 List of ints indicating if any of the input bands are flagged for some reason. 

951 These flags are ORed into the final_flags outputs. 

952 all_wgt_res : list of dicts, optional 

953 If not None, then additional weighting by the flux moment ratio of 

954 `all_wgt_res[i]["sums"][5] / all_res[i]["sums"][5]` is applied to the weighting 

955 for the moments sums. This field is used to weight the PSF moment sums by 

956 the fluxes of the objects so that the band-averaged sizes of stars match the 

957 band-averaged PSF size. 

958 

959 Returns 

960 ------- 

961 res : dict 

962 A dictionary with the following keys: 

963 

964 raw_mom : np.ndarray 

965 The array of summed moments. You need to divide by `wgt_sum` to 

966 normalize the moments properly. 

967 raw_mom_cov : np.ndarray 

968 The covariance of the summed moments. You need to divide by 

969 `wgt_sum**2` to normalize the moments properly. 

970 wgt_sum : float 

971 The sum of the weights used in the moments sum. To get final moments, 

972 divide `raw_mom` by `wgt_sum` and `raw_mom_cov` by `wgt_sum**2`. 

973 final_flags : int 

974 A final set of bit flags for the summed moments. This field marks 

975 missing bands, missing moments, or zero weights. It is also ORed 

976 with all of the input `all_flags` entires. 

977 used_shear_bands : list of bool 

978 A list of which bands were used for shear. This field can be used to 

979 track if two successive calls to this function with slightly different 

980 inputs end up using the same bands. 

981 flux : float 

982 The total flux. You must divide by `wgt_sum` to get the output 

983 flux. 

984 flux_var : float 

985 The variance in the total flux. You must divide by `wgt_sum**2` 

986 to get the output flux variance. 

987 """ 

988 tot_nband = len(all_res) 

989 raw_mom = np.zeros(6, dtype=np.float64) 

990 raw_mom_cov = np.zeros((6, 6), dtype=np.float64) 

991 wgt_sum = 0.0 

992 used_shear_bands = [False] * tot_nband 

993 final_flags = 0 

994 flux = 0.0 

995 flux_var = 0.0 

996 

997 for iband, (wgt, res, issb, flags) in enumerate(zip( 

998 all_wgts, all_res, all_is_shear_band, all_flags 

999 )): 

1000 if all_wgt_res is not None: 

1001 wgt_res = all_wgt_res[iband] 

1002 else: 

1003 wgt_res = None 

1004 

1005 if issb: 

1006 # the input flags mark very basic failures and are ORed across all bands 

1007 # these are things like missing and or all zero-weight data, edges, etc. 

1008 final_flags |= flags 

1009 

1010 # we mark missing data or moments for PSF and objects separately 

1011 if res is None or (all_wgt_res is not None and wgt_res is None): 

1012 final_flags |= procflags.MISSING_BAND 

1013 elif ( 

1014 (MOMNAME not in res or MOMNAME+"_cov" not in res) 

1015 or ( 

1016 all_wgt_res is not None and wgt_res is not None and ( 

1017 MOMNAME not in wgt_res or MOMNAME+"_cov" not in wgt_res 

1018 ) 

1019 ) 

1020 ): 

1021 final_flags |= procflags.NOMOMENTS_FAILURE 

1022 

1023 if ( 

1024 res is not None 

1025 and MOMNAME+"_norm" in res 

1026 and np.isfinite(res[MOMNAME+"_norm"]) 

1027 ): 

1028 mom_norm = res[MOMNAME+"_norm"] 

1029 else: 

1030 mom_norm = 1.0 

1031 

1032 if ( 

1033 all_wgt_res is not None 

1034 and wgt_res is not None 

1035 and res is not None 

1036 and MOMNAME in res 

1037 and MOMNAME in wgt_res 

1038 ): 

1039 if res[MOMNAME][5] != 0: 

1040 if ( 

1041 MOMNAME+"_norm" in res 

1042 and np.isfinite(res[MOMNAME+"_norm"]) 

1043 and MOMNAME+"_norm" in wgt_res 

1044 and np.isfinite(wgt_res[MOMNAME+"_norm"]) 

1045 ): 

1046 if wgt_res[MOMNAME+"_norm"] == 0: 

1047 flux_mom_ratio = 1.0 

1048 final_flags |= procflags.ZERO_WEIGHTS 

1049 else: 

1050 flux_mom_ratio = ( 

1051 wgt_res[MOMNAME][5] 

1052 / wgt_res[MOMNAME+"_norm"] 

1053 / res[MOMNAME][5] 

1054 * res[MOMNAME+"_norm"] 

1055 ) 

1056 else: 

1057 flux_mom_ratio = wgt_res[MOMNAME][5] / res[MOMNAME][5] 

1058 else: 

1059 flux_mom_ratio = 1.0 

1060 final_flags |= procflags.ZERO_WEIGHTS 

1061 else: 

1062 # we do not flag zero weight here since this is a missing band 

1063 # or missign moments or no flux weighting 

1064 flux_mom_ratio = 1.0 

1065 

1066 if wgt <= 0: 

1067 final_flags |= procflags.ZERO_WEIGHTS 

1068 

1069 if res is not None and MOMNAME in res and MOMNAME+"_cov" in res: 

1070 flux += (wgt * res[MOMNAME][5]) 

1071 flux_var += (wgt**2 * res[MOMNAME+"_cov"][5, 5]) 

1072 

1073 # there are a few factors here 

1074 # wgt - the averaging weight for the moments 

1075 # flux_mom_ratio - the ratio of the flux in wgt_res to res 

1076 # This factor used to properly weight PSF model averages for objects 

1077 # since the sums are actually things like flux * T. So for the PSF T 

1078 # we actually average 

1079 # flux_mom_ratio * sums[4] = (object flux/psf flux) * (psf_flux * T) 

1080 # since sums[4] = flux * T. 

1081 # mom_norm - sum of the moments weight function 

1082 # This factor removes the moments dependence on area of the stamp. 

1083 # The flux averages above do not get these factors since we want the 

1084 # weight function to peak at 1 for flux measurements (and only care 

1085 # about the object). 

1086 

1087 # we added an extra sum for AM, for rho4 

1088 rmoms = res[MOMNAME][:raw_mom.size] 

1089 rcovs = res[MOMNAME + '_cov'][:raw_mom.size, :raw_mom.size] 

1090 

1091 raw_mom += (wgt * flux_mom_ratio / mom_norm * rmoms) 

1092 raw_mom_cov += ( 

1093 (wgt * flux_mom_ratio / mom_norm)**2 

1094 * rcovs 

1095 ) 

1096 wgt_sum += wgt 

1097 

1098 used_shear_bands[iband] = True 

1099 

1100 # make sure we flag missing data or all zero weight sums 

1101 if sum(used_shear_bands) > 0 and wgt_sum <= 0: 

1102 final_flags |= procflags.ZERO_WEIGHTS 

1103 

1104 return dict( 

1105 raw_mom=raw_mom, 

1106 raw_mom_cov=raw_mom_cov, 

1107 wgt_sum=wgt_sum, 

1108 final_flags=final_flags, 

1109 used_shear_bands=used_shear_bands, 

1110 flux=flux, 

1111 flux_var=flux_var, 

1112 ) 

1113 

1114 

1115def _make_mom_res(*, raw_mom, raw_mom_cov, raw_flux, raw_flux_var, fwhm_reg): 

1116 if fwhm_reg > 0: 

1117 momres_t = make_mom_result(raw_mom, raw_mom_cov) 

1118 

1119 T_reg = fwhm_to_T(fwhm_reg) 

1120 

1121 # the moments are not normalized and are sums, so convert T_reg to a sum using 

1122 # the flux sum first via T_reg -> T_reg * raw_mom[5] 

1123 amat = np.eye(6) 

1124 amat[4, 5] = T_reg 

1125 

1126 raw_mom_orig = raw_mom.copy() 

1127 if np.isnan(raw_mom_orig[0]): 

1128 raw_mom[0] = 0 

1129 if np.isnan(raw_mom_orig[1]): 

1130 raw_mom[1] = 0 

1131 reg_mom = np.dot(amat, raw_mom) 

1132 if np.isnan(raw_mom_orig[0]): 

1133 raw_mom[0] = np.nan 

1134 reg_mom[0] = np.nan 

1135 if np.isnan(raw_mom_orig[1]): 

1136 raw_mom[1] = np.nan 

1137 reg_mom[1] = np.nan 

1138 

1139 reg_mom_cov = np.dot(amat, np.dot(raw_mom_cov, amat.T)) 

1140 momres = make_mom_result(reg_mom, reg_mom_cov) 

1141 

1142 # use old T 

1143 for col in ["T", "T_err", "T_flags", "T_flagstr"]: 

1144 momres[col] = momres_t[col] 

1145 momres["flags"] |= momres_t["flags"] 

1146 else: 

1147 momres = make_mom_result(raw_mom, raw_mom_cov) 

1148 

1149 momres["flux"] = raw_flux 

1150 if momres["flux"] <= 0: 

1151 momres["flags"] |= ngmix.flags.NONPOS_FLUX 

1152 

1153 if raw_flux_var > 0: 

1154 momres["flux_err"] = np.sqrt(raw_flux_var) 

1155 momres["s2n"] = momres["flux"] / momres["flux_err"] 

1156 else: 

1157 momres["flux_flags"] |= ngmix.flags.NONPOS_VAR 

1158 momres["flux_err"] = np.nan 

1159 momres["s2n"] = np.nan 

1160 momres["flags"] |= ngmix.flags.NONPOS_VAR 

1161 

1162 momres["flux_flagstr"] = procflags.get_procflags_str(momres["flux_flags"]) 

1163 momres["flagstr"] = procflags.get_procflags_str(momres["flags"]) 

1164 

1165 return momres 

1166 

1167 

1168def _combine_fit_results_wavg( 

1169 *, all_res, all_psf_res, all_is_shear_band, all_wgts, model, all_flags, shear_bands, 

1170 fwhm_reg, 

1171): 

1172 tot_nband = len(all_res) 

1173 nband = ( 

1174 sum(1 if issb else 0 for issb in all_is_shear_band) 

1175 if all_is_shear_band 

1176 else 0 

1177 ) 

1178 nonshear_nband = ( 

1179 sum(0 if issb else 1 for issb in all_is_shear_band) 

1180 if all_is_shear_band 

1181 else 0 

1182 ) 

1183 blens = [ 

1184 len(all_res), len(all_psf_res), len(all_is_shear_band), 

1185 len(all_wgts), len(all_flags) 

1186 ] 

1187 

1188 n = Namer(front=model) 

1189 

1190 data = get_wavg_output_struct(tot_nband, model, shear_bands=shear_bands) 

1191 

1192 if ( 

1193 tot_nband == 0 

1194 or nband == 0 

1195 or tot_nband != nband + nonshear_nband 

1196 or not all(b == tot_nband for b in blens) 

1197 ): 

1198 if nband == 0 or tot_nband == 0: 

1199 psf_flags = procflags.MISSING_BAND 

1200 mdet_flags = procflags.MISSING_BAND 

1201 flux_flags = [procflags.MISSING_BAND] * tot_nband 

1202 else: 

1203 psf_flags = procflags.INCONSISTENT_BANDS 

1204 mdet_flags = procflags.INCONSISTENT_BANDS 

1205 flux_flags = [procflags.INCONSISTENT_BANDS] * tot_nband 

1206 band_flux = [np.nan] * tot_nband 

1207 band_flux_err = [np.nan] * tot_nband 

1208 else: 

1209 sum_data = _sum_bands_wavg( 

1210 all_res=all_res, 

1211 all_is_shear_band=all_is_shear_band, 

1212 all_wgts=all_wgts, 

1213 all_flags=all_flags, 

1214 all_wgt_res=None, 

1215 ) 

1216 mdet_flags = copy.copy(sum_data["final_flags"]) 

1217 

1218 psf_sum_data = _sum_bands_wavg( 

1219 all_res=all_psf_res, 

1220 all_is_shear_band=all_is_shear_band, 

1221 all_wgts=all_wgts, 

1222 all_flags=all_flags, 

1223 all_wgt_res=all_res, 

1224 ) 

1225 psf_flags = copy.copy(psf_sum_data["final_flags"]) 

1226 

1227 if ( 

1228 sum_data["final_flags"] == 0 

1229 and psf_sum_data["final_flags"] == 0 

1230 and sum_data["used_shear_bands"] != psf_sum_data["used_shear_bands"] 

1231 ): 

1232 psf_flags |= procflags.INCONSISTENT_BANDS 

1233 mdet_flags |= procflags.INCONSISTENT_BANDS 

1234 

1235 band_flux = [] 

1236 band_flux_err = [] 

1237 flux_flags = [] 

1238 for gres, flags in zip(all_res, all_flags): 

1239 # the input flags mark very basic failures and are ORed across all bands 

1240 # these are things like missing and or all zero-weight data, edges, etc. 

1241 _flux_flags = 0 

1242 _flux_flags |= flags 

1243 

1244 # an object fit missing in any band is bad too 

1245 if gres is None: 

1246 _flux_flags |= procflags.MISSING_BAND 

1247 band_flux.append(np.nan) 

1248 band_flux_err.append(np.nan) 

1249 elif ( 

1250 "flux" not in gres 

1251 or "flux_err" not in gres 

1252 or "flux_flags" not in gres 

1253 ): 

1254 _flux_flags |= procflags.NOMOMENTS_FAILURE 

1255 band_flux.append(np.nan) 

1256 band_flux_err.append(np.nan) 

1257 else: 

1258 _flux_flags |= gres["flux_flags"] 

1259 band_flux.append(gres['flux']) 

1260 band_flux_err.append(gres['flux_err']) 

1261 flux_flags.append(_flux_flags) 

1262 

1263 if psf_flags == 0: 

1264 psf_raw_mom = psf_sum_data["raw_mom"] / psf_sum_data["wgt_sum"] 

1265 psf_raw_mom_cov = psf_sum_data["raw_mom_cov"] / (psf_sum_data["wgt_sum"]**2) 

1266 psf_raw_flux = psf_sum_data["flux"] / psf_sum_data["wgt_sum"] 

1267 psf_raw_flux_var = psf_sum_data["flux_var"] / (psf_sum_data["wgt_sum"]**2) 

1268 

1269 psf_momres = _make_mom_res( 

1270 raw_mom=psf_raw_mom, 

1271 raw_mom_cov=psf_raw_mom_cov, 

1272 raw_flux=psf_raw_flux, 

1273 raw_flux_var=psf_raw_flux_var, 

1274 fwhm_reg=0, 

1275 ) 

1276 

1277 psf_flags |= psf_momres["flags"] 

1278 data[n("psf_g")] = psf_momres['e'] 

1279 data[n("psf_T")] = psf_momres['T'] 

1280 

1281 if mdet_flags == 0: 

1282 raw_mom = sum_data["raw_mom"] / sum_data["wgt_sum"] 

1283 raw_mom_cov = sum_data["raw_mom_cov"] / (sum_data["wgt_sum"]**2) 

1284 raw_flux = sum_data["flux"] / sum_data["wgt_sum"] 

1285 raw_flux_var = sum_data["flux_var"] / (sum_data["wgt_sum"]**2) 

1286 

1287 momres = _make_mom_res( 

1288 raw_mom=raw_mom, 

1289 raw_mom_cov=raw_mom_cov, 

1290 raw_flux=raw_flux, 

1291 raw_flux_var=raw_flux_var, 

1292 fwhm_reg=fwhm_reg, 

1293 ) 

1294 

1295 mdet_flags |= momres["flags"] 

1296 for col in ['s2n', 'T', 'T_err', 'T_flags']: 

1297 data[n(col)] = momres[col] 

1298 for col in ['e', 'e_cov']: 

1299 data[n(col.replace('e', 'g'))] = momres[col] 

1300 if psf_flags == 0: 

1301 data[n('T_ratio')] = data[n('T')] / data[n('psf_T')] 

1302 

1303 if psf_flags != 0: 

1304 mdet_flags |= procflags.PSF_FAILURE 

1305 

1306 if tot_nband > 1: 

1307 data[n('band_flux')] = np.array(band_flux) 

1308 data[n('band_flux_err')] = np.array(band_flux_err) 

1309 data[n('band_flux_flags')] = np.array(flux_flags) 

1310 elif tot_nband == 1: 

1311 data[n('band_flux')] = band_flux[0] 

1312 data[n('band_flux_err')] = band_flux_err[0] 

1313 data[n('band_flux_flags')] = flux_flags[0] 

1314 else: 

1315 data[n('band_flux_flags')] = procflags.MISSING_BAND 

1316 

1317 # now we set the flags as they would have been set in our moments code 

1318 # any PSF failure in a shear band causes a non-zero flags value 

1319 data[n('psf_flags')] = psf_flags 

1320 data[n('obj_flags')] = mdet_flags 

1321 all_flags = mdet_flags 

1322 for f in flux_flags: 

1323 all_flags |= f 

1324 data[n('flags')] = all_flags 

1325 

1326 if data[n('flags')] != 0: 

1327 logger.debug( 

1328 "fitter failed: flags = %s", 

1329 procflags.get_procflags_str(data[n("flags")]) 

1330 ) 

1331 

1332 return data 

1333 

1334 

1335def get_wavg_output_struct(nband, model, shear_bands=None): 

1336 """ 

1337 make an output struct with default values set 

1338 

1339 flags and psf_flags are set to NO_ATTEMPT. The float 

1340 fields are set to np.nan 

1341 

1342 Parameters 

1343 ---------- 

1344 nband: int 

1345 Number of bands 

1346 model: str 

1347 The model or "kind" of fitter 

1348 shear_bands : list of int, optional 

1349 A list of indices into each mbobs that denotes which band is used for shear. 

1350 If given, these are added to the output as a shear_bands field. If not given, 

1351 this field is left empty. 

1352 

1353 Returns 

1354 ------- 

1355 ndarray with fields 

1356 """ 

1357 dt = _make_combine_fit_results_wavg_dtype( 

1358 nband=nband, model=model, shear_bands=shear_bands 

1359 ) 

1360 data = np.zeros(1, dtype=dt) 

1361 

1362 n = Namer(front=model) 

1363 data[n('flags')] = procflags.NO_ATTEMPT 

1364 data[n('psf_flags')] = procflags.NO_ATTEMPT 

1365 data[n("obj_flags")] = procflags.NO_ATTEMPT 

1366 data[n("T_flags")] = procflags.NO_ATTEMPT 

1367 data[n("band_flux_flags")] = procflags.NO_ATTEMPT 

1368 

1369 for name in data.dtype.names: 

1370 if "flags" not in name: 

1371 # all are float except flags 

1372 data[name] = np.nan 

1373 

1374 if shear_bands is not None: 

1375 assert len(shear_bands) <= MAX_NUM_SHEAR_BANDS 

1376 data["shear_bands"] = "".join("%s" % b for b in sorted(shear_bands)) 

1377 

1378 return data 

1379 

1380 

1381def _make_combine_fit_results_wavg_dtype(nband, model, shear_bands): 

1382 n = Namer(front=model) 

1383 dt = [ 

1384 (n("flags"), 'i4'), 

1385 (n('psf_flags'), 'i4'), 

1386 (n('psf_g'), 'f8', 2), 

1387 (n('psf_T'), 'f8'), 

1388 (n("obj_flags"), 'i4'), 

1389 (n("s2n"), "f8"), 

1390 (n("g"), "f8", 2), 

1391 (n("g_cov"), "f8", (2, 2)), 

1392 (n("T"), "f8"), 

1393 (n("T_flags"), "i4"), 

1394 (n("T_err"), "f8"), 

1395 (n("T_ratio"), "f8"), 

1396 ] 

1397 if nband > 1: 

1398 dt += [ 

1399 (n("band_flux_flags"), 'i4', nband), 

1400 (n("band_flux"), "f8", nband), 

1401 (n("band_flux_err"), "f8", nband), 

1402 ] 

1403 else: 

1404 dt += [ 

1405 (n("band_flux_flags"), 'i4'), 

1406 (n("band_flux"), "f8"), 

1407 (n("band_flux_err"), "f8"), 

1408 ] 

1409 

1410 dt += [("shear_bands", "U%d" % MAX_NUM_SHEAR_BANDS)] 

1411 

1412 return dt 

1413 

1414 

1415def symmetrize_obs_weights(obs): 

1416 """Applies 4-fold symmetry to zero weight pixels in an observation. 

1417 

1418 Parameters 

1419 ---------- 

1420 obs : ngmix.Observation 

1421 The observation to symmetrize. 

1422 

1423 Returns 

1424 ------- 

1425 sym_obs : ngmix.Observation 

1426 A copy of the input observation with a symmetrized weight map. 

1427 """ 

1428 sym_obs = obs.copy() 

1429 if np.any(obs.weight <= 0): 

1430 new_wgt = obs.weight.copy() 

1431 for k in [1, 2, 3]: 

1432 msk = np.rot90(obs.weight, k=k) <= 0 

1433 new_wgt[msk] = 0 

1434 

1435 with sym_obs.writeable(): 

1436 if not np.any(new_wgt > 0): 

1437 sym_obs.ignore_zero_weight = False 

1438 sym_obs.weight[:, :] = new_wgt 

1439 

1440 return sym_obs 

1441 

1442 

1443def combine_fit_res(all_res): 

1444 """Combine fit result data structures. 

1445 

1446 Parameters 

1447 ---------- 

1448 all_res : list of np.ndarray 

1449 The list of structured array results. 

1450 

1451 Returns 

1452 ------- 

1453 res : np.ndarray 

1454 The combined results list. 

1455 """ 

1456 

1457 if len(all_res) == 1: 

1458 return all_res[0] 

1459 

1460 dupe_cols = [ 

1461 "shear_bands", 

1462 ] 

1463 

1464 nobj = None 

1465 dt = [] 

1466 for _res in all_res: 

1467 if _res is not None: 

1468 if nobj is None: 

1469 nobj = _res.shape[0] 

1470 else: 

1471 if nobj != _res.shape[0]: 

1472 raise RuntimeError("All fit results must be the same length!") 

1473 

1474 if len(dt) == 0: 

1475 dt.extend(_res.dtype.descr) 

1476 else: 

1477 for descr in _res.dtype.descr: 

1478 if descr[0] not in dupe_cols: 

1479 dt.append(descr) 

1480 else: 

1481 if nobj is None: 

1482 nobj = 0 

1483 else: 

1484 if nobj != 0: 

1485 raise RuntimeError( 

1486 "All fit results must zero length if one is None!" 

1487 ) 

1488 

1489 if nobj > 0: 

1490 res = np.zeros(nobj, dtype=dt) 

1491 for i, _res in enumerate(all_res): 

1492 for col in _res.dtype.names: 

1493 if col in dupe_cols: 

1494 if i > 0: 

1495 if not np.array_equal(res[col], _res[col]): 

1496 raise RuntimeError( 

1497 "Inconsistent column values " 

1498 "for %s when combining results!" % col 

1499 ) 

1500 else: 

1501 res[col] = _res[col] 

1502 else: 

1503 res[col] = _res[col] 

1504 

1505 return res 

1506 else: 

1507 if any(_res is not None for _res in all_res): 

1508 return np.zeros(0, dtype=dt) 

1509 else: 

1510 return None