Coverage for tests/test_jointcal.py : 20%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# This file is part of jointcal.
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 itertools
23import os.path
24import unittest
25from unittest import mock
27import numpy as np
28import pyarrow.parquet
29import astropy.time
31import lsst.log
32import lsst.utils
34import lsst.afw.table
35import lsst.daf.persistence
36from lsst.daf.base import DateTime
37import lsst.geom
38from lsst.meas.algorithms import getRefFluxField, LoadIndexedReferenceObjectsTask, DatasetConfig
39import lsst.obs.base
40import lsst.pipe.base
41import lsst.jointcal
42from lsst.jointcal import MinimizeResult
43import lsst.jointcal.chi2
44import lsst.jointcal.testUtils
47# for MemoryTestCase
48def setup_module(module):
49 lsst.utils.tests.init()
52def make_fake_refcat(center, flux, filterName):
53 """Make a fake reference catalog."""
54 schema = LoadIndexedReferenceObjectsTask.makeMinimalSchema([filterName],
55 addProperMotion=True)
56 catalog = lsst.afw.table.SimpleCatalog(schema)
57 record = catalog.addNew()
58 record.setCoord(center)
59 record[filterName + '_flux'] = flux
60 record[filterName + '_fluxErr'] = flux*0.1
61 record['pm_ra'] = lsst.geom.Angle(1)
62 record['pm_dec'] = lsst.geom.Angle(2)
63 record['epoch'] = 65432.1
64 return catalog
67def make_fake_wcs():
68 """Return two simple SkyWcs objects, with slightly different sky positions.
70 Use the same pixel origins as the cfht_minimal data, but put the sky origin
71 at RA=0
72 """
73 crpix = lsst.geom.Point2D(931.517869, 2438.572109)
74 cd = np.array([[5.19513851e-05, -2.81124812e-07],
75 [-3.25186974e-07, -5.19112119e-05]])
76 crval1 = lsst.geom.SpherePoint(0.01, -0.01, lsst.geom.degrees)
77 crval2 = lsst.geom.SpherePoint(-0.01, 0.01, lsst.geom.degrees)
78 wcs1 = lsst.afw.geom.makeSkyWcs(crpix, crval1, cd)
79 wcs2 = lsst.afw.geom.makeSkyWcs(crpix, crval2, cd)
80 return wcs1, wcs2
83class TestJointcalVisitCatalog(lsst.utils.tests.TestCase):
84 """Tests of jointcal's sourceTable_visit parquet ->single detector afw
85 table catalog unrolling.
86 """
87 def setUp(self):
88 filename = os.path.join(os.path.dirname(__file__),
89 "data/subselected-sourceTable-0034690.parq")
90 file = pyarrow.parquet.ParquetFile(filename)
91 self.data = file.read(use_pandas_metadata=True).to_pandas()
92 config = lsst.jointcal.jointcal.JointcalConfig()
93 # TODO DM-29008: Remove this (to use the new gen3 default) before gen2 removal.
94 config.sourceFluxType = "ApFlux_12_0"
95 # we don't actually need either fitter to run for these tests
96 config.doAstrometry = False
97 config.doPhotometry = False
98 self.jointcal = lsst.jointcal.JointcalTask(config=config)
100 def test_make_catalog_schema(self):
101 """Check that the slot fields required by CcdImage::loadCatalog are in
102 the schema returned by _make_catalog_schema().
103 """
104 table = self.jointcal._make_schema_table()
105 self.assertTrue(table.getCentroidSlot().getMeasKey().isValid())
106 self.assertTrue(table.getCentroidSlot().getErrKey().isValid())
107 self.assertTrue(table.getShapeSlot().getMeasKey().isValid())
109 def test_extract_detector_catalog_from_visit_catalog(self):
110 """Spot check a value output by the script that generated the test
111 parquet catalog and check that the size of the returned catalog
112 is correct for each detectior.
113 """
114 detectorId = 56
115 table = self.jointcal._make_schema_table()
116 catalog = self.jointcal._extract_detector_catalog_from_visit_catalog(table, self.data, detectorId)
118 # The test catalog has a number of elements for each detector equal to the detector id.
119 self.assertEqual(len(catalog), detectorId)
120 self.assertIn(29798723617816629, catalog['id'])
121 matched = catalog[29798723617816629 == catalog['id']]
122 self.assertEqual(1715.734359473175, matched['slot_Centroid_x'])
123 self.assertEqual(89.06076509964362, matched['slot_Centroid_y'])
126class JointcalTestBase:
127 def setUp(self):
128 # Ensure that the filter list is reset for each test so that we avoid
129 # confusion or contamination each time we create a cfht camera below.
130 lsst.obs.base.FilterDefinitionCollection.reset()
132 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(100, 100)
133 self.ccdImageList = struct.ccdImageList
134 # so that countStars() returns nonzero results
135 for ccdImage in self.ccdImageList:
136 ccdImage.resetCatalogForFit()
138 self.goodChi2 = lsst.jointcal.chi2.Chi2Statistic()
139 # chi2/ndof == 2.0 should be non-bad
140 self.goodChi2.chi2 = 200.0
141 self.goodChi2.ndof = 100
143 self.badChi2 = lsst.jointcal.chi2.Chi2Statistic()
144 self.badChi2.chi2 = 600.0
145 self.badChi2.ndof = 100
147 self.nanChi2 = lsst.jointcal.chi2.Chi2Statistic()
148 self.nanChi2.chi2 = np.nan
149 self.nanChi2.ndof = 100
151 self.maxSteps = 20
152 self.name = "testing"
153 self.dataName = "fake"
154 self.whatToFit = "" # unneeded, since we're mocking the fitter
156 # Mock a Butler so the refObjLoaders have something to call `get()` on.
157 self.butler = unittest.mock.Mock(spec=lsst.daf.persistence.Butler)
158 self.butler.get.return_value.indexer = DatasetConfig().indexer
160 # Mock the association manager and give it access to the ccd list above.
161 self.associations = mock.Mock(spec=lsst.jointcal.Associations)
162 self.associations.getCcdImageList.return_value = self.ccdImageList
164 # a default config to be modified by individual tests
165 self.config = lsst.jointcal.jointcal.JointcalConfig()
168class TestJointcalIterateFit(JointcalTestBase, lsst.utils.tests.TestCase):
169 def setUp(self):
170 super().setUp()
171 # Mock the fitter and model, so we can force particular
172 # return values/exceptions. Default to "good" return values.
173 self.fitter = mock.Mock(spec=lsst.jointcal.PhotometryFit)
174 self.fitter.computeChi2.return_value = self.goodChi2
175 self.fitter.minimize.return_value = MinimizeResult.Converged
176 self.model = mock.Mock(spec=lsst.jointcal.SimpleFluxModel)
178 self.jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
180 def test_iterateFit_success(self):
181 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
182 self.maxSteps, self.name, self.whatToFit)
183 self.assertEqual(chi2, self.goodChi2)
184 # Once for the for loop, the second time for the rank update.
185 self.assertEqual(self.fitter.minimize.call_count, 2)
187 def test_iterateFit_writeChi2Outer(self):
188 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
189 self.maxSteps, self.name, self.whatToFit,
190 dataName=self.dataName)
191 self.assertEqual(chi2, self.goodChi2)
192 # Once for the for loop, the second time for the rank update.
193 self.assertEqual(self.fitter.minimize.call_count, 2)
194 # Default config should not call saveChi2Contributions
195 self.fitter.saveChi2Contributions.assert_not_called()
197 def test_iterateFit_failed(self):
198 self.fitter.minimize.return_value = MinimizeResult.Failed
200 with self.assertRaises(RuntimeError):
201 self.jointcal._iterate_fit(self.associations, self.fitter,
202 self.maxSteps, self.name, self.whatToFit)
203 self.assertEqual(self.fitter.minimize.call_count, 1)
205 def test_iterateFit_badFinalChi2(self):
206 log = mock.Mock(spec=lsst.log.Log)
207 self.jointcal.log = log
208 self.fitter.computeChi2.return_value = self.badChi2
210 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
211 self.maxSteps, self.name, self.whatToFit)
212 self.assertEqual(chi2, self.badChi2)
213 log.info.assert_called_with("%s %s", "Fit completed", self.badChi2)
214 log.error.assert_called_with("Potentially bad fit: High chi-squared/ndof.")
216 def test_iterateFit_exceedMaxSteps(self):
217 log = mock.Mock(spec=lsst.log.Log)
218 self.jointcal.log = log
219 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased
220 maxSteps = 3
222 chi2 = self.jointcal._iterate_fit(self.associations, self.fitter,
223 maxSteps, self.name, self.whatToFit)
224 self.assertEqual(chi2, self.goodChi2)
225 self.assertEqual(self.fitter.minimize.call_count, maxSteps)
226 log.error.assert_called_with("testing failed to converge after %s steps" % maxSteps)
228 def test_moderate_chi2_increase(self):
229 """DM-25159: warn, but don't fail, on moderate chi2 increases between
230 steps.
231 """
232 chi2_1 = lsst.jointcal.chi2.Chi2Statistic()
233 chi2_1.chi2 = 100.0
234 chi2_1.ndof = 100
235 chi2_2 = lsst.jointcal.chi2.Chi2Statistic()
236 chi2_2.chi2 = 300.0
237 chi2_2.ndof = 100
239 chi2s = [self.goodChi2, chi2_1, chi2_2, self.goodChi2, self.goodChi2]
240 self.fitter.computeChi2.side_effect = chi2s
241 self.fitter.minimize.side_effect = [MinimizeResult.Chi2Increased,
242 MinimizeResult.Chi2Increased,
243 MinimizeResult.Chi2Increased,
244 MinimizeResult.Converged,
245 MinimizeResult.Converged]
246 with lsst.log.UsePythonLogging(): # so that assertLogs works with lsst.log
247 with self.assertLogs("jointcal", level="WARN") as logger:
248 self.jointcal._iterate_fit(self.associations, self.fitter,
249 self.maxSteps, self.name, self.whatToFit)
250 msg = "WARNING:jointcal:Significant chi2 increase by a factor of 300 / 100 = 3"
251 self.assertIn(msg, logger.output)
253 def test_large_chi2_increase_fails(self):
254 """DM-25159: fail on large chi2 increases between steps."""
255 chi2_1 = lsst.jointcal.chi2.Chi2Statistic()
256 chi2_1.chi2 = 1e11
257 chi2_1.ndof = 100
258 chi2_2 = lsst.jointcal.chi2.Chi2Statistic()
259 chi2_2.chi2 = 1.123456e13 # to check floating point formatting
260 chi2_2.ndof = 100
262 chi2s = [chi2_1, chi2_1, chi2_2]
263 self.fitter.computeChi2.side_effect = chi2s
264 self.fitter.minimize.return_value = MinimizeResult.Chi2Increased
265 with lsst.log.UsePythonLogging(): # so that assertLogs works with lsst.log
266 with self.assertLogs("jointcal", level="WARN") as logger:
267 with(self.assertRaisesRegex(RuntimeError, "Large chi2 increase")):
268 self.jointcal._iterate_fit(self.associations, self.fitter,
269 self.maxSteps, self.name, self.whatToFit)
270 msg = "WARNING:jointcal:Significant chi2 increase by a factor of 1.123e+13 / 1e+11 = 112.3"
271 self.assertIn(msg, logger.output)
273 def test_invalid_model(self):
274 self.model.validate.return_value = False
275 with(self.assertRaises(ValueError)):
276 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "invalid")
278 def test_nonfinite_chi2(self):
279 self.fitter.computeChi2.return_value = self.nanChi2
280 with(self.assertRaises(FloatingPointError)):
281 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "nonfinite")
283 def test_writeChi2(self):
284 filename = "somefile"
285 self.jointcal._logChi2AndValidate(self.associations, self.fitter, self.model, "writeCh2",
286 writeChi2Name=filename)
287 # logChi2AndValidate prepends `config.debugOutputPath` to the filename
288 self.fitter.saveChi2Contributions.assert_called_with("./"+filename+"{type}")
291class TestJointcalLoadRefCat(JointcalTestBase, lsst.utils.tests.TestCase):
293 def _make_fake_refcat(self):
294 """Mock a fake reference catalog and the bits necessary to use it."""
295 center = lsst.geom.SpherePoint(30, -30, lsst.geom.degrees)
296 flux = 10
297 radius = 1 * lsst.geom.degrees
298 filter = lsst.afw.image.FilterLabel(band='fake', physical="fake-filter")
300 fakeRefCat = make_fake_refcat(center, flux, filter.bandLabel)
301 fluxField = getRefFluxField(fakeRefCat.schema, filter.bandLabel)
302 returnStruct = lsst.pipe.base.Struct(refCat=fakeRefCat, fluxField=fluxField)
303 refObjLoader = mock.Mock(spec=LoadIndexedReferenceObjectsTask)
304 refObjLoader.loadSkyCircle.return_value = returnStruct
306 return refObjLoader, center, radius, filter, fakeRefCat
308 def test_load_reference_catalog(self):
309 refObjLoader, center, radius, filterLabel, fakeRefCat = self._make_fake_refcat()
311 config = lsst.jointcal.jointcal.JointcalConfig()
312 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
313 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
315 # NOTE: we cannot test application of proper motion here, because we
316 # mock the refObjLoader, so the real loader is never called.
317 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
318 jointcal.astrometryReferenceSelector,
319 center,
320 radius,
321 filterLabel)
322 # operator== isn't implemented for Catalogs, so we have to check like
323 # this, in case the records are copied during load.
324 self.assertEqual(len(refCat), len(fakeRefCat))
325 for r1, r2 in zip(refCat, fakeRefCat):
326 self.assertEqual(r1, r2)
328 def test_load_reference_catalog_subselect(self):
329 """Test that we can select out the one source in the fake refcat
330 with a ridiculous S/N cut.
331 """
332 refObjLoader, center, radius, filterLabel, fakeRefCat = self._make_fake_refcat()
334 config = lsst.jointcal.jointcal.JointcalConfig()
335 config.astrometryReferenceErr = 0.1 # our test refcats don't have coord errors
336 config.astrometryReferenceSelector.doSignalToNoise = True
337 config.astrometryReferenceSelector.signalToNoise.minimum = 1e10
338 config.astrometryReferenceSelector.signalToNoise.fluxField = "fake_flux"
339 config.astrometryReferenceSelector.signalToNoise.errField = "fake_fluxErr"
340 jointcal = lsst.jointcal.JointcalTask(config=config, butler=self.butler)
342 refCat, fluxField = jointcal._load_reference_catalog(refObjLoader,
343 jointcal.astrometryReferenceSelector,
344 center,
345 radius,
346 filterLabel)
347 self.assertEqual(len(refCat), 0)
350class TestJointcalFitModel(JointcalTestBase, lsst.utils.tests.TestCase):
351 def test_fit_photometry_writeChi2(self):
352 """Test that we are calling saveChi2 with appropriate file prefixes."""
353 self.config.photometryModel = "constrainedFlux"
354 self.config.writeChi2FilesOuterLoop = True
355 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
356 jointcal.focalPlaneBBox = lsst.geom.Box2D()
358 # Mock the fitter, so we can pretend it found a good fit
359 with mock.patch("lsst.jointcal.PhotometryFit", autospect=True) as fitPatch:
360 fitPatch.return_value.computeChi2.return_value = self.goodChi2
361 fitPatch.return_value.minimize.return_value = MinimizeResult.Converged
363 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
364 expected = ["./photometry_init-ModelVisit_chi2", "./photometry_init-Model_chi2",
365 "./photometry_init-Fluxes_chi2", "./photometry_init-ModelFluxes_chi2"]
366 expected = [mock.call(x+"-fake{type}") for x in expected]
367 jointcal._fit_photometry(self.associations, dataName=self.dataName)
368 fitPatch.return_value.saveChi2Contributions.assert_has_calls(expected)
370 def test_fit_astrometry_writeChi2(self):
371 """Test that we are calling saveChi2 with appropriate file prefixes."""
372 self.config.astrometryModel = "constrained"
373 self.config.writeChi2FilesOuterLoop = True
374 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
375 jointcal.focalPlaneBBox = lsst.geom.Box2D()
377 # Mock the fitter, so we can pretend it found a good fit
378 fitPatch = mock.patch("lsst.jointcal.AstrometryFit")
379 # Mock the projection handler so we don't segfault due to not-fully initialized ccdImages
380 projectorPatch = mock.patch("lsst.jointcal.OneTPPerVisitHandler")
381 with fitPatch as fit, projectorPatch as projector:
382 fit.return_value.computeChi2.return_value = self.goodChi2
383 fit.return_value.minimize.return_value = MinimizeResult.Converged
384 # return a real ProjectionHandler to keep ConstrainedAstrometryModel() happy
385 projector.return_value = lsst.jointcal.IdentityProjectionHandler()
387 # config.debugOutputPath is prepended to the filenames that go into saveChi2Contributions
388 expected = ["./astrometry_init-DistortionsVisit_chi2", "./astrometry_init-Distortions_chi2",
389 "./astrometry_init-Positions_chi2", "./astrometry_init-DistortionsPositions_chi2"]
390 expected = [mock.call(x+"-fake{type}") for x in expected]
391 jointcal._fit_astrometry(self.associations, dataName=self.dataName)
392 fit.return_value.saveChi2Contributions.assert_has_calls(expected)
395class TestComputeBoundingCircle(lsst.utils.tests.TestCase):
396 """Tests of Associations.computeBoundingCircle()"""
397 def _checkPointsInCircle(self, points, center, radius):
398 """Check that all points are within the (center, radius) circle.
400 The test is whether the max(points - center) separation is equal to
401 (or slightly less than) radius.
402 """
403 maxSeparation = 0*lsst.geom.degrees
404 for point in points:
405 maxSeparation = max(maxSeparation, center.separation(point))
406 self.assertAnglesAlmostEqual(maxSeparation, radius, maxDiff=3*lsst.geom.arcseconds)
407 self.assertLess(maxSeparation, radius)
409 def _testPoints(self, ccdImage1, ccdImage2, skyWcs1, skyWcs2, bbox):
410 """Fill an Associations object and test that it computes the correct
411 bounding circle for the input data.
413 Parameters
414 ----------
415 ccdImage1, ccdImage2 : `lsst.jointcal.CcdImage`
416 The CcdImages to add to the Associations object.
417 skyWcs1, skyWcs2 : `lsst.afw.geom.SkyWcs`
418 The WCS of each of the above images.
419 bbox : `lsst.geom.Box2D`
420 The ccd bounding box of both images.
421 """
422 lsst.log.setLevel('jointcal', lsst.log.DEBUG)
423 associations = lsst.jointcal.Associations()
424 associations.addCcdImage(ccdImage1)
425 associations.addCcdImage(ccdImage2)
426 associations.computeCommonTangentPoint()
428 circle = associations.computeBoundingCircle()
429 center = lsst.geom.SpherePoint(circle.getCenter())
430 radius = lsst.geom.Angle(circle.getOpeningAngle().asRadians(), lsst.geom.radians)
431 points = [lsst.geom.SpherePoint(skyWcs1.pixelToSky(lsst.geom.Point2D(x)))
432 for x in bbox.getCorners()]
433 points.extend([lsst.geom.SpherePoint(skyWcs2.pixelToSky(lsst.geom.Point2D(x)))
434 for x in bbox.getCorners()])
435 self._checkPointsInCircle(points, center, radius)
437 def testPoints(self):
438 """Test for points in an "easy" area, far from RA=0 or the poles."""
439 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages()
440 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
441 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
443 def testPointsRA0(self):
444 """Test for CcdImages crossing RA=0; this demonstrates a fix for
445 the bug described in DM-19802.
446 """
447 wcs1, wcs2 = make_fake_wcs()
449 # Put the visit boresights at the WCS origin, for consistency
450 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
451 date=DateTime(date=65321.1),
452 boresightRaDec=wcs1.getSkyOrigin())
453 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
454 date=DateTime(date=65322.1),
455 boresightRaDec=wcs1.getSkyOrigin())
457 struct = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
458 fakeVisitInfos=[visitInfo1, visitInfo2])
459 self._testPoints(struct.ccdImageList[0], struct.ccdImageList[1],
460 struct.skyWcs[0], struct.skyWcs[1], struct.bbox)
463class TestJointcalComputePMDate(JointcalTestBase, lsst.utils.tests.TestCase):
464 """Tests of jointcal._compute_proper_motion_epoch(), using fake dates."""
465 def test_compute_proper_motion_epoch(self):
466 mjds = np.array((65432.1, 66666.0, 55555.0, 44322.2))
468 wcs1, wcs2 = make_fake_wcs()
469 visitInfo1 = lsst.afw.image.VisitInfo(exposureId=30577512,
470 date=DateTime(date=mjds[0]),
471 boresightRaDec=wcs1.getSkyOrigin())
472 visitInfo2 = lsst.afw.image.VisitInfo(exposureId=30621144,
473 date=DateTime(date=mjds[1]),
474 boresightRaDec=wcs2.getSkyOrigin())
475 visitInfo3 = lsst.afw.image.VisitInfo(exposureId=30577513,
476 date=DateTime(date=mjds[2]),
477 boresightRaDec=wcs1.getSkyOrigin())
478 visitInfo4 = lsst.afw.image.VisitInfo(exposureId=30621145,
479 date=DateTime(date=mjds[3]),
480 boresightRaDec=wcs2.getSkyOrigin())
482 struct1 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
483 fakeVisitInfos=[visitInfo1, visitInfo2])
484 struct2 = lsst.jointcal.testUtils.createTwoFakeCcdImages(fakeWcses=[wcs1, wcs2],
485 fakeVisitInfos=[visitInfo3, visitInfo4])
486 ccdImageList = list(itertools.chain(struct1.ccdImageList, struct2.ccdImageList))
487 associations = lsst.jointcal.Associations()
488 for ccdImage in ccdImageList:
489 associations.addCcdImage(ccdImage)
490 associations.computeCommonTangentPoint()
492 jointcal = lsst.jointcal.JointcalTask(config=self.config, butler=self.butler)
493 result = jointcal._compute_proper_motion_epoch(ccdImageList)
494 self.assertEqual(result.jyear, (astropy.time.Time(mjds, format="mjd", scale="tai").jyear).mean())
497class MemoryTester(lsst.utils.tests.MemoryTestCase):
498 pass
501if __name__ == "__main__": 501 ↛ 502line 501 didn't jump to line 502, because the condition on line 501 was never true
502 lsst.utils.tests.init()
503 unittest.main()