Coverage for python/lsst/meas/algorithms/brightStarStamps.py: 24%

155 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-07-27 09:28 +0000

1# This file is part of meas_algorithms. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

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

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

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

12# (at your option) any later version. 

13# 

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

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

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22"""Collection of small images (stamps), each centered on a bright star.""" 

23 

24__all__ = ["BrightStarStamp", "BrightStarStamps"] 

25 

26import logging 

27from collections.abc import Collection 

28from dataclasses import dataclass 

29from functools import reduce 

30from operator import ior 

31 

32import numpy as np 

33from lsst.afw.geom import SpanSet, Stencil 

34from lsst.afw.image import MaskedImageF 

35from lsst.afw.math import Property, StatisticsControl, makeStatistics, stringToStatisticsProperty 

36from lsst.afw.table.io import Persistable 

37from lsst.geom import Point2I 

38 

39from .stamps import AbstractStamp, Stamps, readFitsWithOptions 

40 

41logger = logging.getLogger(__name__) 

42 

43 

44@dataclass 

45class BrightStarStamp(AbstractStamp): 

46 """Single stamp centered on a bright star, normalized by its annularFlux. 

47 

48 Parameters 

49 ---------- 

50 stamp_im : `~lsst.afw.image.MaskedImage` 

51 Pixel data for this postage stamp 

52 gaiaGMag : `float` 

53 Gaia G magnitude for the object in this stamp 

54 gaiaId : `int` 

55 Gaia object identifier 

56 position : `~lsst.geom.Point2I` 

57 Origin of the stamps in its origin exposure (pixels) 

58 archive_element : `~lsst.afw.table.io.Persistable` or None, optional 

59 Archive element (e.g. Transform or WCS) associated with this stamp. 

60 annularFlux : `float` or None, optional 

61 Flux in an annulus around the object 

62 """ 

63 

64 stamp_im: MaskedImageF 

65 gaiaGMag: float 

66 gaiaId: int 

67 position: Point2I 

68 archive_element: Persistable | None = None 

69 annularFlux: float | None = None 

70 minValidAnnulusFraction: float = 0.0 

71 

72 @classmethod 

73 def factory(cls, stamp_im, metadata, idx, archive_element=None, minValidAnnulusFraction=0.0): 

74 """This method is needed to service the FITS reader. We need a standard 

75 interface to construct objects like this. Parameters needed to 

76 construct this object are passed in via a metadata dictionary and then 

77 passed to the constructor of this class. This particular factory 

78 method requires keys: G_MAGS, GAIA_IDS, and ANNULAR_FLUXES. They should 

79 each point to lists of values. 

80 

81 Parameters 

82 ---------- 

83 stamp_im : `~lsst.afw.image.MaskedImage` 

84 Pixel data to pass to the constructor 

85 metadata : `dict` 

86 Dictionary containing the information 

87 needed by the constructor. 

88 idx : `int` 

89 Index into the lists in ``metadata`` 

90 archive_element : `~lsst.afw.table.io.Persistable` or None, optional 

91 Archive element (e.g. Transform or WCS) associated with this stamp. 

92 minValidAnnulusFraction : `float`, optional 

93 The fraction of valid pixels within the normalization annulus of a 

94 star. 

95 

96 Returns 

97 ------- 

98 brightstarstamp : `BrightStarStamp` 

99 An instance of this class 

100 """ 

101 if "X0S" in metadata and "Y0S" in metadata: 

102 x0 = metadata.getArray("X0S")[idx] 

103 y0 = metadata.getArray("Y0S")[idx] 

104 position = Point2I(x0, y0) 

105 else: 

106 position = None 

107 return cls( 

108 stamp_im=stamp_im, 

109 gaiaGMag=metadata.getArray("G_MAGS")[idx], 

110 gaiaId=metadata.getArray("GAIA_IDS")[idx], 

111 position=position, 

112 archive_element=archive_element, 

113 annularFlux=metadata.getArray("ANNULAR_FLUXES")[idx], 

114 minValidAnnulusFraction=minValidAnnulusFraction, 

115 ) 

116 

