Skip to content

Architecture

This document describes the internal architecture of Complexplorer.

Overview

Complexplorer is organized into several core modules, each with specific responsibilities:

complexplorer/
 core/                  # Core functionality
    domain.py          # Domain definitions (Rectangle, Disk, Annulus)
    colormap.py        # Colormap classes (13 families)
    functions.py       # Mathematical functions (phase, sawtooth, stereographic)
    scaling.py         # Modulus scaling for 3D/Riemann sphere
    color_utils.py     # Color space conversions (HSV, OKLAB, OkLCh)
    constants.py       # Mathematical constants

 plotting/              # Visualization functions
    matplotlib/        # Matplotlib-based plotting
       plots_2d.py    # 2D phase portraits
       plots_3d.py    # 3D analytic landscapes
       riemann.py     # Riemann sphere visualization
    pyvista/           # PyVista-based plotting (high-performance)
       plots_3d_pv.py # 3D landscapes (fast)
       riemann_pv.py  # Riemann sphere (interactive)
    validation.py      # Input validation for plotting

 export/                # Export functionality
    stl/               # STL export for 3D printing
        ornament_generator.py  # Main STL generation
        mesh_repair.py         # Mesh repair utilities
        utils.py               # STL utilities

 utils/                 # Shared utilities
    mesh.py            # Mesh generation (Riemann sphere)
    mesh_distortion.py # Mesh distortion for function visualization
    validation.py      # Common validation functions
    backend.py         # Backend detection (matplotlib, PyVista)

 api.py                 # High-level user-facing API
 special.py             # Special visualization functions
 exceptions.py          # Custom exceptions

Core Modules

Domain System (core/domain.py)

Domains define regions of the complex plane for visualization.

Base Class:

class Domain(ABC):
    """Abstract base class for all domains."""

    @abstractmethod
    def contains(self, z: np.ndarray) -> np.ndarray:
        """Check if complex points are in the domain."""
        pass

    def sample(self, resolution: int) -> np.ndarray:
        """Generate grid of points for visualization."""
        # Default implementation - subclasses may override
        pass

Concrete Domains: - Rectangle: Rectangular region (most common) - Disk: Circular region - Annulus: Ring-shaped region (inner/outer radius)

Design Principles: - Domains are immutable after creation - contains() works with both scalars and arrays - Domains generate their own sampling grids

Colormap System (core/colormap.py)

Colormaps transform complex values to colors.

Base Class:

class Colormap(ABC):
    """Abstract base class for all colormaps."""

    @abstractmethod
    def hsv(self, z: np.ndarray) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
        """Map complex values to HSV color space."""
        pass

    def rgb(self, z: np.ndarray) -> np.ndarray:
        """Map complex values to RGB (0-1 range)."""
        h, s, v = self.hsv(z)
        return hsv_to_rgb(h, s, v)

    def __call__(self, z: np.ndarray) -> np.ndarray:
        """Convenience: map complex values to RGB."""
        return self.rgb(z)

Colormap Families:

  1. Classic Phase Portraits:
  2. Phase: HSV-based with optional enhancements
  3. OklabPhase: Perceptually uniform OKLAB space

  4. Perceptually Uniform:

  5. PerceptualPastel: OkLCh pastels
  6. Isoluminant: Constant brightness
  7. CubehelixPhase: Grayscale-safe

  8. Artistic:

  9. AnalogousWedge: Compressed hue range
  10. DivergingWarmCool: Warm/cool divergence
  11. InkPaper: Near-monochrome
  12. EarthTopographic: Terrain-inspired
  13. FourQuadrant: Bauhaus style

  14. Grid Patterns:

  15. Chessboard: Cartesian grid
  16. PolarChessboard: Polar grid
  17. LogRings: Logarithmic rings

Enhanced Phase Portraits:

Colormaps support three types of enhancements:

  1. Phase Sectors (phase_sectors): Angular wedges with sawtooth brightness
  2. Linear Modulus Contours (r_linear_step): Rings at fixed |z| intervals
  3. Logarithmic Modulus Contours (r_log_base): Rings at log-spaced |z|

Auto-Scaling Feature:

The auto_scale_r feature automatically calculates r_linear_step to create square cells:

