Source code for lsst.geom.testUtils

#
# Developed for the LSST Data Management System.
# This product includes software developed by the LSST Project
# (https://www.lsst.org).
# See the COPYRIGHT file at the top-level directory of this distribution
# for details of code ownership.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#

"""Utilities that should be imported into the lsst.geom namespace when lsst.geom is used

In the case of the assert functions, importing them makes them available in lsst.utils.tests.TestCase
"""
__all__ = []

import math

import numpy as np

import lsst.utils.tests
from .angle import arcseconds


def extraMsg(msg):
    """Format extra error message, if any
    """
    if msg:
        return ": " + msg
    return ""


@lsst.utils.tests.inTestCase
def assertAnglesAlmostEqual(testCase, ang0, ang1, maxDiff=0.001*arcseconds,
                            ignoreWrap=True, msg="Angles differ"):
    r"""Assert that two `~lsst.afw.geom.Angle`\ s are almost equal, ignoring wrap differences by default

    Parameters
    ----------
    testCase : `unittest.TestCase`
        test case the test is part of; an object supporting one method: fail(self, msgStr)
    ang0 : `lsst.afw.geom.Angle`
        angle 0
    ang1 : `an lsst.afw.geom.Angle`
        angle 1
    maxDiff : `an lsst.afw.geom.Angle`
        maximum difference between the two angles
    ignoreWrap : `bool`
        ignore wrap when comparing the angles?
        - if True then wrap is ignored, e.g. 0 and 360 degrees are considered equal
        - if False then wrap matters, e.g. 0 and 360 degrees are considered different
    msg : `str`
        exception message prefix; details of the error are appended after ": "

    Raises
    ------
    AssertionError
        Raised if the difference is greater than ``maxDiff``
    """
    measDiff = ang1 - ang0
    if ignoreWrap:
        measDiff = measDiff.wrapCtr()
    if abs(measDiff) > maxDiff:
        testCase.fail("%s: measured difference %s arcsec > max allowed %s arcsec" %
                      (msg, measDiff.asArcseconds(), maxDiff.asArcseconds()))


@lsst.utils.tests.inTestCase
def assertPairsAlmostEqual(testCase, pair0, pair1, maxDiff=1e-7, msg="Pairs differ"):
    """Assert that two Cartesian points are almost equal.

    Each point can be any indexable pair of two floats, including
    Point2D or Extent2D, a list or a tuple.

    Parameters
    ----------
    testCase : `unittest.TestCase`
        test case the test is part of; an object supporting one method: fail(self, msgStr)
    pair0 : pair of `float`
        pair 0
    pair1 : pair of `floats`
        pair 1
    maxDiff : `float`
        maximum radial separation between the two points
    msg : `str`
        exception message prefix; details of the error are appended after ": "

    Raises
    ------
    AssertionError
        Raised if the radial difference is greater than ``maxDiff``

    Notes
    -----
    .. warning::

       Does not compare types, just compares values.
    """
    if len(pair0) != 2:
        raise RuntimeError("len(pair0)=%s != 2" % (len(pair0),))
    if len(pair1) != 2:
        raise RuntimeError("len(pair1)=%s != 2" % (len(pair1),))

    pairDiff = [float(pair1[i] - pair0[i]) for i in range(2)]
    measDiff = math.hypot(*pairDiff)
    if measDiff > maxDiff:
        testCase.fail("%s: measured radial distance = %s > maxDiff = %s, pair0=(%r, %r), pair1=(%r, %r)" %
                      (msg, measDiff, maxDiff, pair0[0], pair0[1], pair1[0], pair1[1]))


@lsst.utils.tests.inTestCase
def assertPairListsAlmostEqual(testCase, list0, list1, maxDiff=1e-7, msg=None):
    """Assert that two lists of Cartesian points are almost equal

    Each point can be any indexable pair of two floats, including
    Point2D or Extent2D, a list or a tuple.

    Parameters
    ----------
    testCase : `unittest.TestCase`
        test case the test is part of; an object supporting one method: fail(self, msgStr)
    list0 : `list` of pairs of `float`
        list of pairs 0
    list1 : `list` of pairs of `float`
        list of pairs 1
    maxDiff : `float`
        maximum radial separation between the two points
    msg : `str`
        additional information for the error message; appended after ": "

    Raises
    ------
    AssertionError
        Raised if the radial difference is greater than ``maxDiff``

    Notes
    -----
    .. warning::

       Does not compare types, just values.
    """
    testCase.assertEqual(len(list0), len(list1))
    lenList1 = np.array([len(val) for val in list0])
    lenList2 = np.array([len(val) for val in list1])
    testCase.assertTrue(np.all(lenList1 == 2))
    testCase.assertTrue(np.all(lenList2 == 2))

    diffArr = np.array([(val0[0] - val1[0], val0[1] - val1[1])
                        for val0, val1 in zip(list0, list1)], dtype=float)
    sepArr = np.hypot(diffArr[:, 0], diffArr[:, 1])
    badArr = sepArr > maxDiff
    if np.any(badArr):
        maxInd = np.argmax(sepArr)
        testCase.fail("PairLists differ in %s places; max separation is at %s: %s > %s%s" %
                      (np.sum(badArr), maxInd, sepArr[maxInd], maxDiff, extraMsg(msg)))