117 def measureAndNormalize( 

118 self, 

119 annulus: SpanSet, 

120 statsControl: StatisticsControl = StatisticsControl(), 

121 statsFlag: Property = stringToStatisticsProperty("MEAN"), 

122 badMaskPlanes: Collection[str] = ("BAD", "SAT", "NO_DATA"), 

123 ): 

124 """Compute "annularFlux", the integrated flux within an annulus 

125 around an object's center, and normalize it. 

126 

127 Since the center of bright stars are saturated and/or heavily affected 

128 by ghosts, we measure their flux in an annulus with a large enough 

129 inner radius to avoid the most severe ghosts and contain enough 

130 non-saturated pixels. 

131 

132 Parameters 

133 ---------- 

134 annulus : `~lsst.afw.geom.spanSet.SpanSet` 

135 SpanSet containing the annulus to use for normalization. 

136 statsControl : `~lsst.afw.math.statistics.StatisticsControl`, optional 

137 StatisticsControl to be used when computing flux over all pixels 

138 within the annulus. 

139 statsFlag : `~lsst.afw.math.statistics.Property`, optional 

140 statsFlag to be passed on to ``afwMath.makeStatistics`` to compute 

141 annularFlux. Defaults to a simple MEAN. 

142 badMaskPlanes : `collections.abc.Collection` [`str`] 

143 Collection of mask planes to ignore when computing annularFlux. 

144 """ 

145 stampSize = self.stamp_im.getDimensions() 

146 # create image: same pixel values within annulus, NO_DATA elsewhere 

147 maskPlaneDict = self.stamp_im.mask.getMaskPlaneDict() 

148 annulusImage = MaskedImageF(stampSize, planeDict=maskPlaneDict) 

149 annulusMask = annulusImage.mask 

150 annulusMask.array[:] = 2 ** maskPlaneDict["NO_DATA"] 

151 annulus.copyMaskedImage(self.stamp_im, annulusImage) 

152 # set mask planes to be ignored 

153 andMask = reduce(ior, (annulusMask.getPlaneBitMask(bm) for bm in badMaskPlanes)) 

154 statsControl.setAndMask(andMask) 

155 

156 annulusStat = makeStatistics(annulusImage, statsFlag, statsControl) 

157 # determining the number of valid (unmasked) pixels within the annulus 

158 unMasked = annulusMask.array.size - np.count_nonzero(annulusMask.array) 

159 # should we have some messages printed here? 

160 logger.info( 

161 "The Star's annulus contains %s valid pixels and the annulus itself contains %s pixels.", 

162 unMasked, 

163 annulus.getArea(), 

164 ) 

165 if unMasked > (annulus.getArea() * self.minValidAnnulusFraction): 

166 # compute annularFlux 

167 self.annularFlux = annulusStat.getValue() 

168 logger.info("Annular flux is: %s", self.annularFlux) 

169 else: 

170 raise RuntimeError( 

171 f"Less than {self.minValidAnnulusFraction * 100}% of pixels within the annulus are valid." 

172 ) 

173 if np.isnan(self.annularFlux): 

174 raise RuntimeError("Annular flux computation failed, likely because there are no valid pixels.") 

175 if self.annularFlux < 0: 

176 raise RuntimeError("The annular flux is negative. The stamp can not be normalized!") 

177 # normalize stamps 

178 self.stamp_im.image.array /= self.annularFlux 

179 return None 

180 

181 

182class BrightStarStamps(Stamps): 

