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

531

532

533

534

535

536

537

538

539

540

541

542

543

544

545

546

547

548

549

550

551

552

553

554

555

556

557

558

559

560

561

562

563

564

565

566

567

568

569

570

571

572

573

574

575

576

577

578

579

580

581

582

583

584

585

586

587

588

589

590

591

592

593

594

595

596

597

598

599

600

601

602

603

604

605

606

607

608

609

610

611

612

613

614

615

616

617

618

619

620

621

622

623

624

625

626

627

628

629

630

631

632

633

634

635

636

637

638

639

640

641

642

643

644

645

646

647

648

649

650

651

652

653

654

655

656

657

658

659

660

661

662

663

664

665

666

667

668

669

670

671

672

673

674

675

676

677

678

679

680

681

682

683

684

685

686

687

688

689

690

691

692

693

694

695

696

697

698

699

700

701

702

703

704

705

706

707

708

709

710

711

712

713

714

715

716

717

718

719

720

721

722

# 

# 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/>. 

# 

# @package lsst.pipe.tasks. 

import math 

import sys 

 

import numpy as np 

 

import lsst.pex.config as pexConf 

import lsst.pipe.base as pipeBase 

from lsst.afw.image import abMagFromFlux, abMagErrFromFluxErr, Calib 

from lsst.meas.astrom import DirectMatchTask, DirectMatchConfigWithoutLoader 

import lsst.afw.display.ds9 as ds9 

from lsst.meas.algorithms import getRefFluxField, ReserveSourcesTask 

from .colorterms import ColortermLibrary 

 

__all__ = ["PhotoCalTask", "PhotoCalConfig"] 

 

 

class PhotoCalConfig(pexConf.Config): 

"""Config for PhotoCal""" 

match = pexConf.ConfigField("Match to reference catalog", 

DirectMatchConfigWithoutLoader) 

reserve = pexConf.ConfigurableField(target=ReserveSourcesTask, doc="Reserve sources from fitting") 

fluxField = pexConf.Field( 

dtype=str, 

default="slot_CalibFlux_instFlux", 

doc=("Name of the source instFlux field to use. The associated flag field\n" 

"('<name>_flags') will be implicitly included in badFlags."), 

) 

applyColorTerms = pexConf.Field( 

dtype=bool, 

default=None, 

doc=("Apply photometric color terms to reference stars? One of:\n" 

"None: apply if colorterms and photoCatName are not None;\n" 

" fail if color term data is not available for the specified ref catalog and filter.\n" 

"True: always apply colorterms; fail if color term data is not available for the\n" 

" specified reference catalog and filter.\n" 

"False: do not apply."), 

optional=True, 

) 

sigmaMax = pexConf.Field( 

dtype=float, 

default=0.25, 

doc="maximum sigma to use when clipping", 

optional=True, 

) 

nSigma = pexConf.Field( 

dtype=float, 

default=3.0, 

doc="clip at nSigma", 

) 

useMedian = pexConf.Field( 

dtype=bool, 

default=True, 

doc="use median instead of mean to compute zeropoint", 

) 

nIter = pexConf.Field( 

dtype=int, 

default=20, 

doc="number of iterations", 

) 

colorterms = pexConf.ConfigField( 

dtype=ColortermLibrary, 

doc="Library of photometric reference catalog name: color term dict", 

) 

photoCatName = pexConf.Field( 

dtype=str, 

optional=True, 

doc=("Name of photometric reference catalog; used to select a color term dict in colorterms." 

" see also applyColorTerms"), 

) 

magErrFloor = pexConf.RangeField( 

dtype=float, 

default=0.0, 

doc="Additional magnitude uncertainty to be added in quadrature with measurement errors.", 

min=0.0, 

) 

 

def validate(self): 

pexConf.Config.validate(self) 

101 ↛ 102line 101 didn't jump to line 102, because the condition on line 101 was never true if self.applyColorTerms and self.photoCatName is None: 

raise RuntimeError("applyColorTerms=True requires photoCatName is non-None") 

103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true if self.applyColorTerms and len(self.colorterms.data) == 0: 

raise RuntimeError("applyColorTerms=True requires colorterms be provided") 

 

def setDefaults(self): 

pexConf.Config.setDefaults(self) 

self.match.sourceSelection.doFlags = True 

