Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

# 

# LSST Data Management System 

# Copyright 2008-2016 AURA/LSST. 

# 

# This product includes software developed by the 

# LSST Project (http://www.lsst.org/). 

# 

# This program is free software: you can redistribute it and/or modify 

# it under the terms of the GNU General Public License as published by 

# the Free Software Foundation, either version 3 of the License, or 

# (at your option) any later version. 

# 

# This program is distributed in the hope that it will be useful, 

# but WITHOUT ANY WARRANTY; without even the implied warranty of 

# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

# GNU General Public License for more details. 

# 

# You should have received a copy of the LSST License Statement and 

# the GNU General Public License along with this program. If not, 

# see <https://www.lsstcorp.org/LegalNotices/>. 

# 

 

 

import numpy as np 

 

import lsst.afw.geom as afwGeom 

import lsst.afw.image as afwImage 

import lsst.pex.config as pexConfig 

from lsst.log import Log 

import lsst.meas.deblender.baseline as deblendBaseline 

from lsst.meas.base.pluginRegistry import register 

from lsst.meas.base import SingleFrameMeasurementTask, SingleFrameMeasurementConfig, \ 

SingleFramePluginConfig, SingleFramePlugin 

import lsst.afw.display.ds9 as ds9 

 

__all__ = ("DipoleMeasurementConfig", "DipoleMeasurementTask", "DipoleAnalysis", "DipoleDeblender", 

"SourceFlagChecker", "ClassificationDipoleConfig", "ClassificationDipolePlugin") 

 

 

class ClassificationDipoleConfig(SingleFramePluginConfig): 

"""Configuration for classification of detected diaSources as dipole or not""" 

minSn = pexConfig.Field( 

doc="Minimum quadrature sum of positive+negative lobe S/N to be considered a dipole", 

dtype=float, default=np.sqrt(2) * 5.0, 

) 

maxFluxRatio = pexConfig.Field( 

doc="Maximum flux ratio in either lobe to be considered a dipole", 

dtype=float, default=0.65 

) 

 

 

@register("ip_diffim_ClassificationDipole") 

class ClassificationDipolePlugin(SingleFramePlugin): 

"""A plugin to classify whether a diaSource is a dipole. 

""" 

 

ConfigClass = ClassificationDipoleConfig 

 

@classmethod 

def getExecutionOrder(cls): 

return cls.APCORR_ORDER 

 

def __init__(self, config, name, schema, metadata): 

SingleFramePlugin.__init__(self, config, name, schema, metadata) 

self.dipoleAnalysis = DipoleAnalysis() 

self.keyProbability = schema.addField(name + "_value", type="D", 

doc="Set to 1 for dipoles, else 0.") 

self.keyFlag = schema.addField(name + "_flag", type="Flag", doc="Set to 1 for any fatal failure.") 

 

def measure(self, measRecord, exposure): 

passesSn = self.dipoleAnalysis.getSn(measRecord) > self.config.minSn 

negFlux = np.abs(measRecord.get("ip_diffim_PsfDipoleFlux_neg_flux")) 

negFluxFlag = measRecord.get("ip_diffim_PsfDipoleFlux_neg_flag") 

posFlux = np.abs(measRecord.get("ip_diffim_PsfDipoleFlux_pos_flux")) 

posFluxFlag = measRecord.get("ip_diffim_PsfDipoleFlux_pos_flag") 

 

77 ↛ 78line 77 didn't jump to line 78, because the condition on line 77 was never true if negFluxFlag or posFluxFlag: 

self.fail(measRecord) 

# continue on to classify 

 

totalFlux = negFlux + posFlux 

 

# If negFlux or posFlux are NaN, these evaluate to False 

passesFluxNeg = (negFlux / totalFlux) < self.config.maxFluxRatio 

passesFluxPos = (posFlux / totalFlux) < self.config.maxFluxRatio 

86 ↛ 89line 86 didn't jump to line 89, because the condition on line 86 was never false if (passesSn and passesFluxPos and passesFluxNeg): 

val = 1.0 

else: 

val = 0.0 

 

measRecord.set(self.keyProbability, val) 

 

