Coverage for tests/test_plotImageSubtractionCutouts.py: 17%

234 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-18 12:10 +0000

1# This file is part of analysis_ap. 

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 

22import os 

23import pickle 

24import sys 

25import tempfile 

26import unittest 

27 

28import lsst.afw.table 

29import lsst.geom 

30import lsst.meas.base.tests 

31import lsst.utils.tests 

32import numpy as np 

33import pandas as pd 

34import PIL 

35from lsst.analysis.ap import plotImageSubtractionCutouts 

36from lsst.meas.algorithms import SourceDetectionTask 

37 

38# Sky center chosen to test metadata annotations (3-digit RA and negative Dec). 

39skyCenter = lsst.geom.SpherePoint(245.0, -45.0, lsst.geom.degrees) 

40 

41# A two-row mock APDB DiaSource table. 

42DATA = pd.DataFrame( 

43 data={ 

44 "diaSourceId": [506428274000265570, 527736141479149732], 

45 "ra": [skyCenter.getRa().asDegrees()+0.0001, skyCenter.getRa().asDegrees()-0.0001], 

46 "dec": [skyCenter.getDec().asDegrees()+0.0001, skyCenter.getDec().asDegrees()-0.001], 

47 "detector": [50, 60], 

48 "visit": [1234, 5678], 

49 "instrument": ["TestMock", "TestMock"], 

50 "band": ['r', 'g'], 

51 "psfFlux": [1234.5, 1234.5], 

52 "psfFluxErr": [123.5, 123.5], 

53 "snr": [10.0, 11.0], 

54 "psfChi2": [40.0, 50.0], 

55 "psfNdata": [10, 100], 

56 "apFlux": [2222.5, 3333.4], 

57 "apFluxErr": [222.5, 333.4], 

58 "scienceFlux": [2222000.5, 33330000.4], 

59 "scienceFluxErr": [22200.5, 333000.4], 

60 "isDipole": [True, False], 

61 "reliability": [0, 1.0], 

62 # First diaSource has all flags set, and second diaSource has none 

63 "slot_PsfFlux_flag": [1, 0], 

64 "slot_PsfFlux_flag_noGoodPixels": [1, 0], 

65 "slot_PsfFlux_flag_edge": [1, 0], 

66 "slot_ApFlux_flag": [1, 0], 

67 "slot_ApFlux_flag_apertureTruncated": [1, 0], 

68 "ip_diffim_forced_PsfFlux_flag": [1, 0], 

69 "ip_diffim_forced_PsfFlux_flag_noGoodPixels": [1, 0], 

70 "ip_diffim_forced_PsfFlux_flag_edge": [1, 0], 

71 "pixelFlags_edge": [1, 0], 

72 "pixelFlags_interpolated": [1, 0], 

73 "pixelFlags_interpolatedCenter": [1, 0], 

74 "pixelFlags_saturated": [1, 0], 

75 "pixelFlags_saturatedCenter": [1, 0], 

76 "pixelFlags_cr": [1, 0], 

77 "pixelFlags_crCenter": [1, 0], 

78 "pixelFlags_bad": [1, 0], 

79 "pixelFlags_suspect": [1, 0], 

80 "pixelFlags_suspectCenter": [1, 0], 

81 "slot_Centroid_flag": [1, 0], 

82 "slot_Shape_flag": [1, 0], 

83 "slot_Shape_flag_no_pixels": [1, 0], 

84 "slot_Shape_flag_not_contained": [1, 0], 

85 "slot_Shape_flag_parent_source": [1, 0], 

86 } 

87) 

88 

89 

90def make_mock_catalog(image): 

91 """Make a simple SourceCatalog from the image, containing Footprints. 

92 """ 

93 schema = lsst.afw.table.SourceTable.makeMinimalSchema() 

94 table = lsst.afw.table.SourceTable.make(schema) 

95 detect = SourceDetectionTask() 

96 return detect.run(table, image).sources 

97 

98 

99class TestPlotImageSubtractionCutouts(lsst.utils.tests.TestCase): 

100 """Test that PlotImageSubtractionCutoutsTask generates images and manifest 

101 files correctly. 

102 """ 