183 """Collection of bright star stamps and associated metadata. 

184 

185 Parameters 

186 ---------- 

187 starStamps : `collections.abc.Sequence` [`BrightStarStamp`] 

188 Sequence of star stamps. Cannot contain both normalized and 

189 unnormalized stamps. 

190 innerRadius : `int`, optional 

191 Inner radius value, in pixels. This and ``outerRadius`` define the 

192 annulus used to compute the ``"annularFlux"`` values within each 

193 ``starStamp``. Must be provided if ``normalize`` is True. 

194 outerRadius : `int`, optional 

195 Outer radius value, in pixels. This and ``innerRadius`` define the 

196 annulus used to compute the ``"annularFlux"`` values within each 

197 ``starStamp``. Must be provided if ``normalize`` is True. 

198 nb90Rots : `int`, optional 

199 Number of 90 degree rotations required to compensate for detector 

200 orientation. 

201 metadata : `~lsst.daf.base.PropertyList`, optional 

202 Metadata associated with the bright stars. 

203 use_mask : `bool` 

204 If `True` read and write mask data. Default `True`. 

205 use_variance : `bool` 

206 If ``True`` read and write variance data. Default ``False``. 

207 use_archive : `bool` 

208 If ``True`` read and write an Archive that contains a Persistable 

209 associated with each stamp. In the case of bright stars, this is 

210 usually a ``TransformPoint2ToPoint2``, used to warp each stamp 

211 to the same pixel grid before stacking. 

212 

213 Raises 

214 ------ 

215 ValueError 

216 Raised if one of the star stamps provided does not contain the 

217 required keys. 

218 AttributeError 

219 Raised if there is a mix-and-match of normalized and unnormalized 

220 stamps, stamps normalized with different annulus definitions, or if 

221 stamps are to be normalized but annular radii were not provided. 

222 

223 Notes 

224 ----- 

225 A butler can be used to read only a part of the stamps, specified by a 

226 bbox: 

227 

228 >>> starSubregions = butler.get( 

229 "brightStarStamps", 

230 dataId, 

231 parameters={"bbox": bbox} 

232 ) 

233 """ 

234 

235 def __init__( 

236 self, 

237 starStamps, 

238 innerRadius=None, 

239 outerRadius=None, 

240 nb90Rots=None, 

241 metadata=None, 

242 use_mask=True, 

243 use_variance=False, 

244 use_archive=False, 

245 ): 

246 super().__init__(starStamps, metadata, use_mask, use_variance, use_archive) 

247 # Ensure stamps contain a flux measurement if and only if they are 

248 # already expected to be normalized 

249 self._checkNormalization(False, innerRadius, outerRadius) 

250 self._innerRadius, self._outerRadius = innerRadius, outerRadius 

251 if innerRadius is not None and outerRadius is not None: 

252 self.normalized = True 

253 else: 

254 self.normalized = False 

255 self.nb90Rots = nb90Rots 

256 

257 @classmethod 

258 def initAndNormalize( 

259 cls, 

260 starStamps, 

261 innerRadius, 

262 outerRadius, 

263 nb90Rots=None, 

264 metadata=None, 

265 use_mask=True, 

266 use_variance=False, 

267 use_archive=False, 

268 imCenter=None, 

269 discardNanFluxObjects=True, 

270 statsControl=StatisticsControl(), 

271 statsFlag=stringToStatisticsProperty("MEAN"), 

272 badMaskPlanes=("BAD", "SAT", "NO_DATA"), 

273 ): 

274 """Normalize a set of bright star stamps and initialize a 

275 BrightStarStamps instance. 

276 

277 Since the center of bright stars are saturated and/or heavily affected 

278 by ghosts, we measure their flux in an annulus with a large enough 

279 inner radius to avoid the most severe ghosts and contain enough 

280 non-saturated pixels. 

281 

282 Parameters 

283 ---------- 

284 starStamps : `collections.abc.Sequence` [`BrightStarStamp`] 

285 Sequence of star stamps. Cannot contain both normalized and 

286 unnormalized stamps. 

287 innerRadius : `int` 

288 Inner radius value, in pixels. This and ``outerRadius`` define the 

289 annulus used to compute the ``"annularFlux"`` values within each 

290 ``starStamp``. 

291 outerRadius : `int` 

292 Outer radius value, in pixels. This and ``innerRadius`` define the 

293 annulus used to compute the ``"annularFlux"`` values within each 

294 ``starStamp``. 

295 nb90Rots : `int`, optional 

296 Number of 90 degree rotations required to compensate for detector 

297 orientation. 

298 metadata : `~lsst.daf.base.PropertyList`, optional 

299 Metadata associated with the bright stars. 

300 use_mask : `bool` 

301 If `True` read and write mask data. Default `True`. 

302 use_variance : `bool` 

303 If ``True`` read and write variance data. Default ``False``. 

304 use_archive : `bool` 

305 If ``True`` read and write an Archive that contains a Persistable 

306 associated with each stamp. In the case of bright stars, this is 

307 usually a ``TransformPoint2ToPoint2``, used to warp each stamp 

308 to the same pixel grid before stacking. 

309 imCenter : `collections.abc.Sequence`, optional 

310 Center of the object, in pixels. If not provided, the center of the 

311 first stamp's pixel grid will be used. 

312 discardNanFluxObjects : `bool` 

313 Whether objects with NaN annular flux should be discarded. 

314 If False, these objects will not be normalized. 

315 statsControl : `~lsst.afw.math.statistics.StatisticsControl`, optional 

316 StatisticsControl to be used when computing flux over all pixels 

317 within the annulus. 

318 statsFlag : `~lsst.afw.math.statistics.Property`, optional 

319 statsFlag to be passed on to ``~lsst.afw.math.makeStatistics`` to 

320 compute annularFlux. Defaults to a simple MEAN. 

321 badMaskPlanes : `collections.abc.Collection` [`str`] 

322 Collection of mask planes to ignore when computing annularFlux. 

323 

324 Raises 

325 ------ 

326 ValueError 

327 Raised if one of the star stamps provided does not contain the 

328 required keys. 

329 AttributeError 

330 Raised if there is a mix-and-match of normalized and unnormalized 

331 stamps, stamps normalized with different annulus definitions, or if 

332 stamps are to be normalized but annular radii were not provided. 

333 """ 