@lsst.utils.tests.inTestCase
def assertSpherePointsAlmostEqual(testCase, sp0, sp1, maxSep=0.001*arcseconds, msg=""):
    r"""Assert that two `~lsst.afw.geom.SpherePoint`\ s are almost equal

    Parameters
    ----------
    testCase : `unittest.TestCase`
        test case the test is part of; an object supporting one method: fail(self, msgStr)
    sp0 : `lsst.afw.geom.SpherePoint`
        SpherePoint 0
    sp1 : `lsst.afw.geom.SpherePoint`
        SpherePoint 1
    maxSep : `lsst.afw.geom.Angle`
        maximum separation
    msg : `str`
        extra information to be printed with any error message
    """
    if sp0.separation(sp1) > maxSep:
        testCase.fail("Angular separation between %s and %s = %s\" > maxSep = %s\"%s" %
                      (sp0, sp1, sp0.separation(sp1).asArcseconds(), maxSep.asArcseconds(), extraMsg(msg)))


@lsst.utils.tests.inTestCase
def assertSpherePointListsAlmostEqual(testCase, splist0, splist1, maxSep=0.001*arcseconds, msg=None):
    r"""Assert that two lists of `~lsst.afw.geom.SpherePoint`\ s are almost equal

    Parameters
    ----------
    testCase : `unittest.TestCase`
        test case the test is part of; an object supporting one method: fail(self, msgStr)
    splist0 : `list` of `lsst.afw.geom.SpherePoint`
        list of SpherePoints 0
    splist1 : `list` of `lsst.afw.geom.SpherePoint`
        list of SpherePoints 1
    maxSep : `lsst.afw.geom.Angle`
        maximum separation
    msg : `str`
        exception message prefix; details of the error are appended after ": "
    """
    testCase.assertEqual(len(splist0), len(splist1), msg=msg)
    sepArr = np.array([sp0.separation(sp1)
                       for sp0, sp1 in zip(splist0, splist1)])
    badArr = sepArr > maxSep
    if np.any(badArr):
        maxInd = np.argmax(sepArr)
        testCase.fail("SpherePointLists differ in %s places; max separation is at %s: %s\" > %s\"%s" %
                      (np.sum(badArr), maxInd, sepArr[maxInd].asArcseconds(),
                       maxSep.asArcseconds(), extraMsg(msg)))


@lsst.utils.tests.inTestCase
def assertBoxesAlmostEqual(testCase, box0, box1, maxDiff=1e-7, msg="Boxes differ"):
    """Assert that two boxes (`~lsst.afw.geom.Box2D` or `~lsst.afw.geom.Box2I`) are almost equal

    Parameters
    ----------
    testCase : `unittest.TestCase`
        test case the test is part of; an object supporting one method: fail(self, msgStr)
    box0 : `lsst.afw.geom.Box2D` or `lsst.afw.geom.Box2I`
        box 0
    box1 : `lsst.afw.geom.Box2D` or `lsst.afw.geom.Box2I`
        box 1
    maxDiff : `float`
        maximum radial separation between the min points and max points
    msg : `str`
        exception message prefix; details of the error are appended after ": "

    Raises
    ------
    AssertionError
        Raised if the radial difference of the min points or max points is greater than maxDiff

    Notes
    -----
    .. warning::

       Does not compare types, just compares values.
    """
    assertPairsAlmostEqual(testCase, box0.getMin(),
                           box1.getMin(), maxDiff=maxDiff, msg=msg + ": min")
    assertPairsAlmostEqual(testCase, box0.getMax(),
                           box1.getMax(), maxDiff=maxDiff, msg=msg + ": max")