103 def setUp(self): 

104 bbox = lsst.geom.Box2I(lsst.geom.Point2I(0, 0), lsst.geom.Point2I(100, 100)) 

105 # source at the center of the image 

106 self.centroid = lsst.geom.Point2D(50, 50) 

107 dataset = lsst.meas.base.tests.TestDataset(bbox, crval=skyCenter) 

108 self.scale = 0.3 # arbitrary arcseconds/pixel 

109 dataset.addSource(instFlux=1e5, centroid=self.centroid) 

110 self.science, self.scienceCat = dataset.realize( 

111 noise=1000.0, schema=dataset.makeMinimalSchema() 

112 ) 

113 self.template, self.templateCat = dataset.realize( 

114 noise=5.0, schema=dataset.makeMinimalSchema() 

115 ) 

116 # A simple image difference to have something to plot. 

117 self.difference = lsst.afw.image.ExposureF(self.science, deep=True) 

118 self.difference.image -= self.template.image 

119 

120 def test_generate_image(self): 

121 """Test that we get some kind of image out. 

122 

123 It's useful to have a person look at the output via: 

124 im.show() 

125 """ 

126 # output_path does nothing here, since we never write the file to disk. 

127 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(output_path="") 

128 cutout = cutouts.generate_image(self.science, self.template, self.difference, skyCenter, self.scale) 

129 with PIL.Image.open(cutout) as im: 

130 # NOTE: uncomment this to show the resulting image. 

131 # im.show() 

132 # NOTE: the dimensions here are determined by the matplotlib figure 

133 # size (in inches) and the dpi (default=100), plus borders. 

134 self.assertEqual((im.height, im.width), (233, 630)) 

135 

136 def test_generate_image_larger_cutout(self): 

137 """A different cutout size: the resulting cutout image is the same 

138 size but shows more pixels. 

139 """ 

140 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

141 config.sizes = [100] 

142 # output_path does nothing here, since we never write the file to disk. 

143 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, output_path="") 

144 cutout = cutouts.generate_image(self.science, self.template, self.difference, skyCenter, self.scale) 

145 with PIL.Image.open(cutout) as im: 

146 # NOTE: uncomment this to show the resulting image. 

147 # im.show() 

148 # NOTE: the dimensions here are determined by the matplotlib figure 

149 # size (in inches) and the dpi (default=100), plus borders. 

150 self.assertEqual((im.height, im.width), (233, 630)) 

151 

152 def test_generate_image_metadata(self): 

153 """Test that we can add metadata to the image; it changes the height 

154 a lot, and the width a little for the text boxes. 

155 

156 It's useful to have a person look at the output via: 

157 im.show() 

158 """ 

159 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

160 config.add_metadata = True 

161 # output_path does nothing here, since we never write the file to disk. 

162 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, output_path="") 

163 cutout = cutouts.generate_image(self.science, 

164 self.template, 

165 self.difference, 

166 skyCenter, 

167 self.scale, 

168 source=DATA.iloc[0]) 

169 with PIL.Image.open(cutout) as im: 

170 # NOTE: uncomment this to show the resulting image. 

171 # im.show() 

172 # NOTE: the dimensions here are determined by the matplotlib figure 

173 # size (in inches) and the dpi (default=100), plus borders. 

174 self.assertEqual((im.height, im.width), (343, 645)) 

175 

176 # A cutout without any flags: the dimensions should be unchanged. 

177 cutout = cutouts.generate_image(self.science, 

178 self.template, 

179 self.difference, 

180 skyCenter, 

181 self.scale, 

182 source=DATA.iloc[1]) 

183 with PIL.Image.open(cutout) as im: 

184 # NOTE: uncomment this to show the resulting image. 

185 # im.show() 

186 # NOTE: the dimensions here are determined by the matplotlib figure 

187 # size (in inches) and the dpi (default=100), plus borders. 

188 self.assertEqual((im.height, im.width), (343, 645)) 

189 

190 def test_generate_image_multisize_cutouts_without_metadata(self): 

191 """Multiple cutout sizes: the resulting image is larger in size 

192 and contains cutouts of multiple sizes. 

193 """ 