334 if imCenter is None: 

335 stampSize = starStamps[0].stamp_im.getDimensions() 

336 imCenter = stampSize[0] // 2, stampSize[1] // 2 

337 # Create SpanSet of annulus 

338 outerCircle = SpanSet.fromShape(outerRadius, Stencil.CIRCLE, offset=imCenter) 

339 innerCircle = SpanSet.fromShape(innerRadius, Stencil.CIRCLE, offset=imCenter) 

340 annulus = outerCircle.intersectNot(innerCircle) 

341 # Initialize (unnormalized) brightStarStamps instance 

342 bss = cls( 

343 starStamps, 

344 innerRadius=None, 

345 outerRadius=None, 

346 nb90Rots=nb90Rots, 

347 metadata=metadata, 

348 use_mask=use_mask, 

349 use_variance=use_variance, 

350 use_archive=use_archive, 

351 ) 

352 # Ensure no stamps had already been normalized. 

353 bss._checkNormalization(True, innerRadius, outerRadius) 

354 bss._innerRadius, bss._outerRadius = innerRadius, outerRadius 

355 # Create a list to contain rejected stamps. 

356 rejects = [] 

357 # Apply normalization 

358 for stamp in bss._stamps: 

359 try: 

360 stamp.measureAndNormalize( 

361 annulus, statsControl=statsControl, statsFlag=statsFlag, badMaskPlanes=badMaskPlanes 

362 ) 

363 except RuntimeError as err: 

364 logger.error(err) 

365 # Optionally keep NaN flux objects, for bookkeeping purposes, 

366 # and to avoid having to re-find and redo the preprocessing 

367 # steps needed before bright stars can be subtracted. 

368 if discardNanFluxObjects: 

369 rejects.append(stamp) 

370 else: 

371 stamp.annularFlux = np.nan 

372 # Remove rejected stamps. 

373 if discardNanFluxObjects: 

374 for reject in rejects: 

375 bss._stamps.remove(reject) 

376 bss.normalized = True 

377 return bss 

378 

379 def _refresh_metadata(self): 

380 """Refresh metadata. Should be called before writing the object out.""" 

381 # add full list of positions, Gaia magnitudes, IDs and annularFlxes to 

382 # shared metadata 

383 self._metadata["G_MAGS"] = self.getMagnitudes() 

384 self._metadata["GAIA_IDS"] = self.getGaiaIds() 

385 positions = self.getPositions() 

386 self._metadata["X0S"] = [xy0[0] for xy0 in positions] 

387 self._metadata["Y0S"] = [xy0[1] for xy0 in positions] 

388 self._metadata["ANNULAR_FLUXES"] = self.getAnnularFluxes() 

389 self._metadata["NORMALIZED"] = self.normalized 