self.match.sourceSelection.flags.bad = [ 

"base_PixelFlags_flag_edge", 

"base_PixelFlags_flag_interpolated", 

"base_PixelFlags_flag_saturated", 

] 

self.match.sourceSelection.doUnresolved = True 

 

 

## @addtogroup LSST_task_documentation 

## @{ 

## @page photoCalTask 

## @ref PhotoCalTask_ "PhotoCalTask" 

## Detect positive and negative sources on an exposure and return a new SourceCatalog. 

## @} 

 

class PhotoCalTask(pipeBase.Task): 

r"""! 

@anchor PhotoCalTask_ 

 

@brief Calculate the zero point of an exposure given a lsst.afw.table.ReferenceMatchVector. 

 

@section pipe_tasks_photocal_Contents Contents 

 

- @ref pipe_tasks_photocal_Purpose 

- @ref pipe_tasks_photocal_Initialize 

- @ref pipe_tasks_photocal_IO 

- @ref pipe_tasks_photocal_Config 

- @ref pipe_tasks_photocal_Debug 

- @ref pipe_tasks_photocal_Example 

 

@section pipe_tasks_photocal_Purpose Description 

 

@copybrief PhotoCalTask 

 

Calculate an Exposure's zero-point given a set of flux measurements of stars matched to an input catalogue. 

The type of flux to use is specified by PhotoCalConfig.fluxField. 

 

The algorithm clips outliers iteratively, with parameters set in the configuration. 

 

@note This task can adds fields to the schema, so any code calling this task must ensure that 

these columns are indeed present in the input match list; see @ref pipe_tasks_photocal_Example 

 

@section pipe_tasks_photocal_Initialize Task initialisation 

 

@copydoc \_\_init\_\_ 

 

@section pipe_tasks_photocal_IO Inputs/Outputs to the run method 

 

@copydoc run 

 

@section pipe_tasks_photocal_Config Configuration parameters 

 

See @ref PhotoCalConfig 

 

@section pipe_tasks_photocal_Debug Debug variables 

 

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

flag @c -d to import @b debug.py from your @c PYTHONPATH; see @ref baseDebug for more about @b debug.py files. 

 

The available variables in PhotoCalTask are: 

<DL> 

<DT> @c display 

<DD> If True enable other debug outputs 

<DT> @c displaySources 

<DD> If True, display the exposure on ds9's frame 1 and overlay the source catalogue. 

<DL> 

<DT> red o 

<DD> Reserved objects 

<DT> green o 

<DD> Objects used in the photometric calibration 

</DL> 

<DT> @c scatterPlot 

<DD> Make a scatter plot of flux v. reference magnitude as a function of reference magnitude. 

- good objects in blue 

- rejected objects in red 

(if @c scatterPlot is 2 or more, prompt to continue after each iteration) 

</DL> 

 

@section pipe_tasks_photocal_Example A complete example of using PhotoCalTask 

 

This code is in @link examples/photoCalTask.py@endlink, and can be run as @em e.g. 

@code 

examples/photoCalTask.py 

@endcode 

@dontinclude photoCalTask.py 

 

Import the tasks (there are some other standard imports; read the file for details) 

@skipline from lsst.pipe.tasks.astrometry 

@skipline measPhotocal 

 

We need to create both our tasks before processing any data as the task constructors 

can add extra columns to the schema which we get from the input catalogue, @c scrCat: 

@skipline getSchema 

 

Astrometry first: 

@skip AstrometryTask.ConfigClass 

@until aTask 

(that @c filterMap line is because our test code doesn't use a filter that the reference catalogue recognises, 

so we tell it to use the @c r band) 

 

Then photometry: 

@skip measPhotocal 

@until pTask 

 

If the schema has indeed changed we need to add the new columns to the source table 

(yes; this should be easier!) 

@skip srcCat 

@until srcCat = cat 

 

We're now ready to process the data (we could loop over multiple exposures/catalogues using the same 

task objects): 

@skip matches 

@until result 

 

We can then unpack and use the results: 

@skip calib 

@until np.log 

 

<HR> 

To investigate the @ref pipe_tasks_photocal_Debug, put something like 

@code{.py} 

import lsstDebug 

def DebugInfo(name): 

di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would call us recursively 

if name.endswith(".PhotoCal"): 

di.display = 1 

 

return di 

 

lsstDebug.Info = DebugInfo 

@endcode 

into your debug.py file and run photoCalTask.py with the @c --debug flag. 

""" 