194 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

195 config.sizes = [32, 64] 

196 # output_path does nothing here, since we never write the file to disk. 

197 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, output_path="") 

198 cutout = cutouts.generate_image(self.science, self.template, self.difference, skyCenter, self.scale) 

199 with PIL.Image.open(cutout) as im: 

200 # NOTE: uncomment this to show the resulting image. 

201 # im.show() 

202 # NOTE: the dimensions here are determined by the matplotlib figure 

203 # size (in inches) and the dpi (default=100), plus borders. 

204 self.assertEqual((im.height, im.width), (450, 630)) 

205 

206 def test_generate_image_multisize_cutouts_with_metadata(self): 

207 """Test that we can add metadata to the image; it changes the height 

208 a lot, and the width a little for the text boxes. 

209 

210 It's useful to have a person look at the output via: 

211 im.show() 

212 """ 

213 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

214 config.add_metadata = True 

215 config.sizes = [32, 64] 

216 # output_path does nothing here, since we never write the file to disk. 

217 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, output_path="") 

218 cutout = cutouts.generate_image(self.science, 

219 self.template, 

220 self.difference, 

221 skyCenter, 

222 self.scale, 

223 source=DATA.iloc[0]) 

224 with PIL.Image.open(cutout) as im: 

225 # NOTE: uncomment this to show the resulting image. 

226 # im.show() 

227 # NOTE: the dimensions here are determined by the matplotlib figure 

228 # size (in inches) and the dpi (default=100), plus borders. 

229 self.assertEqual((im.height, im.width), (576, 645)) 

230 

231 # A cutout without any flags: the dimensions should be unchanged. 

232 cutout = cutouts.generate_image(self.science, 

233 self.template, 

234 self.difference, 

235 skyCenter, 

236 self.scale, 

237 source=DATA.iloc[1]) 

238 with PIL.Image.open(cutout) as im: 

239 # NOTE: uncomment this to show the resulting image. 

240 # im.show() 

241 # NOTE: the dimensions here are determined by the matplotlib figure 

242 # size (in inches) and the dpi (default=100), plus borders. 

243 self.assertEqual((im.height, im.width), (576, 645)) 

244 

245 def test_generate_image_and_save_as_numpy(self): 

246 """Test that we can save an image as a .npy file and then read it back. 

247 """ 

248 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

249 config.sizes = [100] 

250 with tempfile.TemporaryDirectory() as path: 

251 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

252 output_path=path) 

253 cutouts.generate_image(self.science, self.template, self.difference, skyCenter, self.scale, 

254 dia_source_id=506428274000265570, save_as_numpy=True) 

255 numpy_dir_path = os.path.join(path, "raw_npy") 

256 for file in os.listdir(numpy_dir_path): 

257 image_with_channel = np.load(numpy_dir_path + "/" + file) 

258 image = np.squeeze(image_with_channel, axis=0) 

259 self.assertEqual((image.shape[0], image.shape[1]), (100, 100)) 

260 

261 def test_write_images(self): 

262 """Test that images get written to a temporary directory.""" 

263 butler = unittest.mock.Mock(spec=lsst.daf.butler.Butler) 

264 # We don't care what the output images look like here, just that 

265 # butler.get() returns an Exposure for every call. 

266 butler.get.return_value = self.science 

267 

268 with tempfile.TemporaryDirectory() as path: 

269 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

270 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

271 output_path=path) 

272 result = cutouts.write_images(DATA, butler) 

273 self.assertEqual(result, list(DATA["diaSourceId"])) 

274 for file in ("images/506428274000260000/506428274000265570.png", 

275 "images/527736141479140000/527736141479149732.png"): 

276 filename = os.path.join(path, file) 

277 self.assertTrue(os.path.exists(filename)) 

278 with PIL.Image.open(filename) as image: 

279 self.assertEqual(image.format, "PNG") 

280 

281 def test_use_footprint(self): 

282 """Test the use_footprint config option, generating a fake diaSrc 

283 catalog that contains footprints that get used instead of config.sizes. 

284 """ 

