Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add unit tests for array.py #308

Merged
merged 1 commit into from Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
81 changes: 74 additions & 7 deletions lib/contourpy/array.py
@@ -1,7 +1,7 @@
from __future__ import annotations

from itertools import chain
from typing import TYPE_CHECKING, TypeVar
from typing import TYPE_CHECKING, Any, TypeVar

import numpy as np

Expand All @@ -15,9 +15,40 @@
T = TypeVar("T", bound=np.generic) # Type for generic np.ndarray


# Minimalist array-checking functions that check dtype, ndims and shape only.
# They do not walk the arrays to check the contents for performance reasons.
def _check_code_array(codes: Any) -> None:
if not isinstance(codes, np.ndarray):
raise TypeError(f"Expected numpy array not {type(codes)}")
if codes.dtype != code_dtype:
raise ValueError(f"Expected numpy array of dtype {code_dtype} not {codes.dtype}")
if not (codes.ndim == 1 and len(codes) > 1):
raise ValueError(f"Expected numpy array of shape (?,) not {codes.shape}")


def _check_offset_array(offsets: Any) -> None:
if not isinstance(offsets, np.ndarray):
raise TypeError(f"Expected numpy array not {type(offsets)}")
if offsets.dtype != offset_dtype:
raise ValueError(f"Expected numpy array of dtype {offset_dtype} not {offsets.dtype}")
if not (offsets.ndim == 1 and len(offsets) > 1):
raise ValueError(f"Expected numpy array of shape (?,) not {offsets.shape}")


def _check_point_array(points: Any) -> None:
if not isinstance(points, np.ndarray):
raise TypeError(f"Expected numpy array not {type(points)}")
if points.dtype != point_dtype:
raise ValueError(f"Expected numpy array of dtype {point_dtype} not {points.dtype}")
if not (points.ndim == 2 and points.shape[1] ==2 and points.shape[0] > 1):
raise ValueError(f"Expected numpy array of shape (?, 2) not {points.shape}")


def codes_from_offsets(offsets: cpy.OffsetArray) -> cpy.CodeArray:
"""Determine codes from offsets, assuming they all correspond to closed polygons.
"""
_check_offset_array(offsets)