ConfigClass = PhotoCalConfig 

_DefaultName = "photoCal" 

 

def __init__(self, refObjLoader, schema=None, **kwds): 

"""!Create the photometric calibration task. See PhotoCalTask.init for documentation 

""" 

pipeBase.Task.__init__(self, **kwds) 

self.scatterPlot = None 

self.fig = None 

251 ↛ 255line 251 didn't jump to line 255, because the condition on line 251 was never false if schema is not None: 

self.usedKey = schema.addField("calib_photometry_used", type="Flag", 

doc="set if source was used in photometric calibration") 

else: 

self.usedKey = None 

self.match = DirectMatchTask(self.config.match, refObjLoader=refObjLoader, 

name="match", parentTask=self) 

self.makeSubtask("reserve", columnName="calib_photometry", schema=schema, 

doc="set if source was reserved from photometric calibration") 

 

def getSourceKeys(self, schema): 

"""Return a struct containing the source catalog keys for fields used 

by PhotoCalTask. 

 

 

Parameters 

---------- 

schema : `lsst.afw.table.schema` 

Schema of the catalog to get keys from. 

 

Returns 

------- 

result : `lsst.pipe.base.Struct` 

Result struct with components: 

 

- ``instFlux``: Instrument flux key. 

- ``instFluxErr``: Instrument flux error key. 

""" 

instFlux = schema.find(self.config.fluxField).key 

instFluxErr = schema.find(self.config.fluxField + "Err").key 

return pipeBase.Struct(instFlux=instFlux, instFluxErr=instFluxErr) 

 

@pipeBase.timeMethod 

def extractMagArrays(self, matches, filterName, sourceKeys): 

"""!Extract magnitude and magnitude error arrays from the given matches. 

 

@param[in] matches Reference/source matches, a @link lsst::afw::table::ReferenceMatchVector@endlink 

@param[in] filterName Name of filter being calibrated 

@param[in] sourceKeys Struct of source catalog keys, as returned by getSourceKeys() 

 

@return Struct containing srcMag, refMag, srcMagErr, refMagErr, and magErr numpy arrays 

where magErr is an error in the magnitude; the error in srcMag - refMag 

If nonzero, config.magErrFloor will be added to magErr *only* (not srcMagErr or refMagErr), as 

magErr is what is later used to determine the zero point. 

Struct also contains refFluxFieldList: a list of field names of the reference catalog used for fluxes 

(1 or 2 strings) 

@note These magnitude arrays are the @em inputs to the photometric calibration, some may have been 

discarded by clipping while estimating the calibration (https://jira.lsstcorp.org/browse/DM-813) 

""" 

srcInstFluxArr = np.array([m.second.get(sourceKeys.instFlux) for m in matches]) 

srcInstFluxErrArr = np.array([m.second.get(sourceKeys.instFluxErr) for m in matches]) 

302 ↛ 304line 302 didn't jump to line 304, because the condition on line 302 was never true if not np.all(np.isfinite(srcInstFluxErrArr)): 

# this is an unpleasant hack; see DM-2308 requesting a better solution 

self.log.warn("Source catalog does not have flux uncertainties; using sqrt(flux).") 

srcInstFluxErrArr = np.sqrt(srcInstFluxArr) 

 

# convert source instFlux from DN to an estimate of Jy 

JanskysPerABFlux = 3631.0 

srcInstFluxArr = srcInstFluxArr * JanskysPerABFlux 

srcInstFluxErrArr = srcInstFluxErrArr * JanskysPerABFlux 

 

312 ↛ 313line 312 didn't jump to line 313, because the condition on line 312 was never true if not matches: 

raise RuntimeError("No reference stars are available") 

refSchema = matches[0].first.schema 

 

applyColorTerms = self.config.applyColorTerms 

applyCTReason = "config.applyColorTerms is %s" % (self.config.applyColorTerms,) 

if self.config.applyColorTerms is None: 

# apply color terms if color term data is available and photoCatName specified 

ctDataAvail = len(self.config.colorterms.data) > 0 

photoCatSpecified = self.config.photoCatName is not None 

applyCTReason += " and data %s available" % ("is" if ctDataAvail else "is not") 

applyCTReason += " and photoRefCat %s provided" % ("is" if photoCatSpecified else "is not") 

