Skip to content

Commit ac3b978

Browse files
committed
add basic visualization helpers
1 parent 3c8edda commit ac3b978

File tree

4 files changed

+1134
-0
lines changed

4 files changed

+1134
-0
lines changed

src/surfaces/visualize/__init__.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Author: Simon Blanke
2+
3+
# License: MIT License
4+
5+
"""
6+
Visualization module for Surfaces test functions.
7+
8+
This module provides various plot types for visualizing objective function
9+
landscapes and optimization progress. It includes a compatibility system
10+
that helps users discover which plots work with their specific functions.
11+
12+
Quick Start
13+
-----------
14+
>>> from surfaces.test_functions import SphereFunction, AckleyFunction
15+
>>> from surfaces.visualize import auto_plot, available_plots
16+
17+
# See what plots work with your function
18+
>>> func = SphereFunction(n_dim=5)
19+
>>> available_plots(func)
20+
[{'name': 'multi_slice', 'description': '...'},
21+
{'name': 'fitness_distribution', 'description': '...'}]
22+
23+
# Auto-select the best visualization
24+
>>> fig = auto_plot(func)
25+
>>> fig.show()
26+
27+
# For 2D functions, surface plots are available
28+
>>> func_2d = AckleyFunction()
29+
>>> fig = plot_surface(func_2d)
30+
>>> fig.show()
31+
32+
Plot Types
33+
----------
34+
- surface: 3D surface plot (2D functions only)
35+
- contour: 2D contour plot (2D functions only)
36+
- multi_slice: 1D slices through each dimension (any N-D)
37+
- convergence: Best-so-far vs evaluations (requires history)
38+
- fitness_distribution: Histogram of sampled values (any N-D)
39+
40+
Discovery Functions
41+
-------------------
42+
- available_plots(func): List plots compatible with a function
43+
- check_compatibility(func, plot_name): Check if specific plot works
44+
- plot_info(plot_name): Get info about a plot type
45+
- list_all_plots(): List all available plot types
46+
- auto_plot(func): Automatically select best visualization
47+
"""
48+
49+
from ._compatibility import (
50+
available_plots,
51+
check_compatibility,
52+
list_all_plots,
53+
plot_info,
54+
)
55+
from ._errors import (
56+
MissingDataError,
57+
MissingDependencyError,
58+
PlotCompatibilityError,
59+
VisualizationError,
60+
)
61+
from ._plots import (
62+
auto_plot,
63+
plot_contour,
64+
plot_convergence,
65+
plot_fitness_distribution,
66+
plot_multi_slice,
67+
plot_surface,
68+
)
69+
70+
__all__ = [
71+
# Discovery functions
72+
"available_plots",
73+
"check_compatibility",
74+
"plot_info",
75+
"list_all_plots",
76+
# Plot functions
77+
"plot_surface",
78+
"plot_contour",
79+
"plot_multi_slice",
80+
"plot_convergence",
81+
"plot_fitness_distribution",
82+
"auto_plot",
83+
# Errors
84+
"VisualizationError",
85+
"PlotCompatibilityError",
86+
"MissingDataError",
87+
"MissingDependencyError",
88+
]
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# Author: Simon Blanke
2+
3+
# License: MIT License
4+
5+
"""Compatibility system for matching plots with test functions."""
6+
7+
from __future__ import annotations
8+
9+
from dataclasses import dataclass
10+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
11+
12+
if TYPE_CHECKING:
13+
from ..test_functions._base_test_function import BaseTestFunction
14+
15+
16+
@dataclass
17+
class PlotRequirements:
18+
"""Requirements for a specific plot type."""
19+
20+
name: str
21+
description: str
22+
dimensions: Union[int, Tuple[Optional[int], Optional[int]]]
23+
requires_history: bool = False
24+
function_types: Tuple[str, ...] = ("algebraic", "ml", "cec", "bbob", "engineering")
25+
26+
def check(
27+
self, func: "BaseTestFunction", has_history: bool = False
28+
) -> Tuple[bool, Optional[str]]:
29+
"""Check if a function is compatible with this plot.
30+
31+
Args:
32+
func: The test function to check.
33+
has_history: Whether optimization history data is available.
34+
35+
Returns:
36+
Tuple of (is_compatible, reason_if_not).
37+
"""
38+
# Check dimensions
39+
func_dims = _get_function_dimensions(func)
40+
41+
if isinstance(self.dimensions, int):
42+
if func_dims != self.dimensions:
43+
return (
44+
False,
45+
f"requires exactly {self.dimensions}D function, got {func_dims}D",
46+
)
47+
elif isinstance(self.dimensions, tuple):
48+
min_dim, max_dim = self.dimensions
49+
if min_dim is not None and func_dims < min_dim:
50+
return (
51+
False,
52+
f"requires at least {min_dim}D function, got {func_dims}D",
53+
)
54+
if max_dim is not None and func_dims > max_dim:
55+
return (
56+
False,
57+
f"requires at most {max_dim}D function, got {func_dims}D",
58+
)
59+
60+
# Check history requirement
61+
if self.requires_history and not has_history:
62+
return (False, "requires optimization history data")
63+
64+
return (True, None)
65+
66+
67+
# Registry of all available plots and their requirements
68+
PLOT_REGISTRY: Dict[str, PlotRequirements] = {
69+
"surface": PlotRequirements(
70+
name="surface",
71+
description="3D surface plot showing the objective landscape",
72+
dimensions=2,
73+
),
74+
"contour": PlotRequirements(
75+
name="contour",
76+
description="2D contour plot with isolines of equal objective value",
77+
dimensions=2,
78+
),
79+
"heatmap": PlotRequirements(
80+
name="heatmap",
81+
description="2D heatmap showing objective values as colors",
82+
dimensions=2,
83+
),
84+
"multi_slice": PlotRequirements(
85+
name="multi_slice",
86+
description="Multiple 1D slices through each dimension",
87+
dimensions=(1, None), # Works with 1D and up
88+
),
89+
"convergence": PlotRequirements(
90+
name="convergence",
91+
description="Best-so-far objective value vs evaluation number",
92+
dimensions=(1, None),
93+
requires_history=True,
94+
),
95+
"fitness_distribution": PlotRequirements(
96+
name="fitness_distribution",
97+
description="Histogram of objective values from random sampling",
98+
dimensions=(1, None),
99+
),
100+
}
101+
102+
103+
def _get_function_dimensions(func: "BaseTestFunction") -> int:
104+
"""Extract the number of dimensions from a test function."""
105+
# Try n_dim attribute first
106+
if hasattr(func, "n_dim"):
107+
return func.n_dim
108+
109+
# Try to infer from search space
110+
if hasattr(func, "search_space"):
111+
try:
112+
return len(func.search_space)
113+
except (TypeError, AttributeError):
114+
pass
115+
116+
# Try spec
117+
if hasattr(func, "spec"):
118+
spec = func.spec
119+
if "n_dim" in spec and spec["n_dim"] is not None:
120+
return spec["n_dim"]
121+
122+
raise ValueError(f"Cannot determine dimensions for function: {type(func).__name__}")
123+
124+
125+
def _get_function_type(func: "BaseTestFunction") -> str:
126+
"""Determine the type category of a test function."""
127+
class_name = type(func).__name__
128+
module_name = type(func).__module__
129+
130+
if "algebraic" in module_name:
131+
return "algebraic"
132+
elif "machine_learning" in module_name or "ml" in module_name:
133+
return "ml"
134+
elif "cec" in module_name:
135+
return "cec"
136+
elif "bbob" in module_name:
137+
return "bbob"
138+
elif "engineering" in module_name:
139+
return "engineering"
140+
else:
141+
return "unknown"
142+
143+
144+
def available_plots(
145+
func: "BaseTestFunction", has_history: bool = False
146+
) -> List[Dict[str, Any]]:
147+
"""Get list of plots compatible with the given function.
148+
149+
Args:
150+
func: The test function to check compatibility for.
151+
has_history: Whether optimization history data is available.
152+
153+
Returns:
154+
List of dicts with 'name' and 'description' for each compatible plot.
155+
156+
Examples:
157+
>>> from surfaces.test_functions import SphereFunction
158+
>>> from surfaces.visualize import available_plots
159+
>>> func = SphereFunction(n_dim=2)
160+
>>> plots = available_plots(func)
161+
>>> [p['name'] for p in plots]
162+
['surface', 'contour', 'heatmap', 'multi_slice', 'fitness_distribution']
163+
"""
164+
compatible = []
165+
166+
for name, requirements in PLOT_REGISTRY.items():
167+
is_compatible, _ = requirements.check(func, has_history)
168+
if is_compatible:
169+
compatible.append(
170+
{
171+
"name": name,
172+
"description": requirements.description,
173+
}
174+
)
175+
176+
return compatible
177+
178+
179+
def check_compatibility(
180+
func: "BaseTestFunction", plot_name: str, has_history: bool = False
181+
) -> Tuple[bool, Optional[str]]:
182+
"""Check if a specific plot is compatible with a function.
183+
184+
Args:
185+
func: The test function to check.
186+
plot_name: Name of the plot type.
187+
has_history: Whether optimization history data is available.
188+
189+
Returns:
190+
Tuple of (is_compatible, reason_if_not).
191+
192+
Examples:
193+
>>> from surfaces.test_functions import SphereFunction
194+
>>> from surfaces.visualize import check_compatibility
195+
>>> func = SphereFunction(n_dim=5)
196+
>>> compatible, reason = check_compatibility(func, 'surface')
197+
>>> compatible
198+
False
199+
>>> reason
200+
'requires exactly 2D function, got 5D'
201+
"""
202+
if plot_name not in PLOT_REGISTRY:
203+
return (False, f"unknown plot type: '{plot_name}'")
204+
205+
return PLOT_REGISTRY[plot_name].check(func, has_history)
206+
207+
208+
def plot_info(plot_name: str) -> Optional[Dict[str, Any]]:
209+
"""Get information about a specific plot type.
210+
211+
Args:
212+
plot_name: Name of the plot type.
213+
214+
Returns:
215+
Dict with plot information, or None if plot not found.
216+
217+
Examples:
218+
>>> from surfaces.visualize import plot_info
219+
>>> info = plot_info('surface')
220+
>>> info['description']
221+
'3D surface plot showing the objective landscape'
222+
"""
223+
if plot_name not in PLOT_REGISTRY:
224+
return None
225+
226+
req = PLOT_REGISTRY[plot_name]
227+
return {
228+
"name": req.name,
229+
"description": req.description,
230+
"dimensions": req.dimensions,
231+
"requires_history": req.requires_history,
232+
}
233+
234+
235+
def list_all_plots() -> List[Dict[str, Any]]:
236+
"""Get information about all available plot types.
237+
238+
Returns:
239+
List of dicts with information about each plot type.
240+
"""
241+
return [plot_info(name) for name in PLOT_REGISTRY.keys()]

0 commit comments

Comments
 (0)