Skip to content

Latest commit

 

History

History
533 lines (406 loc) · 13.2 KB

File metadata and controls

533 lines (406 loc) · 13.2 KB

Crystal-Renderer Optimizations & Improvements

Status: Pending Implementation Priority: Low-Medium (stable, quality improvements needed) Date: January 2026


Overview

The crystal-renderer package is production-ready (8/10 quality). It successfully converts crystal geometries to SVG, STL, and glTF formats with proper type annotations and clean architecture. Key areas for improvement are test coverage expansion and performance optimization.


Critical Issues

None

The package has no blocking issues. All core functionality works correctly.


High-Priority Improvements

1. Test Coverage Gaps

Current Coverage: ~60% (STL, glTF, projection fully tested; visualization untested)

Missing Test Categories:

Module Coverage Gap
visualization.py 0% High-level SVG generation
conversion.py 0% SVG→raster (requires cairosvg)
rendering.py drawing functions ~30% matplotlib-dependent functions

Proposed Tests for visualization.py:

# tests/test_visualization.py
import pytest
from pathlib import Path
import tempfile

class TestVisualization:
    def test_generate_cdl_svg_basic(self):
        """Test basic CDL to SVG generation."""
        from crystal_renderer import generate_cdl_svg

        with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as f:
            output_path = Path(f.name)

        result = generate_cdl_svg("cubic[m3m]:{111}", output_path)
        assert result.exists()
        assert result.stat().st_size > 0

        content = result.read_text()
        assert '<svg' in content
        assert '</svg>' in content
        output_path.unlink()

    def test_generate_cdl_svg_with_axes(self):
        """Test SVG with crystallographic axes."""
        from crystal_renderer import generate_cdl_svg

        with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as f:
            output_path = Path(f.name)

        result = generate_cdl_svg(
            "cubic[m3m]:{111}",
            output_path,
            show_axes=True
        )
        content = result.read_text()
        # Axes should add arrow elements
        assert 'a' in content or 'quiver' in content.lower()
        output_path.unlink()

    def test_generate_geometry_svg(self):
        """Test raw geometry SVG generation."""
        import numpy as np
        from crystal_renderer import generate_geometry_svg

        # Simple tetrahedron
        vertices = np.array([
            [1, 1, 1], [-1, -1, 1], [-1, 1, -1], [1, -1, -1]
        ], dtype=float)
        faces = [[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]]

        with tempfile.NamedTemporaryFile(suffix='.svg', delete=False) as f:
            output_path = Path(f.name)

        result = generate_geometry_svg(vertices, faces, output_path)
        assert result.exists()
        output_path.unlink()

2. Missing GEMCAD Export

Issue: GEMCAD export mentioned in docstrings but not implemented.

Location: Referenced but absent from formats/ directory.

Proposed Implementation:

# formats/gemcad.py
"""GEMCAD format export for crystal geometry."""

from pathlib import Path
from typing import List
import numpy as np

def geometry_to_gemcad(
    vertices: np.ndarray,
    faces: List[List[int]],
    name: str = "crystal"
) -> str:
    """Convert geometry to GEMCAD ASC format.

    GEMCAD is a gemstone faceting design program.
    ASC format is its native ASCII format.

    Format structure:
    - Header with gem name
    - Vertex count and vertices
    - Face count and face definitions
    """
    lines = []
    lines.append(f"# GEMCAD ASC file generated by crystal-renderer")
    lines.append(f"# Name: {name}")
    lines.append("")
    lines.append(f"VERTICES {len(vertices)}")

    for i, v in enumerate(vertices):
        lines.append(f"  {i+1}  {v[0]:.6f}  {v[1]:.6f}  {v[2]:.6f}")

    lines.append("")
    lines.append(f"FACETS {len(faces)}")

    for i, face in enumerate(faces):
        vertex_refs = " ".join(str(v+1) for v in face)
        lines.append(f"  {i+1}  {vertex_refs}")

    return "\n".join(lines)

def export_gemcad(
    vertices: np.ndarray,
    faces: List[List[int]],
    output_path: Path | str,
    name: str = "crystal"
) -> Path:
    """Export geometry to GEMCAD ASC file."""
    output_path = Path(output_path)
    content = geometry_to_gemcad(vertices, faces, name)
    output_path.write_text(content)
    return output_path

3. Per-Face Colors in glTF

Issue: glTF only supports single material per mesh.