applyColorTerms = ctDataAvail and photoCatSpecified 

 

if applyColorTerms: 

self.log.info("Applying color terms for filterName=%r, config.photoCatName=%s because %s", 

filterName, self.config.photoCatName, applyCTReason) 

ct = self.config.colorterms.getColorterm( 

filterName=filterName, photoCatName=self.config.photoCatName, doRaise=True) 

else: 

self.log.info("Not applying color terms because %s", applyCTReason) 

ct = None 

 

if ct: # we have a color term to worry about 

fluxFieldList = [getRefFluxField(refSchema, filt) for filt in (ct.primary, ct.secondary)] 

missingFluxFieldList = [] 

for fluxField in fluxFieldList: 

try: 

refSchema.find(fluxField).key 

except KeyError: 

missingFluxFieldList.append(fluxField) 

 

344 ↛ 345line 344 didn't jump to line 345, because the condition on line 344 was never true if missingFluxFieldList: 

self.log.warn("Source catalog does not have fluxes for %s; ignoring color terms", 

" ".join(missingFluxFieldList)) 

ct = None 

 

if not ct: 

fluxFieldList = [getRefFluxField(refSchema, filterName)] 

 

refFluxArrList = [] # list of ref arrays, one per flux field 

refFluxErrArrList = [] # list of ref flux arrays, one per flux field 

for fluxField in fluxFieldList: 

fluxKey = refSchema.find(fluxField).key 

refFluxArr = np.array([m.first.get(fluxKey) for m in matches]) 

try: 

fluxErrKey = refSchema.find(fluxField + "Err").key 

refFluxErrArr = np.array([m.first.get(fluxErrKey) for m in matches]) 

except KeyError: 

# Reference catalogue may not have flux uncertainties; HACK 

self.log.warn("Reference catalog does not have flux uncertainties for %s; using sqrt(flux).", 

fluxField) 

refFluxErrArr = np.sqrt(refFluxArr) 

 

refFluxArrList.append(refFluxArr) 

refFluxErrArrList.append(refFluxErrArr) 

 

if ct: # we have a color term to worry about 

refMagArr1 = np.array([abMagFromFlux(rf1) for rf1 in refFluxArrList[0]]) # primary 

refMagArr2 = np.array([abMagFromFlux(rf2) for rf2 in refFluxArrList[1]]) # secondary 

 

refMagArr = ct.transformMags(refMagArr1, refMagArr2) 

refFluxErrArr = ct.propagateFluxErrors(refFluxErrArrList[0], refFluxErrArrList[1]) 

else: 

refMagArr = np.array([abMagFromFlux(rf) for rf in refFluxArrList[0]]) 

 

srcMagArr = np.array([abMagFromFlux(sf) for sf in srcInstFluxArr]) 

 

# Fitting with error bars in both axes is hard 

# for now ignore reference flux error, but ticket DM-2308 is a request for a better solution 

magErrArr = np.array([abMagErrFromFluxErr(fe, sf) 

for fe, sf in zip(srcInstFluxErrArr, srcInstFluxArr)]) 

384 ↛ 385line 384 didn't jump to line 385, because the condition on line 384 was never true if self.config.magErrFloor != 0.0: 

magErrArr = (magErrArr**2 + self.config.magErrFloor**2)**0.5 

 

srcMagErrArr = np.array([abMagErrFromFluxErr(sfe, sf) 

for sfe, sf in zip(srcInstFluxErrArr, srcInstFluxArr)]) 

refMagErrArr = np.array([abMagErrFromFluxErr(rfe, rf) 

for rfe, rf in zip(refFluxErrArr, refFluxArr)]) 

 

good = np.isfinite(srcMagArr) & np.isfinite(refMagArr) 

 

return pipeBase.Struct( 

srcMag=srcMagArr[good], 

refMag=refMagArr[good], 

magErr=magErrArr[good], 

srcMagErr=srcMagErrArr[good], 

refMagErr=refMagErrArr[good], 

refFluxFieldList=fluxFieldList, 

) 

 

@pipeBase.timeMethod 

def run(self, exposure, sourceCat, expId=0): 

