Coverage for tests/test_plotImageSubtractionCutouts.py: 97%
250 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 15:23 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 15:23 +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/>.
22import os
23import pickle
24import sys
25import tempfile
26import unittest
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
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)
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)
93def make_test_images():
94 """Make science, template and difference images with one source at the
95 center.
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"))
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
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()
140 def test_generate_image(self):
141 """Test that we get some kind of image out.
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))
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))
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.
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))
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))
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))
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.
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))
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))
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))
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
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")
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)
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
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
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")
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)
343 with tempfile.TemporaryDirectory() as path:
344 config = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask.ConfigClass()
345 cutouts = plotImageSubtractionCutouts.PlotImageSubtractionCutoutsTask(config=config,
346 output_path=path)
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 )
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)
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)
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)
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)
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"
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)
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)
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)
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)
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])
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)
549class TestCutoutPath(lsst.utils.tests.TestCase):
551 def setUp(self):
552 self.id = 12345678
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")
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")
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")
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)
580 with self.assertRaisesRegex(RuntimeError, "chunk_size must be a power of 10"):
581 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=12300)
583 # should not raise
584 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=1000)
585 plotImageSubtractionCutouts.CutoutPath("some/root/path", chunk_size=1000000)
588class TestMemory(lsst.utils.tests.MemoryTestCase):
589 pass
592def setup_module(module):
593 lsst.utils.tests.init()
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()