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

import numpy as np 

from .baseMetric import BaseMetric 

import lsst.sims.maf.utils as mafUtils 

import lsst.sims.utils as utils 

from scipy.optimize import curve_fit 

from builtins import str 

 

__all__ = ['ParallaxMetric', 'ProperMotionMetric', 'RadiusObsMetric', 

'ParallaxCoverageMetric', 'ParallaxDcrDegenMetric'] 

 

 

class ParallaxMetric(BaseMetric): 

"""Calculate the uncertainty in a parallax measurement given a series of observations. 

 

Uses columns ra_pi_amp and dec_pi_amp, calculated by the ParallaxFactorStacker. 

 

Parameters 

---------- 

metricName : str, opt 

Default 'parallax'. 

m5Col : str, opt 

The default column name for m5 information in the input data. Default fiveSigmaDepth. 

filterCol : str, opt 

The column name for the filter information. Default filter. 

seeingCol : str, opt 

The column name for the seeing information. Since the astrometry errors are based on the physical 

size of the PSF, this should be the FWHM of the physical psf. Default seeingFwhmGeom. 

rmag : float, opt 

The r magnitude of the fiducial star in r band. Other filters are sclaed using sedTemplate keyword. 

Default 20.0 

SedTemplate : str, opt 

The template to use. This can be 'flat' or 'O','B','A','F','G','K','M'. Default flat. 

atm_err : float, opt 

The expected centroiding error due to the atmosphere, in arcseconds. Default 0.01. 

normalize : boolean, opt 

Compare the astrometric uncertainty to the uncertainty that would result if half the observations 

were taken at the start and half at the end. A perfect survey will have a value close to 1, while 

a poorly scheduled survey will be close to 0. Default False. 

badval : float, opt 

The value to return when the metric value cannot be calculated. Default -666. 

""" 

def __init__(self, metricName='parallax', m5Col='fiveSigmaDepth', 

filterCol='filter', seeingCol='seeingFwhmGeom', rmag=20., 

SedTemplate='flat', badval=-666, 

atm_err=0.01, normalize=False, **kwargs): 

Cols = [m5Col, filterCol, seeingCol, 'ra_pi_amp', 'dec_pi_amp'] 

if normalize: 

units = 'ratio' 

else: 

units = 'mas' 

super(ParallaxMetric, self).__init__(Cols, metricName=metricName, units=units, 

badval=badval, **kwargs) 

# set return type 

self.m5Col = m5Col 

self.seeingCol = seeingCol 

self.filterCol = filterCol 

filters = ['u', 'g', 'r', 'i', 'z', 'y'] 

self.mags = {} 

59 ↛ 63line 59 didn't jump to line 63, because the condition on line 59 was never false if SedTemplate == 'flat': 

for f in filters: 

self.mags[f] = rmag 

else: 

self.mags = utils.stellarMags(SedTemplate, rmag=rmag) 

self.atm_err = atm_err 

self.normalize = normalize 

self.comment = 'Estimated uncertainty in parallax measurement ' \ 

'(assuming no proper motion or that proper motion ' 

self.comment += 'is well fit). Uses measurements in all bandpasses, ' \ 

'and estimates astrometric error based on SNR ' 

self.comment += 'in each visit. ' 

71 ↛ 73line 71 didn't jump to line 73, because the condition on line 71 was never false if SedTemplate == 'flat': 

self.comment += 'Assumes a flat SED. ' 

if self.normalize: 

self.comment += 'This normalized version of the metric displays the ' \ 

'estimated uncertainty in the parallax measurement, ' 

self.comment += 'divided by the minimum parallax uncertainty possible ' \ 

'(if all visits were six ' 

self.comment += 'months apart). Values closer to 1 indicate more optimal ' \ 

'scheduling for parallax measurement.' 

 

def _final_sigma(self, position_errors, ra_pi_amp, dec_pi_amp): 

"""Assume parallax in RA and DEC are fit independently, then combined. 

All inputs assumed to be arcsec """ 

sigma_A = position_errors/ra_pi_amp 

