Coverage for tests/test_plotImageSubtractionCutouts.py: 97%

250 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-23 09:35 +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.cameraGeom.testUtils 

29import lsst.afw.image 

30import lsst.afw.table 

31import lsst.geom 

32import lsst.meas.base.tests 

33import lsst.utils.tests 

34import numpy as np 

35import pandas as pd 

36import PIL 

37from lsst.analysis.ap import plotImageSubtractionCutouts 

38from lsst.analysis.ap.imageUtils import as_visit_image 

39from lsst.meas.algorithms import SourceDetectionTask 

40 

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

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

43 

44# A two-row mock APDB DiaSource table. 

45DATA = pd.DataFrame( 

46 data={ 

47 "diaSourceId": [506428274000265570, 527736141479149732], 

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

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

50 "detector": [50, 60], 

51 "visit": [1234, 5678], 

52 "instrument": ["TestMock", "TestMock"], 

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

54 "psfFlux": [1234.5, 1234.5], 

55 "psfFluxErr": [123.5, 123.5], 

56 "snr": [10.0, 11.0], 

57 "psfChi2": [40.0, 50.0], 

58 "psfNdata": [10, 100], 

59 "apFlux": [2222.5, 3333.4], 

60 "apFluxErr": [222.5, 333.4], 

61 "scienceFlux": [2222000.5, 33330000.4], 

62 "scienceFluxErr": [22200.5, 333000.4], 

63 "isDipole": [True, False], 

64 "reliability": [0, 1.0], 

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

66 "psfFlux_flag": [1, 0], 

67 "psfFlux_flag_noGoodPixels": [1, 0], 

68 "psfFlux_flag_edge": [1, 0], 

69 "apFlux_flag": [1, 0], 

70 "apFlux_flag_apertureTruncated": [1, 0], 

71 "forced_PsfFlux_flag": [1, 0], 

72 "forced_PsfFlux_flag_noGoodPixels": [1, 0], 

73 "forced_PsfFlux_flag_edge": [1, 0], 

74 "pixelFlags_edge": [1, 0], 

75 "pixelFlags_interpolated": [1, 0], 

76 "pixelFlags_interpolatedCenter": [1, 0], 

77 "pixelFlags_saturated": [1, 0], 

78 "pixelFlags_saturatedCenter": [1, 0], 

79 "pixelFlags_cr": [1, 0], 

80 "pixelFlags_crCenter": [1, 0], 

81 "pixelFlags_bad": [1, 0], 

82 "pixelFlags_suspect": [1, 0], 

83 "pixelFlags_suspectCenter": [1, 0], 

84 "centroid_flag": [1, 0], 

85 "shape_flag": [1, 0], 

86 "shape_flag_no_pixels": [1, 0], 

87 "shape_flag_not_contained": [1, 0], 

88 "shape_flag_parent_source": [1, 0], 

89 } 

90) 

91 

92 

93def make_test_images(): 

94 """Make science, template and difference images with one source at the 

95 center. 

96 

97 Returns 

98 ------- 

99 legacy_science : `lsst.afw.image.ExposureF` 

100 The science image as a legacy exposure, for source detection. 

101 science : `lsst.images.VisitImage` 

102 The science image. 

103 template, difference : `lsst.images.DifferenceImage` 

104 The template and difference images. 

105 """ 

106 # CameraWrapper, unlike the TestDataset detector, has the amplifier 

107 # geometry that lsst.images conversion needs. 

108 detector = list(lsst.afw.cameraGeom.testUtils.CameraWrapper().camera)[0] 

109 dataset = lsst.meas.base.tests.TestDataset(detector.getBBox(), crval=skyCenter) 

110 dataset.addSource(instFlux=1e5, centroid=lsst.geom.Point2D(detector.getBBox().getCenter())) 

111 science, _ = dataset.realize(noise=1000.0, schema=dataset.makeMinimalSchema()) 

112 template, _ = dataset.realize(noise=5.0, schema=dataset.makeMinimalSchema()) 

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

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

115 difference.image -= template.image 

116 for exposure in (science, template, difference): 

117 exposure.setDetector(detector) 

118 exposure.setFilter(lsst.afw.image.FilterLabel(band="r", physical="r_test")) 

119 return (science, as_visit_image(science), as_visit_image(template, image_type="template"), 

120 as_visit_image(difference, image_type="difference")) 

121 

122 

123def make_mock_catalog(image): 

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

