Skip to content
Merged
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
5 changes: 3 additions & 2 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ on:
push:
branches:
- "**"
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
test:
name: Fast pytest
name: Routine pytest
runs-on: macos-15
steps:
- name: Check out repository
Expand All @@ -37,6 +38,6 @@ jobs:
echo "PYTHONPYCACHEPREFIX=$scratch_root/cache/pycache" >> "$GITHUB_ENV"
echo "UV_CACHE_DIR=$scratch_root/cache/uv" >> "$GITHUB_ENV"

- name: Run fast tests
- name: Run routine tests
timeout-minutes: 10
run: uv run pytest --basetemp "$MLX_SPATIAL_TEST_SCRATCH/artifacts/pytest"
56 changes: 45 additions & 11 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,25 @@ user-requested inference results, not test or audit scratch data. Preserve the
temporary root only when its artifacts are needed for diagnosis; otherwise
remove it after recording the relevant result.

## Model-Independent Golden Fixtures

Golden coverage uses two complementary fixture types. Synthetic miniature
checkpoints exercise complete inference orchestration. Real-weight-derived
decoder patches preserve a reviewed real-model boundary without committing or
loading the source checkpoints.
## Representative Pipeline Fixtures

Routine integration coverage uses generated miniature checkpoints, composed
pipeline fixtures, and reviewed real-weight-derived boundary patches. Every
fixture states its provenance and limits. None requires the local production
weight bundles.

- TRELLIS.2 generates and quantizes a miniature selective INT8 bundle, then
runs through the real SpatialKit GLB path.
- Pixal3D combines a synthetic full-orchestration fixture with a compact
real-weight-derived decoder patch.
- LiTo runs its complete source-contract generation path.
- SAM3D preserves CLI reconstruction, mesh extraction, and GLB writing around
deterministic conditioning and flow boundaries.
- HY-World 2 runs fixture reconstruction and writes its staged artifacts below
pytest temporary storage.
- MapAnything generates a miniature checkpoint and runs asset inspection,
safetensors loading, encoder, multi-view information sharing, prediction
heads, geometry postprocess, and NPZ writing.

### TRELLIS.2 Synthetic Miniature

Expand All @@ -93,7 +106,7 @@ sampling, both decoders, artifact serialization, and the real SpatialKit export
path with miniature export settings.

```bash
uv run pytest -m heavy tests/test_trellis2_golden_fixture.py -q
uv run pytest tests/test_trellis2_golden_fixture.py -q
```

Reviewed tensor and GLB expectations live in
Expand All @@ -112,13 +125,15 @@ texture baking, and GLB writing without the 22 GB source bundle.

```bash
uv run pytest tests/test_pixal3d_derived_golden.py -q
uv run pytest -m heavy tests/test_pixal3d_derived_golden.py -q
```

The default test verifies provenance, checksums, and decoded contracts. The
Metal-backed replay is marked `heavy` and normally completes in under one
second. It does not replace the synthetic full-pipeline Pixal3D tests: the
derived fixture begins at the decoder-output boundary.
bounded Metal-backed replay normally completes in under one second. It does not
replace the synthetic full-pipeline Pixal3D test: the derived fixture begins at
the decoder-output boundary. Face counts and artifact structure remain exact;
the manifest permits a small explicit GLB vertex-count tolerance because native
UV seam splitting can duplicate a few vertices differently across macOS
hardware and driver versions.

Rebaseline only from a reviewed real inference result:

Expand All @@ -130,6 +145,25 @@ uv run python scripts/pixal3d/write_derived_golden_fixture.py \
--source-revision 0b31f9160aa400719af409098bff7936a932f726
```

### MapAnything Generated Miniature Scene

The MapAnything fixture generates its checkpoint at runtime from deterministic
tiny tensors. The committed manifest records fixture provenance, covered and
excluded scope, stage order, output schemas, and tolerant numerical summaries.
It is a pipeline regression fixture, not official-weight parity. Stable outputs
retain numerical summaries; recovered intrinsics and world points use strict
shape, finite-value, homogeneous-matrix, sign, and boundedness invariants to
avoid amplifying cross-hardware noise from the deliberately tiny ray field.

```bash
uv run pytest tests/test_mapanything_scene_pipeline.py \
-m 'integration and not real_assets' -q
```

Reviewed expectations live in
`tests/data/mapanything_miniature_scene_golden.json`. Rebaseline them only after
reviewing an intentional production pipeline change.

## Editing Constraints

- Prefer existing module boundaries over new abstractions.
Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,12 @@ exclude = [

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-m 'not heavy' -p no:cacheprovider"
addopts = "-m 'not (heavy or real_assets or torch_parity)' -p no:cacheprovider"
markers = [
"heavy: opt-in tests that load real weights, require Metal, or use large tensors",
"integration: representative tests that cross multiple production component boundaries",
"real_assets: tests that require local weights, inputs, or other uncommitted fixtures",
"metal: tests that require an Apple Metal device",
"heavy: opt-in tests with substantial runtime or memory cost",
"benchmark: resource and performance checks; always paired with heavy",
"torch_parity: optional parity checks against the local PyTorch checkout",
]
64 changes: 32 additions & 32 deletions src/mlx_spatial/trellis2_texturing.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,6 @@ def run(
),
)

preprocessed = preprocess_trellis2_image(image, rmbg_root=self.rmbg_root)
if not preprocessed.ready or preprocessed.image is None:
return Trellis2TexturingResult(
image_path=image,
mesh_path=mesh_file,
blocker=_preprocess_texturing_blocker(preprocessed.blocker),
)

try:
mesh_vertices, mesh_faces = _load_obj_mesh(mesh_file)
except (OSError, ValueError) as error:
Expand All @@ -225,30 +217,6 @@ def run(
),
)

fdg_coords, fdg_dual, fdg_intersected = mesh_to_flexible_dual_grid(
mesh_vertices, mesh_faces, grid_size=grid_size
)

if fdg_coords.shape[0] == 0:
return Trellis2TexturingResult(
image_path=image,
mesh_path=mesh_file,
blocker=Trellis2TexturingBlocker(
stage="mesh-preprocess",
operation="FlexiDualGrid voxelization",
reference=str(mesh_path),
reason="mesh_to_flexible_dual_grid produced no occupied voxels",
next_slice="increase grid_size or provide a mesh within the AABB",
),
)

encoder_coords = np.column_stack(
[np.zeros(fdg_coords.shape[0], dtype=np.int32), fdg_coords]
)
encoder_coords_mx = mx.array(encoder_coords, dtype=mx.int32)
dual_mx = mx.array(fdg_dual, dtype=mx.float32)
intersected_mx = mx.array(fdg_intersected.astype(np.float32), dtype=mx.float32)

discovery = discover_trellis2_conditioning_config(self.root)
if not discovery.ready or discovery.config is None:
return Trellis2TexturingResult(
Expand Down Expand Up @@ -302,6 +270,38 @@ def run(
texture_slat_sampler=replace(config.texture_slat_sampler, steps=slat_steps),
)

preprocessed = preprocess_trellis2_image(image, rmbg_root=self.rmbg_root)
if not preprocessed.ready or preprocessed.image is None:
return Trellis2TexturingResult(
image_path=image,
mesh_path=mesh_file,
blocker=_preprocess_texturing_blocker(preprocessed.blocker),
)

fdg_coords, fdg_dual, fdg_intersected = mesh_to_flexible_dual_grid(
mesh_vertices, mesh_faces, grid_size=grid_size
)

if fdg_coords.shape[0] == 0:
return Trellis2TexturingResult(
image_path=image,
mesh_path=mesh_file,
blocker=Trellis2TexturingBlocker(
stage="mesh-preprocess",
operation="FlexiDualGrid voxelization",
reference=str(mesh_path),
reason="mesh_to_flexible_dual_grid produced no occupied voxels",
next_slice="increase grid_size or provide a mesh within the AABB",
),
)

encoder_coords = np.column_stack(
[np.zeros(fdg_coords.shape[0], dtype=np.int32), fdg_coords]
)
encoder_coords_mx = mx.array(encoder_coords, dtype=mx.int32)
dual_mx = mx.array(fdg_dual, dtype=mx.float32)
intersected_mx = mx.array(fdg_intersected.astype(np.float32), dtype=mx.float32)

resolved_encoder_config_path = self.encoder_config_path or _SHAPE_ENCODER_CONFIG_CONVENTION
try:
encoder_config = read_structured_latent_encoder_config(
Expand Down
72 changes: 62 additions & 10 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,63 @@
# Testing Strategy

Default test runs are CPU-bound and fast. `tests/conftest.py` sets MLX to the CPU device at session start, and pytest uses `-m "not heavy"` by default.
The routine suite is bounded, self-contained, and representative. It includes
unit tests and miniature integration fixtures, while excluding tests marked
`heavy`, `real_assets`, or `torch_parity`.

Use tiny tensors in default tests. Prefer shapes like `shape=(1, 4, 32)` for behavioral checks, not full model-scale shapes like `shape=(1, 1024, 1024)`, unless the test is explicitly marked heavy.
```bash
uv run pytest
```

`tests/conftest.py` starts MLX on the CPU. A test may deliberately exercise
Metal when that boundary is important; such a test carries the `metal` marker.
Metal capability and test cost are separate concerns.

## Markers

| Marker | Meaning | Routine suite |
| --- | --- | --- |
| `integration` | Crosses multiple production component boundaries | Yes, unless paired with an exclusion marker |
| `metal` | Requires an Apple Metal device | Yes, when bounded |
| `real_assets` | Reads local weights, inputs, or uncommitted fixtures | No |
| `heavy` | Has substantial runtime or memory cost | No |
| `benchmark` | Measures resource or performance behavior; also marked `heavy` | No |
| `torch_parity` | Requires the opt-in PyTorch reference environment | No |

Useful focused commands are:

```bash
uv run pytest -m 'integration and not (heavy or real_assets or torch_parity)'
uv run pytest -m 'real_assets and not heavy'
uv run pytest -m heavy
uv run pytest -m benchmark
uv run pytest -m torch_parity
```

## Test Design

- Prefer the smallest tensor shapes that preserve the production branch under
test.
- Keep one representative cross-component fixture per pipeline. Add narrower
tests only when they protect a distinct contract or failure mode.
- Keep each supported pipeline's primary guard in
`ROUTINE_PIPELINE_GUARDS`. A primary guard must remain an `integration` test
and must not carry a routine-exclusion marker.
- Assert stable stage, schema, shape, and numerical-summary contracts. Do not
accept a broad set of unrelated blockers as success.
- Record what a fixture covers and does not cover. Synthetic checkpoints must
not be presented as real-weight numerical parity.
- Write all generated files below `tmp_path` or the task scratch root. The
repository `outputs/` directory belongs to user-requested inference runs.

The bounded pipeline fixtures cover TRELLIS.2 selective INT8 inference,
Pixal3D orchestration and derived decoder replay, LiTo source-contract
generation, SAM3D CLI reconstruction, HY-World 2 reconstruction, and the full
MapAnything scene path. They remain runnable after local model weights are
removed.

## Isolated Runs

Mark tests with `@pytest.mark.heavy` when they load real files from `weights/`, require Metal-specific execution, or allocate model-scale tensors. Run them manually with:
Use a task-specific scratch root for any focused or expensive run:

```bash
export MLX_SPATIAL_TEST_SCRATCH="$(mktemp -d /tmp/mlx-spatial-test.XXXXXX)"
Expand All @@ -14,11 +67,10 @@ uv run pytest -m heavy \
--basetemp "$MLX_SPATIAL_TEST_SCRATCH/artifacts/pytest-heavy"
```

All generated test inputs, outputs, parity bundles, caches, logs, and browser
artifacts must stay below that task root. Optional local reference checkouts use
explicit environment variables such as `MLX_SPATIAL_TORCH_ROOT`; committed
tests and anchor metadata must not contain developer-machine absolute paths.
Optional reference checkouts use explicit environment variables such as
`MLX_SPATIAL_TORCH_ROOT`. Committed fixtures and metadata must not contain
developer-machine absolute paths.

The GitHub Actions workflow runs the unified root suite, including
`tests/spatialkit`, on every branch push. The job creates the same isolated
scratch layout and has a 10-minute timeout so leaked heavy tests fail quickly.
GitHub Actions runs the routine root suite, including `tests/spatialkit`, on
pushes and pull requests. The job uses isolated scratch storage and a 10-minute
timeout.
Loading