Spaces:
Running
Running
# Copyright (c) Meta Platforms, Inc. and affiliates. | |
# All rights reserved. | |
# | |
# This source code is licensed under the BSD-style license found in the | |
# LICENSE file in the root directory of this source tree. | |
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) | |
# We want to generalize allclose(input, output), which is essentially | |
# all(diff <= atol + rtol * other) | |
# but with a sophisticated handling non-finite values. | |
# We work that around by calling allclose() with the following arguments: | |
# allclose(diff + other_, other_). This computes what we want because | |
# all(|diff + other_ - other_| <= atol + rtol * |other_|) == | |
# all(|norm_fn(input - other)| <= atol + rtol * |norm_fn(other)|) == | |
# all(norm_fn(input - other) <= atol + rtol * 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 | |
# handle bool case | |
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: | |
# Avoid a weirdness with zero dimensional arrays. | |
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) | |