acceptance_phase5.py•10.1 kB
#!/usr/bin/env python3
"""
Acceptance tests for Phase 5: Advanced Visualization
Runs scenario-based tests as specified in the requirements.
"""
import json
import os
import sys
import tempfile
import subprocess
from pathlib import Path
# Add the python-worker to the path
sys.path.insert(0, str(Path(__file__).parent.parent / "packages" / "python-worker"))
from worker import handle_request
def test_wavepacket_scenario():
"""
Scenario: Evolve a 1D Gaussian wavepacket, render as MP4 with CSV data.
Re-invoke with same params → cache hit.
"""
print("🌊 Testing wavepacket evolution scenario...")
request = {
"method": "plot_animation",
"params": {
"frame_expr": "exp(-(x-t)**2/2) * cos(5*x - 2*t)",
"x_range": [-5, 5, 100],
"t_range": [0, 5, 30],
"renderer": "line",
"fps": 10,
"format": "mp4",
"emit_csv": True
}
}
# First call
print(" 📊 First call...")
result1 = handle_request(request)
assert "animation_path" in result1, "Animation path missing"
assert "csv_data" in result1, "CSV data missing"
assert os.path.exists(result1["animation_path"]), "Animation file not created"
assert result1["meta"]["format"] == "mp4", "Wrong format"
file_size1 = os.path.getsize(result1["animation_path"])
assert file_size1 > 1000, "Animation file too small"
print(f" ✅ Animation created: {result1['animation_path']} ({file_size1} bytes)")
print(f" ✅ CSV data: {len(result1['csv_data'].split())} lines")
print(f" ✅ Device used: {result1['meta']['device']}")
# Second call (should be same or cached)
print(" 🔄 Second call (cache test)...")
result2 = handle_request(request)
assert "animation_path" in result2, "Second call failed"
print(f" ✅ Second call successful")
return True
def test_phase_portrait_sweep():
"""
Scenario: plot_interactive thumbnail grid over damping parameter with sliders spec.
"""
print("🎛️ Testing phase portrait parameter sweep...")
request = {
"method": "plot_interactive",
"params": {
"expr": "exp(-gamma*t) * sin(omega*t)",
"x_range": [0, 10, 100], # Using x_range as time for this example
"controls": [
{"name": "gamma", "min": 0.1, "max": 2.0, "step": 0.3, "default": 0.5},
{"name": "omega", "min": 1.0, "max": 5.0, "step": 1.0, "default": 2.0}
],
"renderer": "line",
"grid_limit": 12
}
}
result = handle_request(request)
assert "thumbnails" in result, "Thumbnails missing"
assert "ui_spec" in result, "UI spec missing"
assert len(result["thumbnails"]) > 0, "No thumbnails generated"
assert len(result["thumbnails"]) <= 12, "Too many thumbnails"
# Check UI spec structure
ui_spec = result["ui_spec"]
assert ui_spec["type"] == "sliders", "Wrong UI type"
assert len(ui_spec["controls"]) == 2, "Wrong number of controls"
# Check thumbnail structure
for i, thumb in enumerate(result["thumbnails"]):
assert "control_values" in thumb, f"Thumbnail {i} missing control values"
assert "png_b64" in thumb, f"Thumbnail {i} missing image"
assert "gamma" in thumb["control_values"], f"Thumbnail {i} missing gamma"
assert "omega" in thumb["control_values"], f"Thumbnail {i} missing omega"
# Verify base64 PNG
png_data = thumb["png_b64"]
assert len(png_data) > 100, f"Thumbnail {i} image too small"
print(f" ✅ Generated {len(result['thumbnails'])} thumbnails")
print(f" ✅ UI spec with {len(ui_spec['controls'])} controls")
print(f" ✅ Grid size: {result['meta']['grid_size']}")
return True
def test_vr_export_scenario():
"""
Scenario: Sample a 3D scalar field isosurface and export GLB + PLY.
Files should open in standard viewers.
"""
print("🥽 Testing VR export scenario...")
# First, create a simple geometric mesh (icosahedron approximation)
import math
# Golden ratio
phi = (1 + math.sqrt(5)) / 2
# Icosahedron vertices
vertices = [
[-1, phi, 0], [1, phi, 0], [-1, -phi, 0], [1, -phi, 0],
[0, -1, phi], [0, 1, phi], [0, -1, -phi], [0, 1, -phi],
[phi, 0, -1], [phi, 0, 1], [-phi, 0, -1], [-phi, 0, 1]
]
# Icosahedron faces (triangular)
faces = [
[0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11],
[1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8],
[3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9],
[4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1]
]
# Test GLB export
print(" 📦 Testing GLB export...")
glb_request = {
"method": "plot_vr_export",
"params": {
"geometry": {
"vertices": vertices,
"faces": faces
},
"format": "glb",
"extras": {"name": "test_icosahedron", "description": "Phase 5 test"}
}
}
glb_result = handle_request(glb_request)
if "error" in glb_result:
print(f" ⚠️ GLB export skipped: {glb_result['error']}")
return True # Skip if trimesh not available
assert "path" in glb_result, "GLB path missing"
assert glb_result["path"].endswith(".glb"), "Wrong GLB extension"
assert os.path.exists(glb_result["path"]), "GLB file not created"
glb_size = os.path.getsize(glb_result["path"])
assert glb_size > 100, "GLB file too small"
print(f" ✅ GLB exported: {glb_result['path']} ({glb_size} bytes)")
print(f" ✅ Vertices: {glb_result['meta']['vertices']}")
print(f" ✅ Faces: {glb_result['meta']['faces']}")
# Test PLY export
print(" 📦 Testing PLY export...")
ply_request = {
"method": "plot_vr_export",
"params": {
"geometry": {
"vertices": vertices,
"faces": faces
},
"format": "ply"
}
}
ply_result = handle_request(ply_request)
assert "path" in ply_result, "PLY path missing"
assert ply_result["path"].endswith(".ply"), "Wrong PLY extension"
assert os.path.exists(ply_result["path"]), "PLY file not created"
ply_size = os.path.getsize(ply_result["path"])
assert ply_size > 50, "PLY file too small"
print(f" ✅ PLY exported: {ply_result['path']} ({ply_size} bytes)")
# Basic file format validation
with open(ply_result["path"], 'r') as f:
ply_header = f.read(100)
assert "ply" in ply_header.lower(), "Invalid PLY header"
print(f" ✅ PLY format validated")
return True
def test_volume_3d_scenario():
"""
Scenario: Create 3D volume visualization with GPU acceleration.
"""
print("📦 Testing 3D volume visualization...")
request = {
"method": "plot_volume_3d",
"params": {
"f": "exp(-(x**2 + y**2 + z**2)/4) * sin(2*sqrt(x**2 + y**2 + z**2))",
"x": [-3, 3, 30],
"y": [-3, 3, 30],
"z": [-3, 3, 30],
"mode": "slices"
}
}
result = handle_request(request)
assert "png_contact_sheet_b64" in result, "Contact sheet missing"
assert "csv_data" in result, "CSV data missing"
assert "meta" in result, "Metadata missing"
# Check metadata
meta = result["meta"]
assert "device" in meta, "Device info missing"
assert "duration_ms" in meta, "Duration missing"
assert "mesh" in meta, "Mesh info missing"
assert meta["mode"] == "slices", "Wrong mode"
# Validate CSV structure
csv_lines = result["csv_data"].split('\n')
assert len(csv_lines) > 1, "CSV too short"
assert csv_lines[0] == "x,y,z,f", "Wrong CSV header"
# Check that we have actual data
data_lines = [line for line in csv_lines[1:] if line.strip()]
assert len(data_lines) > 10, "Not enough CSV data"
print(f" ✅ Volume rendered on: {meta['device']}")
print(f" ✅ Mesh size: {meta['mesh']}")
print(f" ✅ Duration: {meta['duration_ms']}ms")
print(f" ✅ CSV data: {len(data_lines)} points")
return True
def test_acceleration_detection():
"""
Test that acceleration detection works properly.
"""
print("⚡ Testing acceleration detection...")
request = {
"method": "accel_caps",
"params": {}
}
result = handle_request(request)
assert "active" in result, "Active flag missing"
assert "device" in result, "Device missing"
assert "mode" in result, "Mode missing"
assert "has_torch" in result, "PyTorch flag missing"
print(f" ✅ Acceleration active: {result['active']}")
print(f" ✅ Device: {result['device']}")
print(f" ✅ Mode: {result['mode']}")
print(f" ✅ Has PyTorch: {result['has_torch']}")
return True
def main():
"""Run all acceptance tests."""
print("🚀 Phase 5 Acceptance Tests")
print("=" * 50)
tests = [
test_acceleration_detection,
test_volume_3d_scenario,
test_wavepacket_scenario,
test_phase_portrait_sweep,
test_vr_export_scenario,
]
passed = 0
failed = 0
for test in tests:
try:
if test():
passed += 1
print("✅ PASSED\n")
else:
failed += 1
print("❌ FAILED\n")
except Exception as e:
failed += 1
print(f"❌ FAILED: {e}\n")
print("=" * 50)
print(f"📊 Results: {passed} passed, {failed} failed")
if failed == 0:
print("🎉 All Phase 5 acceptance tests passed!")
return 0
else:
print("⚠️ Some tests failed. Check the output above.")
return 1
if __name__ == "__main__":
exit(main())