Impact: Multi-form crystals lose color differentiation in 3D exports.

Proposed Solution: Split into multiple meshes (one per form):

def geometry_to_gltf_multicolor(
    vertices: np.ndarray,
    faces: List[List[int]],
    face_colors: List[str] | None = None,
    name: str = "crystal"
) -> dict:
    """Generate glTF with per-form coloring via multiple meshes."""
    if face_colors is None:
        return geometry_to_gltf(vertices, faces, name=name)

    # Group faces by color
    from collections import defaultdict
    color_groups = defaultdict(list)
    for i, face in enumerate(faces):
        color = face_colors[i] if i < len(face_colors) else "#808080"
        color_groups[color].append(face)

    # Create mesh per color group
    nodes = []
    meshes = []
    materials = []

    for color_idx, (color, group_faces) in enumerate(color_groups.items()):
        # Extract vertices used by this group
        # Build submesh...
        pass  # Implementation details

    # Combine into single glTF
    return gltf_dict

Medium-Priority Improvements

4. Performance: Visibility Caching

Issue: Vertex visibility recalculated on every render.

Impact: Slow for animations or multiple views.

Proposed Fix:

# projection.py
from functools import lru_cache

@lru_cache(maxsize=128)
def calculate_vertex_visibility_cached(
    vertices_hash: int,  # hash of vertices.tobytes()
    faces_hash: int,     # hash of tuple(tuple(f) for f in faces)
    elev: float,
    azim: float,
    threshold: float = 0.1
) -> tuple:
    """Cached visibility calculation."""
    # Reconstruct from cache key...
    pass

def calculate_vertex_visibility(
    vertices: np.ndarray,
    faces: List[List[int]],
    elev: float,
    azim: float,
    threshold: float = 0.1
) -> np.ndarray:
    """Calculate with caching for repeated calls."""
    v_hash = hash(vertices.tobytes())
    f_hash = hash(tuple(tuple(f) for f in faces))

    return np.array(calculate_vertex_visibility_cached(
        v_hash, f_hash, elev, azim, threshold
    ))

5. OBJ Export Format

Issue: No Wavefront OBJ export (simpler than glTF, widely compatible).

Proposed Implementation:

# formats/obj.py
"""Wavefront OBJ format export."""

def geometry_to_obj(
    vertices: np.ndarray,
    faces: List[List[int]],
    name: str = "crystal"
) -> str:
    """Convert to OBJ format string."""
    lines = [f"# {name}", f"o {name}", ""]

    # Vertices
    for v in vertices:
        lines.append(f"v {v[0]:.6f} {v[1]:.6f} {v[2]:.6f}")

    lines.append("")

    # Faces (1-indexed)
    for face in faces:
        indices = " ".join(str(i+1) for i in face)
        lines.append(f"f {indices}")

    return "\n".join(lines)

6. Rotation Matrix Support

Issue: Only elevation/azimuth angles, no arbitrary rotation matrix.

Proposed Enhancement:

# projection.py
def rotation_matrix_from_angles(elev: float, azim: float) -> np.ndarray:
    """Convert elevation/azimuth to rotation matrix."""
    elev_rad = np.radians(elev)
    azim_rad = np.radians(azim)

    # Rotation around z (azimuth), then around x' (elevation)
    Rz = np.array([
        [np.cos(azim_rad), -np.sin(azim_rad), 0],
        [np.sin(azim_rad), np.cos(azim_rad), 0],
        [0, 0, 1]
    ])
    Rx = np.array([
        [1, 0, 0],
        [0, np.cos(elev_rad), -np.sin(elev_rad)],
        [0, np.sin(elev_rad), np.cos(elev_rad)]
    ])
    return Rx @ Rz

def apply_rotation(
    vertices: np.ndarray,
    rotation: np.ndarray | tuple[float, float]
) -> np.ndarray:
    """Apply rotation to vertices.

    Args:
        vertices: Nx3 vertex array
        rotation: 3x3 matrix or (elev, azim) tuple
    """
    if isinstance(rotation, tuple):
        R = rotation_matrix_from_angles(*rotation)
    else:
        R = rotation
    return vertices @ R.T

7. Better Error Types

Issue: Generic ValueError/ImportError used everywhere.

Proposed Custom Exceptions:

# exceptions.py
class CrystalRendererError(Exception):
    """Base exception for crystal-renderer."""
    pass