"""!Do photometric calibration - select matches to use and (possibly iteratively) compute 

the zero point. 

 

@param[in] exposure Exposure upon which the sources in the matches were detected. 

@param[in] sourceCat A catalog of sources to use in the calibration 

(@em i.e. a list of lsst.afw.table.Match with 

@c first being of type lsst.afw.table.SimpleRecord and @c second type lsst.afw.table.SourceRecord --- 

the reference object and matched object respectively). 

(will not be modified except to set the outputField if requested.). 

 

@return Struct of: 

- calib ------- @link lsst::afw::image::Calib@endlink object containing the zero point 

- arrays ------ Magnitude arrays returned be PhotoCalTask.extractMagArrays 

- matches ----- Final ReferenceMatchVector, as returned by PhotoCalTask.selectMatches. 

- zp ---------- Photometric zero point (mag) 

- sigma ------- Standard deviation of fit of photometric zero point (mag) 

- ngood ------- Number of sources used to fit photometric zero point 

 

The exposure is only used to provide the name of the filter being calibrated (it may also be 

used to generate debugging plots). 

 

The reference objects: 

- Must include a field @c photometric; True for objects which should be considered as 

photometric standards 

- Must include a field @c flux; the flux used to impose a magnitude limit and also to calibrate 

the data to (unless a color term is specified, in which case ColorTerm.primary is used; 

See https://jira.lsstcorp.org/browse/DM-933) 

- May include a field @c stargal; if present, True means that the object is a star 

- May include a field @c var; if present, True means that the object is variable 

 

The measured sources: 

- Must include PhotoCalConfig.fluxField; the flux measurement to be used for calibration 

 

@throws RuntimeError with the following strings: 

 

<DL> 

<DT> No matches to use for photocal 

<DD> No matches are available (perhaps no sources/references were selected by the matcher). 

<DT> No reference stars are available 

<DD> No matches are available from which to extract magnitudes. 

</DL> 

""" 

import lsstDebug 

 

display = lsstDebug.Info(__name__).display 

displaySources = display and lsstDebug.Info(__name__).displaySources 

self.scatterPlot = display and lsstDebug.Info(__name__).scatterPlot 

 

453 ↛ 454line 453 didn't jump to line 454, because the condition on line 453 was never true if self.scatterPlot: 

from matplotlib import pyplot 

try: 

self.fig.clf() 

except Exception: 

self.fig = pyplot.figure() 

 

filterName = exposure.getFilter().getName() 

 

# Match sources 

matchResults = self.match.run(sourceCat, filterName) 

matches = matchResults.matches 

reserveResults = self.reserve.run([mm.second for mm in matches], expId=expId) 

466 ↛ 467line 466 didn't jump to line 467, because the condition on line 466 was never true if displaySources: 

self.displaySources(exposure, matches, reserveResults.reserved) 

468 ↛ 469line 468 didn't jump to line 469, because the condition on line 468 was never true if reserveResults.reserved.sum() > 0: 

matches = [mm for mm, use in zip(matches, reserveResults.use) if use] 

470 ↛ 471line 470 didn't jump to line 471, because the condition on line 470 was never true if len(matches) == 0: 

raise RuntimeError("No matches to use for photocal") 

472 ↛ 477line 472 didn't jump to line 477, because the condition on line 472 was never false if self.usedKey is not None: 

for mm in matches: 

mm.second.set(self.usedKey, True) 

 

# Prepare for fitting 

sourceKeys = self.getSourceKeys(matches[0].second.schema) 

arrays = self.extractMagArrays(matches=matches, filterName=filterName, sourceKeys=sourceKeys) 

 

# Fit for zeropoint 

r = self.getZeroPoint(arrays.srcMag, arrays.refMag, arrays.magErr) 

self.log.info("Magnitude zero point: %f +/- %f from %d stars", r.zp, r.sigma, r.ngood) 

 

# Prepare the results 

flux0 = 10**(0.4*r.zp) # Flux of mag=0 star 

flux0err = 0.4*math.log(10)*flux0*r.sigma # Error in flux0 

calib = Calib() 

calib.setFluxMag0(flux0, flux0err) 

 

return pipeBase.Struct( 

calib=calib, 

arrays=arrays, 

matches=matches, 

zp=r.zp, 

sigma=r.sigma, 

ngood=r.ngood, 

) 

 

def displaySources(self, exposure, matches, reserved, frame=1): 

