Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,41 @@ dmypy.json
# Datasets
data/
logs/

# Claude settings
.claude/*

# Poetry
poetry.lock

# IDE files
.vscode/
.idea/
*.swp
*.swo
*~

# OS files
.DS_Store
Thumbs.db

# Testing artifacts
.pytest_cache/
.coverage
htmlcov/
coverage.xml
*.cover
.hypothesis/
.tox/
.nox/

# Build artifacts
build/
dist/
*.egg-info/
.eggs/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg
MANIFEST
97 changes: 97 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
[tool.poetry]
name = "hashnerf-pytorch"
version = "0.1.0"
description = "A pure PyTorch implementation of Instant-NGP for Neural Radiance Fields"
authors = ["Your Name <your.email@example.com>"]
readme = "README.md"
packages = [{include = "*.py"}]

[tool.poetry.dependencies]
python = "^3.8"
torch = "^2.0.0"
numpy = "^1.21.0"
imageio = "^2.31.0"
matplotlib = "^3.5.0"
opencv-python = "^4.8.0"
tqdm = "^4.65.0"
kornia = "^0.7.0"
pyvista = {version = "^0.42.0", optional = true}
pillow = "^10.0.0"

[tool.poetry.extras]
scannet = ["pyvista"]

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
pytest-mock = "^3.11.1"

[tool.poetry.scripts]
test = "pytest:main"
tests = "pytest:main"

[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["tests"]
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = [
"--strict-markers",
"--verbose",
"--cov=.",
"--cov-config=pyproject.toml",
"--cov-report=term-missing",
"--cov-report=html",
"--cov-report=xml",
"--cov-fail-under=80",
]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow running tests",
]
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning",
]

[tool.coverage.run]
source = ["."]
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
"*/site-packages/*",
"setup.py",
"*/migrations/*",
"*/config/*",
"*/scripts/*",
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
fail_under = 80
exclude_lines = [
"pragma: no cover",
"def __repr__",
"def __str__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Empty file added tests/__init__.py
Empty file.
222 changes: 222 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import pytest
import tempfile
import shutil
import os
import json
import numpy as np
import torch
from pathlib import Path
from unittest.mock import Mock, MagicMock


@pytest.fixture
def temp_dir():
"""Create a temporary directory for test files."""
temp_dir = tempfile.mkdtemp()
yield temp_dir
shutil.rmtree(temp_dir)


@pytest.fixture
def temp_file(temp_dir):
"""Create a temporary file path."""
def _temp_file(filename="test_file.txt"):
return os.path.join(temp_dir, filename)
return _temp_file


@pytest.fixture
def mock_config():
"""Create a mock configuration dictionary."""
return {
"expname": "test_experiment",
"basedir": "./logs",
"datadir": "./data/test",
"N_rand": 1024,
"N_samples": 64,
"N_importance": 128,
"perturb": 1.0,
"use_viewdirs": True,
"i_embed": 0,
"multires": 10,
"multires_views": 4,
"raw_noise_std": 0.0,
"render_only": False,
"render_test": False,
"render_factor": 0,
"precrop_iters": 0,
"precrop_frac": 0.5,
"dataset_type": "blender",
"testskip": 8,
"shape": "greek",
"white_bkgd": False,
"half_res": False,
"factor": 8,
"no_ndc": True,
"lindisp": False,
"spherify": False,
"llffhold": 8,
"i_print": 100,
"i_img": 500,
"i_weights": 10000,
"i_testset": 50000,
"i_video": 50000,
"N_iters": 200000,
"finest_res": 512,
"log2_hashmap_size": 19,
"sparse_loss_weight": 0.0,
"tv_loss_weight": 0.0,
"lrate": 0.01,
"lrate_decay": 10,
"chunk": 1024*32,
"netchunk": 1024*64,
"no_batching": False,
"no_reload": False,
"ft_path": None,
"random_seed": None
}


@pytest.fixture
def sample_images():
"""Create sample image tensors for testing."""
batch_size = 4
height, width = 100, 100
channels = 3
images = torch.rand(batch_size, height, width, channels)
return images


@pytest.fixture
def sample_poses():
"""Create sample camera pose matrices."""
num_poses = 10
poses = torch.eye(4).unsqueeze(0).repeat(num_poses, 1, 1)
poses[:, :3, 3] = torch.randn(num_poses, 3)
return poses


@pytest.fixture
def sample_rays():
"""Create sample ray origins and directions."""
num_rays = 1000
rays_o = torch.randn(num_rays, 3)
rays_d = torch.randn(num_rays, 3)
rays_d = rays_d / rays_d.norm(dim=-1, keepdim=True)
return rays_o, rays_d


@pytest.fixture
def mock_model():
"""Create a mock neural network model."""
model = Mock()
model.forward = MagicMock(return_value=torch.randn(100, 4))
model.parameters = MagicMock(return_value=[torch.randn(10, 10)])
return model


@pytest.fixture
def sample_training_data():
"""Create sample training data for NeRF."""
return {
"images": torch.rand(100, 100, 100, 3),
"poses": torch.eye(4).unsqueeze(0).repeat(100, 1, 1),
"render_poses": torch.eye(4).unsqueeze(0).repeat(40, 1, 1),
"hwf": [100, 100, 50.0],
"i_split": [[0, 80], [80, 90], [90, 100]]
}


@pytest.fixture
def mock_hash_encoding():
"""Create a mock hash encoding module."""
mock = Mock()
mock.n_levels = 16
mock.n_features_per_level = 2
mock.forward = MagicMock(return_value=torch.randn(1000, 32))
return mock


@pytest.fixture
def device():
"""Get the appropriate device for testing."""
return torch.device("cuda" if torch.cuda.is_available() else "cpu")


@pytest.fixture
def random_seed():
"""Set random seed for reproducibility."""
seed = 42
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed(seed)
return seed


@pytest.fixture
def sample_config_file(temp_dir):
"""Create a sample configuration file."""
config_path = os.path.join(temp_dir, "test_config.txt")
config_content = """
expname = test_experiment
basedir = ./logs
datadir = ./data/nerf_synthetic/chair

dataset_type = blender
no_batching = True

use_viewdirs = True
finest_res = 512
log2_hashmap_size = 19

N_samples = 64
N_importance = 64

perturb = 1.
raw_noise_std = 0.

render_only = False
render_test = False

chunk = 32768
netchunk = 65536

lrate = 0.01
lrate_decay = 10

N_iters = 30000
i_testset = 2500
i_video = 10000
i_print = 100
"""
with open(config_path, 'w') as f:
f.write(config_content)
return config_path


@pytest.fixture
def mock_dataloader():
"""Create a mock data loader."""
loader = Mock()
loader.__iter__ = MagicMock(return_value=iter([
(torch.randn(32, 3), torch.randn(32, 3), torch.randn(32))
for _ in range(10)
]))
loader.__len__ = MagicMock(return_value=10)
return loader


@pytest.fixture(autouse=True)
def cleanup_gpu():
"""Clean up GPU memory after each test."""
yield
if torch.cuda.is_available():
torch.cuda.empty_cache()


@pytest.fixture
def capture_logs(caplog):
"""Fixture to capture log messages during tests."""
with caplog.at_level("DEBUG"):
yield caplog
Empty file added tests/integration/__init__.py
Empty file.
Loading