class InvalidGeometryError(CrystalRendererError):
    """Raised when geometry is invalid."""
    pass

class UnsupportedFormatError(CrystalRendererError):
    """Raised for unsupported export formats."""
    pass

class DependencyMissingError(CrystalRendererError):
    """Raised when optional dependency is missing."""

    def __init__(self, package: str, feature: str):
        self.package = package
        self.feature = feature
        super().__init__(
            f"Package '{package}' required for {feature}. "
            f"Install with: pip install {package}"
        )

Low-Priority Improvements

8. Bond Detection Optimization

Issue: O(n²) brute-force distance check for bonds.

Location: rendering.py:draw_bonds()

Proposed Fix:

from scipy.spatial import cKDTree

def draw_bonds_optimized(ax, atoms, cutoff: float = 3.0):
    """Draw bonds using KD-tree for efficiency."""
    positions = atoms.get_positions()
    tree = cKDTree(positions)
    pairs = tree.query_pairs(r=cutoff)

    for i, j in pairs:
        # Draw bond...
        pass

9. LOD (Level of Detail) System

Issue: No simplification for complex meshes.

Proposed Design:

def simplify_geometry(
    vertices: np.ndarray,
    faces: List[List[int]],
    target_faces: int
) -> tuple[np.ndarray, List[List[int]]]:
    """Reduce mesh complexity for rendering.

    Uses edge collapse algorithm to simplify
    while preserving crystal form.
    """
    # Would require implementing edge collapse
    # or using external library (e.g., pymeshlab)
    pass

10. Documentation Build

Issue: No Sphinx documentation.

Proposed Structure:

docs/
├── conf.py
├── index.rst
├── api/
│   ├── visualization.rst
│   ├── projection.rst
│   ├── formats.rst
│   └── info_panel.rst
├── examples/
│   ├── basic_svg.rst
│   ├── 3d_export.rst
│   └── custom_colors.rst
└── performance.rst

CDL v2 Preparation

11. Aggregate Rendering Support

For CDL v2 ~ operator (crystal aggregates):

def render_aggregate(
    components: List[CrystalGeometry],
    arrangement: str,  # 'parallel', 'radial', 'cluster'
    output_path: Path
) -> Path:
    """Render multiple crystal components as aggregate."""
    # Combine all vertices with offset
    # Render with unique colors per component
    pass

12. Twinning Visualization

For CDL v2 | twin(law) modifier:

def render_twinned_crystal(
    geometry: CrystalGeometry,
    twin_law: str,
    count: int = 2
) -> CrystalGeometry:
    """Generate twinned crystal geometry."""
    from crystal_geometry.twinning import apply_twin

    components = apply_twin(geometry, twin_law, count)
    # Merge with distinct colors per individual
    return merged_geometry

13. Growth Feature Rendering

For CDL v2 features ([phantom:N], [smoky], etc.):

def render_with_features(
    geometry: CrystalGeometry,
    features: dict
) -> None:
    """Apply visual features to crystal rendering.

    Features:
    - phantom:N - Render N internal phantom layers
    - smoky - Apply grey gradient
    - colour:name - Apply specific coloring
    - silk - Add needle inclusion texture
    """
    pass

Implementation Priority

Priority Task Effort Impact
P1 Add visualization.py tests 4 hours Test coverage
P1 Implement GEMCAD export 2 hours Feature completeness
P2 Add OBJ export 1 hour Wider compatibility
P2 Per-face glTF colors 4 hours Better 3D output
P2 Visibility caching 2 hours Performance
P3 Custom exceptions 1 hour Error clarity
P3 Rotation matrix API 2 hours Flexibility
P3 Sphinx documentation 1 day Docs quality
P3 CDL v2 rendering 1 week Future features

Verification Checklist

After implementing improvements:

  • All existing tests pass
  • visualization.py tests added and passing
  • GEMCAD export works with sample crystals
  • glTF multi-color works for diamond preset
  • No performance regression (benchmark SVG generation)
  • mypy passes with zero errors
  • ruff check passes

Performance Benchmarks (Baseline)

Measure before optimization:

Operation Crystal Time Notes
CDL→SVG Octahedron (8 faces) TBD Baseline
CDL→SVG Diamond (14 faces) TBD Truncated
CDL→SVG Garnet (36 faces) TBD Complex
SVG→PNG 1000×1000 TBD With cairosvg
glTF export 100 vertices TBD Baseline

Document created: 2026-01-20