"""Display sources we'll use for photocal 

 

Sources that will be actually used will be green. 

Sources reserved from the fit will be red. 

 

Parameters 

---------- 

exposure : `lsst.afw.image.ExposureF` 

Exposure to display. 

matches : `list` of `lsst.afw.table.RefMatch` 

Matches used for photocal. 

reserved : `numpy.ndarray` of type `bool` 

Boolean array indicating sources that are reserved. 

frame : `int` 

Frame number for display. 

""" 

ds9.mtv(exposure, frame=frame, title="photocal") 

with ds9.Buffering(): 

for mm, rr in zip(matches, reserved): 

x, y = mm.second.getCentroid() 

ctype = ds9.RED if rr else ds9.GREEN 

ds9.dot("o", x, y, size=4, frame=frame, ctype=ctype) 

 

def getZeroPoint(self, src, ref, srcErr=None, zp0=None): 

"""!Flux calibration code, returning (ZeroPoint, Distribution Width, Number of stars) 

 

We perform nIter iterations of a simple sigma-clipping algorithm with a couple of twists: 

1. We use the median/interquartile range to estimate the position to clip around, and the 

"sigma" to use. 

2. We never allow sigma to go _above_ a critical value sigmaMax --- if we do, a sufficiently 

large estimate will prevent the clipping from ever taking effect. 

3. Rather than start with the median we start with a crude mode. This means that a set of magnitude 

residuals with a tight core and asymmetrical outliers will start in the core. We use the width of 

this core to set our maximum sigma (see 2.) 

 

@return Struct of: 

- zp ---------- Photometric zero point (mag) 

- sigma ------- Standard deviation of fit of zero point (mag) 

- ngood ------- Number of sources used to fit zero point 

""" 

sigmaMax = self.config.sigmaMax 

 

dmag = ref - src 

 

indArr = np.argsort(dmag) 

dmag = dmag[indArr] 

 

547 ↛ 550line 547 didn't jump to line 550, because the condition on line 547 was never false if srcErr is not None: 

dmagErr = srcErr[indArr] 

else: 

dmagErr = np.ones(len(dmag)) 

 

# need to remove nan elements to avoid errors in stats calculation with numpy 

ind_noNan = np.array([i for i in range(len(dmag)) 

if (not np.isnan(dmag[i]) and not np.isnan(dmagErr[i]))]) 

dmag = dmag[ind_noNan] 

dmagErr = dmagErr[ind_noNan] 

 

IQ_TO_STDEV = 0.741301109252802 # 1 sigma in units of interquartile (assume Gaussian) 

 

npt = len(dmag) 

ngood = npt 

good = None # set at end of first iteration 

563 ↛ 714line 563 didn't jump to line 714, because the loop on line 563 didn't complete for i in range(self.config.nIter): 

if i > 0: 

npt = sum(good) 

 

center = None 

if i == 0: 

# 

# Start by finding the mode 

# 

nhist = 20 

try: 

hist, edges = np.histogram(dmag, nhist, new=True) 

except TypeError: 

hist, edges = np.histogram(dmag, nhist) # they removed new=True around numpy 1.5 

imode = np.arange(nhist)[np.where(hist == hist.max())] 

 

579 ↛ 613line 579 didn't jump to line 613, because the condition on line 579 was never false if imode[-1] - imode[0] + 1 == len(imode): # Multiple modes, but all contiguous 

580 ↛ 581line 580 didn't jump to line 581, because the condition on line 580 was never true if zp0: 

center = zp0 

else: 

center = 0.5*(edges[imode[0]] + edges[imode[-1] + 1]) 

 

peak = sum(hist[imode])/len(imode) # peak height 

 

# Estimate FWHM of mode 

j = imode[0] 

while j >= 0 and hist[j] > 0.5*peak: 

j -= 1 

j = max(j, 0) 

q1 = dmag[sum(hist[range(j)])] 

 

j = imode[-1] 

while j < nhist and hist[j] > 0.5*peak: 

j += 1 

j = min(j, nhist - 1) 

j = min(sum(hist[range(j)]), npt - 1) 

q3 = dmag[j] 

 

601 ↛ 602line 601 didn't jump to line 602, because the condition on line 601 was never true if q1 == q3: 

q1 = dmag[int(0.25*npt)] 

q3 = dmag[int(0.75*npt)] 

 

sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian) 

 

607 ↛ 608line 607 didn't jump to line 608, because the condition on line 607 was never true if sigmaMax is None: 