def fail(self, measRecord, error=None): 

measRecord.set(self.keyFlag, True) 

 

 

class DipoleMeasurementConfig(SingleFrameMeasurementConfig): 

"""!Measurement of detected diaSources as dipoles""" 

 

def setDefaults(self): 

SingleFrameMeasurementConfig.setDefaults(self) 

self.plugins = ["base_CircularApertureFlux", 

"base_PixelFlags", 

"base_SkyCoord", 

"base_PsfFlux", 

"ip_diffim_NaiveDipoleCentroid", 

"ip_diffim_NaiveDipoleFlux", 

"ip_diffim_PsfDipoleFlux", 

"ip_diffim_ClassificationDipole", 

] 

 

self.slots.calibFlux = None 

self.slots.modelFlux = None 

self.slots.instFlux = None 

self.slots.shape = None 

self.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 

self.doReplaceWithNoise = False 

 

## @addtogroup LSST_task_documentation 

## @{ 

## @page DipoleMeasurementTask 

## @ref DipoleMeasurementTask_ "DipoleMeasurementTask" 

## @copybrief DipoleMeasurementTask 

## @} 

 

 

class DipoleMeasurementTask(SingleFrameMeasurementTask): 

"""! 

@anchor DipoleMeasurementTask_ 

 

@brief Measurement of Sources, specifically ones from difference images, for characterization as dipoles 

 

@section ip_diffim_dipolemeas_Contents Contents 

 

- @ref ip_diffim_dipolemeas_Purpose 

- @ref ip_diffim_dipolemeas_Initialize 

- @ref ip_diffim_dipolemeas_IO 

- @ref ip_diffim_dipolemeas_Config 

- @ref ip_diffim_dipolemeas_Metadata 

- @ref ip_diffim_dipolemeas_Debug 

- @ref ip_diffim_dipolemeas_Example 

 

#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

 

@section ip_diffim_dipolemeas_Purpose Description 

 

This class provides a default configuration for running Source measurement on image differences. 

 

These default plugins include: 

@dontinclude dipoleMeasurement.py 

@skip class DipoleMeasurementConfig 

@until self.doReplaceWithNoise 

 

These plugins enabled by default allow the user to test the hypothesis that the Source is a dipole. 

This includes a set of measurements derived from intermediate base classes 

DipoleCentroidAlgorithm and DipoleFluxAlgorithm. Their respective algorithm control classes are defined in 

DipoleCentroidControl and DipoleFluxControl. Each centroid and flux measurement will have _neg (negative) 

and _pos (positive lobe) fields. 

 

The first set of measurements uses a "naive" alrogithm for centroid and flux measurements, implemented in 

NaiveDipoleCentroidControl and NaiveDipoleFluxControl. The algorithm uses a naive 3x3 weighted moment around 

the nominal centroids of each peak in the Source Footprint. These algorithms fill the table fields 

ip_diffim_NaiveDipoleCentroid* and ip_diffim_NaiveDipoleFlux* 

 

The second set of measurements undertakes a joint-Psf model on the negative and positive lobe simultaneously. 

This fit simultaneously solves for the negative and positive lobe centroids and fluxes using non-linear 

least squares minimization. The fields are stored in table elements ip_diffim_PsfDipoleFlux*. 

 

Because this Task is just a config for SourceMeasurementTask, the same result may be acheived by manually 

editing the config and running SourceMeasurementTask. For example: 

 

@code 

config = SingleFrameMeasurementConfig() 

config.plugins.names = ["base_PsfFlux", 

"ip_diffim_PsfDipoleFlux", 

"ip_diffim_NaiveDipoleFlux", 

"ip_diffim_NaiveDipoleCentroid", 

"ip_diffim_ClassificationDipole", 

"base_CircularApertureFlux", 

"base_SkyCoord"] 

 

config.slots.calibFlux = None 

config.slots.modelFlux = None 

config.slots.instFlux = None 

config.slots.shape = None 

config.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 

config.doReplaceWithNoise = False 

 

schema = afwTable.SourceTable.makeMinimalSchema() 

task = SingleFrameMeasurementTask(schema, config=config) 

 

task.run(sources, exposure) 

@endcode 

 

 

#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

 

@section ip_diffim_dipolemeas_Initialize Task initialization 

 

@copydoc \_\_init\_\_ 

 

#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

 

@section ip_diffim_dipolemeas_IO Invoking the Task 

 

@copydoc run 

 

#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

 

@section ip_diffim_dipolemeas_Config Configuration parameters 

 

See @ref DipoleMeasurementConfig 

 

#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

 

@section ip_diffim_dipolemeas_Metadata Quantities set in Metadata 

 

No specific values are set in the Task metadata. However, the Source schema are modified to store the 

results of the dipole-specific measurements. 

 

 

#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

 

@section ip_diffim_dipolemeas_Debug Debug variables 

 

The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink interface supports a 

flag @c -d/--debug to import @b debug.py from your @c PYTHONPATH. The relevant contents of debug.py 

for this Task include: 

 

@code{.py} 

import sys 

import lsstDebug 

def DebugInfo(name): 

di = lsstDebug.getInfo(name) 

if name == "lsst.ip.diffim.dipoleMeasurement": 

di.display = True # enable debug output 

di.maskTransparency = 90 # ds9 mask transparency 

di.displayDiaSources = True # show exposure with dipole results 

return di 

lsstDebug.Info = DebugInfo 

lsstDebug.frame = 1 

@endcode 

 

#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

 

@section ip_diffim_dipolemeas_Example A complete example of using DipoleMeasurementTask 

 

This code is dipoleMeasTask.py in the examples directory, and can be run as @em e.g. 

@code 

examples/dipoleMeasTask.py 

examples/dipoleMeasTask.py --debug 

examples/dipoleMeasTask.py --debug --image /path/to/image.fits 

@endcode 

 

@dontinclude dipoleMeasTask.py 

Start the processing by parsing the command line, where the user has the option of enabling debugging output 

and/or sending their own image for demonstration (in case they have not downloaded the afwdata package). 

@skip main 

@until run 

 

@dontinclude dipoleMeasTask.py 

The processing occurs in the run function. We first extract an exposure from disk or afwdata, displaying 

it if requested: 

@skip args 

@until mtv 

 

Create a default source schema that we will append fields to as we add more algorithms: 

@skip makeMinimalSchema 

@until makeMinimalSchema 

 

Create the detection and measurement Tasks, with some minor tweaking of their configs: 

@skip Create 

@until measureTask 

 

Having fully initialied the schema, we create a Source table from it: 

@skip output 

@until SourceTable 

 

Run detection: 

@skip Process 

@until detectionTask 

 

Because we are looking for dipoles, we need to merge the positive and negative detections: 

@skip Merge 

@until numNeg 

 

Finally, perform measurement (both standard and dipole-specialized) on the merged sources: 

@skip measureTask 

@until measureTask 

 

Optionally display debugging information: 

@skip Display 

@until displayDipoles 

#-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- 

 

""" 