390 self._metadata["INNER_RADIUS"] = self._innerRadius 

391 self._metadata["OUTER_RADIUS"] = self._outerRadius 

392 if self.nb90Rots is not None: 

393 self._metadata["NB_90_ROTS"] = self.nb90Rots 

394 return None 

395 

396 @classmethod 

397 def readFits(cls, filename): 

398 """Build an instance of this class from a file. 

399 

400 Parameters 

401 ---------- 

402 filename : `str` 

403 Name of the file to read 

404 """ 

405 return cls.readFitsWithOptions(filename, None) 

406 

407 @classmethod 

408 def readFitsWithOptions(cls, filename, options): 

409 """Build an instance of this class with options. 

410 

411 Parameters 

412 ---------- 

413 filename : `str` 

414 Name of the file to read 

415 options : `PropertyList` 

416 Collection of metadata parameters 

417 """ 

418 stamps, metadata = readFitsWithOptions(filename, BrightStarStamp.factory, options) 

419 nb90Rots = metadata["NB_90_ROTS"] if "NB_90_ROTS" in metadata else None 

420 if metadata["NORMALIZED"]: 

421 return cls( 

422 stamps, 

423 innerRadius=metadata["INNER_RADIUS"], 

424 outerRadius=metadata["OUTER_RADIUS"], 

425 nb90Rots=nb90Rots, 

426 metadata=metadata, 

427 use_mask=metadata["HAS_MASK"], 

428 use_variance=metadata["HAS_VARIANCE"], 

429 use_archive=metadata["HAS_ARCHIVE"], 

430 ) 

431 else: 

432 return cls( 

433 stamps, 

434 nb90Rots=nb90Rots, 

435 metadata=metadata, 

436 use_mask=metadata["HAS_MASK"], 

437 use_variance=metadata["HAS_VARIANCE"], 

438 use_archive=metadata["HAS_ARCHIVE"], 

439 ) 

440 

441 def append(self, item, innerRadius=None, outerRadius=None): 

442 """Add an additional bright star stamp. 

443 

444 Parameters 

445 ---------- 

446 item : `BrightStarStamp` 

447 Bright star stamp to append. 

448 innerRadius : `int`, optional 

449 Inner radius value, in pixels. This and ``outerRadius`` define the 

450 annulus used to compute the ``"annularFlux"`` values within each 

451 ``BrightStarStamp``. 

452 outerRadius : `int`, optional 

453 Outer radius value, in pixels. This and ``innerRadius`` define the 

454 annulus used to compute the ``"annularFlux"`` values within each 

455 ``BrightStarStamp``. 

456 """ 

457 if not isinstance(item, BrightStarStamp): 

458 raise ValueError(f"Can only add instances of BrightStarStamp, got {type(item)}.") 

459 if (item.annularFlux is None) == self.normalized: 

460 raise AttributeError( 

461 "Trying to append an unnormalized stamp to a normalized BrightStarStamps " 

462 "instance, or vice-versa." 

463 ) 

464 else: 

465 self._checkRadius(innerRadius, outerRadius) 

466 self._stamps.append(item) 

467 return None 

468 

469 def extend(self, bss): 

470 """Extend BrightStarStamps instance by appending elements from another 

471 instance. 

472 

473 Parameters 

474 ---------- 

475 bss : `BrightStarStamps` 

476 Other instance to concatenate. 

477 """ 

478 if not isinstance(bss, BrightStarStamps): 

479 raise ValueError(f"Can only extend with a BrightStarStamps object. Got {type(bss)}.") 

480 self._checkRadius(bss._innerRadius, bss._outerRadius) 

481 self._stamps += bss._stamps 

482 

483 def getMagnitudes(self): 

484 """Retrieve Gaia G magnitudes for each star. 

485 

486 Returns 

487 ------- 

488 gaiaGMags : `list` [`float`] 

489 """ 

490 return [stamp.gaiaGMag for stamp in self._stamps] 

491 

492 def getGaiaIds(self): 

493 """Retrieve Gaia IDs for each star. 

494 

495 Returns 

496 ------- 

497 gaiaIds : `list` [`int`] 

498 """ 

499 return [stamp.gaiaId for stamp in self._stamps] 

500 