if auto_scale_r and phase_sectors > 0:
    # Calculate step for square cells at reference radius
    arc_length = 2 * pi * scale_radius / phase_sectors
    r_linear_step = arc_length  # Match radial and angular spacing

This ensures that phase sectors and modulus contours create a visually square grid.

Modulus Scaling (core/scaling.py)

Controls how |z| is mapped to height in 3D visualizations and Riemann spheres.

Available Modes:

Mode Formula Purpose
constant 1.0 Flat surface
linear |z| Direct mapping
arctan arctan(±|z|) Bounded smooth
logarithmic log(1 + |z|) Emphasize poles
adaptive Percentile-based Auto-adjust
linear_clamp min(|z|, max) Capped linear
power |z|^p Power law
sigmoid 1/(1 + e^(-|z|)) S-curve
hybrid Combination Complex behavior

Function Signature:

def apply_modulus_scaling(
    z: np.ndarray,
    mode: str = 'arctan',
    params: Optional[Dict[str, Any]] = None,
    for_stl: bool = False
) -> np.ndarray:
    """Apply modulus scaling to complex values."""
    pass

STL-Specific Defaults:

For 3D printing, scaling parameters are adjusted to ensure printability:

def get_default_scaling_params(mode: str, for_stl: bool = False) -> Dict[str, Any]:
    """Get default parameters for scaling mode."""
    if for_stl:
        # More conservative for 3D printing
        if mode == 'arctan':
            return {'r_min': 0.3, 'r_max': 0.9}  # Safer range
    else:
        # More aggressive for visualization
        if mode == 'arctan':
            return {'r_min': 0.2, 'r_max': 0.95}  # Fuller range

Plotting System

Two-Tier Architecture

Complexplorer uses a two-tier plotting system:

  1. High-Level API (api.py): User-friendly functions with sensible defaults
  2. Low-Level Plotting (plotting/): Backend-specific implementation

Example:

# api.py - High-level
def plot(domain, func, cmap=None, resolution=500, **kwargs):
    """User-friendly plotting function."""
    if cmap is None:
        cmap = Phase(phase_sectors=6, auto_scale_r=True)

    # Delegate to backend
    from .plotting.matplotlib.plots_2d import plot_2d
    return plot_2d(domain, func, cmap, resolution, **kwargs)

# plotting/matplotlib/plots_2d.py - Low-level
def plot_2d(domain, func, cmap, resolution, figsize=(8, 8), **kwargs):
    """Backend-specific 2D plotting."""
    # Actual implementation
    pass

Matplotlib Backend

2D Plotting (plotting/matplotlib/plots_2d.py): - plot_2d(): Basic 2D phase portrait - pair_plot_2d(): Side-by-side domain/codomain

3D Plotting (plotting/matplotlib/plots_3d.py): - plot_landscape(): 3D surface with height = modulus - pair_plot_landscape(): Side-by-side 3D landscapes

Riemann Sphere (plotting/matplotlib/riemann.py): - riemann(): Matplotlib 3D sphere visualization

Limitations: - Slow for high resolutions (>500x500) - Poor 3D rendering quality - Limited interactivity

PyVista Backend

3D Plotting (plotting/pyvista/plots_3d_pv.py): - plot_landscape_pv(): High-performance 3D landscapes - pair_plot_landscape_pv(): Side-by-side PyVista landscapes

Riemann Sphere (plotting/pyvista/riemann_pv.py): - riemann_pv(): Interactive Riemann sphere with modulus scaling

Advantages: - 15-30x faster than matplotlib - Superior rendering quality - Interactive camera controls - Better antialiasing

Challenges: - Jupyter notebook backend has aliasing issues - Requires PyVista dependency - More complex installation

Mesh Generation System

Riemann Sphere Mesh (utils/mesh.py)

RectangularSphereGenerator:

Generates meshes for Riemann sphere visualization using a rectangular (lat/lon) grid.

class RectangularSphereGenerator:
    """Generate rectangular mesh on sphere.

    Parameters
    ----------
    radius : float
        Sphere radius (default 1.0).
    n_theta : int
        Latitudinal divisions (pole to pole).
    n_phi : int
        Longitudinal divisions (around equator).
    avoid_poles : bool
        Skip exact poles to avoid singularities.
    domain : Domain, optional
        Restrict evaluation to domain.
    """

