mirror of
https://github.com/facebookresearch/pytorch3d.git
synced 2026-10-29 09:29:58 +08:00
Compare commits
7 Commits
0a7d4c1a17
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
204fe5831b | ||
|
|
8b3ba6ede4 | ||
|
|
88e182f989 | ||
|
|
978cd99221 | ||
|
|
716c594eab | ||
|
|
87a6e51e34 | ||
|
|
fa22ad16c4 |
@@ -77,6 +77,20 @@ class _SymEig3x3(nn.Module):
|
|||||||
if inputs.shape[-2:] != (3, 3):
|
if inputs.shape[-2:] != (3, 3):
|
||||||
raise ValueError("Only inputs of shape (..., 3, 3) are supported.")
|
raise ValueError("Only inputs of shape (..., 3, 3) are supported.")
|
||||||
|
|
||||||
|
# The thresholds and regularizers used below are absolute, so they are
|
||||||
|
# only meaningful for inputs of order 1: rescale each matrix to unit
|
||||||
|
# maximum magnitude and undo it on the eigenvalues. The clamp only
|
||||||
|
# matters for the all-zero matrix. The scale is detached because the
|
||||||
|
# decomposition is homogeneous of degree 1, so its Jacobian is
|
||||||
|
# homogeneous of degree 0 and holding the scale constant is exact.
|
||||||
|
scale = (
|
||||||
|
inputs.detach()
|
||||||
|
.abs()
|
||||||
|
.amax(dim=(-2, -1), keepdim=True)
|
||||||
|
.clamp(min=torch.finfo(inputs.dtype).tiny)
|
||||||
|
)
|
||||||
|
inputs = inputs / scale
|
||||||
|
|
||||||
inputs_diag = inputs.diagonal(dim1=-2, dim2=-1)
|
inputs_diag = inputs.diagonal(dim1=-2, dim2=-1)
|
||||||
inputs_trace = inputs_diag.sum(-1)
|
inputs_trace = inputs_diag.sum(-1)
|
||||||
q = inputs_trace / 3.0
|
q = inputs_trace / 3.0
|
||||||
@@ -90,7 +104,6 @@ class _SymEig3x3(nn.Module):
|
|||||||
)
|
)
|
||||||
|
|
||||||
p = torch.sqrt(p2 / 6.0)
|
p = torch.sqrt(p2 / 6.0)
|
||||||
# pyrefly: ignore [unsupported-operation]
|
|
||||||
B = (inputs - q[..., None, None] * self._identity) / p[..., None, None]
|
B = (inputs - q[..., None, None] * self._identity) / p[..., None, None]
|
||||||
|
|
||||||
r = torch.det(B) / 2.0
|
r = torch.det(B) / 2.0
|
||||||
@@ -120,7 +133,7 @@ class _SymEig3x3(nn.Module):
|
|||||||
else:
|
else:
|
||||||
eigenvecs = None
|
eigenvecs = None
|
||||||
|
|
||||||
return eigenvals, eigenvecs
|
return eigenvals * scale.squeeze(-1), eigenvecs
|
||||||
|
|
||||||
def _construct_eigenvecs_set(
|
def _construct_eigenvecs_set(
|
||||||
self, inputs: torch.Tensor, eigenvals: torch.Tensor
|
self, inputs: torch.Tensor, eigenvals: torch.Tensor
|
||||||
@@ -175,10 +188,8 @@ class _SymEig3x3(nn.Module):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Find the eigenvector corresponding to alpha0, its eigenvalue is distinct
|
# Find the eigenvector corresponding to alpha0, its eigenvalue is distinct
|
||||||
# pyrefly: ignore [unsupported-operation]
|
|
||||||
ev0 = self._get_ev0(inputs - alpha0[..., None, None] * self._identity)
|
ev0 = self._get_ev0(inputs - alpha0[..., None, None] * self._identity)
|
||||||
u, v = self._get_uv(ev0)
|
u, v = self._get_uv(ev0)
|
||||||
# pyrefly: ignore [unsupported-operation]
|
|
||||||
ev1 = self._get_ev1(inputs - alpha1[..., None, None] * self._identity, u, v)
|
ev1 = self._get_ev1(inputs - alpha1[..., None, None] * self._identity, u, v)
|
||||||
# Third eigenvector is computed as the cross-product of the other two
|
# Third eigenvector is computed as the cross-product of the other two
|
||||||
ev2 = torch.cross(ev0, ev1, dim=-1)
|
ev2 = torch.cross(ev0, ev1, dim=-1)
|
||||||
@@ -253,7 +264,6 @@ class _SymEig3x3(nn.Module):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
min_idx = w.abs().argmin(dim=-1)
|
min_idx = w.abs().argmin(dim=-1)
|
||||||
# pyrefly: ignore [bad-index]
|
|
||||||
rotation_2d = self._rotations_3d[min_idx].to(w)
|
rotation_2d = self._rotations_3d[min_idx].to(w)
|
||||||
|
|
||||||
u = F.normalize((rotation_2d @ w[..., None])[..., 0], dim=-1)
|
u = F.normalize((rotation_2d @ w[..., None])[..., 0], dim=-1)
|
||||||
|
|||||||
@@ -159,8 +159,6 @@ INLINE DEVICE float3 WARP_SUM_FLOAT3(
|
|||||||
#define FFLOOR(a) floorf(a)
|
#define FFLOOR(a) floorf(a)
|
||||||
#define FROUND(x) nearbyintf(x)
|
#define FROUND(x) nearbyintf(x)
|
||||||
#define FABS(a) abs(a)
|
#define FABS(a) abs(a)
|
||||||
#define IASF(a, loc) (loc) = __int_as_float(a)
|
|
||||||
#define FASI(a, loc) (loc) = __float_as_int(a)
|
|
||||||
#define FABSLEQAS(a, b, c) \
|
#define FABSLEQAS(a, b, c) \
|
||||||
((a) <= (b) ? FSUB((b), (a)) <= (c) : FSUB((a), (b)) < (c))
|
((a) <= (b) ? FSUB((b), (a)) <= (c) : FSUB((a), (b)) < (c))
|
||||||
#define I2F(a) __int2float_rn(a)
|
#define I2F(a) __int2float_rn(a)
|
||||||
|
|||||||
@@ -114,8 +114,6 @@ INLINE float atomicMin(float* address, float val) {
|
|||||||
#define FMA(x, y, z) ((x) * (y) + (z))
|
#define FMA(x, y, z) ((x) * (y) + (z))
|
||||||
#define I2F(a) static_cast<float>(a)
|
#define I2F(a) static_cast<float>(a)
|
||||||
#define FRCP(x) (1.f / (x))
|
#define FRCP(x) (1.f / (x))
|
||||||
#define IASF(x, loc) memcpy(&(loc), &(x), sizeof(x))
|
|
||||||
#define FASI(x, loc) memcpy(&(loc), &(x), sizeof(x))
|
|
||||||
#define DMAX(a, b) std::max((a), (b))
|
#define DMAX(a, b) std::max((a), (b))
|
||||||
#define DMIN(a, b) std::min((a), (b))
|
#define DMIN(a, b) std::min((a), (b))
|
||||||
#define DSATURATE(a) DMIN(1., DMAX(0., (a)))
|
#define DSATURATE(a) DMIN(1., DMAX(0., (a)))
|
||||||
|
|||||||
@@ -9,7 +9,11 @@
|
|||||||
#ifndef PULSAR_NATIVE_COMMANDS_ROUTING_H_
|
#ifndef PULSAR_NATIVE_COMMANDS_ROUTING_H_
|
||||||
#define PULSAR_NATIVE_COMMANDS_ROUTING_H_
|
#define PULSAR_NATIVE_COMMANDS_ROUTING_H_
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
#include "../global.h"
|
#include "../global.h"
|
||||||
|
#if __cplusplus >= 202002L
|
||||||
|
#include <bit>
|
||||||
|
#endif
|
||||||
|
|
||||||
// Commands available everywhere.
|
// Commands available everywhere.
|
||||||
#define MALLOC_HOST(VAR, TYPE, SIZE) \
|
#define MALLOC_HOST(VAR, TYPE, SIZE) \
|
||||||
@@ -32,4 +36,37 @@
|
|||||||
#include "../host/commands.h"
|
#include "../host/commands.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Host + device int <-> float bit punning. Replaces IASF / FASI macros.
|
||||||
|
IHD float pulsar_int_as_float(int v) noexcept {
|
||||||
|
#ifdef __CUDA_ARCH__
|
||||||
|
return __int_as_float(v);
|
||||||
|
#elif defined(__HIP_DEVICE_COMPILE__)
|
||||||
|
return __int_as_float(v);
|
||||||
|
#else
|
||||||
|
#if __cpp_lib_bit_cast >= 201806L
|
||||||
|
return std::bit_cast<float>(v);
|
||||||
|
#else
|
||||||
|
float f;
|
||||||
|
std::memcpy(&f, &v, sizeof(f));
|
||||||
|
return f;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
IHD int pulsar_float_as_int(float v) noexcept {
|
||||||
|
#ifdef __CUDA_ARCH__
|
||||||
|
return __float_as_int(v);
|
||||||
|
#elif defined(__HIP_DEVICE_COMPILE__)
|
||||||
|
return __float_as_int(v);
|
||||||
|
#else
|
||||||
|
#if __cpp_lib_bit_cast >= 201806L
|
||||||
|
return std::bit_cast<int>(v);
|
||||||
|
#else
|
||||||
|
int i;
|
||||||
|
std::memcpy(&i, &v, sizeof(i));
|
||||||
|
return i;
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -121,8 +121,8 @@ GLOBAL void calc_gradients(
|
|||||||
grad_im_l[2]);
|
grad_im_l[2]);
|
||||||
// Start processing.
|
// Start processing.
|
||||||
for (int grad_idx = 0; grad_idx < n_track; ++grad_idx) {
|
for (int grad_idx = 0; grad_idx < n_track; ++grad_idx) {
|
||||||
int sphere_idx;
|
int sphere_idx =
|
||||||
FASI(forw_info_d[fwi_loc + 3 + 2 * grad_idx], sphere_idx);
|
pulsar_float_as_int(forw_info_d[fwi_loc + 3 + 2 * grad_idx]);
|
||||||
PASSERT(
|
PASSERT(
|
||||||
sphere_idx == -1 ||
|
sphere_idx == -1 ||
|
||||||
sphere_idx >= 0 && static_cast<uint>(sphere_idx) < num_balls);
|
sphere_idx >= 0 && static_cast<uint>(sphere_idx) < num_balls);
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ GLOBAL void fill_bg(
|
|||||||
// sphere IDs and intersection depths.
|
// sphere IDs and intersection depths.
|
||||||
for (int i = 0; i < renderer.n_track; ++i) {
|
for (int i = 0; i < renderer.n_track; ++i) {
|
||||||
int sphere_id = -1;
|
int sphere_id = -1;
|
||||||
IASF(sphere_id, renderer.forw_info_d[write_loc + 3 + i * 2]);
|
renderer.forw_info_d[write_loc + 3 + i * 2] =
|
||||||
|
pulsar_int_as_float(sphere_id);
|
||||||
renderer.forw_info_d[write_loc + 3 + i * 2 + 1] = -1.f;
|
renderer.forw_info_d[write_loc + 3 + i * 2 + 1] = -1.f;
|
||||||
}
|
}
|
||||||
if (mode == 0) {
|
if (mode == 0) {
|
||||||
|
|||||||
@@ -406,7 +406,7 @@ GLOBAL void render(
|
|||||||
IMIN(n_track, tracker.get_n_hits()));
|
IMIN(n_track, tracker.get_n_hits()));
|
||||||
for (int i = 0; i < n_track; ++i) {
|
for (int i = 0; i < n_track; ++i) {
|
||||||
int sphere_id = tracker.get_closest_sphere_id(i);
|
int sphere_id = tracker.get_closest_sphere_id(i);
|
||||||
IASF(sphere_id, forw_info_d[write_loc + 3 + i * 2]);
|
forw_info_d[write_loc + 3 + i * 2] = pulsar_int_as_float(sphere_id);
|
||||||
forw_info_d[write_loc + 3 + i * 2 + 1] =
|
forw_info_d[write_loc + 3 + i * 2 + 1] =
|
||||||
tracker.get_closest_sphere_depth(i) == MAX_FLOAT
|
tracker.get_closest_sphere_depth(i) == MAX_FLOAT
|
||||||
? -1.f
|
? -1.f
|
||||||
|
|||||||
@@ -182,10 +182,12 @@ def generate_eval_video_cameras(
|
|||||||
)
|
)
|
||||||
|
|
||||||
test_cameras = PerspectiveCameras(
|
test_cameras = PerspectiveCameras(
|
||||||
|
# pyrefly: ignore [bad-argument-type]
|
||||||
focal_length=focal_length,
|
focal_length=focal_length,
|
||||||
principal_point=principal_point,
|
principal_point=principal_point,
|
||||||
R=R,
|
R=R,
|
||||||
T=T,
|
T=T,
|
||||||
|
# pyrefly: ignore [missing-attribute]
|
||||||
device=focal_length.device,
|
device=focal_length.device,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -472,26 +472,26 @@ def _parse_obj(f, data_dir: str):
|
|||||||
materials_idx = len(material_names) - 1
|
materials_idx = len(material_names) - 1
|
||||||
else:
|
else:
|
||||||
materials_idx = material_names.index(material_name)
|
materials_idx = material_names.index(material_name)
|
||||||
elif line.startswith("v "): # Line is a vertex.
|
elif tokens[:1] == ["v"]: # Line is a vertex.
|
||||||
vert = [float(x) for x in tokens[1:4]]
|
vert = [float(x) for x in tokens[1:4]]
|
||||||
if len(vert) != 3:
|
if len(vert) != 3:
|
||||||
msg = "Vertex %s does not have 3 values. Line: %s"
|
msg = "Vertex %s does not have 3 values. Line: %s"
|
||||||
raise ValueError(msg % (str(vert), str(line)))
|
raise ValueError(msg % (str(vert), str(line)))
|
||||||
verts.append(vert)
|
verts.append(vert)
|
||||||
elif line.startswith("vt "): # Line is a texture.
|
elif tokens[:1] == ["vt"]: # Line is a texture.
|
||||||
tx = [float(x) for x in tokens[1:3]]
|
tx = [float(x) for x in tokens[1:3]]
|
||||||
if len(tx) != 2:
|
if len(tx) != 2:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Texture %s does not have 2 values. Line: %s" % (str(tx), str(line))
|
"Texture %s does not have 2 values. Line: %s" % (str(tx), str(line))
|
||||||
)
|
)
|
||||||
verts_uvs.append(tx)
|
verts_uvs.append(tx)
|
||||||
elif line.startswith("vn "): # Line is a normal.
|
elif tokens[:1] == ["vn"]: # Line is a normal.
|
||||||
norm = [float(x) for x in tokens[1:4]]
|
norm = [float(x) for x in tokens[1:4]]
|
||||||
if len(norm) != 3:
|
if len(norm) != 3:
|
||||||
msg = "Normal %s does not have 3 values. Line: %s"
|
msg = "Normal %s does not have 3 values. Line: %s"
|
||||||
raise ValueError(msg % (str(norm), str(line)))
|
raise ValueError(msg % (str(norm), str(line)))
|
||||||
normals.append(norm)
|
normals.append(norm)
|
||||||
elif line.startswith("f "): # Line is a face.
|
elif tokens[:1] == ["f"]: # Line is a face.
|
||||||
# Update face properties info.
|
# Update face properties info.
|
||||||
_parse_face(
|
_parse_face(
|
||||||
line,
|
line,
|
||||||
@@ -712,6 +712,7 @@ def save_obj(
|
|||||||
faces_normals_idx: LongTensor of shape (F, 3) giving the index into
|
faces_normals_idx: LongTensor of shape (F, 3) giving the index into
|
||||||
normals for each vertex in the face.
|
normals for each vertex in the face.
|
||||||
verts_uvs: FloatTensor of shape (V, 2) giving the uv coordinate per vertex.
|
verts_uvs: FloatTensor of shape (V, 2) giving the uv coordinate per vertex.
|
||||||
|
When provided with faces_uvs, UVs are saved even without a texture_map.
|
||||||
faces_uvs: LongTensor of shape (F, 3) giving the index into verts_uvs for
|
faces_uvs: LongTensor of shape (F, 3) giving the index into verts_uvs for
|
||||||
each vertex in the face.
|
each vertex in the face.
|
||||||
texture_map: FloatTensor of shape (H, W, 3) representing the texture map
|
texture_map: FloatTensor of shape (H, W, 3) representing the texture map
|
||||||
@@ -757,7 +758,8 @@ def save_obj(
|
|||||||
if path_manager is None:
|
if path_manager is None:
|
||||||
path_manager = PathManager()
|
path_manager = PathManager()
|
||||||
|
|
||||||
save_texture = all(t is not None for t in [faces_uvs, verts_uvs, texture_map])
|
save_uvs = faces_uvs is not None and verts_uvs is not None
|
||||||
|
save_texture = save_uvs and texture_map is not None
|
||||||
output_path = Path(f)
|
output_path = Path(f)
|
||||||
|
|
||||||
# Save the .obj file
|
# Save the .obj file
|
||||||
@@ -778,7 +780,7 @@ def save_obj(
|
|||||||
faces_normals_idx=faces_normals_idx,
|
faces_normals_idx=faces_normals_idx,
|
||||||
verts_uvs=verts_uvs,
|
verts_uvs=verts_uvs,
|
||||||
faces_uvs=faces_uvs,
|
faces_uvs=faces_uvs,
|
||||||
save_texture=save_texture,
|
save_uvs=save_uvs,
|
||||||
save_normals=normals is not None,
|
save_normals=normals is not None,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -792,7 +794,7 @@ def save_obj(
|
|||||||
mtl_path = str(mtl_path)
|
mtl_path = str(mtl_path)
|
||||||
|
|
||||||
# Save texture map to output folder
|
# Save texture map to output folder
|
||||||
# pyre-fixme[16] # undefined attribute cpu
|
# undefined attribute cpu
|
||||||
texture_map = texture_map.detach().cpu() * 255.0
|
texture_map = texture_map.detach().cpu() * 255.0
|
||||||
image = Image.fromarray(texture_map.numpy().astype(np.uint8))
|
image = Image.fromarray(texture_map.numpy().astype(np.uint8))
|
||||||
with _open_file(image_path, path_manager, "wb") as im_f:
|
with _open_file(image_path, path_manager, "wb") as im_f:
|
||||||
@@ -816,7 +818,7 @@ def _save(
|
|||||||
faces_normals_idx: Optional[torch.Tensor] = None,
|
faces_normals_idx: Optional[torch.Tensor] = None,
|
||||||
verts_uvs: Optional[torch.Tensor] = None,
|
verts_uvs: Optional[torch.Tensor] = None,
|
||||||
faces_uvs: Optional[torch.Tensor] = None,
|
faces_uvs: Optional[torch.Tensor] = None,
|
||||||
save_texture: bool = False,
|
save_uvs: bool = False,
|
||||||
save_normals: bool = False,
|
save_normals: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
if len(verts) and (verts.dim() != 2 or verts.size(1) != 3):
|
if len(verts) and (verts.dim() != 2 or verts.size(1) != 3):
|
||||||
@@ -832,26 +834,24 @@ def _save(
|
|||||||
return
|
return
|
||||||
|
|
||||||
verts, faces = verts.cpu(), faces.cpu()
|
verts, faces = verts.cpu(), faces.cpu()
|
||||||
|
vert_rows = verts.detach().cpu().numpy()
|
||||||
|
|
||||||
lines = ""
|
lines = []
|
||||||
|
|
||||||
if decimal_places is None:
|
if decimal_places is None:
|
||||||
float_str = "%f"
|
float_str = "%f"
|
||||||
else:
|
else:
|
||||||
float_str = "%" + ".%df" % decimal_places
|
float_str = "%" + ".%df" % decimal_places
|
||||||
|
|
||||||
if len(verts):
|
for vert in vert_rows:
|
||||||
V, D = verts.shape
|
lines.append("v %s\n" % " ".join(float_str % v for v in vert))
|
||||||
for i in range(V):
|
|
||||||
vert = [float_str % verts[i, j] for j in range(D)]
|
|
||||||
lines += "v %s\n" % " ".join(vert)
|
|
||||||
|
|
||||||
if save_normals:
|
if save_normals:
|
||||||
assert normals is not None
|
assert normals is not None
|
||||||
assert faces_normals_idx is not None
|
assert faces_normals_idx is not None
|
||||||
lines += _write_normals(normals, faces_normals_idx, float_str)
|
lines.append(_write_normals(normals, float_str))
|
||||||
|
|
||||||
if save_texture:
|
if save_uvs:
|
||||||
assert faces_uvs is not None
|
assert faces_uvs is not None
|
||||||
assert verts_uvs is not None
|
assert verts_uvs is not None
|
||||||
|
|
||||||
@@ -864,15 +864,13 @@ def _save(
|
|||||||
raise ValueError(message)
|
raise ValueError(message)
|
||||||
|
|
||||||
verts_uvs, faces_uvs = verts_uvs.cpu(), faces_uvs.cpu()
|
verts_uvs, faces_uvs = verts_uvs.cpu(), faces_uvs.cpu()
|
||||||
|
uv_rows = verts_uvs.detach().cpu().numpy()
|
||||||
|
|
||||||
# Save verts uvs after verts
|
# Save verts uvs after verts
|
||||||
if len(verts_uvs):
|
for uv in uv_rows:
|
||||||
uV, uD = verts_uvs.shape
|
lines.append("vt %s\n" % " ".join(float_str % u for u in uv))
|
||||||
for i in range(uV):
|
|
||||||
uv = [float_str % verts_uvs[i, j] for j in range(uD)]
|
|
||||||
lines += "vt %s\n" % " ".join(uv)
|
|
||||||
|
|
||||||
f.write(lines)
|
f.write("".join(lines))
|
||||||
|
|
||||||
if torch.any(faces >= verts.shape[0]) or torch.any(faces < 0):
|
if torch.any(faces >= verts.shape[0]) or torch.any(faces < 0):
|
||||||
warnings.warn("Faces have invalid indices")
|
warnings.warn("Faces have invalid indices")
|
||||||
@@ -881,31 +879,21 @@ def _save(
|
|||||||
_write_faces(
|
_write_faces(
|
||||||
f,
|
f,
|
||||||
faces,
|
faces,
|
||||||
faces_uvs if save_texture else None,
|
faces_uvs if save_uvs else None,
|
||||||
faces_normals_idx if save_normals else None,
|
faces_normals_idx if save_normals else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _write_normals(
|
def _write_normals(normals: torch.Tensor, float_str: str) -> str:
|
||||||
normals: torch.Tensor, faces_normals_idx: torch.Tensor, float_str: str
|
|
||||||
) -> str:
|
|
||||||
if faces_normals_idx.dim() != 2 or faces_normals_idx.size(1) != 3:
|
|
||||||
message = (
|
|
||||||
"'faces_normals_idx' should either be empty or of shape (num_faces, 3)."
|
|
||||||
)
|
|
||||||
raise ValueError(message)
|
|
||||||
|
|
||||||
if normals.dim() != 2 or normals.size(1) != 3:
|
if normals.dim() != 2 or normals.size(1) != 3:
|
||||||
message = "'normals' should either be empty or of shape (num_verts, 3)."
|
message = "'normals' should either be empty or of shape (num_verts, 3)."
|
||||||
raise ValueError(message)
|
raise ValueError(message)
|
||||||
|
|
||||||
normals, faces_normals_idx = normals.cpu(), faces_normals_idx.cpu()
|
normal_rows = normals.detach().cpu().numpy()
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
V, D = normals.shape
|
for normal in normal_rows:
|
||||||
for i in range(V):
|
lines.append("vn %s\n" % " ".join(float_str % n for n in normal))
|
||||||
normal = [float_str % normals[i, j] for j in range(D)]
|
|
||||||
lines.append("vn %s\n" % " ".join(normal))
|
|
||||||
return "".join(lines)
|
return "".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -915,31 +903,37 @@ def _write_faces(
|
|||||||
faces_uvs: Optional[torch.Tensor],
|
faces_uvs: Optional[torch.Tensor],
|
||||||
faces_normals_idx: Optional[torch.Tensor],
|
faces_normals_idx: Optional[torch.Tensor],
|
||||||
) -> None:
|
) -> None:
|
||||||
|
face_rows = faces.detach().cpu().numpy()
|
||||||
|
uv_rows = faces_uvs.detach().cpu().numpy() if faces_uvs is not None else None
|
||||||
|
normal_rows = (
|
||||||
|
faces_normals_idx.detach().cpu().numpy()
|
||||||
|
if faces_normals_idx is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
F, P = faces.shape
|
F, P = faces.shape
|
||||||
for i in range(F):
|
for i in range(F):
|
||||||
if faces_normals_idx is not None:
|
face_row = face_rows[i]
|
||||||
if faces_uvs is not None:
|
if normal_rows is not None:
|
||||||
|
normal_row = normal_rows[i]
|
||||||
|
if uv_rows is not None:
|
||||||
|
uv_row = uv_rows[i]
|
||||||
# Format faces as {verts_idx}/{verts_uvs_idx}/{verts_normals_idx}
|
# Format faces as {verts_idx}/{verts_uvs_idx}/{verts_normals_idx}
|
||||||
face = [
|
face = [
|
||||||
"%d/%d/%d"
|
"%d/%d/%d" % (face_row[j] + 1, uv_row[j] + 1, normal_row[j] + 1)
|
||||||
% (
|
|
||||||
faces[i, j] + 1,
|
|
||||||
faces_uvs[i, j] + 1,
|
|
||||||
faces_normals_idx[i, j] + 1,
|
|
||||||
)
|
|
||||||
for j in range(P)
|
for j in range(P)
|
||||||
]
|
]
|
||||||
else:
|
else:
|
||||||
# Format faces as {verts_idx}//{verts_normals_idx}
|
# Format faces as {verts_idx}//{verts_normals_idx}
|
||||||
face = [
|
face = [
|
||||||
"%d//%d" % (faces[i, j] + 1, faces_normals_idx[i, j] + 1)
|
"%d//%d" % (face_row[j] + 1, normal_row[j] + 1) for j in range(P)
|
||||||
for j in range(P)
|
|
||||||
]
|
]
|
||||||
elif faces_uvs is not None:
|
elif uv_rows is not None:
|
||||||
|
uv_row = uv_rows[i]
|
||||||
# Format faces as {verts_idx}/{verts_uvs_idx}
|
# Format faces as {verts_idx}/{verts_uvs_idx}
|
||||||
face = ["%d/%d" % (faces[i, j] + 1, faces_uvs[i, j] + 1) for j in range(P)]
|
face = ["%d/%d" % (face_row[j] + 1, uv_row[j] + 1) for j in range(P)]
|
||||||
else:
|
else:
|
||||||
face = ["%d" % (faces[i, j] + 1) for j in range(P)]
|
face = ["%d" % (face_row[j] + 1) for j in range(P)]
|
||||||
|
|
||||||
if i + 1 < F:
|
if i + 1 < F:
|
||||||
f.write("f %s\n" % " ".join(face))
|
f.write("f %s\n" % " ".join(face))
|
||||||
|
|||||||
@@ -340,8 +340,11 @@ def corresponding_points_alignment(
|
|||||||
total_weight = torch.clamp(num_points, 1)
|
total_weight = torch.clamp(num_points, 1)
|
||||||
# special handling for heterogeneous point clouds and/or input weights
|
# special handling for heterogeneous point clouds and/or input weights
|
||||||
if weights is not None:
|
if weights is not None:
|
||||||
|
# pyrefly: ignore [bad-index]
|
||||||
Xc *= weights[:, :, None]
|
Xc *= weights[:, :, None]
|
||||||
|
# pyrefly: ignore [bad-index]
|
||||||
Yc *= weights[:, :, None]
|
Yc *= weights[:, :, None]
|
||||||
|
# pyrefly: ignore [missing-attribute]
|
||||||
total_weight = torch.clamp(weights.sum(1), eps)
|
total_weight = torch.clamp(weights.sum(1), eps)
|
||||||
|
|
||||||
if (num_points < (dim + 1)).any():
|
if (num_points < (dim + 1)).any():
|
||||||
|
|||||||
@@ -427,9 +427,11 @@ class CamerasBase(TensorProperties):
|
|||||||
if isinstance(index, tensor_types["bool"]):
|
if isinstance(index, tensor_types["bool"]):
|
||||||
# LongTensor]` has no attribute `ndim`.
|
# LongTensor]` has no attribute `ndim`.
|
||||||
# LongTensor]` has no attribute `shape`.
|
# LongTensor]` has no attribute `shape`.
|
||||||
|
# pyrefly: ignore [missing-attribute]
|
||||||
if index.ndim != 1 or index.shape[0] != len(self):
|
if index.ndim != 1 or index.shape[0] != len(self):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
# LongTensor]` has no attribute `shape`.
|
# LongTensor]` has no attribute `shape`.
|
||||||
|
# pyrefly: ignore [missing-attribute]
|
||||||
f"Boolean index of shape {index.shape} does not match cameras"
|
f"Boolean index of shape {index.shape} does not match cameras"
|
||||||
)
|
)
|
||||||
elif max(index) >= len(self):
|
elif max(index) >= len(self):
|
||||||
@@ -1790,11 +1792,13 @@ def get_ndc_to_screen_transform(
|
|||||||
image_size = torch.tensor(image_size, device=cameras.device)
|
image_size = torch.tensor(image_size, device=cameras.device)
|
||||||
# pyrefly: ignore [missing-attribute]
|
# pyrefly: ignore [missing-attribute]
|
||||||
image_size = image_size.view(-1, 2) # of shape (1 or B)x2
|
image_size = image_size.view(-1, 2) # of shape (1 or B)x2
|
||||||
|
# pyrefly: ignore [missing-attribute]
|
||||||
height, width = image_size.unbind(1)
|
height, width = image_size.unbind(1)
|
||||||
|
|
||||||
# For non square images, we scale the points such that smallest side
|
# For non square images, we scale the points such that smallest side
|
||||||
# has range [-1, 1] and the largest side has range [-u, u], with u > 1.
|
# has range [-1, 1] and the largest side has range [-u, u], with u > 1.
|
||||||
# This convention is consistent with the PyTorch3D renderer
|
# This convention is consistent with the PyTorch3D renderer
|
||||||
|
# pyrefly: ignore [missing-attribute]
|
||||||
scale = (image_size.min(dim=1).values - 0.0) / 2.0
|
scale = (image_size.min(dim=1).values - 0.0) / 2.0
|
||||||
|
|
||||||
K[:, 0, 0] = scale
|
K[:, 0, 0] = scale
|
||||||
|
|||||||
@@ -136,16 +136,13 @@ class HarmonicEmbedding(torch.nn.Module):
|
|||||||
[..., (n_harmonic_functions * 2 + int(append_input)) * num_points_per_ray]
|
[..., (n_harmonic_functions * 2 + int(append_input)) * num_points_per_ray]
|
||||||
"""
|
"""
|
||||||
# [..., dim, n_harmonic_functions]
|
# [..., dim, n_harmonic_functions]
|
||||||
# pyrefly: ignore [unsupported-operation]
|
|
||||||
embed = x[..., None] * self._frequencies
|
embed = x[..., None] * self._frequencies
|
||||||
# [..., 1, dim, n_harmonic_functions] + [2, 1, 1] => [..., 2, dim, n_harmonic_functions]
|
# [..., 1, dim, n_harmonic_functions] + [2, 1, 1] => [..., 2, dim, n_harmonic_functions]
|
||||||
# pyrefly: ignore [bad-index]
|
|
||||||
embed = embed[..., None, :, :] + self._zero_half_pi[..., None, None]
|
embed = embed[..., None, :, :] + self._zero_half_pi[..., None, None]
|
||||||
# Use the trig identity cos(x) = sin(x + pi/2)
|
# Use the trig identity cos(x) = sin(x + pi/2)
|
||||||
# and do one vectorized call to sin([x, x+pi/2]) instead of (sin(x), cos(x)).
|
# and do one vectorized call to sin([x, x+pi/2]) instead of (sin(x), cos(x)).
|
||||||
embed = embed.sin()
|
embed = embed.sin()
|
||||||
if diag_cov is not None:
|
if diag_cov is not None:
|
||||||
# pyrefly: ignore [no-matching-overload]
|
|
||||||
x_var = diag_cov[..., None] * torch.pow(self._frequencies, 2)
|
x_var = diag_cov[..., None] * torch.pow(self._frequencies, 2)
|
||||||
exp_var = torch.exp(-0.5 * x_var)
|
exp_var = torch.exp(-0.5 * x_var)
|
||||||
# [..., 2, dim, n_harmonic_functions]
|
# [..., 2, dim, n_harmonic_functions]
|
||||||
@@ -184,7 +181,6 @@ class HarmonicEmbedding(torch.nn.Module):
|
|||||||
"""
|
"""
|
||||||
return self.get_output_dim_static(
|
return self.get_output_dim_static(
|
||||||
input_dims,
|
input_dims,
|
||||||
# pyrefly: ignore [bad-argument-type]
|
|
||||||
len(self._frequencies),
|
len(self._frequencies),
|
||||||
self.append_input,
|
self.append_input,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -239,12 +239,10 @@ class MeshRasterizer(nn.Module):
|
|||||||
if raster_settings.perspective_correct is not None:
|
if raster_settings.perspective_correct is not None:
|
||||||
perspective_correct = raster_settings.perspective_correct
|
perspective_correct = raster_settings.perspective_correct
|
||||||
else:
|
else:
|
||||||
# pyrefly: ignore [missing-attribute]
|
|
||||||
perspective_correct = cameras.is_perspective()
|
perspective_correct = cameras.is_perspective()
|
||||||
if raster_settings.z_clip_value is not None:
|
if raster_settings.z_clip_value is not None:
|
||||||
z_clip = raster_settings.z_clip_value
|
z_clip = raster_settings.z_clip_value
|
||||||
else:
|
else:
|
||||||
# pyrefly: ignore [missing-attribute]
|
|
||||||
znear = cameras.get_znear()
|
znear = cameras.get_znear()
|
||||||
if isinstance(znear, torch.Tensor):
|
if isinstance(znear, torch.Tensor):
|
||||||
znear = znear.min().item()
|
znear = znear.min().item()
|
||||||
|
|||||||
@@ -510,7 +510,9 @@ class Meshes:
|
|||||||
index = index.squeeze(1) if index.numel() > 0 else index
|
index = index.squeeze(1) if index.numel() > 0 else index
|
||||||
# pyrefly: ignore [missing-attribute]
|
# pyrefly: ignore [missing-attribute]
|
||||||
index = index.tolist()
|
index = index.tolist()
|
||||||
|
# pyrefly: ignore [not-iterable]
|
||||||
verts = [self.verts_list()[i] for i in index]
|
verts = [self.verts_list()[i] for i in index]
|
||||||
|
# pyrefly: ignore [not-iterable]
|
||||||
faces = [self.faces_list()[i] for i in index]
|
faces = [self.faces_list()[i] for i in index]
|
||||||
else:
|
else:
|
||||||
raise IndexError(index)
|
raise IndexError(index)
|
||||||
|
|||||||
@@ -411,10 +411,13 @@ class Pointclouds:
|
|||||||
index = index.squeeze(1) if index.numel() > 0 else index
|
index = index.squeeze(1) if index.numel() > 0 else index
|
||||||
# pyrefly: ignore [missing-attribute]
|
# pyrefly: ignore [missing-attribute]
|
||||||
index = index.tolist()
|
index = index.tolist()
|
||||||
|
# pyrefly: ignore [not-iterable]
|
||||||
points = [self.points_list()[i] for i in index]
|
points = [self.points_list()[i] for i in index]
|
||||||
if normals_list is not None:
|
if normals_list is not None:
|
||||||
|
# pyrefly: ignore [not-iterable]
|
||||||
normals = [normals_list[i] for i in index]
|
normals = [normals_list[i] for i in index]
|
||||||
if features_list is not None:
|
if features_list is not None:
|
||||||
|
# pyrefly: ignore [not-iterable]
|
||||||
features = [features_list[i] for i in index]
|
features = [features_list[i] for i in index]
|
||||||
else:
|
else:
|
||||||
raise IndexError(index)
|
raise IndexError(index)
|
||||||
|
|||||||
@@ -319,6 +319,29 @@ class TestMeshObjIO(TestCaseMixin, unittest.TestCase):
|
|||||||
self.assertTrue(materials is None)
|
self.assertTrue(materials is None)
|
||||||
self.assertTrue(tex_maps is None)
|
self.assertTrue(tex_maps is None)
|
||||||
|
|
||||||
|
def test_load_obj_tab_separated(self):
|
||||||
|
# Tabs are whitespace too, including between the keyword and the values.
|
||||||
|
obj_file = "\n".join(
|
||||||
|
[
|
||||||
|
"v\t0.1\t0.2\t0.3",
|
||||||
|
"v 0.2 0.3 0.4",
|
||||||
|
"v\t0.3 0.4 0.5",
|
||||||
|
"vt\t0.5\t0.5",
|
||||||
|
"vn\t0.0\t0.0\t1.0",
|
||||||
|
"f\t1/1/1\t2/1/1\t3/1/1",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
verts, faces, aux = load_obj(StringIO(obj_file))
|
||||||
|
expected_verts = torch.tensor(
|
||||||
|
[[0.1, 0.2, 0.3], [0.2, 0.3, 0.4], [0.3, 0.4, 0.5]], dtype=torch.float32
|
||||||
|
)
|
||||||
|
self.assertClose(verts, expected_verts)
|
||||||
|
self.assertClose(faces.verts_idx, torch.tensor([[0, 1, 2]]))
|
||||||
|
self.assertClose(faces.textures_idx, torch.tensor([[0, 0, 0]]))
|
||||||
|
self.assertClose(faces.normals_idx, torch.tensor([[0, 0, 0]]))
|
||||||
|
self.assertClose(aux.verts_uvs, torch.tensor([[0.5, 0.5]]))
|
||||||
|
self.assertClose(aux.normals, torch.tensor([[0.0, 0.0, 1.0]]))
|
||||||
|
|
||||||
def test_load_obj_error_textures(self):
|
def test_load_obj_error_textures(self):
|
||||||
obj_file = "\n".join(["vt 0.1"])
|
obj_file = "\n".join(["vt 0.1"])
|
||||||
with NamedTemporaryFile(mode="w", suffix=".obj") as f:
|
with NamedTemporaryFile(mode="w", suffix=".obj") as f:
|
||||||
@@ -955,6 +978,60 @@ class TestMeshObjIO(TestCaseMixin, unittest.TestCase):
|
|||||||
with open(obj_file, "r") as actual_file:
|
with open(obj_file, "r") as actual_file:
|
||||||
self.assertEqual(actual_file.read(), expected_obj_file)
|
self.assertEqual(actual_file.read(), expected_obj_file)
|
||||||
|
|
||||||
|
def test_save_obj_with_uvs_without_texture_map(self):
|
||||||
|
verts = torch.tensor(
|
||||||
|
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 1.0, 0.0]],
|
||||||
|
dtype=torch.float32,
|
||||||
|
)
|
||||||
|
faces = torch.tensor([[0, 2, 1]], dtype=torch.int64)
|
||||||
|
verts_uvs = torch.tensor(
|
||||||
|
[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]],
|
||||||
|
dtype=torch.float32,
|
||||||
|
)
|
||||||
|
faces_uvs = torch.tensor([[3, 1, 2]], dtype=torch.int64)
|
||||||
|
normals = torch.tensor([[0.0, 0.0, 1.0], [0.0, 1.0, 0.0]])
|
||||||
|
faces_normals_idx = torch.tensor([[1, 0, 1]], dtype=torch.int64)
|
||||||
|
|
||||||
|
for save_normals in (False, True):
|
||||||
|
with (
|
||||||
|
self.subTest(save_normals=save_normals),
|
||||||
|
TemporaryDirectory() as temp_dir,
|
||||||
|
):
|
||||||
|
obj_file = Path(temp_dir) / "mesh.obj"
|
||||||
|
save_obj(
|
||||||
|
obj_file,
|
||||||
|
verts,
|
||||||
|
faces,
|
||||||
|
verts_uvs=verts_uvs,
|
||||||
|
faces_uvs=faces_uvs,
|
||||||
|
normals=normals if save_normals else None,
|
||||||
|
faces_normals_idx=faces_normals_idx if save_normals else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(os.listdir(temp_dir), ["mesh.obj"])
|
||||||
|
obj_text = obj_file.read_text()
|
||||||
|
self.assertNotIn("mtllib", obj_text)
|
||||||
|
self.assertNotIn("usemtl", obj_text)
|
||||||
|
expected_face = (
|
||||||
|
"f 1/4/2 3/2/1 2/3/2" if save_normals else "f 1/4 3/2 2/3"
|
||||||
|
)
|
||||||
|
self.assertIn(expected_face, obj_text)
|
||||||
|
|
||||||
|
loaded_verts, loaded_faces, aux = load_obj(
|
||||||
|
obj_file, load_textures=False
|
||||||
|
)
|
||||||
|
self.assertClose(loaded_verts, verts)
|
||||||
|
self.assertClose(loaded_faces.verts_idx, faces)
|
||||||
|
self.assertClose(aux.verts_uvs, verts_uvs)
|
||||||
|
self.assertClose(loaded_faces.textures_idx, faces_uvs)
|
||||||
|
self.assertIsNone(aux.material_colors)
|
||||||
|
self.assertIsNone(aux.texture_images)
|
||||||
|
if save_normals:
|
||||||
|
self.assertClose(aux.normals, normals)
|
||||||
|
self.assertClose(loaded_faces.normals_idx, faces_normals_idx)
|
||||||
|
else:
|
||||||
|
self.assertIsNone(aux.normals)
|
||||||
|
|
||||||
def test_save_obj_with_texture(self):
|
def test_save_obj_with_texture(self):
|
||||||
verts = torch.tensor(
|
verts = torch.tensor(
|
||||||
[[0.01, 0.2, 0.301], [0.2, 0.03, 0.408], [0.3, 0.4, 0.05], [0.6, 0.7, 0.8]],
|
[[0.01, 0.2, 0.301], [0.2, 0.03, 0.408], [0.3, 0.4, 0.05], [0.6, 0.7, 0.8]],
|
||||||
|
|||||||
@@ -66,6 +66,51 @@ class TestSymEig3x3(TestCaseMixin, unittest.TestCase):
|
|||||||
rtol=rtol,
|
rtol=rtol,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_is_eigen_scaled_gpu(self):
|
||||||
|
self._test_is_eigen_scaled(self._gpu)
|
||||||
|
|
||||||
|
def test_is_eigen_scaled_cpu(self):
|
||||||
|
self._test_is_eigen_scaled(self._cpu)
|
||||||
|
|
||||||
|
def _test_is_eigen_scaled(self, device):
|
||||||
|
"""
|
||||||
|
The decomposition is homogeneous, so scaling the input must not affect
|
||||||
|
the relative accuracy of the result.
|
||||||
|
"""
|
||||||
|
test_input = self.create_random_sym3x3(device, n=self.TEST_BATCH_SIZE)
|
||||||
|
|
||||||
|
for scale in (1e-6, 1e-4, 1e-2, 1e2, 1e4, 1e6):
|
||||||
|
scaled_input = test_input * scale
|
||||||
|
|
||||||
|
self._test_is_not_nan_or_inf(scaled_input)
|
||||||
|
self._test_is_eigen(scaled_input, atol=1e-04 * scale, rtol=1e-02)
|
||||||
|
self._test_eigenvectors_are_orthonormal(scaled_input)
|
||||||
|
|
||||||
|
def test_mixed_magnitudes_gpu(self):
|
||||||
|
self._test_mixed_magnitudes(self._gpu)
|
||||||
|
|
||||||
|
def test_mixed_magnitudes_cpu(self):
|
||||||
|
self._test_mixed_magnitudes(self._cpu)
|
||||||
|
|
||||||
|
def _test_mixed_magnitudes(self, device):
|
||||||
|
"""
|
||||||
|
Each matrix in a batch must be conditioned on its own magnitude, so
|
||||||
|
matrices spanning many orders of magnitude can be batched together.
|
||||||
|
"""
|
||||||
|
test_input = self.create_random_sym3x3(device, n=self.TEST_BATCH_SIZE)
|
||||||
|
scales = torch.logspace(-6, 6, self.TEST_BATCH_SIZE, device=device)[
|
||||||
|
:, None, None
|
||||||
|
]
|
||||||
|
scaled_input = test_input * scales
|
||||||
|
|
||||||
|
eigenvalues, eigenvectors = symeig3x3(scaled_input, eigenvectors=True)
|
||||||
|
reconstruction = (
|
||||||
|
eigenvectors @ eigenvalues.diag_embed() @ eigenvectors.transpose(-2, -1)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertClose(test_input, reconstruction / scales, atol=1e-04, rtol=1e-02)
|
||||||
|
self._test_eigenvectors_are_orthonormal(scaled_input)
|
||||||
|
|
||||||
def test_eigenvectors_are_orthonormal_gpu(self):
|
def test_eigenvectors_are_orthonormal_gpu(self):
|
||||||
test_input = self.create_random_sym3x3(self._gpu, n=self.TEST_BATCH_SIZE)
|
test_input = self.create_random_sym3x3(self._gpu, n=self.TEST_BATCH_SIZE)
|
||||||
|
|
||||||
@@ -192,21 +237,31 @@ class TestSymEig3x3(TestCaseMixin, unittest.TestCase):
|
|||||||
def test_degenerate_eigenvalues_cpu(self):
|
def test_degenerate_eigenvalues_cpu(self):
|
||||||
self._test_degenerate_eigenvalues(self._cpu)
|
self._test_degenerate_eigenvalues(self._cpu)
|
||||||
|
|
||||||
def _test_degenerate_eigenvalues(self, device):
|
def test_degenerate_eigenvalues_scaled_gpu(self):
|
||||||
|
for scale in (1e-6, 1e6):
|
||||||
|
self._test_degenerate_eigenvalues(self._gpu, scale=scale)
|
||||||
|
|
||||||
|
def test_degenerate_eigenvalues_scaled_cpu(self):
|
||||||
|
for scale in (1e-6, 1e6):
|
||||||
|
self._test_degenerate_eigenvalues(self._cpu, scale=scale)
|
||||||
|
|
||||||
|
def _test_degenerate_eigenvalues(self, device, scale: float = 1.0):
|
||||||
"""
|
"""
|
||||||
Test degenerate eigenvalues like zero-valued and with 2-/3-multiplicity
|
Test degenerate eigenvalues like zero-valued and with 2-/3-multiplicity.
|
||||||
|
These are the cases held together by the regularizers inside the
|
||||||
|
decomposition, which only bite at the right input magnitude.
|
||||||
"""
|
"""
|
||||||
# Error tolerances for degenerate values are increased as things might become
|
# Error tolerances for degenerate values are increased as things might become
|
||||||
# numerically unstable
|
# numerically unstable
|
||||||
deg_atol = 1e-3
|
deg_atol = 1e-3 * scale
|
||||||
deg_rtol = 1.0
|
deg_rtol = 1.0
|
||||||
|
|
||||||
# Construct random orthonormal sets
|
# Construct random orthonormal sets
|
||||||
test_eigenvecs = random_rotations(n=self.TEST_BATCH_SIZE, device=device)
|
test_eigenvecs = random_rotations(n=self.TEST_BATCH_SIZE, device=device)
|
||||||
|
|
||||||
# Construct random eigenvalues
|
# Construct random eigenvalues
|
||||||
test_eigenvals = torch.randn(
|
test_eigenvals = (
|
||||||
(self.TEST_BATCH_SIZE, 3), device=test_eigenvecs.device
|
torch.randn((self.TEST_BATCH_SIZE, 3), device=test_eigenvecs.device) * scale
|
||||||
)
|
)
|
||||||
self._test_eigenvalues_and_eigenvectors(
|
self._test_eigenvalues_and_eigenvectors(
|
||||||
test_eigenvecs, test_eigenvals, atol=deg_atol, rtol=deg_rtol
|
test_eigenvecs, test_eigenvals, atol=deg_atol, rtol=deg_rtol
|
||||||
|
|||||||
Reference in New Issue
Block a user