sigma_B = position_errors/dec_pi_amp 

sigma_ra = np.sqrt(1./np.sum(1./sigma_A**2)) 

sigma_dec = np.sqrt(1./np.sum(1./sigma_B**2)) 

# Combine RA and Dec uncertainties, convert to mas 

sigma = np.sqrt(1./(1./sigma_ra**2+1./sigma_dec**2))*1e3 

return sigma 

 

def run(self, dataslice, slicePoint=None): 

filters = np.unique(dataslice[self.filterCol]) 

94 ↛ 95line 94 didn't jump to line 95, because the condition on line 94 was never true if hasattr(filters[0], 'decode'): 

filters = [str(f.decode('utf-8')) for f in filters] 

snr = np.zeros(len(dataslice), dtype='float') 

# compute SNR for all observations 

for filt in filters: 

good = np.where(dataslice[self.filterCol] == filt) 

snr[good] = mafUtils.m52snr(self.mags[str(filt)], dataslice[self.m5Col][good]) 

position_errors = np.sqrt(mafUtils.astrom_precision(dataslice[self.seeingCol], 

snr)**2+self.atm_err**2) 

sigma = self._final_sigma(position_errors, dataslice['ra_pi_amp'], dataslice['dec_pi_amp']) 

if self.normalize: 

# Leave the dec parallax as zero since one can't have ra and dec maximized at the same time. 

sigma = self._final_sigma(position_errors, 

dataslice['ra_pi_amp']*0+1., dataslice['dec_pi_amp']*0)/sigma 

return sigma 

 

 

class ProperMotionMetric(BaseMetric): 

"""Calculate the uncertainty in the returned proper motion. 

 

This metric assumes gaussian errors in the astrometry measurements. 

 

Parameters 

---------- 

metricName : str, opt 

Default 'properMotion'. 

m5Col : str, opt 

The default column name for m5 information in the input data. Default fiveSigmaDepth. 

mjdCol : str, opt 

The column name for the exposure time. Default observationStartMJD. 

filterCol : str, opt 

The column name for the filter information. Default filter. 

seeingCol : str, opt 

The column name for the seeing information. Since the astrometry errors are based on the physical 

size of the PSF, this should be the FWHM of the physical psf. Default seeingFwhmGeom. 

rmag : float, opt 

The r magnitude of the fiducial star in r band. Other filters are sclaed using sedTemplate keyword. 

Default 20.0 

SedTemplate : str, opt 

The template to use. This can be 'flat' or 'O','B','A','F','G','K','M'. Default flat. 

atm_err : float, opt 

The expected centroiding error due to the atmosphere, in arcseconds. Default 0.01. 

normalize : boolean, opt 

Compare the astrometric uncertainty to the uncertainty that would result if half the observations 

were taken at the start and half at the end. A perfect survey will have a value close to 1, while 

a poorly scheduled survey will be close to 0. Default False. 

baseline : float, opt 

The length of the survey used for the normalization, in years. Default 10. 

badval : float, opt 

The value to return when the metric value cannot be calculated. Default -666. 

""" 

def __init__(self, metricName='properMotion', 

m5Col='fiveSigmaDepth', mjdCol='observationStartMJD', 

filterCol='filter', seeingCol='seeingFwhmGeom', rmag=20., 

SedTemplate='flat', badval= -666, 

atm_err=0.01, normalize=False, 

baseline=10., **kwargs): 

cols = [m5Col, mjdCol, filterCol, seeingCol] 

if normalize: 

units = 'ratio' 

else: 

units = 'mas/yr' 

super(ProperMotionMetric, self).__init__(col=cols, metricName=metricName, units=units, 

badval=badval, **kwargs) 

# set return type 

self.mjdCol = mjdCol 

self.seeingCol = seeingCol 

self.m5Col = m5Col 

filters = ['u', 'g', 'r', 'i', 'z', 'y'] 

self.mags = {} 

164 ↛ 168line 164 didn't jump to line 168, because the condition on line 164 was never false if SedTemplate == 'flat': 