ConfigClass = DipoleMeasurementConfig 

_DefaultName = "dipoleMeasurement" 

 

 

######### 

# Other Support classs 

######### 

 

class SourceFlagChecker(object): 

"""!Functor class to check whether a diaSource has flags set that should cause it to be labeled bad.""" 

 

def __init__(self, sources, badFlags=None): 

"""!Constructor 

 

@param sources Sources that will be measured 

@param badFlags A list of flags that will be used to determine if there was a measurement problem 

 

The list of badFlags will be used to make a list of keys to check for measurement flags on. By 

default the centroid keys are added to this list""" 

 

self.badFlags = ['base_PixelFlags_flag_edge', 'base_PixelFlags_flag_interpolatedCenter', 

'base_PixelFlags_flag_saturatedCenter'] 

if badFlags is not None: 

for flag in badFlags: 

self.badFlags.append(flag) 

self.keys = [sources.getSchema().find(name).key for name in self.badFlags] 

self.keys.append(sources.table.getCentroidFlagKey()) 

 

def __call__(self, source): 

"""!Call the source flag checker on a single Source 

 

@param source Source that will be examined""" 

for k in self.keys: 

if source.get(k): 

return False 

return True 

 

 

class DipoleAnalysis(object): 

"""!Functor class that provides (S/N, position, orientation) of measured dipoles""" 

 