285 butler = unittest.mock.Mock(spec=lsst.daf.butler.Butler) 

286 

287 def mock_get(dataset, dataId, *args, **kwargs): 

288 if "_diaSrc" in dataset: 

289 # The science image is the only mock image with a source in it. 

290 catalog = make_mock_catalog(self.science) 

291 # Assign the matching source id to the detection. 

292 match = DATA["visit"] == dataId["visit"] 

293 catalog["id"] = DATA["diaSourceId"].to_numpy()[match][0] 

294 return catalog 

295 else: 

296 return self.science 

297 

298 butler.get.side_effect = mock_get 

299 

300 with tempfile.TemporaryDirectory() as path: 

301 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

302 config.use_footprint = True 

303 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

304 output_path=path) 

305 result = cutouts.write_images(DATA, butler) 

306 self.assertEqual(result, list(DATA["diaSourceId"])) 

307 for file in ("images/506428274000260000/506428274000265570.png", 

308 "images/527736141479140000/527736141479149732.png"): 

309 filename = os.path.join(path, file) 

310 self.assertTrue(os.path.exists(filename)) 

311 with PIL.Image.open(filename) as image: 

312 self.assertEqual(image.format, "PNG") 

313 

314 def test_write_images_exception(self): 

315 """Test that write_images() catches errors in loading data.""" 

316 butler = unittest.mock.Mock(spec=lsst.daf.butler.Butler) 

317 err = "Dataset not found" 

318 butler.get.side_effect = LookupError(err) 

319 

320 with tempfile.TemporaryDirectory() as path: 

321 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

322 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

323 output_path=path) 

324 

325 with self.assertLogs("lsst.plotImageSubtractionCutouts", "ERROR") as cm: 

326 cutouts.write_images(DATA, butler) 

327 self.assertIn( 

328 "LookupError processing diaSourceId 506428274000265570: Dataset not found", cm.output[0] 

329 ) 

330 self.assertIn( 

331 "LookupError processing diaSourceId 527736141479149732: Dataset not found", cm.output[1] 

332 ) 

333 

334 def check_make_manifest(self, url_root, url_list): 

335 """Check that make_manifest returns an appropriate DataFrame.""" 

336 data = [5, 10, 20] 

337 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

338 config.url_root = url_root 

339 # output_path does nothing here 

340 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, output_path="") 

341 manifest = cutouts._make_manifest(data) 

342 self.assertEqual(manifest["metadata:diaSourceId"].to_list(), [5, 10, 20]) 

343 self.assertEqual(manifest["location:1"].to_list(), url_list) 

344 

345 def test_make_manifest(self): 

346 # check without an ending slash 

347 root = "http://example.org/zooniverse" 

348 url_list = [ 

349 f"{root}/images/5.png", 

350 f"{root}/images/10.png", 

351 f"{root}/images/20.png", 

352 ] 

353 self.check_make_manifest(root, url_list) 

354 

355 # check with an ending slash 

356 root = "http://example.org/zooniverse/" 

357 url_list = [ 

358 f"{root}images/5.png", 

359 f"{root}images/10.png", 

360 f"{root}images/20.png", 

361 ] 

362 self.check_make_manifest(root, url_list) 

363 

364 def test_pickle(self): 

365 """Test that the task is pickleable (necessary for multiprocessing). 

366 """ 

367 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

368 config.sizes = [63] 

369 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

370 output_path="something") 

371 other = pickle.loads(pickle.dumps(cutouts)) 

372 self.assertEqual(cutouts.config.sizes, other.config.sizes) 

373 self.assertEqual(cutouts._output_path, other._output_path) 

374 

375 

376class TestPlotImageSubtractionCutoutsMain(lsst.utils.tests.TestCase): 

377 """Test the commandline interface main() function via mocks.""" 

378 def setUp(self): 

379 datadir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data/") 

380 self.sqlitefile = os.path.join(datadir, "apdb.sqlite3") 

381 self.repo = "/not/a/real/butler" 

382 self.collection = "mockRun" 

383 self.outputPath = "/an/output/path" 

384 self.configFile = os.path.join(datadir, "plotImageSubtractionCutoutsConfig.py") 