for f in filters: 

self.mags[f] = rmag 

else: 

self.mags = utils.stellarMags(SedTemplate, rmag=rmag) 

self.atm_err = atm_err 

self.normalize = normalize 

self.baseline = baseline 

self.comment = 'Estimated uncertainty of the proper motion fit ' \ 

'(assuming no parallax or that parallax is well fit). ' 

self.comment += 'Uses visits in all bands, and generates approximate ' \ 

'astrometric errors using the SNR in each visit. ' 

176 ↛ 178line 176 didn't jump to line 178, because the condition on line 176 was never false if SedTemplate == 'flat': 

self.comment += 'Assumes a flat SED. ' 

if self.normalize: 

self.comment += 'This normalized version of the metric represents ' \ 

'the estimated uncertainty in the proper ' 

self.comment += 'motion divided by the minimum uncertainty possible ' \ 

'(if all visits were ' 

self.comment += 'obtained on the first and last days of the survey). ' 

self.comment += 'Values closer to 1 indicate more optimal scheduling.' 

 

def run(self, dataslice, slicePoint=None): 

filters = np.unique(dataslice['filter']) 

filters = [str(f) for f in filters] 

precis = np.zeros(dataslice.size, dtype='float') 

for f in filters: 

observations = np.where(dataslice['filter'] == f) 

192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true if np.size(observations[0]) < 2: 

precis[observations] = self.badval 

else: 

snr = mafUtils.m52snr(self.mags[f], 

dataslice[self.m5Col][observations]) 

precis[observations] = mafUtils.astrom_precision( 

dataslice[self.seeingCol][observations], snr) 

precis[observations] = np.sqrt(precis[observations]**2 + self.atm_err**2) 

good = np.where(precis != self.badval) 

result = mafUtils.sigma_slope(dataslice[self.mjdCol][good], precis[good]) 

result = result*365.25*1e3 # Convert to mas/yr 

if (self.normalize) & (good[0].size > 0): 

new_dates = dataslice[self.mjdCol][good]*0 

nDates = new_dates.size 

