ChenmingWu commited on
Commit
195056b
·
verified ·
1 Parent(s): e45fa1e

Upload folder using huggingface_hub

Browse files
tests/__init__.py ADDED
File without changes
tests/conftest.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+
4
+ import pytest
5
+ import torch
6
+
7
+ from mapgs.config import load_config
8
+
9
+ CUDA = torch.cuda.is_available()
10
+ requires_cuda = pytest.mark.skipif(not CUDA, reason="needs CUDA (gsplat rasterizer)")
11
+
12
+
13
+ def tiny_overrides(root):
14
+ return [
15
+ "model.embed_dim=128", "model.enc_depth=1", "model.dec_depth=2", "model.n_heads=4",
16
+ "model.tokens.n_map=128", "model.tokens.n_free=128",
17
+ "model.tokens.gaussians_per_token=4", "model.tokens.n_dyn_per_instance=16",
18
+ "model.feature_dim=8",
19
+ "data.height=48", "data.width=64", "data.num_frames=8", "data.synth_dynamic_actors=1",
20
+ f"data.root={root}",
21
+ "train.batch_size=2", "train.amp=false", "train.num_workers=0",
22
+ "train.warmup=2", "train.extrap_ramp_iter=2", "train.log_every=1000", "train.ckpt_every=0",
23
+ "loss.lambda_lpips=0.0", "tt.steps=3",
24
+ ]
25
+
26
+
27
+ @pytest.fixture(scope="session")
28
+ def device():
29
+ return "cuda" if CUDA else "cpu"
30
+
31
+
32
+ @pytest.fixture(scope="session")
33
+ def data_root(tmp_path_factory):
34
+ return str(tmp_path_factory.mktemp("synthetic"))
35
+
36
+
37
+ @pytest.fixture(scope="session")
38
+ def cfg(data_root):
39
+ return load_config(overrides=tiny_overrides(data_root))
40
+
41
+
42
+ @pytest.fixture(scope="session")
43
+ def dataset(cfg):
44
+ if not CUDA:
45
+ pytest.skip("synthetic GT rendering needs CUDA")
46
+ from mapgs.data import SyntheticDataset
47
+ return SyntheticDataset(cfg, "train", n_scenes=4, n_sup_views=4, device="cuda")
tests/test_data_train_eval.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from mapgs.data import collate_samples
4
+ from mapgs.train import Trainer, compute_step_losses
5
+ from mapgs.train.trainer import _move_batch
6
+ from mapgs.eval import Evaluator, psnr, d_rmse
7
+ from .conftest import requires_cuda
8
+
9
+
10
+ @requires_cuda
11
+ def test_sample_shapes(cfg, dataset):
12
+ from mapgs.data.synthetic import _context_frames
13
+ s = dataset[0]
14
+ Vc = len(_context_frames(cfg)) * 3 # context frames clamp+dedup within num_frames
15
+ assert s.ctx_images.shape == (Vc, 3, cfg.data.height, cfg.data.width)
16
+ assert s.ctx_tids.shape[0] == Vc
17
+ assert s.anchor_pos.shape == (cfg.model.tokens.n_map, 3)
18
+ assert s.sup_depth.shape[0] == s.sup_images.shape[0]
19
+
20
+
21
+ @requires_cuda
22
+ def test_collate_and_dynamic_padding(cfg, dataset):
23
+ batch = collate_samples([dataset[0], dataset[1]])
24
+ assert batch["ctx_images"].shape[0] == 2
25
+ if batch["dynamic"] is not None:
26
+ assert batch["dynamic"]["box_centers"].shape[0] == 2
27
+ assert batch["dynamic"]["valid"].shape[0] == 2
28
+
29
+
30
+ @requires_cuda
31
+ def test_step_losses_finite_and_backward(cfg, dataset):
32
+ trainer = Trainer(cfg)
33
+ batch = _move_batch(collate_samples([dataset[0], dataset[1]]), "cuda")
34
+ total, log = compute_step_losses(trainer.model, trainer.ras, batch, trainer.criterion, 100, cfg, "cuda")
35
+ assert torch.isfinite(total)
36
+ for k in ["rgb", "mapdepth", "vis", "vert", "free_space"]:
37
+ assert k in log
38
+ total.backward()
39
+ assert sum(p.grad.abs().sum() for p in trainer.model.parameters() if p.grad is not None) > 0
40
+
41
+
42
+ @requires_cuda
43
+ def test_amp_train_step_runs(cfg, dataset):
44
+ # guards the autocast (bf16) -> fp32 rendering boundary
45
+ prev = cfg.train.amp
46
+ cfg.train.amp = True
47
+ try:
48
+ trainer = Trainer(cfg)
49
+ log = trainer.train_step(collate_samples([dataset[0], dataset[1]]))
50
+ assert log["total"] == log["total"] # finite (not NaN)
51
+ assert log["grad_norm"] >= 0
52
+ finally:
53
+ cfg.train.amp = prev
54
+
55
+
56
+ @requires_cuda
57
+ def test_training_decreases_loss(cfg, dataset):
58
+ trainer = Trainer(cfg)
59
+ losses = []
60
+ for _ in range(20):
61
+ batch = collate_samples([dataset[0], dataset[1]])
62
+ losses.append(trainer.train_step(batch)["total"])
63
+ assert min(losses[10:]) < losses[0] # improved over the first step
64
+
65
+
66
+ @requires_cuda
67
+ def test_metrics_sanity():
68
+ img = torch.rand(3, 16, 16, device="cuda")
69
+ assert psnr(img, img) > 50
70
+ d = torch.rand(1, 8, 8, device="cuda") + 1
71
+ assert d_rmse(d, d.clone()) < 1e-5
72
+
73
+
74
+ @requires_cuda
75
+ def test_evaluator_runs(cfg, dataset):
76
+ trainer = Trainer(cfg)
77
+ ev = Evaluator(trainer.model, cfg, device="cuda")
78
+ interp = ev.interpolation(dataset, max_scenes=2)
79
+ assert "PSNR" in interp and "D-RMSE" in interp
80
+ sweep = ev.extrapolation_sweep(dataset, shifts=[2.0], max_scenes=2, frame=cfg.data.num_frames // 2)
81
+ assert 2.0 in sweep
82
+ lane = ev.lane_consistency(dataset, max_scenes=2, frame=cfg.data.num_frames // 2)
83
+ assert "lane_mIoU" in lane
tests/test_geometry.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+
3
+ import torch
4
+
5
+ from mapgs.geometry import (
6
+ quat_to_rotmat, rotmat_to_quat, se3_inverse, project_points,
7
+ backproject_depth, plucker_embedding, look_at_pose,
8
+ )
9
+
10
+
11
+ def test_quat_rotmat_roundtrip():
12
+ q = torch.randn(20, 4)
13
+ q = q / q.norm(dim=-1, keepdim=True)
14
+ R = quat_to_rotmat(q)
15
+ # orthonormal, det 1
16
+ eye = R @ R.transpose(-1, -2)
17
+ assert torch.allclose(eye, torch.eye(3).expand_as(eye), atol=1e-5)
18
+ assert torch.allclose(torch.det(R), torch.ones(20), atol=1e-5)
19
+ q2 = rotmat_to_quat(R)
20
+ R2 = quat_to_rotmat(q2)
21
+ assert torch.allclose(R, R2, atol=1e-5)
22
+
23
+
24
+ def test_se3_inverse():
25
+ T = torch.eye(4).repeat(5, 1, 1)
26
+ T[:, :3, :3] = quat_to_rotmat(torch.randn(5, 4))
27
+ T[:, :3, 3] = torch.randn(5, 3)
28
+ I = T @ se3_inverse(T)
29
+ assert torch.allclose(I, torch.eye(4).expand_as(I), atol=1e-5)
30
+
31
+
32
+ def test_projection_roundtrip():
33
+ K = torch.tensor([[300., 0, 224], [0, 300, 128], [0, 0, 1]])[None]
34
+ c2w = look_at_pose(torch.tensor([0., 0, 1.5]), torch.tensor([0., 20, 0.5]))[None]
35
+ pts = torch.randn(1, 100, 3)
36
+ pts[..., 1] = pts[..., 1].abs() + 5 # in front
37
+ uv, z = project_points(pts, K, se3_inverse(c2w))
38
+ back = backproject_depth(uv, z, K, c2w)
39
+ assert torch.allclose(back, pts, atol=1e-3)
40
+ assert (z > 0).all()
41
+
42
+
43
+ def test_look_at_forward_is_positive_depth():
44
+ # a point in front of a forward-looking camera must have positive cam-z
45
+ c2w = look_at_pose(torch.tensor([0., 0, 1.5]), torch.tensor([0., 30, 0.5]))[None]
46
+ K = torch.tensor([[300., 0, 224], [0, 300, 128], [0, 0, 1]])[None]
47
+ pt = torch.tensor([[[0., 10, 0.5]]])
48
+ _, z = project_points(pt, K, se3_inverse(c2w))
49
+ assert z.item() > 0
50
+
51
+
52
+ def test_plucker_shape():
53
+ K = torch.tensor([[300., 0, 224], [0, 300, 128], [0, 0, 1]])[None].repeat(3, 1, 1)
54
+ c2w = torch.eye(4)[None].repeat(3, 1, 1)
55
+ pl = plucker_embedding(K, c2w, 32, 48)
56
+ assert pl.shape == (3, 6, 32, 48)
tests/test_hdmap.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+
4
+ from mapgs.config import MapConfig
5
+ from mapgs.hdmap import (
6
+ grid_field_from_points, HDMap, sample_anchors, rasterize_map_depth, project_polylines,
7
+ )
8
+ from mapgs.hdmap.anchors import resample_polyline, weighted_fps
9
+ from .conftest import requires_cuda
10
+
11
+
12
+ def _toy_map():
13
+ pts = np.random.RandomState(0).uniform([-20, -5], [20, 50], size=(2000, 2))
14
+ z = 0.02 * pts[:, 0]
15
+ gp = np.concatenate([pts, z[:, None]], 1)
16
+ gf = grid_field_from_points(gp, 0.5)
17
+ lanes = [torch.tensor([[x, y, 0.02 * x] for y in np.arange(0, 45, 1.0)], dtype=torch.float32)
18
+ for x in [-3.5, 0, 3.5]]
19
+ bnds = [torch.tensor([[x, y, 0.02 * x] for y in np.arange(0, 45, 2.0)], dtype=torch.float32)
20
+ for x in [-7, 7]]
21
+ return HDMap(ground=gf, lanes=lanes, boundaries=bnds)
22
+
23
+
24
+ def test_ground_field_bilinear_and_grad():
25
+ hd = _toy_map()
26
+ xy = torch.tensor([[1.0, 2.0], [3.0, 4.0]], requires_grad=True)
27
+ z, valid = hd.height_at(xy)
28
+ assert valid.all()
29
+ assert torch.allclose(z, 0.02 * xy[:, 0].detach(), atol=0.05)
30
+ z.sum().backward()
31
+ assert xy.grad.abs().sum() > 0
32
+
33
+
34
+ def test_anchor_ratios_and_count():
35
+ hd = _toy_map()
36
+ cfg = MapConfig(n_anchors=1000)
37
+ A = sample_anchors(hd, cfg, seed=0)
38
+ assert len(A) == 1000
39
+ counts = torch.bincount(A.types, minlength=3).float() / 1000
40
+ assert abs(counts[0] - 0.6) < 0.05
41
+ assert abs(counts[1] - 0.3) < 0.05
42
+ assert abs(counts[2] - 0.1) < 0.05
43
+
44
+
45
+ def test_resample_polyline_spacing():
46
+ pl = torch.tensor([[0., 0, 0], [0, 10, 0]])
47
+ out = resample_polyline(pl, 1.0)
48
+ assert out.shape[0] >= 9
49
+
50
+
51
+ def test_weighted_fps_prefers_high_weight():
52
+ pts = torch.randn(200, 3)
53
+ w = torch.ones(200)
54
+ w[0] = 100.0
55
+ sel = weighted_fps(pts, w, 10)
56
+ assert 0 in sel.tolist() # highest-weight point is picked first
57
+
58
+
59
+ @requires_cuda
60
+ def test_map_depth_coverage():
61
+ from mapgs.geometry import look_at_pose
62
+ hd = _toy_map().to("cuda")
63
+ K = torch.tensor([[60., 0, 32], [0, 60, 24], [0, 0, 1]], device="cuda")[None]
64
+ c2w = look_at_pose(torch.tensor([0., 0, 1.5]), torch.tensor([0., 20, 0.5]))[None].cuda()
65
+ depth, mask = rasterize_map_depth(hd.ground, K, c2w, 48, 64)
66
+ assert mask.float().mean() > 0.2 # lower half sees ground
67
+ assert (depth[mask] > 0).all()
68
+ uv = project_polylines(hd.lanes, K, c2w, 48, 64)
69
+ assert uv[0].shape[0] > 0
tests/test_losses.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from mapgs.config import LossConfig, TokenConfig
4
+ from mapgs.losses import (
5
+ Tempering, ssim, chamfer_2d, mapdepth_loss, free_space_loss,
6
+ ground_coupling_loss, silog_loss, visibility_loss, huber,
7
+ )
8
+ from mapgs.losses.extrap import perturb_pose
9
+ from mapgs.geometry import look_at_pose
10
+ from mapgs.hdmap import grid_field_from_points
11
+ from mapgs.render.gaussians import Gaussians, GROUP_DYNAMIC
12
+ import numpy as np
13
+
14
+
15
+ def test_tempering_monotone_and_capped():
16
+ t = Tempering(LossConfig(), TokenConfig(), total_iters=150000)
17
+ assert t.eps(0) < t.eps(1000) <= t.eps_max
18
+ assert t.s(0) < t.s(1000) <= t.s_max
19
+ assert t.eps(10_000_000) == t.eps_max # capped
20
+ assert t.lambda_md_scale(0) == 1.0
21
+ assert t.lambda_md_scale(149000) == LossConfig().md_late_decay
22
+
23
+
24
+ def test_ssim_identical_is_one():
25
+ img = torch.rand(1, 3, 32, 32)
26
+ assert ssim(img, img) > 0.999
27
+
28
+
29
+ def test_mapdepth_zero_when_equal():
30
+ d = torch.rand(2, 16, 16) + 1
31
+ mask = torch.ones(2, 16, 16, dtype=torch.bool)
32
+ assert mapdepth_loss(d, d.clone(), mask, eps=0.3) < 1e-6
33
+
34
+
35
+ def test_huber_matches_quadratic_for_small():
36
+ x = torch.tensor([0.1, -0.1])
37
+ assert torch.allclose(huber(x, 1.0), 0.5 * x ** 2)
38
+
39
+
40
+ def test_visibility_inside_vs_outside():
41
+ K = torch.tensor([[60., 0, 32], [0, 60, 24], [0, 0, 1]])[None]
42
+ c2w = look_at_pose(torch.tensor([0., 0, 1.5]), torch.tensor([0., 20, 0.5]))[None]
43
+ inside = torch.tensor([[0., 12, 0.5]]) # in front, centered
44
+ outside = torch.tensor([[0., -30, 0.5]]) # behind camera
45
+ assert visibility_loss(inside, K, c2w, 48, 64) < 1e-4
46
+ assert visibility_loss(outside, K, c2w, 48, 64) > 0
47
+
48
+
49
+ def test_free_space_and_ground_coupling():
50
+ gp = np.concatenate([np.random.RandomState(0).uniform([-10, -5], [10, 20], (500, 2)),
51
+ np.zeros((500, 1))], 1)
52
+ gf = grid_field_from_points(gp, 0.5)
53
+ n = 50
54
+ means = torch.zeros(n, 3); means[:, 2] = -1.0 # all below ground
55
+ g = Gaussians(means, torch.full((n, 3), 0.1), _identq(n), torch.full((n,), 0.8), torch.rand(n, 3),
56
+ group=torch.full((n,), GROUP_DYNAMIC))
57
+ assert free_space_loss(g, gf, delta=0.1) > 0 # penalizes below-ground opacity
58
+ assert ground_coupling_loss(g, gf, eps=1.0) > 0 # pulls dynamic z up
59
+
60
+
61
+ def test_silog_zero_when_equal():
62
+ d = torch.rand(1, 16, 16) + 1
63
+ mask = torch.ones(1, 16, 16, dtype=torch.bool)
64
+ assert abs(float(silog_loss(d, d.clone(), mask))) < 1e-6
65
+
66
+
67
+ def test_chamfer_zero_for_identical():
68
+ a = torch.randn(20, 2)
69
+ assert chamfer_2d(a, a.clone()) < 1e-6
70
+
71
+
72
+ def test_perturb_pose_shifts_laterally():
73
+ c2w = look_at_pose(torch.tensor([0., 0, 1.5]), torch.tensor([0., 20, 0.5]))
74
+ dev = perturb_pose(c2w, lateral=2.0)
75
+ shift = (dev[:3, 3] - c2w[:3, 3]).norm()
76
+ assert abs(float(shift) - 2.0) < 1e-4
77
+
78
+
79
+ def _identq(n):
80
+ q = torch.zeros(n, 4); q[:, 0] = 1
81
+ return q
tests/test_model_render.py ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from mapgs.model import MapGS
4
+ from mapgs.render import Gaussians, GaussianRasterizer, GROUP_MAP, GROUP_DYNAMIC
5
+ from mapgs.geometry import plucker_embedding, look_at_pose
6
+ from .conftest import requires_cuda
7
+
8
+
9
+ def _model_inputs(cfg, device, B=1, dynamic=False):
10
+ V, H, W = 3, cfg.data.height, cfg.data.width
11
+ imgs = torch.rand(B, V, 3, H, W, device=device)
12
+ K = torch.tensor([[60., 0, W / 2], [0, 60, H / 2], [0, 0, 1]], device=device)[None, None].repeat(B, V, 1, 1)
13
+ c2w = torch.stack([look_at_pose(torch.tensor([dx, 0., 1.5]), torch.tensor([dx, 20., 0.5]))
14
+ for dx in [-1., 0, 1]]).to(device)[None].repeat(B, 1, 1, 1)
15
+ pl = torch.stack([plucker_embedding(K[b], c2w[b], H, W) for b in range(B)])
16
+ tids = torch.zeros(B, V, dtype=torch.long, device=device)
17
+ nmap = cfg.model.tokens.n_map
18
+ apos = torch.randn(B, nmap, 3, device=device); apos[..., 2] *= 0.1
19
+ atype = torch.randint(0, 3, (B, nmap), device=device)
20
+ anorm = torch.zeros(B, nmap, 3, device=device); anorm[..., 2] = 1
21
+ dyn = None
22
+ if dynamic:
23
+ I, F = 2, cfg.data.num_frames
24
+ centers = torch.zeros(B, I, F, 3, device=device)
25
+ for f in range(F):
26
+ centers[:, 0, f] = torch.tensor([3., 5 + 0.5 * f, 0.5], device=device)
27
+ centers[:, 1, f] = torch.tensor([-3., 8., 0.5], device=device)
28
+ dyn = dict(box_centers=centers, box_rots=torch.eye(3, device=device).view(1, 1, 1, 3, 3).repeat(B, I, F, 1, 1),
29
+ box_size=torch.ones(B, I, 3, device=device) * 2, valid=torch.ones(B, I, dtype=torch.bool, device=device),
30
+ canon_idx=torch.zeros(B, I, dtype=torch.long, device=device))
31
+ return imgs, pl, tids, apos, atype, anorm, K, c2w, dyn
32
+
33
+
34
+ @requires_cuda
35
+ def test_forward_shapes_and_budget(cfg):
36
+ model = MapGS(cfg).cuda()
37
+ imgs, pl, tids, apos, atype, anorm, K, c2w, _ = _model_inputs(cfg, "cuda")
38
+ g = model(imgs, pl, tids, apos, atype, anorm, s_t=0.5)
39
+ M = (cfg.model.tokens.n_map + cfg.model.tokens.n_free) * cfg.model.tokens.gaussians_per_token
40
+ assert g.means.shape == (1, M, 3)
41
+ assert g.colors.shape[-1] == cfg.model.feature_dim
42
+
43
+
44
+ @requires_cuda
45
+ def test_bounded_residual_for_map_tokens(cfg):
46
+ model = MapGS(cfg).cuda()
47
+ imgs, pl, tids, apos, atype, anorm, K, c2w, _ = _model_inputs(cfg, "cuda")
48
+ s_t = 0.5
49
+ g = model(imgs, pl, tids, apos, atype, anorm, s_t=s_t)
50
+ is_map = g.group[0] == GROUP_MAP
51
+ # map gaussian means must lie within s_t of their anchor (sqrt(3)*s_t bound on the cube)
52
+ means = g.means[0][is_map]
53
+ ng = cfg.model.tokens.gaussians_per_token
54
+ anchors = apos[0].repeat_interleave(ng, 0)[: means.shape[0]]
55
+ dev = (means - anchors).abs().max()
56
+ assert dev <= s_t + 1e-4
57
+
58
+
59
+ @requires_cuda
60
+ def test_dynamic_placement_moves_only_dynamic(cfg):
61
+ model = MapGS(cfg).cuda()
62
+ imgs, pl, tids, apos, atype, anorm, K, c2w, dyn = _model_inputs(cfg, "cuda", dynamic=True)
63
+ g = model(imgs, pl, tids, apos, atype, anorm, s_t=0.5, dynamic=dyn)
64
+ g0 = model.place_dynamics(g, dyn, 0)
65
+ g1 = model.place_dynamics(g, dyn, cfg.data.num_frames - 1)
66
+ dynm = g.group[0] == GROUP_DYNAMIC
67
+ assert (g1.means[0][dynm] - g0.means[0][dynm]).norm(dim=-1).mean() > 0.1
68
+ assert torch.allclose(g1.means[0][~dynm], g0.means[0][~dynm])
69
+
70
+
71
+ @requires_cuda
72
+ def test_render_and_backward(cfg):
73
+ model = MapGS(cfg).cuda()
74
+ imgs, pl, tids, apos, atype, anorm, K, c2w, _ = _model_inputs(cfg, "cuda")
75
+ g = model(imgs, pl, tids, apos, atype, anorm, s_t=0.5)
76
+ ras = GaussianRasterizer()
77
+ out = ras.render(g.scene(0), K[0], c2w[0], cfg.data.height, cfg.data.width)
78
+ rgb = model.feature_to_rgb(out.color)
79
+ assert rgb.shape == (3, 3, cfg.data.height, cfg.data.width)
80
+ assert out.aux is not None # lane channel rendered
81
+ (rgb.mean() + out.depth.mean()).backward()
82
+ assert sum(p.grad.abs().sum() for p in model.parameters() if p.grad is not None) > 0
tests/test_ttt.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from mapgs.model import MapGS
4
+ from mapgs.ttt import token_tuning, map_guided_densify
5
+ from mapgs.ttt.densify import anchors_along_trajectory
6
+ from .conftest import requires_cuda
7
+
8
+
9
+ @requires_cuda
10
+ def test_token_tuning_runs(cfg, dataset):
11
+ model = MapGS(cfg).cuda()
12
+ g = token_tuning(model, dataset[0], cfg, steps=3)
13
+ M = (cfg.model.tokens.n_map + cfg.model.tokens.n_free) * cfg.model.tokens.gaussians_per_token
14
+ assert g.means.shape == (1, M, 3)
15
+ # network weights must be unchanged by TT (only tokens are tuned)
16
+ before = {n: p.clone() for n, p in model.named_parameters()}
17
+ token_tuning(model, dataset[0], cfg, steps=2)
18
+ for n, p in model.named_parameters():
19
+ assert torch.allclose(before[n], p)
20
+
21
+
22
+ @requires_cuda
23
+ def test_densify_increases_gaussians(cfg, dataset):
24
+ model = MapGS(cfg).cuda()
25
+ s = dataset[0]
26
+ traj = torch.stack([torch.zeros(15, device="cuda"), torch.linspace(2, 25, 15, device="cuda")], -1)
27
+ npos, ntyp, nnrm = anchors_along_trajectory(s.ground.to("cuda"), traj, spacing=1.0)
28
+ n_new = min(64, npos.shape[0])
29
+ g = map_guided_densify(model, s, npos[:n_new], ntyp[:n_new], nnrm[:n_new], cfg)
30
+ base = (cfg.model.tokens.n_map + cfg.model.tokens.n_free) * cfg.model.tokens.gaussians_per_token
31
+ assert g.means.shape[1] == base + n_new * cfg.model.tokens.gaussians_per_token
tests/test_unified.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import torch
4
+
5
+ from mapgs.config import load_config
6
+ from mapgs.geometry import resize_with_intrinsics
7
+ from mapgs.hdmap.ground_field import GridGroundField
8
+ from .conftest import requires_cuda, tiny_overrides
9
+
10
+
11
+ def test_ground_field_from_raster_and_scaled():
12
+ gf = GridGroundField.from_raster(torch.ones(10, 12) * 2.0, x0=-3.0, y0=-2.0, dx=0.5)
13
+ z, valid = gf.height_at(torch.tensor([[0.0, 0.0]]))
14
+ assert valid.all() and abs(float(z[0]) - 2.0) < 1e-4
15
+ g2 = gf.scaled(0.5)
16
+ assert abs(g2.x0 - (-1.5)) < 1e-6 and abs(g2.dx - 0.25) < 1e-6
17
+ # scaling halves heights too (spatial)
18
+ z2, _ = g2.height_at(torch.tensor([[0.0, 0.0]]))
19
+ assert abs(float(z2[0]) - 1.0) < 1e-4
20
+
21
+
22
+ def test_resize_with_intrinsics():
23
+ img = torch.rand(3, 100, 200)
24
+ K = torch.tensor([[200.0, 0, 100], [0, 200, 50], [0, 0, 1]])
25
+ out, Ks = resize_with_intrinsics(img, K, 50, 50)
26
+ assert out.shape == (3, 50, 50)
27
+ assert abs(float(Ks[0, 0]) - 50.0) < 1e-3 # fx * (50/200)
28
+ assert abs(float(Ks[1, 1]) - 100.0) < 1e-3 # fy * (50/100)
29
+ assert abs(float(Ks[0, 2]) - 25.0) < 1e-3 # cx * (50/200)
30
+
31
+
32
+ @requires_cuda
33
+ def test_unified_convert_load_and_mixed_roots(tmp_path):
34
+ cfg = load_config(overrides=tiny_overrides(str(tmp_path / "src")) + ["data.name=unified"])
35
+ from mapgs.data.convert import convert_synthetic
36
+ from mapgs.data import UnifiedClipDataset, collate_samples
37
+
38
+ ra = str(tmp_path / "ua"); rb = str(tmp_path / "ub")
39
+ convert_synthetic(cfg, ra, "train", n_clips=2, device="cuda")
40
+ convert_synthetic(cfg, rb, "train", n_clips=2, device="cuda")
41
+
42
+ # on-disk layout
43
+ clip = sorted(os.listdir(os.path.join(ra, "train")))[0]
44
+ files = os.listdir(os.path.join(ra, "train", clip))
45
+ assert "meta.pt" in files and "images" in files
46
+
47
+ ds = UnifiedClipDataset(cfg, roots=[ra, rb], split="train", n_sup_views=3)
48
+ assert len(ds) == 4 # mixed roots concatenated
49
+ s = ds[0]
50
+ assert s.ctx_images.shape[-2:] == (cfg.data.height, cfg.data.width)
51
+ assert s.anchor_pos.shape[0] == cfg.model.tokens.n_map
52
+ batch = collate_samples([ds[0], ds[1]])
53
+ assert batch["ctx_images"].shape[0] == 2
54
+
55
+
56
+ @requires_cuda
57
+ def test_unified_train_step(tmp_path):
58
+ cfg = load_config(overrides=tiny_overrides(str(tmp_path / "src")) + ["data.name=unified"])
59
+ from mapgs.data.convert import convert_synthetic
60
+ from mapgs.data import UnifiedClipDataset, collate_samples
61
+ from mapgs.train import Trainer
62
+
63
+ root = str(tmp_path / "u")
64
+ convert_synthetic(cfg, root, "train", n_clips=2, device="cuda")
65
+ ds = UnifiedClipDataset(cfg, roots=root, split="train", n_sup_views=3)
66
+ tr = Trainer(cfg)
67
+ log = tr.train_step(collate_samples([ds[0], ds[1]]))
68
+ assert log["total"] == log["total"] # finite