debugging 如何调试numpy掩码

py49o6xq  于 2022-11-14  发布在  其他
关注(0)|答案(1)|浏览(143)

此问题与this one有关。
我有一个函数要矢量化,这是原始函数:

def aspect_good(angle: float, planet1_good: bool, planet2_good: bool):
    """
    Decides if the angle represents a good aspect.
    NOTE: returns None if the angle doesn't represent an aspect.
    """

    if 112 <= angle <= 128 or 52 <= angle <= 68:
        return True
    elif 174 <= angle <= 186 or 84 <= angle <= 96:
        return False
    elif 0 <= angle <= 8 and planet1_good and planet2_good:
        return True
    elif 0 <= angle <= 6:
        return False
    else:
        return None

这是我目前所知道的:

def aspects_good(
    angles: npt.ArrayLike,
    planets1_good: npt.ArrayLike,
    planets2_good: npt.ArrayLike,
) -> npt.NDArray:
    """
    Decides if the angles represent good aspects.

    Note: this function was contributed by Mad Physicist. Thank you.
    https://stackoverflow.com/q/73672739/11004423

    :returns: an array with values as follows:
        1 – the angle is a good aspect
        0 – the angle is a bad aspect
       -1 – the angle doesn't represent an aspect
    """
    result = np.full_like(angles, -1, dtype=np.int8)

    bad_mask = np.abs(angles % 90) <= 6
    result[bad_mask] = 0

    good_mask = (np.abs(angles - 120) <= 8) |\
                (np.abs(angles - 60) <= 8) |\
                ((np.abs(angles - 4) <= 4) & planets1_good & planets2_good)
    result[good_mask] = 1

    return result

然而,它并没有像预期的那样工作,我用pytest写了一个测试:

def test_aspects_good():
    tests = np.array([
        [120, True, False, True],
        [60, True, False, True],
        [180, True, False, False],
        [90, True, False, False],

        [129, True, False, -1],
        [111, True, False, -1],
        [69, True, False, -1],
        [51, True, False, -1],
        [187, True, False, -1],
        [173, True, False, -1],
        [97, True, False, -1],
        [83, True, False, -1],

        [0, True, True, True],
        [0, True, False, False],
        [0, False, True, False],
        [0, False, False, False],

        [7, False, False, -1],
        [7, True, True, True],
        [9, True, True, -1],
    ])

    angles = tests[:, 0]
    planets1_good = tests[:, 1]
    planets2_good = tests[:, 2]
    expected = tests[:, 3]

    result = aspects_good(angles, planets1_good, planets2_good)
    assert np.array_equal(result, expected)

并且失败,返回False,则数组不同。
这里,我将resultexpected数组并排组合在一起:

array([[ 1,  1],
│      [ 1,  1],
│      [ 0,  0],
│      [ 0,  0],
│      [-1, -1],
│      [-1, -1],
│      [-1, -1],
│      [-1, -1],
│      [-1, -1],
│      [-1, -1],
│      [-1, -1],
│      [-1, -1],
│      [ 0,  1],
│      [ 0,  0],
│      [ 0,  0],
│      [ 0,  0],
│      [-1, -1],
│      [-1,  1],
│      [-1, -1]])

注意:第一列是result数组,第二列是expected。正如你所看到的,它们在两个地方不同。现在的问题是“如何调试这个?”通常我会使用调试器,并逐步通过每个if/elif/else条件。但我不知道如何调试numpy掩码。

iqih9akk

iqih9akk1#

这个问题似乎是三件事的结合:

  1. Numpy在整个数组中使用同构类型。
    您会发现tests.dtypedtype('int64')还是dtype('int32')取决于您的体系结构。这意味着包含planet1_goodplanet2_good的列也是整数,而不是布尔值。
    1.按位AND(&)* 不是 * 逻辑运算符。
    按位AND运算将返回一个输入类型中最大的结果。特别是<=的结果,它 * 是 * 一个布尔值,和一个int数组,结果将是int。这意味着你可以做类似np.array([1, 2]) & np.array([True, True])的事情来得到array([1, 0])而不是array([True, False])
  2. Numpy通过数据类型区分布尔掩码和花式索引,即使花式索引只包含0和1。如果你有一个2元素数组x,那么x[[True, True]] = 11赋给x的两个元素。然而,x[[1, 1]] = 1只将1赋给x的第二个元素。
    这就是这里发生的事情。bad_mask是一个布尔掩码,它的工作原理和你所期望的完全一样。但是,good_mask与几个整型数组进行AND运算,所以变成了一个包含0和1的整型数组。表达式result[good_mask] = 1实际上是将result的第一个和第二个元素赋值为1,这恰好对应于您的两个测试。剩余的True结果不能也不会被分配1
    有几种方法可以解决这个问题,按偏好降序列出(我最喜欢的在上面):
    1.将所有数组转换为正确类型的numpy数组。现在你的函数不符合接受任何类似数组的约定。如果你为angles传入一个列表,你将得到TypeError: unsupported operand type(s) for %: 'list' and 'int'。这是一个相当惯用的方法:
angles = np.asanyarray(angles)
planets1_good = np.asanyarray(planets1_good, dtype=bool)
planets2_good = np.asanyarray(planets2_good, dtype=bool)

result = np.full_like(angles, -1, dtype=np.int8)

bad_mask = np.abs(angles % 90) <= 6
result[bad_mask] = 0

good_mask = (np.abs(angles - 120) <= 8) |\
            (np.abs(angles - 60) <= 8) |\
            ((np.abs(angles - 4) <= 4) & planets1_good & planets2_good)
result[good_mask] = 1
return result

1.在应用good_mask之前,请确保它实际上是一个掩码。您仍然应该转换angles,但其他数组将由&运算符自动转换:

good_mask = ((np.abs(angles - 120) <= 8) |\
             (np.abs(angles - 60) <= 8) |\
             ((np.abs(angles - 4) <= 4) & planets1_good & planets2_good)).astype(bool)

您也可以执行类似于bad_mask的操作:

good_mask = (np.abs(angles % 60) <= 8) & (angles >= -8) & (angles <= 128)

1.将掩码转换为索引,它不关心原始的dtype:

result[np.flatnonzero(good_mask)] = 1

相关问题