125 """ 

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

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

128 detect = SourceDetectionTask() 

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

130 

131 

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

133 """Test that PlotImageSubtractionCutoutsTask generates images and manifest 

134 files correctly. 

135 """ 

136 def setUp(self): 

137 self.scale = 0.3 # arbitrary arcseconds/pixel 

138 self.legacy_science, self.science, self.template, self.difference = make_test_images() 

139 

140 def test_generate_image(self): 

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

142 

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

144 im.show() 

145 """ 

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

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

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

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

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

151 # im.show() 

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

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

154 self.assertEqual((im.height, im.width), (245, 651)) 

155 

156 def test_generate_image_larger_cutout(self): 

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

158 size but shows more pixels. 

159 """ 

160 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

161 config.sizes = [100] 

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

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

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

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

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

167 # im.show() 

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

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

170 self.assertEqual((im.height, im.width), (245, 651)) 

171 

172 def test_generate_image_metadata(self): 

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

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

175 

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

177 im.show() 

178 """ 

179 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

180 config.add_metadata = True 

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

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

183 cutout = cutouts.generate_image(self.science, 

184 self.template, 

185 self.difference, 

186 skyCenter, 

187 self.scale, 

188 source=DATA.iloc[0]) 

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

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

191 # im.show() 

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

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

194 self.assertEqual((im.height, im.width), (349, 655)) 

195 

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

197 cutout = cutouts.generate_image(self.science, 

198 self.template, 

199 self.difference, 

200 skyCenter, 

201 self.scale, 

202 source=DATA.iloc[1]) 

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

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

205 # im.show() 

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

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

208 self.assertEqual((im.height, im.width), (349, 655)) 

209 

210 def test_generate_image_multisize_cutouts_without_metadata(self): 

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

212 and contains cutouts of multiple sizes. 

213 """ 

214 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

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, self.template, self.difference, skyCenter, self.scale) 

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

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

221 # im.show() 

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

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

224 self.assertEqual((im.height, im.width), (480, 651)) 

225 

226 def test_generate_image_multisize_cutouts_with_metadata(self): 

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

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

229 

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

231 im.show() 

232 """ 

233 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

234 config.add_metadata = True 

235 config.sizes = [32, 64] 

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

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

238 cutout = cutouts.generate_image(self.science, 

239 self.template, 

240 self.difference, 

241 skyCenter, 

242 self.scale, 

243 source=DATA.iloc[0]) 

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

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

246 # im.show() 

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

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

249 self.assertEqual((im.height, im.width), (591, 655)) 

250 

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

252 cutout = cutouts.generate_image(self.science, 

253 self.template, 

254 self.difference, 

255 skyCenter, 

256 self.scale, 

257 source=DATA.iloc[1]) 

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

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

260 # im.show() 

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

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

263 self.assertEqual((im.height, im.width), (591, 655)) 

264 

265 def test_generate_image_and_save_as_numpy(self): 

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

267 """ 

268 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

269 config.sizes = [100] 

270 with tempfile.TemporaryDirectory() as path: 

271 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

272 output_path=path) 

273 dia_source_id = 506428274000265570 

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

275 dia_source_id=dia_source_id, save_as_numpy=True) 

276 numpy_dir_path = cutouts.numpy_path.directory(dia_source_id) 

277 for file in os.listdir(numpy_dir_path): 

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

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

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

281 

282 def test_write_images(self): 

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

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

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

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

287 butler.get.return_value = self.science 

288 

289 with tempfile.TemporaryDirectory() as path: 

290 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

291 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

292 output_path=path) 

293 result = cutouts.write_images(DATA, butler) 

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

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

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

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

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

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

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

301 

302 def test_use_footprint(self): 

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

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

305 """ 

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

307 

308 def mock_get(dataset, dataId=None, *args, **kwargs): 

309 if "_diaSrc" in dataset: 

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

311 catalog = make_mock_catalog(self.legacy_science) 

312 # Assign the matching source id to the detection. 

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

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

315 return catalog 

316 else: 

317 return self.science 

318 

319 butler.get.side_effect = mock_get 

320 # Images are read by reference; the dataset name serves as one here. 

321 butler.find_dataset.side_effect = lambda dataset, dataId=None, **kwargs: dataset 

322 

323 with tempfile.TemporaryDirectory() as path: 

324 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

325 config.use_footprint = True 

326 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

327 output_path=path) 

328 result = cutouts.write_images(DATA, butler) 

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

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

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

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

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

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

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

336 

