Coverage for tests / test_plotImageSubtractionCutouts.py: 20%

241 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-23 09:01 +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 "psfFlux_flag": [1, 0], 

64 "psfFlux_flag_noGoodPixels": [1, 0], 

65 "psfFlux_flag_edge": [1, 0], 

66 "apFlux_flag": [1, 0], 

67 "apFlux_flag_apertureTruncated": [1, 0], 

68 "forced_PsfFlux_flag": [1, 0], 

69 "forced_PsfFlux_flag_noGoodPixels": [1, 0], 

70 "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 "centroid_flag": [1, 0], 

82 "shape_flag": [1, 0], 

83 "shape_flag_no_pixels": [1, 0], 

84 "shape_flag_not_contained": [1, 0], 

85 "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), (245, 651)) 

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), (245, 651)) 

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), (349, 655)) 

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), (349, 655)) 

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), (480, 651)) 

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), (591, 655)) 

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), (591, 655)) 

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 dia_source_id = 506428274000265570 

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

255 dia_source_id=dia_source_id, save_as_numpy=True) 

256 numpy_dir_path = cutouts.numpy_path.directory(dia_source_id) 

257 for file in os.listdir(numpy_dir_path): 

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

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

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

261 

262 def test_write_images(self): 

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

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

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

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

267 butler.get.return_value = self.science 

268 

269 with tempfile.TemporaryDirectory() as path: 

270 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

271 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

272 output_path=path) 

273 result = cutouts.write_images(DATA, butler) 

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

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

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

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

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

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

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

281 

282 def test_use_footprint(self): 

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

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

285 """ 

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

287 

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

289 if "_diaSrc" in dataset: 

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

291 catalog = make_mock_catalog(self.science) 

292 # Assign the matching source id to the detection. 

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

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

295 return catalog 

296 else: 

297 return self.science 

298 

299 butler.get.side_effect = mock_get 

300 

301 with tempfile.TemporaryDirectory() as path: 

302 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

303 config.use_footprint = True 

304 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

305 output_path=path) 

306 result = cutouts.write_images(DATA, butler) 

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

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

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

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

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

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

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

314 

315 def test_write_images_exception(self): 

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

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

318 err = "Dataset not found" 

319 butler.get.side_effect = LookupError(err) 

320 

321 with tempfile.TemporaryDirectory() as path: 

322 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

323 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

324 output_path=path) 

325 

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

327 cutouts.write_images(DATA, butler) 

328 self.assertIn( 

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

330 ) 

331 self.assertIn( 

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

333 ) 

334 

335 def check_make_manifest(self, url_root, url_list): 

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

337 data = [5, 10, 20] 

338 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

339 config.url_root = url_root 

340 # output_path does nothing here 

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

342 manifest = cutouts._make_manifest(data) 

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

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

345 

346 def test_make_manifest(self): 

347 # check without an ending slash 

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

349 url_list = [ 

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

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

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

353 ] 

354 self.check_make_manifest(root, url_list) 

355 

356 # check with an ending slash 

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

358 url_list = [ 

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

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

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

362 ] 

363 self.check_make_manifest(root, url_list) 

364 

365 def test_pickle(self): 

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

367 """ 

368 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

369 config.sizes = [63] 

370 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

371 output_path="something") 

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

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

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

375 

376 

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

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

379 def setUp(self): 

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

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

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

383 self.collection = "mockRun" 

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

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

386 self.instrument = "LATISS" 

387 

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

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

390 self._butler = butlerPatch.start() 

391 self.addCleanup(butlerPatch.stop) 

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

393 import lsst.obs.lsst 

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

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

396 universe=universe) 

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

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

399 # this all goes away on DM-39501. 

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

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

402 "make_default_dimension_packer", 

403 return_value=packer) 

404 self._instrument = instrumentPatch.start() 

405 self.addCleanup(instrumentPatch.stop) 

406 

407 def test_main_args(self): 

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

409 args = [ 

410 "plotImageSubtractionCutouts", 

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

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

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

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 self.repo, 

436 self.outputPath, 

437 ] 

438 with unittest.mock.patch.object( 

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

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

441 plotImageSubtractionCutouts.main() 

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

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

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

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

446 

447 def test_main_collection_list(self): 

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

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

450 args = [ 

451 "plotImageSubtractionCutouts", 

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

453 self.repo, 

454 self.outputPath, 

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

456 "--collections", 

457 ] 

458 args.extend(collections) 

459 with unittest.mock.patch.object( 

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

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

462 plotImageSubtractionCutouts.main() 

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

464 self.assertEqual( 

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

466 ) 

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

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

469 

470 def test_main_args_limit_offset(self): 

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

472 args = [ 

473 "plotImageSubtractionCutouts", 

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

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

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

477 "--all", 

478 "--limit=5", 

479 self.repo, 

480 self.outputPath, 

481 ] 

482 with unittest.mock.patch.object( 

483 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, 

484 "write_images", 

485 autospec=True, 

486 return_value=[5] 

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

488 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, 

489 "write_manifest", 

490 autospec=True 

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

492 plotImageSubtractionCutouts.main() 

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

494 self.assertEqual( 

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

496 ) 

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

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

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

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

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

502 

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

504 def test_main_args_multiprocessing(self): 

505 """Test running with multiprocessing. 

506 """ 

507 args = [ 

508 "plotImageSubtractionCutouts", 

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

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

511 "-j2", 

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

513 self.repo, 

514 self.outputPath, 

515 ] 

516 with unittest.mock.patch.object( 

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

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

519 plotImageSubtractionCutouts.main() 

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

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

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

523 # reads in a random order every time. 

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

525 

526 

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

528 

529 def setUp(self): 

530 self.id = 12345678 

531 

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=self.id, filename=f"{self.id}.png") 

537 self.assertEqual(path, f"some/root/path/images/{self.id}.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=self.id, filename=f"{self.id}.png") 

545 self.assertEqual(path, f"some/root/path/images/12340000/{self.id}.png") 

546 

547 def test_subdir(self): 

548 manager = plotImageSubtractionCutouts.CutoutPath("some/root/path", subdirectory="foo") 

549 path = manager(id=self.id, filename=f"{self.id}.png") 

550 self.assertEqual(path, f"some/root/path/foo/{self.id}.png") 

551 

552 def test_chunk_sizes(self): 

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

554 """ 

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

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

557 

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

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

560 

561 # should not raise 

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

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

564 

565 

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

567 pass 

568 

569 

570def setup_module(module): 

571 lsst.utils.tests.init() 

572 

573 

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

575 lsst.utils.tests.init() 

576 unittest.main()