def __init__(self): 

"""!Constructor""" 

pass 

 

def __call__(self, source): 

"""!Parse information returned from dipole measurement 

 

@param source The source that will be examined""" 

return self.getSn(source), self.getCentroid(source), self.getOrientation(source) 

 

def getSn(self, source): 

"""!Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe 

 

@param source The source that will be examined""" 

 

posflux = source.get("ip_diffim_PsfDipoleFlux_pos_flux") 

posfluxErr = source.get("ip_diffim_PsfDipoleFlux_pos_fluxSigma") 

negflux = source.get("ip_diffim_PsfDipoleFlux_neg_flux") 

negfluxErr = source.get("ip_diffim_PsfDipoleFlux_neg_fluxSigma") 

 

# Not a dipole! 

359 ↛ 360line 359 didn't jump to line 360, because the condition on line 359 was never true if (posflux < 0) is (negflux < 0): 

return 0 

 

return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2) 

 

def getCentroid(self, source): 

"""!Get the centroid of the dipole; average of positive and negative lobe 

 

@param source The source that will be examined""" 

 

negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x") 

negCenY = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_y") 

posCenX = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_x") 

posCenY = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_y") 

373 ↛ 374line 373 didn't jump to line 374, because the condition on line 373 was never true if (np.isinf(negCenX) or np.isinf(negCenY) or np.isinf(posCenX) or np.isinf(posCenY)): 

return None 

 

center = afwGeom.Point2D(0.5*(negCenX+posCenX), 

0.5*(negCenY+posCenY)) 

return center 

 

def getOrientation(self, source): 

"""!Calculate the orientation of dipole; vector from negative to positive lobe 

 

@param source The source that will be examined""" 

 

negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x") 

negCenY = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_y") 

posCenX = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_x") 

posCenY = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_y") 

389 ↛ 390line 389 didn't jump to line 390, because the condition on line 389 was never true if (np.isinf(negCenX) or np.isinf(negCenY) or np.isinf(posCenX) or np.isinf(posCenY)): 

return None 

 

dx, dy = posCenX-negCenX, posCenY-negCenY 

angle = afwGeom.Angle(np.arctan2(dx, dy), afwGeom.radians) 

return angle 

 

def displayDipoles(self, exposure, sources): 

"""!Display debugging information on the detected dipoles 

 

@param exposure Image the dipoles were measured on 

@param sources The set of diaSources that were measured""" 

 

import lsstDebug 

display = lsstDebug.Info(__name__).display 

displayDiaSources = lsstDebug.Info(__name__).displayDiaSources 

maskTransparency = lsstDebug.Info(__name__).maskTransparency 

if not maskTransparency: 

maskTransparency = 90 

ds9.setMaskTransparency(maskTransparency) 

ds9.mtv(exposure, frame=lsstDebug.frame) 

 

if display and displayDiaSources: 

with ds9.Buffering(): 

for source in sources: 

cenX, cenY = source.get("ipdiffim_DipolePsfFlux_centroid") 

if np.isinf(cenX) or np.isinf(cenY): 

cenX, cenY = source.getCentroid() 

 

isdipole = source.get("classification.dipole") 

if isdipole and np.isfinite(isdipole): 

# Dipole 

ctype = "green" 

else: 

# Not dipole 

ctype = "red" 

 

ds9.dot("o", cenX, cenY, size=2, ctype=ctype, frame=lsstDebug.frame) 

 

negCenX = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_x") 

negCenY = source.get("ip_diffim_PsfDipoleFlux_neg_centroid_y") 

posCenX = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_x") 

posCenY = source.get("ip_diffim_PsfDipoleFlux_pos_centroid_y") 

if (np.isinf(negCenX) or np.isinf(negCenY) or np.isinf(posCenX) or np.isinf(posCenY)): 

continue 

 