337 def test_write_images_exception(self): 

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

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

340 err = "Dataset not found" 

341 butler.get.side_effect = LookupError(err) 

342 

343 with tempfile.TemporaryDirectory() as path: 

344 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

345 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

346 output_path=path) 

347 

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

349 cutouts.write_images(DATA, butler) 

350 self.assertIn( 

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

352 ) 

353 self.assertIn( 

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

355 ) 

356 

357 def check_make_manifest(self, url_root, url_list): 

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

359 data = [5, 10, 20] 

360 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

361 config.url_root = url_root 

362 # output_path does nothing here 

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

364 manifest = cutouts._make_manifest(data) 

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

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

367 

368 def test_make_manifest(self): 

369 # check without an ending slash 

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

371 url_list = [ 

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

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

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

375 ] 

376 self.check_make_manifest(root, url_list) 

377 

378 # check with an ending slash 

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

380 url_list = [ 

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

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

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

384 ] 

385 self.check_make_manifest(root, url_list) 

386 

387 def test_pickle(self): 

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

389 """ 

390 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass() 

391 config.sizes = [63] 

392 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config, 

393 output_path="something") 

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

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

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

397 

398 

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

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

401 def setUp(self): 

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

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

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

405 self.collection = "mockRun" 

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

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

408 self.instrument = "LATISS" 

409 

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

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

412 self._butler = butlerPatch.start() 

413 self.addCleanup(butlerPatch.stop) 

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

415 import lsst.obs.lsst 

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

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

418 universe=universe) 

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

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

421 # this all goes away on DM-39501. 

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

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

424 "make_default_dimension_packer", 

425 return_value=packer) 

426 self._instrument = instrumentPatch.start() 

427 self.addCleanup(instrumentPatch.stop) 

428 

429 def test_main_args(self): 

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

431 args = [ 

432 "plotImageSubtractionCutouts", 

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

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

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

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( 

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

446 ) 

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

448 # reads in a random order every time. 

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

450 

451 def test_main_args_no_collections(self): 

452 """Test with no collections argument.""" 

453 args = [ 

454 "plotImageSubtractionCutouts", 

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

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

457 self.repo, 

458 self.outputPath, 

459 ] 

460 with unittest.mock.patch.object( 

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

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

463 plotImageSubtractionCutouts.main() 

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

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

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

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

468 

469 def test_main_collection_list(self): 

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

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

472 args = [ 

473 "plotImageSubtractionCutouts", 

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

475 self.repo, 

476 self.outputPath, 

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

478 "--collections", 

479 ] 

480 args.extend(collections) 

481 with unittest.mock.patch.object( 

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

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

484 plotImageSubtractionCutouts.main() 

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

486 self.assertEqual( 

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

488 ) 

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

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

491 

492 def test_main_args_limit_offset(self): 

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

494 args = [ 

495 "plotImageSubtractionCutouts", 

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

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

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

499 "--all", 

500 "--limit=5", 

501 self.repo, 

502 self.outputPath, 

503 ] 

504 with unittest.mock.patch.object( 

505 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, 

506 "write_images", 

507 autospec=True, 

508 return_value=[5] 

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

510 plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask, 

511 "write_manifest", 

512 autospec=True 

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

514 plotImageSubtractionCutouts.main() 

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

516 self.assertEqual( 

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

518 ) 

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

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

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

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

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

524 

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

526 def test_main_args_multiprocessing(self): 

527 """Test running with multiprocessing. 

528 """ 

529 args = [ 

530 "plotImageSubtractionCutouts", 

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

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

533 "-j2", 

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

535 self.repo, 

536 self.outputPath, 

537 ] 

538 with unittest.mock.patch.object( 

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

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

541 plotImageSubtractionCutouts.main() 

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

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

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

545 # reads in a random order every time. 

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

547 

548 

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

550 

551 def setUp(self): 

552 self.id = 12345678 

553 

554 def test_normal_path(self): 

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

556 """ 

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

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

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

560 

561 def test_chunking(self): 

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

563 directories? 

564 """ 

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

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

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

568 

569 def test_subdir(self): 

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

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

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

573 

574 def test_chunk_sizes(self): 

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

576 """ 

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

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

579 

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

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

582 

583 # should not raise 

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

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

586 

587 

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

589 pass 

590 

591 

592def setup_module(module): 

593 lsst.utils.tests.init() 

594 

595 

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

597 lsst.utils.tests.init() 

598 unittest.main()