new_dates[nDates//2:] = self.baseline*365.25 

result = (mafUtils.sigma_slope(new_dates, precis[good])*365.25*1e3)/result 

# Observations that are very close together can still fail 

209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true if np.isnan(result): 

result = self.badval 

return result 

 

 

class ParallaxCoverageMetric(BaseMetric): 

""" 

Check how well the parallax factor is distributed. Subtracts the weighted mean position of the 

parallax offsets, then computes the weighted mean radius of the points. 

If points are well distributed, the mean radius will be near 1. If phase coverage is bad, 

radius will be close to zero. 

 

For points on the Ecliptic, uniform sampling should result in a metric value of ~0.5. 

At the poles, uniform sampling would result in a metric value of ~1. 

Conceptually, it is helpful to remember that the parallax motion of a star at the pole is 

a (nearly circular) ellipse while the motion of a star on the ecliptic is a straight line. Thus, any 

pair of observations separated by 6 months will give the full parallax range for a star on the pole 

but only observations on very specific dates will give the full range for a star on the ecliptic. 

 

Optionally also demand that there are observations above the snrLimit kwarg spanning thetaRange radians. 

 

Parameters 

---------- 

m5Col: str, opt 

Column name for individual visit m5. Default fiveSigmaDepth. 

mjdCol: str, opt 

Column name for exposure time dates. Default observationStartMJD. 

filterCol: str, opt 

Column name for filter. Default filter. 

seeingCol: str, opt 

Column name for seeing (assumed FWHM). Default seeingFwhmGeom. 

rmag: float, opt 

Magnitude of fiducial star in r filter. Other filters are scaled using sedTemplate keyword. 

Default 20.0 

sedTemplate: str, opt 

Template to use (can be 'flat' or 'O','B','A','F','G','K','M'). Default 'flat'. 

atm_err: float, opt 

Centroiding error due to atmosphere in arcsec. Default 0.01 (arcseconds). 

thetaRange: float, opt 

Range of parallax offset angles to demand (in radians). Default=0 (means no range requirement). 

snrLimit: float, opt 

Only include points above the snrLimit when computing thetaRange. Default 5. 

 

Returns 

-------- 

metricValue: float 

Returns a weighted mean of the length of the parallax factor vectors. 

Values near 1 imply that the points are well distributed. 

Values near 0 imply that the parallax phase coverage is bad. 

Near the ecliptic, uniform sampling results in metric values of about 0.5. 

 

Notes 

----- 

Uses the ParallaxFactor stacker to calculate ra_pi_amp and dec_pi_amp. 

""" 

def __init__(self, metricName='ParallaxCoverageMetric', m5Col='fiveSigmaDepth', 

mjdCol='observationStartMJD', filterCol='filter', seeingCol='seeingFwhmGeom', 

rmag=20., SedTemplate='flat', 

atm_err=0.01, thetaRange=0., snrLimit=5, **kwargs): 

cols = ['ra_pi_amp', 'dec_pi_amp', m5Col, mjdCol, filterCol, seeingCol] 

units = 'ratio' 

super(ParallaxCoverageMetric, self).__init__(cols, 

metricName=metricName, units=units, 

**kwargs) 

self.m5Col = m5Col 

self.seeingCol = seeingCol 

self.filterCol = filterCol 

self.mjdCol = mjdCol 

 

# Demand the range of theta values 

self.thetaRange = thetaRange 

self.snrLimit = snrLimit 

 

filters = ['u', 'g', 'r', 'i', 'z', 'y'] 

self.mags = {} 

284 ↛ 288line 284 didn't jump to line 288, because the condition on line 284 was never false if SedTemplate == 'flat': 

for f in filters: 

self.mags[f] = rmag 

else: 

self.mags = utils.stellarMags(SedTemplate, rmag=rmag) 

self.atm_err = atm_err 

caption = "Parallax factor coverage for an r=%.2f star (0 is bad, 0.5-1 is good). " % (rmag) 

caption += "One expects the parallax factor coverage to vary because stars on the ecliptic " 

caption += "can be observed when they have no parallax offset while stars at the pole are always " 

caption += "offset by the full parallax offset.""" 

self.comment = caption 

 

def _thetaCheck(self, ra_pi_amp, dec_pi_amp, snr): 

good = np.where(snr >= self.snrLimit) 

theta = np.arctan2(dec_pi_amp[good], ra_pi_amp[good]) 

# Make values between 0 and 2pi 

theta = theta-np.min(theta) 

result = 0. 

if np.max(theta) >= self.thetaRange: 

# Check that things are in differnet quadrants 

theta = (theta+np.pi) % 2.*np.pi 

theta = theta-np.min(theta) 

if np.max(theta) >= self.thetaRange: 

result = 1 

return result 

 

def _computeWeights(self, dataSlice, snr): 

# Compute centroid uncertainty in each visit 

position_errors = np.sqrt(mafUtils.astrom_precision(dataSlice[self.seeingCol], 

snr)**2+self.atm_err**2) 

weights = 1./position_errors**2 

return weights 

 

def _weightedR(self, dec_pi_amp, ra_pi_amp, weights): 

ycoord = dec_pi_amp-np.average(dec_pi_amp, weights=weights) 

xcoord = ra_pi_amp-np.average(ra_pi_amp, weights=weights) 

radius = np.sqrt(xcoord**2+ycoord**2) 

aveRad = np.average(radius, weights=weights) 

return aveRad 

 

def run(self, dataSlice, slicePoint=None): 

325 ↛ 326line 325 didn't jump to line 326, because the condition on line 325 was never true if np.size(dataSlice) < 2: 

return self.badval 

 

filters = np.unique(dataSlice[self.filterCol]) 

filters = [str(f) for f in filters] 

snr = np.zeros(len(dataSlice), dtype='float') 

# compute SNR for all observations 

for filt in filters: 

inFilt = np.where(dataSlice[self.filterCol] == filt) 

snr[inFilt] = mafUtils.m52snr(self.mags[str(filt)], dataSlice[self.m5Col][inFilt]) 

 

weights = self._computeWeights(dataSlice, snr) 

aveR = self._weightedR(dataSlice['ra_pi_amp'], dataSlice['dec_pi_amp'], weights) 

338 ↛ 339line 338 didn't jump to line 339, because the condition on line 338 was never true if self.thetaRange > 0: 

thetaCheck = self._thetaCheck(dataSlice['ra_pi_amp'], dataSlice['dec_pi_amp'], snr) 

else: 

thetaCheck = 1. 

result = aveR*thetaCheck 

return result 

 

 

class ParallaxDcrDegenMetric(BaseMetric): 

"""Use the full parallax and DCR displacement vectors to find if they are degenerate. 

 

Parameters 

---------- 

metricName : str, opt 

Default 'ParallaxDcrDegenMetric'. 

seeingCol : str, opt 

Default 'FWHMgeom' 

m5Col : str, opt 

Default 'fiveSigmaDepth' 

filterCol : str 

Default 'filter' 

atm_err : float 

Minimum error in photometry centroids introduced by the atmosphere (arcseconds). Default 0.01. 

rmag : float 

r-band magnitude of the fiducual star that is being used (mag). 

SedTemplate : str 

The SED template to use for fiducia star colors, passed to lsst.sims.utils.stellarMags. 

Default 'flat' 

tol : float 

Tolerance for how well curve_fit needs to work before believing the covariance result. 

Default 0.05. 

 

Returns 

------- 

metricValue : float 

Returns the correlation coefficient between the best-fit parallax amplitude and DCR amplitude. 

The RA and Dec offsets are fit simultaneously. Values close to zero are good, values close to +/- 1 

are bad. Experience with fitting Monte Carlo simulations suggests the astrometric fits start 

becoming poor around a correlation of 0.7. 

""" 

def __init__(self, metricName='ParallaxDcrDegenMetric', seeingCol='seeingFwhmGeom', 

m5Col='fiveSigmaDepth', atm_err=0.01, rmag=20., SedTemplate='flat', 

filterCol='filter', tol=0.05, **kwargs): 

self.m5Col = m5Col 

self.seeingCol = seeingCol 

self.filterCol = filterCol 

self.tol = tol 

units = 'Correlation' 

# just put all the columns that all the stackers will need here? 

cols = ['ra_pi_amp', 'dec_pi_amp', 'ra_dcr_amp', 'dec_dcr_amp', 

seeingCol, m5Col] 

super(ParallaxDcrDegenMetric, self).__init__(cols, metricName=metricName, units=units, 

**kwargs) 

self.filters = ['u', 'g', 'r', 'i', 'z', 'y'] 

self.mags = {} 

393 ↛ 397line 393 didn't jump to line 397, because the condition on line 393 was never false if SedTemplate == 'flat': 

for f in self.filters: 

self.mags[f] = rmag 

else: 

self.mags = utils.stellarMags(SedTemplate, rmag=rmag) 

self.atm_err = atm_err 

 

def _positions(self, x, a, b): 

""" 

Function to find parallax and dcr amplitudes 

 

x should be a vector with [[parallax_x1, parallax_x2..., parallax_y1, parallax_y2...], 

[dcr_x1, dcr_x2..., dcr_y1, dcr_y2...]] 

""" 

result = a*x[0, :] + b*x[1, :] 

return result 

 

def run(self, dataSlice, slicePoint=None): 

# The idea here is that we calculate position errors (in RA and Dec) for all observations. 

# Then we generate arrays of the parallax offsets (delta RA parallax = ra_pi_amp, etc) 

# and the DCR offsets (delta RA DCR = ra_dcr_amp, etc), and just add them together into one 

# RA (and Dec) offset. Then, we try to fit for how we combined these offsets, but while 

# considering the astrometric noise. If we can figure out that we just added them together 

# (i.e. the curve_fit result is [a=1, b=1] for the function _positions above) 

# then we should be able to disentangle the parallax and DCR offsets when fitting 'for real'. 

# compute SNR for all observations 

snr = np.zeros(len(dataSlice), dtype='float') 

for filt in self.filters: 

inFilt = np.where(dataSlice[self.filterCol] == filt) 

snr[inFilt] = mafUtils.m52snr(self.mags[filt], dataSlice[self.m5Col][inFilt]) 

# Compute the centroiding uncertainties 

# Note that these centroiding uncertainties depend on the physical size of the PSF, thus 

# we are using seeingFwhmGeom for these metrics, not seeingFwhmEff. 

position_errors = np.sqrt(mafUtils.astrom_precision(dataSlice[self.seeingCol], snr)**2 + 

self.atm_err**2) 

# Construct the vectors of RA/Dec offsets. xdata is the "input data". ydata is the "output". 

xdata = np.empty((2, dataSlice.size * 2), dtype=float) 

xdata[0, :] = np.concatenate((dataSlice['ra_pi_amp'], dataSlice['dec_pi_amp'])) 

xdata[1, :] = np.concatenate((dataSlice['ra_dcr_amp'], dataSlice['dec_dcr_amp'])) 

ydata = np.sum(xdata, axis=0) 

# Use curve_fit to compute covariance between parallax and dcr amplitudes 

# Set the initial guess slightly off from the correct [1,1] to make sure it iterates. 

popt, pcov = curve_fit(self._positions, xdata, ydata, p0=[1.1, 0.9], 

sigma=np.concatenate((position_errors, position_errors)), 

absolute_sigma=True) 

# Catch if the fit failed to converge on the correct solution. 

439 ↛ 440line 439 didn't jump to line 440, because the condition on line 439 was never true if np.max(np.abs(popt - np.array([1., 1.]))) > self.tol: 

return self.badval 

# Covariance between best fit parallax amplitude and DCR amplitude. 

cov = pcov[1, 0] 

# Convert covarience between parallax and DCR amplitudes to normalized correlation 

perr = np.sqrt(np.diag(pcov)) 

correlation = cov/(perr[0]*perr[1]) 

result = correlation 

# This can throw infs. 

448 ↛ 449line 448 didn't jump to line 449, because the condition on line 448 was never true if np.isinf(result): 

result = self.badval 

return result 

 

 

def calcDist_cosines(RA1, Dec1, RA2, Dec2): 

# Taken from simSelfCalib.py 

"""Calculates distance on a sphere using spherical law of cosines. 

 

Give this function RA/Dec values in radians. Returns angular distance(s), in radians. 

Note that since this is all numpy, you could input arrays of RA/Decs.""" 

# This formula can have rounding errors for case where distances are small. 

# Oh, the joys of wikipedia - http://en.wikipedia.org/wiki/Great-circle_distance 

# For the purposes of these calculations, this is probably accurate enough. 

D = np.sin(Dec2)*np.sin(Dec1) + np.cos(Dec1)*np.cos(Dec2)*np.cos(RA2-RA1) 

D = np.arccos(D) 

return D 

 

 

class RadiusObsMetric(BaseMetric): 

"""find the radius in the focal plane. returns things in degrees.""" 

 

def __init__(self, metricName='radiusObs', raCol='fieldRA', decCol='fieldDec', 

units='radians', **kwargs): 

self.raCol = raCol 

self.decCol = decCol 

super(RadiusObsMetric, self).__init__(col=[self.raCol, self.decCol], 

metricName=metricName, units=units, **kwargs) 

 

def run(self, dataSlice, slicePoint): 

ra = slicePoint['ra'] 

dec = slicePoint['dec'] 

distances = calcDist_cosines(ra, dec, np.radians(dataSlice[self.raCol]), 

np.radians(dataSlice[self.decCol])) 

distances = np.degrees(distances) 

return distances 

 

def reduceMean(self, distances): 

return np.mean(distances) 

 

def reduceRMS(self, distances): 

return np.std(distances) 

 

def reduceFullRange(self, distances): 

return np.max(distances)-np.min(distances)