"""Comprehensive Regression & Acceptance Tests for the Visualization Graph and Topology Pipeline. Verifies: 1. Mathematical Geometry Graph vs Visualization Graph separation. 2. Complete 3D Solid Topology (Pyramid, Prism, Cube, Cuboid, Tetrahedron). 3. Automatic derivation of visual topology (vertices, edges, faces, connectivity). 4. Solution-dependent Auxiliary Geometry (Heights, Feet, Medians, Bisectors, Diagonals). 5. Minimal Sufficient Graph & Importance Tiers (REQUIRED, HELPFUL, OPTIONAL). 6. Surface & Face representations with cyclic vertex order and parent solid metadata. 7. VisualizationSpec integration and schema serialization. """ from __future__ import annotations import pytest import numpy as np from solver.dsl_parser import DSLParser from solver.engine import GeometryEngine from solver.vis_graph import ( EdgeStyle, EntityKind, ImportanceTier, VisualizationGraph, ) from solver.vis_planner import VisualizationPlanner from manim_client.schemas import build_visualization_spec, VisualizationSpec # ============================================================================ # 1. 2D POLYGON TOPOLOGY & AUXILIARY CONSTRUCTIONS # ============================================================================ def test_2d_rectangle_with_diagonals_and_midpoint(): """ 2D Rectangle ABCD with center O and midpoint M of AB. Verifies that Visualization Graph contains: - 4 primary vertices + auxiliary center O + auxiliary midpoint M - 4 perimeter edges + 2 diagonal edges + auxiliary segments - 1 polygon face - Correct importance tiers. """ dsl = """ RECTANGLE(ABCD) LENGTH(AB, 8) LENGTH(BC, 6) CENTER(O, ABCD) MIDPOINT(M, AB) """ parser = DSLParser() points, constraints, is_3d = parser.parse(dsl) assert not is_3d engine = GeometryEngine() result = engine.solve(points, constraints, is_3d) assert result is not None assert "visualization_graph" in result vis_graph_data = result["visualization_graph"] vis_graph = VisualizationGraph.model_validate(vis_graph_data) # 1. Vertices assert "A" in vis_graph.vertices assert "B" in vis_graph.vertices assert "C" in vis_graph.vertices assert "D" in vis_graph.vertices assert "O" in vis_graph.vertices assert "M" in vis_graph.vertices assert vis_graph.vertices["O"].role == "center" assert vis_graph.vertices["O"].kind == EntityKind.AUXILIARY assert vis_graph.vertices["M"].role == "midpoint" # 2. Edges: Perimeter + Diagonals edge_ids = list(vis_graph.edges.keys()) assert any("A" in eid and "B" in eid for eid in edge_ids) assert any("B" in eid and "C" in eid for eid in edge_ids) assert any("C" in eid and "D" in eid for eid in edge_ids) assert any("A" in eid and "D" in eid for eid in edge_ids) # Diagonals AC and BD assert any("A" in eid and "C" in eid for eid in edge_ids) assert any("B" in eid and "D" in eid for eid in edge_ids) # 3. Faces assert len(vis_graph.faces) >= 1 face = next(iter(vis_graph.faces.values())) assert len(face.vertices) == 4 assert set(face.vertices) == {"A", "B", "C", "D"} # 4. Minimal Sufficient Graph min_graph = vis_graph.get_minimal_sufficient_graph(ImportanceTier.REQUIRED) assert "A" in min_graph["vertices"] assert "B" in min_graph["vertices"] def test_2d_triangle_with_median_and_foot_altitude(): """ 2D Triangle ABC with foot of altitude H and median AM. Verifies automatic derivation of auxiliary lines and perpendicular marks. """ dsl = """ TRIANGLE(ABC) POINT(A, 0, 4) POINT(B, -3, 0) POINT(C, 3, 0) FOOT(H, A, BC) MEDIAN(A, M, BC) """ parser = DSLParser() points, constraints, is_3d = parser.parse(dsl) engine = GeometryEngine() result = engine.solve(points, constraints, is_3d) assert result is not None vis_graph = VisualizationGraph.model_validate(result["visualization_graph"]) # Foot H and Median M assert "H" in vis_graph.vertices assert "M" in vis_graph.vertices assert vis_graph.vertices["H"].role == "foot" # Auxiliary construction records aux_types = [a.type for a in vis_graph.auxiliary] assert "foot" in aux_types assert "median" in aux_types # Edges include AH and AM edge_ids = list(vis_graph.edges.keys()) assert any("A" in eid and "H" in eid for eid in edge_ids) assert any("A" in eid and "M" in eid for eid in edge_ids) # ============================================================================ # 2. 3D SOLID TOPOLOGY (PYRAMID, PRISM, CUBE, CUBOID, TETRAHEDRON) # ============================================================================ def test_3d_pyramid_full_topology_and_height(): """ Square Pyramid S.ABCD with height SO = 8. Verifies: - Complete Solid Topology: 5 vertices, 8 primary edges, 5 faces (1 base + 4 lateral). - Height SO auxiliary construction with dashed style. - Base diagonals AC, BD automatically derived to ground foot O. - Solid-to-face and solid-to-edge connectivity. """ dsl = """ PYRAMID(S_ABCD) SQUARE(ABCD) LENGTH(AB, 6) CENTER(O, ABCD) HEIGHT(S, O, ABCD, 8) """ parser = DSLParser() points, constraints, is_3d = parser.parse(dsl) assert is_3d engine = GeometryEngine() result = engine.solve(points, constraints, is_3d) assert result is not None vis_graph = VisualizationGraph.model_validate(result["visualization_graph"]) # 1. Solid Topology Record assert len(vis_graph.solids) >= 1 solid = next(iter(vis_graph.solids.values())) assert solid.type == "pyramid" assert solid.apex == "S" assert set(solid.base_vertices) == {"A", "B", "C", "D"} assert len(solid.edges) == 8 # 4 base + 4 lateral assert len(solid.faces) == 5 # 1 base + 4 lateral # 2. Faces (1 quadrilateral base + 4 triangular lateral faces) assert len(vis_graph.faces) >= 5 base_faces = [f for f in vis_graph.faces.values() if f.role == "base_face"] lat_faces = [f for f in vis_graph.faces.values() if f.role == "lateral_face"] assert len(base_faces) == 1 assert len(lat_faces) == 4 assert set(base_faces[0].vertices) == {"A", "B", "C", "D"} # 3. Altitude SO & Base Diagonals edge_so = next((e for e in vis_graph.edges.values() if "S" in e.id and "O" in e.id), None) assert edge_so is not None assert edge_so.role == "altitude" assert edge_so.style == EdgeStyle.DASHED # Diagonals AC and BD exist to anchor O assert any("A" in e.id and "C" in e.id for e in vis_graph.edges.values()) assert any("B" in e.id and "D" in e.id for e in vis_graph.edges.values()) # 4. Auxiliary Entity Record height_aux = next((a for a in vis_graph.auxiliary if a.type == "height"), None) assert height_aux is not None assert height_aux.source_entity == "S" assert height_aux.target_entity == "O" def test_3d_triangular_prism_topology(): """ Triangular Prism ABC.DEF with base side 5 and height 10. Verifies: - 6 vertices (A, B, C, D, E, F). - 9 edges (3 base1 + 3 base2 + 3 lateral). - 5 faces (2 triangular bases + 3 rectangular lateral faces). - Face connectivity and parent solid references. """ dsl = """ PRISM(ABC_DEF) EQUILATERAL_TRIANGLE(ABC, 5) LENGTH(AD, 10) """ parser = DSLParser() points, constraints, is_3d = parser.parse(dsl) assert is_3d engine = GeometryEngine() result = engine.solve(points, constraints, is_3d) assert result is not None vis_graph = VisualizationGraph.model_validate(result["visualization_graph"]) assert len(vis_graph.solids) >= 1 solid = next(iter(vis_graph.solids.values())) assert solid.type == "prism" assert set(solid.base_vertices) == {"A", "B", "C"} assert set(solid.top_vertices) == {"D", "E", "F"} assert len(solid.edges) == 9 assert len(solid.faces) == 5 # 2 Base faces (triangles) + 3 Lateral faces (quadrilaterals) tri_faces = [f for f in vis_graph.faces.values() if len(f.vertices) == 3] quad_faces = [f for f in vis_graph.faces.values() if len(f.vertices) == 4] assert len(tri_faces) == 2 assert len(quad_faces) == 3 def test_3d_cube_topology(): """ Cube ABCD.A1B1C1D1 with side a=5. Verifies: - 8 vertices. - 12 edges. - 6 quadrilateral faces. """ dsl = """ POINT(A, 0, 0, 0) POINT(B, 5, 0, 0) POINT(C, 5, 5, 0) POINT(D, 0, 5, 0) POINT(A1) POINT(B1) POINT(C1) POINT(D1) LENGTH(AA1, 5) PERPENDICULAR_PLANE(AA1, ABCD) CUBE(ABCD_A1B1C1D1) """ parser = DSLParser() points, constraints, is_3d = parser.parse(dsl) engine = GeometryEngine() result = engine.solve(points, constraints, is_3d) assert result is not None vis_graph = VisualizationGraph.model_validate(result["visualization_graph"]) assert len(vis_graph.vertices) == 8 assert len(vis_graph.faces) == 6 for f in vis_graph.faces.values(): assert len(f.vertices) == 4 def test_3d_tetrahedron_topology(): """ Regular Tetrahedron ABCD. Verifies: - 4 vertices. - 6 edges. - 4 triangular faces. """ dsl = """ TETRAHEDRON(ABCD) LENGTH(AB, 6) """ parser = DSLParser() points, constraints, is_3d = parser.parse(dsl) engine = GeometryEngine() result = engine.solve(points, constraints, is_3d) assert result is not None vis_graph = VisualizationGraph.model_validate(result["visualization_graph"]) assert len(vis_graph.vertices) == 4 assert len(vis_graph.faces) == 4 for f in vis_graph.faces.values(): assert len(f.vertices) == 3 def test_3d_triangular_pyramid_s_abc_with_height_and_auxiliary_midpoint(): """ Triangular Pyramid S.ABC with centroid foot H and midpoint M of BC. Verifies: - 4 primary vertices (S, A, B, C) + 2 auxiliary (H, M). - Base edges AB, BC, CA + lateral edges SA, SB, SC. - 4 faces (1 base + 3 lateral). - Height SH and median AM auxiliary constructions. """ dsl = """ PYRAMID(S_ABC) EQUILATERAL_TRIANGLE(ABC, 6) CENTER(H, ABC) HEIGHT(S, H, ABC, 9) MIDPOINT(M, BC) """ parser = DSLParser() points, constraints, is_3d = parser.parse(dsl) assert is_3d engine = GeometryEngine() result = engine.solve(points, constraints, is_3d) assert result is not None vis_graph = VisualizationGraph.model_validate(result["visualization_graph"]) assert len(vis_graph.solids) >= 1 solid = next(iter(vis_graph.solids.values())) assert solid.type == "pyramid" assert solid.apex == "S" assert set(solid.base_vertices) == {"A", "B", "C"} assert len(solid.faces) == 4 # Check auxiliary vertices & edges assert "H" in vis_graph.vertices assert "M" in vis_graph.vertices assert vis_graph.vertices["H"].role in ("foot", "center") assert vis_graph.vertices["M"].role == "midpoint" def test_3d_cuboid_topology(): """ Cuboid ABCD.A1B1C1D1 with length=8, width=6, height=10. Verifies 8 vertices, 12 edges, 6 faces. """ dsl = """ POINT(A, 0, 0, 0) POINT(B, 8, 0, 0) POINT(C, 8, 6, 0) POINT(D, 0, 6, 0) POINT(A1) POINT(B1) POINT(C1) POINT(D1) LENGTH(AA1, 10) PERPENDICULAR_PLANE(AA1, ABCD) PRISM(ABCD_A1B1C1D1) """ parser = DSLParser() points, constraints, is_3d = parser.parse(dsl) engine = GeometryEngine() result = engine.solve(points, constraints, is_3d) assert result is not None vis_graph = VisualizationGraph.model_validate(result["visualization_graph"]) assert len(vis_graph.vertices) == 8 assert len(vis_graph.faces) == 6 assert len(vis_graph.edges) >= 12 # ============================================================================ # 3. VISUALIZATION SPEC INTEGRATION & MINIMAL SUFFICIENT GRAPH # ============================================================================ def test_visualization_spec_rich_topology_generation(): """ Verifies that build_visualization_spec generates rich GeometryObject entries including points, styled segments, faces with opacity, and solid containers. """ dsl = """ PYRAMID(S_ABCD) SQUARE(ABCD) LENGTH(AB, 4) CENTER(O, ABCD) HEIGHT(S, O, ABCD, 6) """ parser = DSLParser() points, constraints, is_3d = parser.parse(dsl) engine = GeometryEngine() result = engine.solve(points, constraints, is_3d) spec = build_visualization_spec( problem_text="Tính thể tích khối chóp S.ABCD", solution_steps=["Dựng hình chóp S.ABCD với đáy hình vuông.", "Dựng đường cao SO."], engine_result=result, is_3d=True, ) assert isinstance(spec, VisualizationSpec) assert spec.visualization_graph is not None types = [g.type for g in spec.geometry] assert "point_3d" in types assert "segment_3d" in types assert "face_3d" in types assert "pyramid" in types # Verify Manim dictionary serialization manim_dict = spec.to_manim_dict() assert "geometry" in manim_dict assert len(manim_dict["geometry"]) >= 5 assert len(manim_dict["solution_steps"]) == 2 def test_minimal_sufficient_graph_filtering(): """ Verifies that get_minimal_sufficient_graph properly filters between REQUIRED and HELPFUL tiers without dropping critical elements. """ graph = VisualizationGraph(is_3d=True) graph.add_vertex("A", [0, 0, 0], tier=ImportanceTier.REQUIRED) graph.add_vertex("B", [5, 0, 0], tier=ImportanceTier.REQUIRED) graph.add_vertex("P_extra", [10, 10, 10], tier=ImportanceTier.OPTIONAL) graph.add_edge("A", "B", tier=ImportanceTier.REQUIRED) graph.add_edge("A", "P_extra", tier=ImportanceTier.OPTIONAL) # Filter REQUIRED only filtered = graph.get_minimal_sufficient_graph(ImportanceTier.REQUIRED) assert "A" in filtered["vertices"] assert "B" in filtered["vertices"] assert "P_extra" not in filtered["vertices"] assert len(filtered["edges"]) == 1