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:
- Classic Phase Portraits:
Phase: HSV-based with optional enhancements-
OklabPhase: Perceptually uniform OKLAB space -
Perceptually Uniform:
PerceptualPastel: OkLCh pastelsIsoluminant: Constant brightness-
CubehelixPhase: Grayscale-safe -
Artistic:
AnalogousWedge: Compressed hue rangeDivergingWarmCool: Warm/cool divergenceInkPaper: Near-monochromeEarthTopographic: Terrain-inspired-
FourQuadrant: Bauhaus style -
Grid Patterns:
Chessboard: Cartesian gridPolarChessboard: Polar gridLogRings: Logarithmic rings
Enhanced Phase Portraits:
Colormaps support three types of enhancements:
- Phase Sectors (
phase_sectors): Angular wedges with sawtooth brightness - Linear Modulus Contours (
r_linear_step): Rings at fixed |z| intervals - 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:
- High-Level API (
api.py): User-friendly functions with sensible defaults - 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:
- Inverse Projection: Map sphere points to complex plane
- Function Evaluation: Compute f(z)
- Modulus Scaling: Apply scaling mode
- 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:
- HSV (Hue, Saturation, Value)
- Classic phase portraits
- Simple to implement
-
Not perceptually uniform
-
OKLAB (Lightness, a, b)
- Perceptually uniform
- Better for comparisons
-
More complex conversion
-
OkLCh (Lightness, Chroma, Hue)
- Cylindrical OKLAB
- Intuitive parameters
- Ideal for pastels
Conversion Pipeline:
Implementation:
def hsv_to_rgb(h: np.ndarray, s: np.ndarray, v: np.ndarray) -> np.ndarray:
"""Convert HSV to RGB (vectorized)."""
# Standard HSVRGB 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:
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:
4. Fail Fast¶
Validate inputs immediately:
5. NumPy-First¶
All numerical operations use NumPy arrays:
Future Enhancements¶
Planned Features¶
- Domain Composition: Union, intersection, difference operations
- Animation Support: Parameter sweeps and time evolution
- Interactive Widgets: Jupyter/web-based exploration
- GPU Acceleration: For high-resolution renders
- Additional Backends: WebGL, Three.js for web
Technical Debt¶
- Riemann Sphere Mesh: Rectangular grid is inefficient at poles
- Consider icosphere or UV sphere
- Color Space Conversions: Could be optimized with lookup tables
- Type Hints: Some functions missing type annotations
- Documentation: Some internal functions lack docstrings
See Also¶
- Contributing Guide - How to contribute
- API Reference - Public API documentation
- User Guide - Usage examples