501 def getAnnularFluxes(self): 

502 """Retrieve normalization factors for each star. 

503 

504 These are computed by integrating the flux in annulus centered on the 

505 bright star, far enough from center to be beyond most severe ghosts and 

506 saturation. The inner and outer radii that define the annulus can be 

507 recovered from the metadata. 

508 

509 Returns 

510 ------- 

511 annularFluxes : `list` [`float`] 

512 """ 

513 return [stamp.annularFlux for stamp in self._stamps] 

514 

515 def selectByMag(self, magMin=None, magMax=None): 

516 """Return the subset of bright star stamps for objects with specified 

517 magnitude cuts (in Gaia G). 

518 

519 Parameters 

520 ---------- 

521 magMin : `float`, optional 

522 Keep only stars fainter than this value. 

523 magMax : `float`, optional 

524 Keep only stars brighter than this value. 

525 """ 

526 subset = [ 

527 stamp 

528 for stamp in self._stamps 

529 if (magMin is None or stamp.gaiaGMag > magMin) and (magMax is None or stamp.gaiaGMag < magMax) 

530 ] 

531 # This is an optimization to save looping over the init argument when 

532 # it is already guaranteed to be the correct type 

533 instance = BrightStarStamps( 

534 (), innerRadius=self._innerRadius, outerRadius=self._outerRadius, metadata=self._metadata 

535 ) 

536 instance._stamps = subset 

537 return instance 

538 

539 def _checkRadius(self, innerRadius, outerRadius): 

540 """Ensure provided annulus radius is consistent with that already 

541 present in the instance, or with arguments passed on at initialization. 

542 """ 

543 if innerRadius != self._innerRadius or outerRadius != self._outerRadius: 

544 raise AttributeError( 

545 "Trying to mix stamps normalized with annulus radii " 

546 f"{innerRadius, outerRadius} with those of BrightStarStamp instance\n" 

547 f"(computed with annular radii {self._innerRadius, self._outerRadius})." 

548 ) 

549 

550 def _checkNormalization(self, normalize, innerRadius, outerRadius): 

551 """Ensure there is no mixing of normalized and unnormalized stars, and 

552 that, if requested, normalization can be performed. 

553 """ 

554 noneFluxCount = self.getAnnularFluxes().count(None) 

555 nStamps = len(self) 

556 nFluxVals = nStamps - noneFluxCount 

557 if noneFluxCount and noneFluxCount < nStamps: 

558 # at least one stamp contains an annularFlux value (i.e. has been 

559 # normalized), but not all of them do 

560 raise AttributeError( 

561 f"Only {nFluxVals} stamps contain an annularFlux value.\nAll stamps in a " 

562 "BrightStarStamps instance must either be normalized with the same annulus " 

563 "definition, or none of them can contain an annularFlux value." 

564 ) 

565 elif normalize: 

566 # stamps are to be normalized; ensure annular radii are specified 

567 # and they have no annularFlux 

568 if innerRadius is None or outerRadius is None: 

569 raise AttributeError( 

570 "For stamps to be normalized (normalize=True), please provide a valid " 

571 "value (in pixels) for both innerRadius and outerRadius." 

572 ) 

573 elif noneFluxCount < nStamps: 

574 raise AttributeError( 

575 f"{nFluxVals} stamps already contain an annularFlux value. For stamps to " 

576 "be normalized, all their annularFlux must be None." 

577 ) 

578 elif innerRadius is not None and outerRadius is not None: 

579 # Radii provided, but normalize=False; check that stamps 

580 # already contain annularFluxes 

581 if noneFluxCount: 

582 raise AttributeError( 

583 f"{noneFluxCount} stamps contain no annularFlux, but annular radius " 

584 "values were provided and normalize=False.\nTo normalize stamps, set " 

585 "normalize to True." 

586 ) 

587 else: 

588 # At least one radius value is missing; ensure no stamps have 

589 # already been normalized 

590 if nFluxVals: 

591 raise AttributeError( 

592 f"{nFluxVals} stamps contain an annularFlux value. If stamps have " 

593 "been normalized, the innerRadius and outerRadius values used must " 

594 "be provided." 

595 ) 

596 return None