Mesh Structure:

  • Vertices: (n_theta+1) × (n_phi+1) grid
  • Faces: Quadrilaterals (converted to triangles)
  • UV Coordinates: For texture mapping
  • Normals: Per-vertex normals for lighting

Pole Handling:

When avoid_poles=True, the mesh skips the exact poles to avoid: - Division by zero in stereographic projection - Degenerate triangles at poles - Numerical instability

Mesh Distortion (utils/mesh_distortion.py)

compute_riemann_sphere_distortion():

Applies function evaluation and modulus scaling to sphere mesh:

def compute_riemann_sphere_distortion(
    sphere_mesh: pv.PolyData,
    func: Callable,
    scaling_mode: str = 'arctan',
    scaling_params: Optional[Dict[str, Any]] = None,
    from_north: bool = True
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
    """Apply function and scaling to sphere mesh.

    Returns
    -------
    scaled_points : np.ndarray
        New 3D coordinates after scaling.
    f_vals : np.ndarray
        Function values at each point.
    radii : np.ndarray
        Radii after scaling.
    """

Process:

  1. Inverse Projection: Map sphere points to complex plane
  2. Function Evaluation: Compute f(z)
  3. Modulus Scaling: Apply scaling mode
  4. Radial Displacement: Scale sphere radius by modulus

Export System

STL Generation (export/stl/)

OrnamentGenerator:

Creates 3D-printable ornaments from complex functions.

class OrnamentGenerator:
    """Generate 3D-printable ornaments.

    Workflow:
    1. Generate base Riemann sphere mesh
    2. Evaluate function on mesh
    3. Apply modulus scaling
    4. Add color information
    5. Repair mesh (fill holes)
    6. Validate printability
    7. Export as STL
    """

Mesh Repair (mesh_repair.py):

Simple repair operations for 3D printing: - Fill holes to make watertight - Remove duplicate vertices - Clean degenerate faces

Note: For complex repairs, external tools (MeshLab, Meshmixer) are recommended.

Validation (utils.py):

Checks mesh printability: - Watertight (no holes) - Manifold edges - Physical dimensions - Vertex/face counts

Color Space System

Color Utilities (core/color_utils.py)

Supported Color Spaces:

  1. HSV (Hue, Saturation, Value)
  2. Classic phase portraits
  3. Simple to implement
  4. Not perceptually uniform

  5. OKLAB (Lightness, a, b)

  6. Perceptually uniform
  7. Better for comparisons
  8. More complex conversion

  9. OkLCh (Lightness, Chroma, Hue)

  10. Cylindrical OKLAB
  11. Intuitive parameters
  12. Ideal for pastels

Conversion Pipeline:

HSV ’ RGB (simple formula)
OkLCh ’ OKLAB ’ Linear RGB ’ sRGB (complex)

Implementation:

def hsv_to_rgb(h: np.ndarray, s: np.ndarray, v: np.ndarray) -> np.ndarray:
    """Convert HSV to RGB (vectorized)."""
    # Standard HSV’RGB algorithm
    pass

def oklab_to_rgb(L: np.ndarray, a: np.ndarray, b: np.ndarray) -> np.ndarray:
    """Convert OKLAB to RGB (perceptually uniform)."""
    # Matrix transformations
    pass

def oklch_to_rgb(L: np.ndarray, C: np.ndarray, h: np.ndarray) -> np.ndarray:
    """Convert OkLCh to RGB (cylindrical OKLAB)."""
    # Convert to OKLAB first
    a = C * np.cos(h)
    b = C * np.sin(h)
    return oklab_to_rgb(L, a, b)

Validation System

Input Validation (utils/validation.py, plotting/validation.py)

Centralized Validation:

All user inputs are validated before processing:

def validate_function(func: Callable) -> None:
    """Validate that func is callable."""
    if not callable(func):
        raise ValidationError("func must be callable")

def validate_resolution(resolution: int) -> None:
    """Validate resolution parameter."""
    if not isinstance(resolution, int) or resolution <= 0:
        raise ValidationError("resolution must be positive integer")

def validate_domain(domain: Domain) -> None:
    """Validate domain object."""
    if not isinstance(domain, Domain):
        raise ValidationError("domain must be Domain instance")

Error Messages:

Clear, actionable error messages guide users:

raise ValidationError(
    f"resolution must be between 10 and 5000, got {resolution}"
)

Performance Considerations

Vectorization

All numerical operations are vectorized using NumPy:

# Bad: Loop over points
colors = []
for z in z_array:
    h = np.angle(z) / (2 * np.pi)
    colors.append(h)

# Good: Vectorized operation
h = np.angle(z_array) / (2 * np.pi)

Lazy Evaluation

Expensive computations are deferred until needed:

class Domain:
    def sample(self, resolution):
        # Generate grid only when requested
        # Not in __init__
        pass

Caching

Consider caching for expensive operations:

from functools import lru_cache

@lru_cache(maxsize=128)
def get_colormap_instance(name: str, **params):
    """Cache colormap instances."""
    pass

Testing Strategy

Unit Tests (tests/unit/)

Test individual components in isolation:

tests/unit/
 core/
    test_domain.py        # Domain classes
    test_colormap.py      # All 13 colormaps
    test_functions.py     # Mathematical functions
    test_scaling.py       # Modulus scaling
    test_color_utils.py   # Color conversions
 test_api.py               # High-level API
 test_special.py           # Special functions
 test_validation.py        # Input validation

Coverage Target: >80% overall, >90% for core modules

Integration Tests

Test component interactions:

def test_plot_with_custom_colormap():
    """Test complete plotting pipeline."""
    domain = Rectangle(4, 4)
    func = lambda z: z**2
    cmap = Phase(phase_sectors=6, auto_scale_r=True)

    # Should not raise
    ax = plot(domain, func, cmap=cmap, show=False)
    assert ax is not None

Parametrized Tests

Test multiple scenarios efficiently:

@pytest.mark.parametrize("mode,expected_range", [
    ('constant', (1.0, 1.0)),
    ('linear', (0.0, 10.0)),
    ('arctan', (0.0, 1.57)),
])
def test_modulus_scaling_modes(mode, expected_range):
    z = np.linspace(0, 10, 100)
    scaled = apply_modulus_scaling(z, mode=mode)
    assert scaled.min() >= expected_range[0]
    assert scaled.max() <= expected_range[1]

Extension Points

Adding New Features

New Colormap: 1. Subclass Colormap 2. Implement hsv() method 3. Add to __init__.py 4. Write tests 5. Document

New Domain: 1. Subclass Domain 2. Implement contains() method 3. Optionally override sample() 4. Add to __init__.py 5. Write tests 6. Document

New Scaling Mode: 1. Add to scaling.py 2. Update apply_modulus_scaling() 3. Add default params 4. Write tests 5. Document

Design Principles

1. Simplicity Over Flexibility

Provide sensible defaults for 90% of use cases:

# Simple case works out of the box
cp.show(lambda z: z**2)

# Advanced case requires explicit configuration
cp.plot(domain, func, cmap=cmap, resolution=800)

2. Composability

Components work together seamlessly:

domain = cp.Disk(radius=2)
cmap = cp.Phase(phase_sectors=6, auto_scale_r=True)
cp.plot(domain, func, cmap=cmap)

3. Explicit Over Implicit

No hidden state or global configuration:

# Each plot is independent
cp.plot(domain1, func1, cmap=cmap1)
cp.plot(domain2, func2, cmap=cmap2)

4. Fail Fast

Validate inputs immediately:

domain = Disk(radius=-1)  # Raises ValidationError immediately

5. NumPy-First

All numerical operations use NumPy arrays:

def contains(self, z: np.ndarray) -> np.ndarray:
    # Works with arrays, not lists
    pass

Future Enhancements

Planned Features

  1. Domain Composition: Union, intersection, difference operations
  2. Animation Support: Parameter sweeps and time evolution
  3. Interactive Widgets: Jupyter/web-based exploration
  4. GPU Acceleration: For high-resolution renders
  5. Additional Backends: WebGL, Three.js for web

Technical Debt

  1. Riemann Sphere Mesh: Rectangular grid is inefficient at poles
  2. Consider icosphere or UV sphere
  3. Color Space Conversions: Could be optimized with lookup tables
  4. Type Hints: Some functions missing type annotations
  5. Documentation: Some internal functions lack docstrings

See Also