ds9.line([(negCenX, negCenY), (posCenX, posCenY)], ctype="yellow", frame=lsstDebug.frame) 

 

lsstDebug.frame += 1 

 

 

class DipoleDeblender(object): 

"""!Functor to deblend a source as a dipole, and return a new source with deblended footprints. 

 

This necessarily overrides some of the functionality from 

meas_algorithms/python/lsst/meas/algorithms/deblend.py since we 

need a single source that contains the blended peaks, not 

multiple children sources. This directly calls the core 

deblending code deblendBaseline.deblend (optionally _fitPsf for 

debugging). 

 

Not actively being used, but there is a unit test for it in 

dipoleAlgorithm.py. 

""" 

 

def __init__(self): 

# Set up defaults to send to deblender 

 

# Always deblend as Psf 

self.psfChisqCut1 = self.psfChisqCut2 = self.psfChisqCut2b = np.inf 

self.log = Log.getLogger('ip.diffim.DipoleDeblender') 

self.sigma2fwhm = 2. * np.sqrt(2. * np.log(2.)) 

 

def __call__(self, source, exposure): 

fp = source.getFootprint() 

peaks = fp.getPeaks() 

peaksF = [pk.getF() for pk in peaks] 

fbb = fp.getBBox() 

fmask = afwImage.Mask(fbb) 

fmask.setXY0(fbb.getMinX(), fbb.getMinY()) 

fp.spans.setMask(fmask, 1) 

 

psf = exposure.getPsf() 

psfSigPix = psf.computeShape().getDeterminantRadius() 

psfFwhmPix = psfSigPix * self.sigma2fwhm 

subimage = afwImage.ExposureF(exposure, bbox=fbb, deep=True) 

cpsf = deblendBaseline.CachingPsf(psf) 

 

# if fewer than 2 peaks, just return a copy of the source 

478 ↛ 479line 478 didn't jump to line 479, because the condition on line 478 was never true if len(peaks) < 2: 

return source.getTable().copyRecord(source) 

 

# make sure you only deblend 2 peaks; take the brighest and faintest 

speaks = [(p.getPeakValue(), p) for p in peaks] 

speaks.sort() 

dpeaks = [speaks[0][1], speaks[-1][1]] 

 

# and only set these peaks in the footprint (peaks is mutable) 

peaks.clear() 

for peak in dpeaks: 

peaks.append(peak) 

 

if True: 

# Call top-level deblend task 

fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix, 

log=self.log, 

psfChisqCut1=self.psfChisqCut1, 

psfChisqCut2=self.psfChisqCut2, 

psfChisqCut2b=self.psfChisqCut2b) 

else: 

# Call lower-level _fit_psf task 

 

# Prepare results structure 

fpres = deblendBaseline.DeblenderResult(fp, exposure.getMaskedImage(), psf, psfFwhmPix, self.log) 

 

for pki, (pk, pkres, pkF) in enumerate(zip(dpeaks, fpres.deblendedParents[0].peaks, peaksF)): 

self.log.debug('Peak %i', pki) 

deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.log, 

cpsf, psfFwhmPix, 

subimage.getMaskedImage().getImage(), 

subimage.getMaskedImage().getVariance(), 

self.psfChisqCut1, self.psfChisqCut2, self.psfChisqCut2b) 

 

deblendedSource = source.getTable().copyRecord(source) 

deblendedSource.setParent(source.getId()) 

peakList = deblendedSource.getFootprint().getPeaks() 

peakList.clear() 

 

for i, peak in enumerate(fpres.deblendedParents[0].peaks): 

if peak.psfFitFlux > 0: 

suffix = "pos" 

else: 

suffix = "neg" 

c = peak.psfFitCenter 

self.log.info("deblended.centroid.dipole.psf.%s %f %f", 

suffix, c[0], c[1]) 

self.log.info("deblended.chi2dof.dipole.%s %f", 

suffix, peak.psfFitChisq / peak.psfFitDof) 

self.log.info("deblended.flux.dipole.psf.%s %f", 

suffix, peak.psfFitFlux * np.sum(peak.templateImage.getArray())) 

peakList.append(peak.peak) 

return deblendedSource