LSSTApplications  19.0.0-10-g920eed2,19.0.0-11-g48a0200+2,19.0.0-18-gfc4e62b+17,19.0.0-2-g3b2f90d+2,19.0.0-2-gd671419+5,19.0.0-20-g5a5a17ab+15,19.0.0-21-g2644856+18,19.0.0-23-g84eeccb+6,19.0.0-24-g878c510+5,19.0.0-25-g6c8df7140,19.0.0-25-gb330496+5,19.0.0-3-g2b32d65+5,19.0.0-3-g8227491+16,19.0.0-3-g9c54d0d+16,19.0.0-3-gca68e65+12,19.0.0-3-gcfc5f51+5,19.0.0-3-ge110943+15,19.0.0-3-ge74d124,19.0.0-3-gfe04aa6+16,19.0.0-30-g9c3fd16+6,19.0.0-4-g06f5963+5,19.0.0-4-g3d16501+18,19.0.0-4-g4a9c019+5,19.0.0-4-g5a8b323,19.0.0-4-g66397f0+1,19.0.0-4-g8278b9b+1,19.0.0-4-g8557e14,19.0.0-4-g8964aba+17,19.0.0-4-ge404a01+16,19.0.0-5-g40f3a5a,19.0.0-5-g4db63b3,19.0.0-5-gfb03ce7+17,19.0.0-6-gbaebbfb+16,19.0.0-61-gec4c6e08+6,19.0.0-7-g039c0b5+16,19.0.0-7-gbea9075+4,19.0.0-7-gc567de5+17,19.0.0-70-g334bf3e+1,19.0.0-9-g463f923+16,b.20.0.x-g5487ab2134,v20.0.0.rc1
LSSTDataManagementBasePackage
testUtils.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2017 LSST/AURA.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <http://www.lsstcorp.org/LegalNotices/>.
21 #
22 
23 # the asserts are automatically imported so unit tests can find them without special imports;
24 # the other functions are hidden unless explicitly asked for
25 __all__ = ["assertImagesAlmostEqual", "assertImagesEqual", "assertMasksEqual",
26  "assertMaskedImagesAlmostEqual", "assertMaskedImagesEqual"]
27 
28 import numpy as np
29 
30 import lsst.utils.tests
31 from .image import ImageF
32 from .basicUtils import makeMaskedImageFromArrays
33 
34 
35 def makeGaussianNoiseMaskedImage(dimensions, sigma, variance=1.0):
36  """Make a gaussian noise MaskedImageF
37 
38  Inputs:
39  - dimensions: dimensions of output array (cols, rows)
40  - sigma; sigma of image plane's noise distribution
41  - variance: constant value for variance plane
42  """
43  npSize = (dimensions[1], dimensions[0])
44  image = np.random.normal(loc=0.0, scale=sigma,
45  size=npSize).astype(np.float32)
46  mask = np.zeros(npSize, dtype=np.int32)
47  variance = np.zeros(npSize, dtype=np.float32) + variance
48 
49  return makeMaskedImageFromArrays(image, mask, variance)
50 
51 
52 def makeRampImage(bbox, start=0, stop=None, imageClass=ImageF):
53  """!Make an image whose values are a linear ramp
54 
55  @param[in] bbox bounding box of image (an lsst.geom.Box2I)
56  @param[in] start starting ramp value, inclusive
57  @param[in] stop ending ramp value, inclusive; if None, increase by integer values
58  @param[in] imageClass type of image (e.g. lsst.afw.image.ImageF)
59  """
60  im = imageClass(bbox)
61  imDim = im.getDimensions()
62  numPix = imDim[0]*imDim[1]
63  imArr = im.getArray()
64  if stop is None:
65  # increase by integer values
66  stop = start + numPix - 1
67  rampArr = np.linspace(start=start, stop=stop,
68  endpoint=True, num=numPix, dtype=imArr.dtype)
69  # numpy arrays are transposed w.r.t. afwImage
70  imArr[:] = np.reshape(rampArr, (imDim[1], imDim[0]))
71  return im
72 
73 
74 @lsst.utils.tests.inTestCase
75 def assertImagesAlmostEqual(testCase, image0, image1, skipMask=None,
76  rtol=1.0e-05, atol=1e-08, msg="Images differ"):
77  """!Assert that two images are almost equal, including non-finite values
78 
79  @param[in] testCase unittest.TestCase instance the test is part of;
80  an object supporting one method: fail(self, msgStr)
81  @param[in] image0 image 0, an lsst.afw.image.Image, lsst.afw.image.Mask,
82  or transposed numpy array (see warning)
83  @param[in] image1 image 1, an lsst.afw.image.Image, lsst.afw.image.Mask,
84  or transposed numpy array (see warning)
85  @param[in] skipMask mask of pixels to skip, or None to compare all pixels;
86  an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array (see warning);
87  all non-zero pixels are skipped
88  @param[in] rtol maximum allowed relative tolerance; more info below
89  @param[in] atol maximum allowed absolute tolerance; more info below
90  @param[in] msg exception message prefix; details of the error are appended after ": "
91 
92  The images are nearly equal if all pixels obey:
93  |val1 - val0| <= rtol*|val1| + atol
94  or, for float types, if nan/inf/-inf pixels match.
95 
96  @warning the comparison equation is not symmetric, so in rare cases the assertion
97  may give different results depending on which image comes first.
98 
99  @warning the axes of numpy arrays are transposed with respect to Image and Mask data.
100  Thus for example if image0 and image1 are both lsst.afw.image.ImageD with dimensions (2, 3)
101  and skipMask is a numpy array, then skipMask must have shape (3, 2).
102 
103  @throw self.failureException (usually AssertionError) if any of the following are true
104  for un-skipped pixels:
105  - non-finite values differ in any way (e.g. one is "nan" and another is not)
106  - finite values differ by too much, as defined by atol and rtol
107 
108  @throw TypeError if the dimensions of image0, image1 and skipMask do not match,
109  or any are not of a numeric data type.
110  """
111  errStr = imagesDiffer(
112  image0, image1, skipMask=skipMask, rtol=rtol, atol=atol)
113  if errStr:
114  testCase.fail(f"{msg}: {errStr}")
115 
116 
117 @lsst.utils.tests.inTestCase
118 def assertImagesEqual(*args, **kwds):
119  """!Assert that two images are exactly equal, including non-finite values.
120 
121  All arguments are forwarded to assertAnglesAlmostEqual aside from atol and rtol,
122  which are set to zero.
123  """
124  return assertImagesAlmostEqual(*args, atol=0, rtol=0, **kwds)
125 
126 
127 @lsst.utils.tests.inTestCase
128 def assertMasksEqual(testCase, mask0, mask1, skipMask=None, msg="Masks differ"):
129  """!Assert that two masks are equal
130 
131  @param[in] testCase unittest.TestCase instance the test is part of;
132  an object supporting one method: fail(self, msgStr)
133  @param[in] mask0 mask 0, an lsst.afw.image.Mask, lsst.afw.image.Image,
134  or transposed numpy array (see warning)
135  @param[in] mask1 mask 1, an lsst.afw.image.Mask, lsst.afw.image.Image,
136  or transposed numpy array (see warning)
137  @param[in] skipMask mask of pixels to skip, or None to compare all pixels;
138  an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array (see warning);
139  all non-zero pixels are skipped
140  @param[in] msg exception message prefix; details of the error are appended after ": "
141 
142  @warning the axes of numpy arrays are transposed with respect to Mask and Image.
143  Thus for example if mask0 and mask1 are both lsst.afw.image.Mask with dimensions (2, 3)
144  and skipMask is a numpy array, then skipMask must have shape (3, 2).
145 
146  @throw self.failureException (usually AssertionError) if any any un-skipped pixels differ
147 
148  @throw TypeError if the dimensions of mask0, mask1 and skipMask do not match,
149  or any are not of a numeric data type.
150  """
151  errStr = imagesDiffer(mask0, mask1, skipMask=skipMask, rtol=0, atol=0)
152  if errStr:
153  testCase.fail(f"{msg}: {errStr}")
154 
155 
156 @lsst.utils.tests.inTestCase
158  testCase, maskedImage0, maskedImage1,
159  doImage=True, doMask=True, doVariance=True, skipMask=None,
160  rtol=1.0e-05, atol=1e-08, msg="Masked images differ",
161 ):
162  """!Assert that two masked images are nearly equal, including non-finite values
163 
164  @param[in] testCase unittest.TestCase instance the test is part of;
165  an object supporting one method: fail(self, msgStr)
166  @param[in] maskedImage0 masked image 0 (an lsst.afw.image.MaskedImage or
167  collection of three transposed numpy arrays: image, mask, variance)
168  @param[in] maskedImage1 masked image 1 (an lsst.afw.image.MaskedImage or
169  collection of three transposed numpy arrays: image, mask, variance)
170  @param[in] doImage compare image planes if True
171  @param[in] doMask compare mask planes if True
172  @param[in] doVariance compare variance planes if True
173  @param[in] skipMask mask of pixels to skip, or None to compare all pixels;
174  an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array;
175  all non-zero pixels are skipped
176  @param[in] rtol maximum allowed relative tolerance; more info below
177  @param[in] atol maximum allowed absolute tolerance; more info below
178  @param[in] msg exception message prefix; details of the error are appended after ": "
179 
180  The mask planes must match exactly. The image and variance planes are nearly equal if all pixels obey:
181  |val1 - val0| <= rtol*|val1| + atol
182  or, for float types, if nan/inf/-inf pixels match.
183 
184  @warning the comparison equation is not symmetric, so in rare cases the assertion
185  may give different results depending on which masked image comes first.
186 
187  @warning the axes of numpy arrays are transposed with respect to MaskedImage data.
188  Thus for example if maskedImage0 and maskedImage1 are both lsst.afw.image.MaskedImageD
189  with dimensions (2, 3) and skipMask is a numpy array, then skipMask must have shape (3, 2).
190 
191  @throw self.failureException (usually AssertionError) if any of the following are true
192  for un-skipped pixels:
193  - non-finite image or variance values differ in any way (e.g. one is "nan" and another is not)
194  - finite values differ by too much, as defined by atol and rtol
195  - mask pixels differ at all
196 
197  @throw TypeError if the dimensions of maskedImage0, maskedImage1 and skipMask do not match,
198  either image or variance plane is not of a numeric data type,
199  either mask plane is not of an integer type (unsigned or signed),
200  or skipMask is not of a numeric data type.
201  """
202  maskedImageArrList0 = maskedImage0.getArrays() if hasattr(
203  maskedImage0, "getArrays") else maskedImage0
204  maskedImageArrList1 = maskedImage1.getArrays() if hasattr(
205  maskedImage1, "getArrays") else maskedImage1
206 
207  for arrList, arg, name in (
208  (maskedImageArrList0, maskedImage0, "maskedImage0"),
209  (maskedImageArrList1, maskedImage1, "maskedImage1"),
210  ):
211  try:
212  assert len(arrList) == 3
213  # check that array shapes are all identical
214  # check that image and variance are float or int of some kind
215  # and mask is int of some kind
216  for i in (0, 2):
217  assert arrList[i].shape == arrList[1].shape
218  assert arrList[i].dtype.kind in ("b", "i", "u", "f", "c")
219  assert arrList[1].dtype.kind in ("b", "i", "u")
220  except Exception:
221  raise TypeError(f"{name}={arg!r} is not a supported type")
222 
223  errStrList = []
224  for ind, (doPlane, planeName) in enumerate(((doImage, "image"),
225  (doMask, "mask"),
226  (doVariance, "variance"))):
227  if not doPlane:
228  continue
229 
230  if planeName == "mask":
231  errStr = imagesDiffer(maskedImageArrList0[ind], maskedImageArrList1[ind], skipMask=skipMask,
232  rtol=0, atol=0)
233  if errStr:
234  errStrList.append(errStr)
235  else:
236  errStr = imagesDiffer(maskedImageArrList0[ind], maskedImageArrList1[ind],
237  skipMask=skipMask, rtol=rtol, atol=atol)
238  if errStr:
239  errStrList.append(f"{planeName} planes differ: {errStr}")
240 
241  if errStrList:
242  errStr = "; ".join(errStrList)
243  testCase.fail(f"{msg}: {errStr}")
244 
245 
246 @lsst.utils.tests.inTestCase
247 def assertMaskedImagesEqual(*args, **kwds):
248  """!Assert that two masked images are exactly equal, including non-finite values.
249 
250  All arguments are forwarded to assertMaskedImagesAlmostEqual aside from atol and rtol,
251  which are set to zero.
252  """
253  return assertMaskedImagesAlmostEqual(*args, atol=0, rtol=0, **kwds)
254 
255 
256 def imagesDiffer(image0, image1, skipMask=None, rtol=1.0e-05, atol=1e-08):
257  """!Compare the pixels of two image or mask arrays; return True if close, False otherwise
258 
259  @param[in] image0 image 0, an lsst.afw.image.Image, lsst.afw.image.Mask,
260  or transposed numpy array (see warning)
261  @param[in] image1 image 1, an lsst.afw.image.Image, lsst.afw.image.Mask,
262  or transposed numpy array (see warning)
263  @param[in] skipMask mask of pixels to skip, or None to compare all pixels;
264  an lsst.afw.image.Mask, lsst.afw.image.Image, or transposed numpy array (see warning);
265  all non-zero pixels are skipped
266  @param[in] rtol maximum allowed relative tolerance; more info below
267  @param[in] atol maximum allowed absolute tolerance; more info below
268 
269  The images are nearly equal if all pixels obey:
270  |val1 - val0| <= rtol*|val1| + atol
271  or, for float types, if nan/inf/-inf pixels match.
272 
273  @warning the comparison equation is not symmetric, so in rare cases the assertion
274  may give different results depending on which image comes first.
275 
276  @warning the axes of numpy arrays are transposed with respect to Image and Mask data.
277  Thus for example if image0 and image1 are both lsst.afw.image.ImageD with dimensions (2, 3)
278  and skipMask is a numpy array, then skipMask must have shape (3, 2).
279 
280  @return a string which is non-empty if the images differ
281 
282  @throw TypeError if the dimensions of image0, image1 and skipMask do not match,
283  or any are not of a numeric data type.
284  """
285  errStrList = []
286  imageArr0 = image0.getArray() if hasattr(image0, "getArray") else image0
287  imageArr1 = image1.getArray() if hasattr(image1, "getArray") else image1
288  skipMaskArr = skipMask.getArray() if hasattr(skipMask, "getArray") else skipMask
289 
290  # check the inputs
291  arrArgNameList = [
292  (imageArr0, image0, "image0"),
293  (imageArr1, image1, "image1"),
294  ]
295  if skipMask is not None:
296  arrArgNameList.append((skipMaskArr, skipMask, "skipMask"))
297  for i, (arr, arg, name) in enumerate(arrArgNameList):
298  try:
299  assert arr.dtype.kind in ("b", "i", "u", "f", "c")
300  except Exception:
301  raise TypeError(f"{name!r}={arg!r} is not a supported type")
302  if i != 0:
303  if arr.shape != imageArr0.shape:
304  raise TypeError(f"{name} shape = {arr.shape} != {imageArr0.shape} = image0 shape")
305 
306  # np.allclose mis-handled unsigned ints in numpy 1.8
307  # and subtraction doesn't give the desired answer in any case
308  # so cast unsigned arrays into int64 (there may be a simple
309  # way to safely use a smaller data type but I've not found it)
310  if imageArr0.dtype.kind == "u":
311  imageArr0 = imageArr0.astype(
312  np.promote_types(imageArr0.dtype, np.int8))
313  if imageArr1.dtype.kind == "u":
314  imageArr1 = imageArr1.astype(
315  np.promote_types(imageArr1.dtype, np.int8))
316 
317  if skipMaskArr is not None:
318  skipMaskArr = np.array(skipMaskArr, dtype=bool)
319  maskedArr0 = np.ma.array(imageArr0, copy=False, mask=skipMaskArr)
320  maskedArr1 = np.ma.array(imageArr1, copy=False, mask=skipMaskArr)
321  filledArr0 = maskedArr0.filled(0.0)
322  filledArr1 = maskedArr1.filled(0.0)
323  else:
324  skipMaskArr = None
325  filledArr0 = imageArr0
326  filledArr1 = imageArr1
327 
328  try:
329  np.array([np.nan], dtype=imageArr0.dtype)
330  np.array([np.nan], dtype=imageArr1.dtype)
331  except Exception:
332  # one or both images does not support non-finite values (nan, etc.)
333  # so just use value comparison
334  valSkipMaskArr = skipMaskArr
335  else:
336  # both images support non-finite values, of which numpy has exactly three: nan, +inf and -inf;
337  # compare those individually in order to give useful diagnostic output
338  nan0 = np.isnan(filledArr0)
339  nan1 = np.isnan(filledArr1)
340  if np.any(nan0 != nan1):
341  errStrList.append("NaNs differ")
342 
343  posinf0 = np.isposinf(filledArr0)
344  posinf1 = np.isposinf(filledArr1)
345  if np.any(posinf0 != posinf1):
346  errStrList.append("+infs differ")
347 
348  neginf0 = np.isneginf(filledArr0)
349  neginf1 = np.isneginf(filledArr1)
350  if np.any(neginf0 != neginf1):
351  errStrList.append("-infs differ")
352 
353  valSkipMaskArr = nan0 | nan1 | posinf0 | posinf1 | neginf0 | neginf1
354  if skipMaskArr is not None:
355  valSkipMaskArr |= skipMaskArr
356 
357  # compare values that should be comparable (are finite and not masked)
358  valMaskedArr1 = np.ma.array(imageArr0, copy=False, mask=valSkipMaskArr)
359  valMaskedArr2 = np.ma.array(imageArr1, copy=False, mask=valSkipMaskArr)
360  valFilledArr1 = valMaskedArr1.filled(0.0)
361  valFilledArr2 = valMaskedArr2.filled(0.0)
362 
363  if not np.allclose(valFilledArr1, valFilledArr2, rtol=rtol, atol=atol):
364  errArr = np.abs(valFilledArr1 - valFilledArr2)
365  maxErr = errArr.max()
366  maxPosInd = np.where(errArr == maxErr)
367  maxPosTuple = (maxPosInd[1][0], maxPosInd[0][0])
368  errStr = f"maxDiff={maxErr} at position {maxPosTuple}; " \
369  f"value={valFilledArr1[maxPosInd][0]} vs. {valFilledArr2[maxPosInd][0]}"
370  errStrList.insert(0, errStr)
371 
372  return "; ".join(errStrList)
lsst::afw::image.testUtils.assertImagesEqual
def assertImagesEqual(*args, **kwds)
Assert that two images are exactly equal, including non-finite values.
Definition: testUtils.py:118
lsst::afw::image.testUtils.makeGaussianNoiseMaskedImage
def makeGaussianNoiseMaskedImage(dimensions, sigma, variance=1.0)
Definition: testUtils.py:35
lsst::afw::image.testUtils.makeRampImage
def makeRampImage(bbox, start=0, stop=None, imageClass=ImageF)
Make an image whose values are a linear ramp.
Definition: testUtils.py:52
lsst::afw::image.testUtils.assertMaskedImagesEqual
def assertMaskedImagesEqual(*args, **kwds)
Assert that two masked images are exactly equal, including non-finite values.
Definition: testUtils.py:247
lsst::afw::image.testUtils.assertMasksEqual
def assertMasksEqual(testCase, mask0, mask1, skipMask=None, msg="Masks differ")
Assert that two masks are equal.
Definition: testUtils.py:128
lsst::afw::image.testUtils.imagesDiffer
def imagesDiffer(image0, image1, skipMask=None, rtol=1.0e-05, atol=1e-08)
Compare the pixels of two image or mask arrays; return True if close, False otherwise.
Definition: testUtils.py:256
lsst::utils.tests
Definition: tests.py:1
lsst::afw::image.testUtils.assertMaskedImagesAlmostEqual
def assertMaskedImagesAlmostEqual(testCase, maskedImage0, maskedImage1, doImage=True, doMask=True, doVariance=True, skipMask=None, rtol=1.0e-05, atol=1e-08, msg="Masked images differ")
Assert that two masked images are nearly equal, including non-finite values.
Definition: testUtils.py:157
lsst::afw::image.basicUtils.makeMaskedImageFromArrays
def makeMaskedImageFromArrays(image, mask=None, variance=None)
Definition: basicUtils.py:47
lsst::afw::image.testUtils.assertImagesAlmostEqual
def assertImagesAlmostEqual(testCase, image0, image1, skipMask=None, rtol=1.0e-05, atol=1e-08, msg="Images differ")
Assert that two images are almost equal, including non-finite values.
Definition: testUtils.py:75