n = offsets[-1]
codes = np.full(n, LINETO, dtype=code_dtype)
codes[offsets[:-1]] = MOVETO
Expand All @@ -32,6 +63,9 @@ def codes_from_offsets_and_points(
"""Determine codes from offsets and points, using the equality of the start and end points of
each line to determine if lines are closed or not.
"""
_check_offset_array(offsets)
_check_point_array(points)

codes = np.full(len(points), LINETO, dtype=code_dtype)
codes[offsets[:-1]] = MOVETO

Expand All @@ -46,6 +80,8 @@ def codes_from_points(points: cpy.PointArray) -> cpy.CodeArray:
"""Determine codes for a single line, using the equality of the start and end points to
determine if the line is closed or not.
"""
_check_point_array(points)

n = len(points)
codes = np.full(n, LINETO, dtype=code_dtype)
codes[0] = MOVETO
Expand All @@ -57,6 +93,9 @@ def codes_from_points(points: cpy.PointArray) -> cpy.CodeArray:
def concat_codes(list_of_codes: list[cpy.CodeArray]) -> cpy.CodeArray:
"""Concatenate a list of codes arrays into a single code array.
"""
if not list_of_codes:
raise ValueError("Empty list passed to concat_codes")

return np.concatenate(list_of_codes, dtype=code_dtype)


Expand All @@ -73,6 +112,9 @@ def concat_codes_or_none(list_of_codes_or_none: list[cpy.CodeArray | None]) -> c
def concat_offsets(list_of_offsets: list[cpy.OffsetArray]) -> cpy.OffsetArray:
"""Concatenate a list of offsets arrays into a single offset array.
"""
if not list_of_offsets:
raise ValueError("Empty list passed to concat_offsets")

n = len(list_of_offsets)
cumulative = np.cumsum([offsets[-1] for offsets in list_of_offsets], dtype=offset_dtype)
ret: cpy.OffsetArray = np.concatenate(
Expand All @@ -97,6 +139,9 @@ def concat_offsets_or_none(
def concat_points(list_of_points: list[cpy.PointArray]) -> cpy.PointArray:
"""Concatenate a list of point arrays into a single point array.
"""
if not list_of_points:
raise ValueError("Empty list passed to concat_points")

return np.concatenate(list_of_points, dtype=point_dtype)


Expand Down Expand Up @@ -128,6 +173,9 @@ def concat_points_or_none_with_nan(
def concat_points_with_nan(list_of_points: list[cpy.PointArray],) -> cpy.PointArray:
"""Concatenate a list of points into a single point array with NaNs used to separate each line.
"""
if not list_of_points:
raise ValueError("Empty list passed to concat_points_with_nan")

if len(list_of_points) == 1:
return list_of_points[0]
else:
Expand All @@ -140,6 +188,9 @@ def concat_points_with_nan(list_of_points: list[cpy.PointArray],) -> cpy.PointAr
def insert_nan_at_offsets(points: cpy.PointArray, offsets: cpy.OffsetArray) -> cpy.PointArray:
"""Insert NaNs into a point array at locations specified by an offset array.
"""
_check_point_array(points)
_check_offset_array(offsets)

if len(offsets) <= 2:
return points
else:
Expand All @@ -151,30 +202,44 @@ def insert_nan_at_offsets(points: cpy.PointArray, offsets: cpy.OffsetArray) -> c
def offsets_from_codes(codes: cpy.CodeArray) -> cpy.OffsetArray:
"""Determine offsets from codes using locations of MOVETO codes.
"""
_check_code_array(codes)

return np.append(np.nonzero(codes == MOVETO)[0], len(codes)).astype(offset_dtype)


def offsets_from_lengths(seq: list[cpy.PointArray]) -> cpy.OffsetArray:
def offsets_from_lengths(list_of_points: list[cpy.PointArray]) -> cpy.OffsetArray:
"""Determine offsets from lengths of point arrays.
"""
return np.cumsum([0] + [len(line) for line in seq], dtype=offset_dtype)
if not list_of_points:
raise ValueError("Empty list passed to offsets_from_lengths")

return np.cumsum([0] + [len(line) for line in list_of_points], dtype=offset_dtype)

def outer_offsets_from_list_of_codes(seq: list[cpy.CodeArray]) -> cpy.OffsetArray:

def outer_offsets_from_list_of_codes(list_of_codes: list[cpy.CodeArray]) -> cpy.OffsetArray:
"""Determine outer offsets from codes using locations of MOVETO codes.
"""
return np.cumsum([0] + [np.count_nonzero(codes == MOVETO) for codes in seq], dtype=offset_dtype)
if not list_of_codes:
raise ValueError("Empty list passed to outer_offsets_from_list_of_codes")

return np.cumsum([0] + [np.count_nonzero(codes == MOVETO) for codes in list_of_codes],
dtype=offset_dtype)


def outer_offsets_from_list_of_offsets(seq: list[cpy.OffsetArray]) -> cpy.OffsetArray:
def outer_offsets_from_list_of_offsets(list_of_offsets: list[cpy.OffsetArray]) -> cpy.OffsetArray:
"""Determine outer offsets from a list of offsets.
"""
return np.cumsum([0] + [len(offsets)-1 for offsets in seq], dtype=offset_dtype)
if not list_of_offsets:
raise ValueError("Empty list passed to outer_offsets_from_list_of_offsets")

return np.cumsum([0] + [len(offsets)-1 for offsets in list_of_offsets], dtype=offset_dtype)


def remove_nan(points: cpy.PointArray) -> tuple[cpy.PointArray, cpy.OffsetArray]:
"""Remove NaN from a points array, also return the offsets corresponding to the NaN removed.
"""
_check_point_array(points)

nan_offsets = np.nonzero(np.isnan(points[:, 0]))[0]
if len(nan_offsets) == 0:
return points, np.array([0, len(points)], dtype=offset_dtype)
Expand All @@ -200,6 +265,8 @@ def split_by_offsets(array: npt.NDArray[T], offsets: cpy.OffsetArray) -> list[np
def split_points_at_nan(points: cpy.PointArray) -> list[cpy.PointArray]:
"""Split a points array at NaNs into a list of point arrays.
"""
_check_point_array(points)

nan_offsets = np.nonzero(np.isnan(points[:, 0]))[0]
if len(nan_offsets) == 0:
return [points]
Expand Down