385 self.instrument = "LATISS" 

386 

387 # DM-39501: mock butler, until detector/visit are in APDB. 

388 butlerPatch = unittest.mock.patch("lsst.daf.butler.Butler") 

389 self._butler = butlerPatch.start() 

390 self.addCleanup(butlerPatch.stop) 

391 # DM-39501: mock unpacker, until detector/visit are in APDB. 

392 import lsst.obs.lsst 

393 universe = lsst.daf.butler.DimensionUniverse() 

394 data_id = lsst.daf.butler.DataCoordinate.standardize({"instrument": self.instrument}, 

395 universe=universe) 

396 # ObservationDimensionPacker is harder to use in a butler-less 

397 # environment, so we need a temporary dependency on obs_lsst until 

398 # this all goes away on DM-39501. 

399 packer = lsst.obs.lsst.RubinDimensionPacker(data_id, is_exposure=False) 

400 instrumentPatch = unittest.mock.patch.object(lsst.pipe.base.Instrument, 

401 "make_default_dimension_packer", 

402 return_value=packer) 

403 self._instrument = instrumentPatch.start() 

404 self.addCleanup(instrumentPatch.stop) 

405 

406 def test_main_args(self): 

407 """Test typical arguments to main().""" 

408 args = [ 

409 "plotImageSubtractionCutouts", 

410 f"--sqlitefile={self.sqlitefile}", 

411 f"--collections={self.collection}", 

412 f"-C={self.configFile}", 

413 f"--instrument={self.instrument}", 

414 self.repo, 

415 self.outputPath, 

416 ] 

417 with unittest.mock.patch.object( 

418 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, "run", autospec=True 

419 ) as run, unittest.mock.patch.object(sys, "argv", args): 

420 plotImageSubtractionCutouts.main() 

421 self.assertEqual(self._butler.call_args.args, (self.repo,)) 

422 self.assertEqual( 

423 self._butler.call_args.kwargs, {"collections": [self.collection]} 

424 ) 

425 # NOTE: can't easily test the `data` arg to run, as select_sources 

426 # reads in a random order every time. 

427 self.assertEqual(run.call_args.args[2], self._butler.return_value) 

428 

429 def test_main_args_no_collections(self): 

430 """Test with no collections argument.""" 

431 args = [ 

432 "plotImageSubtractionCutouts", 

433 f"--sqlitefile={self.sqlitefile}", 

434 f"-C={self.configFile}", 

435 f"--instrument={self.instrument}", 

436 self.repo, 

437 self.outputPath, 

438 ] 

439 with unittest.mock.patch.object( 

440 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, "run", autospec=True 

441 ) as run, unittest.mock.patch.object(sys, "argv", args): 

442 plotImageSubtractionCutouts.main() 

443 self.assertEqual(self._butler.call_args.args, (self.repo,)) 

444 self.assertEqual(self._butler.call_args.kwargs, {"collections": None}) 

445 self.assertIsInstance(run.call_args.args[1], pd.DataFrame) 

446 self.assertEqual(run.call_args.args[2], self._butler.return_value) 

447 

448 def test_main_collection_list(self): 

449 """Test passing a list of collections.""" 

450 collections = ["mock1", "mock2", "mock3"] 

451 args = [ 

452 "plotImageSubtractionCutouts", 

453 f"--sqlitefile={self.sqlitefile}", 

454 f"--instrument={self.instrument}", 

455 self.repo, 

456 self.outputPath, 

457 f"-C={self.configFile}", 

458 "--collections", 

459 ] 

460 args.extend(collections) 

461 with unittest.mock.patch.object( 

462 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, "run", autospec=True 

463 ) as run, unittest.mock.patch.object(sys, "argv", args): 

464 plotImageSubtractionCutouts.main() 

465 self.assertEqual(self._butler.call_args.args, (self.repo,)) 

466 self.assertEqual( 

467 self._butler.call_args.kwargs, {"collections": collections} 

468 ) 

469 self.assertIsInstance(run.call_args.args[1], pd.DataFrame) 

470 self.assertEqual(run.call_args.args[2], self._butler.return_value) 

