| |
| |
| |
| |
| |
|
|
| import os |
| import unittest |
| from numbers import Real |
| from pathlib import Path |
| from typing import Callable, Optional, Union |
|
|
| import numpy as np |
| import torch |
| from PIL import Image |
|
|
|
|
| def interactive_testing_requested() -> bool: |
| """ |
| Certain tests are only useful when run interactively, and so are not regularly run. |
| These are activated by this funciton returning True, which the user requests by |
| setting the environment variable `PYTORCH3D_INTERACTIVE_TESTING` to 1. |
| """ |
| return os.environ.get("PYTORCH3D_INTERACTIVE_TESTING", "") == "1" |
|
|
|
|
| def skip_opengl_requested() -> bool: |
| return os.environ.get("PYTORCH3D_NO_TEST_OPENGL", "") == "1" |
|
|
|
|
| usesOpengl = unittest.skipIf(skip_opengl_requested(), "uses opengl") |
|
|
|
|
| def get_tests_dir() -> Path: |
| """ |
| Returns Path for the directory containing this file. |
| """ |
| return Path(__file__).resolve().parent |
|
|
|
|
| def get_pytorch3d_dir() -> Path: |
| """ |
| Returns Path for the root PyTorch3D directory. |
| |
| Meta internal systems need a special case here. |
| """ |
| if os.environ.get("INSIDE_RE_WORKER") is not None: |
| return Path(__file__).resolve().parent.parent |
| elif os.environ.get("CONDA_BUILD_STATE", "") == "TEST": |
| return Path(os.environ["SRC_DIR"]) |
| else: |
| return Path(__file__).resolve().parent.parent |
|
|
|
|
| def load_rgb_image(filename: str, data_dir: Union[str, Path]): |
| filepath = os.path.join(data_dir, filename) |
| with Image.open(filepath) as raw_image: |
| image = torch.from_numpy(np.array(raw_image) / 255.0) |
| image = image.to(dtype=torch.float32) |
| return image[..., :3] |
|
|
|
|
| TensorOrArray = Union[torch.Tensor, np.ndarray] |
|
|
|
|
| def get_random_cuda_device() -> str: |
| """ |
| Function to get a random GPU device from the |
| available devices. This is useful for testing |
| that custom cuda kernels can support inputs on |
| any device without having to set the device explicitly. |
| """ |
| num_devices = torch.cuda.device_count() |
| device_id = ( |
| torch.randint(high=num_devices, size=(1,)).item() if num_devices > 1 else 0 |
| ) |
| return "cuda:%d" % device_id |
|
|
|
|
| class TestCaseMixin(unittest.TestCase): |
| def assertSeparate(self, tensor1, tensor2) -> None: |
| """ |
| Verify that tensor1 and tensor2 have their data in distinct locations. |
| """ |
| self.assertNotEqual(tensor1.storage().data_ptr(), tensor2.storage().data_ptr()) |
|
|
| def assertNotSeparate(self, tensor1, tensor2) -> None: |
| """ |
| Verify that tensor1 and tensor2 have their data in the same locations. |
| """ |
| self.assertEqual(tensor1.storage().data_ptr(), tensor2.storage().data_ptr()) |
|
|
| def assertAllSeparate(self, tensor_list) -> None: |
| """ |
| Verify that all tensors in tensor_list have their data in |
| distinct locations. |
| """ |
| ptrs = [i.storage().data_ptr() for i in tensor_list] |
| self.assertCountEqual(ptrs, set(ptrs)) |
|
|
| def assertNormsClose( |
| self, |
| input: TensorOrArray, |
| other: TensorOrArray, |
| norm_fn: Callable[[TensorOrArray], TensorOrArray], |
| *, |
| rtol: float = 1e-05, |
| atol: float = 1e-08, |
| equal_nan: bool = False, |
| msg: Optional[str] = None, |
| ) -> None: |
| """ |
| Verifies that two tensors or arrays have the same shape and are close |
| given absolute and relative tolerance; raises AssertionError otherwise. |
| A custom norm function is computed before comparison. If no such pre- |
| processing needed, pass `torch.abs` or, equivalently, call `assertClose`. |
| Args: |
| input, other: two tensors or two arrays. |
| norm_fn: The function evaluates |
| `all(norm_fn(input - other) <= atol + rtol * norm_fn(other))`. |
| norm_fn is a tensor -> tensor function; the output has: |
| * all entries non-negative, |
| * shape defined by the input shape only. |
| rtol, atol, equal_nan: as for torch.allclose. |
| msg: message in case the assertion is violated. |
| Note: |
| Optional arguments here are all keyword-only, to avoid confusion |
| with msg arguments on other assert functions. |
| """ |
|
|
| self.assertEqual(np.shape(input), np.shape(other)) |
|
|
| diff = norm_fn(input - other) |
| other_ = norm_fn(other) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| self.assertClose( |
| diff + other_, other_, rtol=rtol, atol=atol, equal_nan=equal_nan, msg=msg |
| ) |
|
|
| def assertClose( |
| self, |
| input: TensorOrArray, |
| other: TensorOrArray, |
| *, |
| rtol: float = 1e-05, |
| atol: float = 1e-08, |
| equal_nan: bool = False, |
| msg: Optional[str] = None, |
| ) -> None: |
| """ |
| Verifies that two tensors or arrays have the same shape and are close |
| given absolute and relative tolerance, i.e. checks |
| `all(|input - other| <= atol + rtol * |other|)`; |
| raises AssertionError otherwise. |
| Args: |
| input, other: two tensors or two arrays. |
| rtol, atol, equal_nan: as for torch.allclose. |
| msg: message in case the assertion is violated. |
| Note: |
| Optional arguments here are all keyword-only, to avoid confusion |
| with msg arguments on other assert functions. |
| """ |
|
|
| self.assertEqual(np.shape(input), np.shape(other)) |
|
|
| backend = torch if torch.is_tensor(input) else np |
| close = backend.allclose( |
| input, other, rtol=rtol, atol=atol, equal_nan=equal_nan |
| ) |
|
|
| if close: |
| return |
|
|
| |
| if backend == torch and input.dtype == torch.bool: |
| diff = (input != other).float() |
| ratio = diff |
| if backend == np and input.dtype == bool: |
| diff = (input != other).astype(float) |
| ratio = diff |
| else: |
| diff = backend.abs(input + 0.0 - other) |
| ratio = diff / backend.abs(other) |
|
|
| try_relative = (diff <= atol) | (backend.isfinite(ratio) & (ratio > 0)) |
| if try_relative.all(): |
| if backend == np: |
| |
| ratio = np.array(ratio) |
| ratio[diff <= atol] = 0 |
| extra = f" Max relative diff {ratio.max()}" |
| else: |
| extra = "" |
| shape = tuple(input.shape) |
| loc = np.unravel_index(int(diff.argmax()), shape) |
| max_diff = diff.max() |
| err = f"Not close. Max diff {max_diff}.{extra} Shape {shape}. At {loc}." |
| if msg is not None: |
| self.fail(f"{msg} {err}") |
| self.fail(err) |
|
|
| def assertConstant( |
| self, input: TensorOrArray, value: Real, *, atol: float = 0 |
| ) -> None: |
| """ |
| Asserts input is entirely filled with value. |
| |
| Args: |
| input: tensor or array |
| value: expected value |
| atol: tolerance |
| """ |
| mn, mx = input.min(), input.max() |
| msg = f"values in range [{mn}, {mx}], not {value}, shape {input.shape}" |
| if atol == 0: |
| self.assertEqual(input.min(), value, msg=msg) |
| self.assertEqual(input.max(), value, msg=msg) |
| else: |
| self.assertGreater(input.min(), value - atol, msg=msg) |
| self.assertLess(input.max(), value + atol, msg=msg) |
|
|