sigmaMax = 2*sig # upper bound on st. dev. for clipping. multiplier is a heuristic 

 

self.log.debug("Photo calibration histogram: center = %.2f, sig = %.2f", center, sig) 

 

else: 

if sigmaMax is None: 

sigmaMax = dmag[-1] - dmag[0] 

 

center = np.median(dmag) 

q1 = dmag[int(0.25*npt)] 

q3 = dmag[int(0.75*npt)] 

sig = (q3 - q1)/2.3 # estimate of standard deviation (based on FWHM; 2.358 for Gaussian) 

 

if center is None: # usually equivalent to (i > 0) 

gdmag = dmag[good] 

623 ↛ 626line 623 didn't jump to line 626, because the condition on line 623 was never false if self.config.useMedian: 

center = np.median(gdmag) 

else: 

gdmagErr = dmagErr[good] 

center = np.average(gdmag, weights=gdmagErr) 

 

q3 = gdmag[min(int(0.75*npt + 0.5), npt - 1)] 

q1 = gdmag[min(int(0.25*npt + 0.5), npt - 1)] 

 

sig = IQ_TO_STDEV*(q3 - q1) # estimate of standard deviation 

 

good = abs(dmag - center) < self.config.nSigma*min(sig, sigmaMax) # don't clip too softly 

 

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

637 ↛ 638line 637 didn't jump to line 638, because the condition on line 637 was never true if self.scatterPlot: 

try: 

self.fig.clf() 

 

axes = self.fig.add_axes((0.1, 0.1, 0.85, 0.80)) 

 

axes.plot(ref[good], dmag[good] - center, "b+") 

axes.errorbar(ref[good], dmag[good] - center, yerr=dmagErr[good], 

linestyle='', color='b') 

 

bad = np.logical_not(good) 

if len(ref[bad]) > 0: 

axes.plot(ref[bad], dmag[bad] - center, "r+") 

axes.errorbar(ref[bad], dmag[bad] - center, yerr=dmagErr[bad], 

linestyle='', color='r') 

 

axes.plot((-100, 100), (0, 0), "g-") 

for x in (-1, 1): 

axes.plot((-100, 100), x*0.05*np.ones(2), "g--") 

 

axes.set_ylim(-1.1, 1.1) 

axes.set_xlim(24, 13) 

axes.set_xlabel("Reference") 

axes.set_ylabel("Reference - Instrumental") 

 

self.fig.show() 

 

if self.scatterPlot > 1: 

reply = None 

while i == 0 or reply != "c": 

try: 

reply = input("Next iteration? [ynhpc] ") 

except EOFError: 

reply = "n" 

 

if reply == "h": 

print("Options: c[ontinue] h[elp] n[o] p[db] y[es]", file=sys.stderr) 

continue 

 

if reply in ("", "c", "n", "p", "y"): 

break 

else: 

print("Unrecognised response: %s" % reply, file=sys.stderr) 

 

if reply == "n": 

break 

elif reply == "p": 

import pdb 

pdb.set_trace() 

except Exception as e: 

print("Error plotting in PhotoCal.getZeroPoint: %s" % e, file=sys.stderr) 

 

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

 

old_ngood = ngood 

ngood = sum(good) 

693 ↛ 694line 693 didn't jump to line 694, because the condition on line 693 was never true if ngood == 0: 

msg = "PhotoCal.getZeroPoint: no good stars remain" 

 

if i == 0: # failed the first time round -- probably all fell in one bin 

center = np.average(dmag, weights=dmagErr) 

msg += " on first iteration; using average of all calibration stars" 

 

self.log.warn(msg) 

 

return pipeBase.Struct( 

zp=center, 

sigma=sig, 

ngood=len(dmag)) 

706 ↛ 709line 706 didn't jump to line 709, because the condition on line 706 was never false elif ngood == old_ngood: 

break 

 

if False: 

ref = ref[good] 

dmag = dmag[good] 

dmagErr = dmagErr[good] 

 

dmag = dmag[good] 

dmagErr = dmagErr[good] 

zp, weightSum = np.average(dmag, weights=1/dmagErr**2, returned=True) 

sigma = np.sqrt(1.0/weightSum) 

return pipeBase.Struct( 

zp=zp, 

sigma=sigma, 

ngood=len(dmag), 

)