471 

472 def test_main_args_limit_offset(self): 

473 """Test typical arguments to main().""" 

474 args = [ 

475 "plotImageSubtractionCutouts", 

476 f"--sqlitefile={self.sqlitefile}", 

477 f"--collections={self.collection}", 

478 f"-C={self.configFile}", 

479 f"--instrument={self.instrument}", 

480 "--all", 

481 "--limit=5", 

482 self.repo, 

483 self.outputPath, 

484 ] 

485 with unittest.mock.patch.object( 

486 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, 

487 "write_images", 

488 autospec=True, 

489 return_value=[5] 

490 ) as write_images, unittest.mock.patch.object( 

491 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, 

492 "write_manifest", 

493 autospec=True 

494 ) as write_manifest, unittest.mock.patch.object(sys, "argv", args): 

495 plotImageSubtractionCutouts.main() 

496 self.assertEqual(self._butler.call_args.args, (self.repo,)) 

497 self.assertEqual( 

498 self._butler.call_args.kwargs, {"collections": [self.collection]} 

499 ) 

500 self.assertIsInstance(write_images.call_args.args[1], pd.DataFrame) 

501 self.assertEqual(write_images.call_args.args[2], self._butler.return_value) 

502 # The test apdb contains 290 DiaSources, so we get the return of 

503 # `write_images` (enforced as `5` above) 58 times. 

504 self.assertEqual(write_manifest.call_args.args[1], 58*[5]) 

505 

506 @unittest.skip("Mock and multiprocess don't mix: https://github.com/python/cpython/issues/100090") 

507 def test_main_args_multiprocessing(self): 

508 """Test running with multiprocessing. 

509 """ 

510 args = [ 

511 "plotImageSubtractionCutouts", 

512 f"--sqlitefile={self.sqlitefile}", 

513 f"--collections={self.collection}", 

514 "-j2", 

515 f"-C={self.configFile}", 

516 f"--instrument={self.instrument}", 

517 self.repo, 

518 self.outputPath, 

519 ] 

520 with unittest.mock.patch.object( 

521 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, "run", autospec=True 

522 ) as run, unittest.mock.patch.object(sys, "argv", args): 

523 plotImageSubtractionCutouts.main() 

524 self.assertEqual(self._butler.call_args.args, (self.repo,)) 

525 self.assertEqual(self._butler.call_args.kwargs, {"collections": [self.collection]}) 

526 # NOTE: can't easily test the `data` arg to run, as select_sources 

527 # reads in a random order every time. 

528 self.assertEqual(run.call_args.args[2], self._butler.return_value) 

529 

530 

531class TestCutoutPath(lsst.utils.tests.TestCase): 

532 def test_normal_path(self): 

533 """Can the path manager handles non-chunked paths? 

534 """ 

535 manager = plotImageSubtractionCutouts.CutoutPath("some/root/path") 

536 path = manager(id=12345678) 

537 self.assertEqual(path, "some/root/path/images/12345678.png") 

538 

539 def test_chunking(self): 

540 """Can the path manager handle ids chunked into 10,000 file 

541 directories? 

542 """ 

543 manager = plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=10000) 

544 path = manager(id=12345678) 

545 self.assertEqual(path, "some/root/path/images/12340000/12345678.png") 

546 

547 def test_chunk_sizes(self): 

548 """Test valid and invalid values for the chunk_size parameter. 

549 """ 

550 with self.assertRaisesRegex(RuntimeError, "chunk_size must be a power of 10"): 

551 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=123) 

552 

553 with self.assertRaisesRegex(RuntimeError, "chunk_size must be a power of 10"): 

554 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=12300) 

555 

556 # should not raise 

557 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=1000) 

558 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=1000000) 

559 

560 

561class TestMemory(lsst.utils.tests.MemoryTestCase): 

562 pass 

563 

564 

565def setup_module(module): 

566 lsst.utils.tests.init() 

567 

568 

569if __name__ == "__main__": 569 ↛ 570line 569 didn't jump to line 570, because the condition on line 569 was never true

570 lsst.utils.tests.init() 

571 unittest.main()