Skip to content

Commit 8b62756

Browse files
committed
add latex based plot
1 parent cec6a5f commit 8b62756

File tree

3 files changed

+255
-0
lines changed

3 files changed

+255
-0
lines changed

src/surfaces/visualize/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
- multi_slice: 1D slices through each dimension (any N-D)
3737
- convergence: Best-so-far vs evaluations (requires history)
3838
- fitness_distribution: Histogram of sampled values (any N-D)
39+
- latex: Publication-quality LaTeX/PDF with formula (2D algebraic only)
3940
4041
Discovery Functions
4142
-------------------
@@ -63,6 +64,7 @@
6364
plot_contour,
6465
plot_convergence,
6566
plot_fitness_distribution,
67+
plot_latex,
6668
plot_multi_slice,
6769
plot_surface,
6870
)
@@ -79,6 +81,7 @@
7981
"plot_multi_slice",
8082
"plot_convergence",
8183
"plot_fitness_distribution",
84+
"plot_latex",
8285
"auto_plot",
8386
# Errors
8487
"VisualizationError",

src/surfaces/visualize/_compatibility.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ def check(
9797
description="Histogram of objective values from random sampling",
9898
dimensions=(1, None),
9999
),
100+
"latex": PlotRequirements(
101+
name="latex",
102+
description="Publication-quality LaTeX/PDF with pgfplots 3D surface and formula",
103+
dimensions=2,
104+
function_types=("algebraic",), # Only algebraic functions have latex_formula
105+
),
100106
}
101107

102108

src/surfaces/visualize/_plots.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,252 @@ def plot_fitness_distribution(
598598
return fig
599599

600600

601+
# =============================================================================
602+
# Plot 6: LaTeX/PDF Plot (2D algebraic functions with formulas)
603+
# =============================================================================
604+
605+
606+
_LATEX_TEMPLATE = r"""\documentclass[border=10pt]{{standalone}}
607+
\usepackage{{pgfplots}}
608+
\usepackage{{amsmath}}
609+
\usepackage{{amssymb}}
610+
611+
\pgfplotsset{{compat=1.18}}
612+
613+
% Define the test function
614+
\pgfmathdeclarefunction{{{func_id}}}{{2}}{{%
615+
\pgfmathparse{{{pgfmath_formula}}}%
616+
}}
617+
618+
\begin{{document}}
619+
620+
\begin{{tikzpicture}}
621+
\begin{{axis}}[
622+
width=14cm,
623+
height=12cm,
624+
view={{{view_azimuth}}}{{{view_elevation}}},
625+
xlabel={{$x$}},
626+
ylabel={{$y$}},
627+
zlabel={{$f(x,y)$}},
628+
xlabel style={{font=\large, sloped}},
629+
ylabel style={{font=\large, sloped}},
630+
zlabel style={{font=\large, rotate=-90}},
631+
title={{\textbf{{\Large {title}}}}},
632+
title style={{yshift=5pt}},
633+
colormap={{jet}}{{
634+
rgb255=(128,0,0)
635+
rgb255=(255,0,0)
636+
rgb255=(255,128,0)
637+
rgb255=(255,255,0)
638+
rgb255=(128,255,128)
639+
rgb255=(0,255,255)
640+
rgb255=(0,128,255)
641+
rgb255=(0,0,255)
642+
rgb255=(0,0,128)
643+
}},
644+
colorbar,
645+
colorbar style={{
646+
ylabel={{$f(x,y)$}},
647+
ylabel style={{font=\normalsize, rotate=-90}},
648+
}},
649+
domain={domain_min}:{domain_max},
650+
y domain={domain_min}:{domain_max},
651+
samples={samples},
652+
samples y={samples},
653+
z buffer=sort,
654+
mesh/ordering=y varies,
655+
shader=interp,
656+
{zmin_line}
657+
{zmax_line}
658+
]
659+
\addplot3[surf, opacity=0.95] {{{func_id}(x,y)}};
660+
\end{{axis}}
661+
662+
% Formula box below the plot
663+
\node[anchor=north, yshift=-0.5cm, align=center] at (current bounding box.south) {{
664+
\fbox{{
665+
\parbox{{12cm}}{{
666+
\centering
667+
\vspace{{0.3cm}}
668+
$\displaystyle {latex_formula}$
669+
\vspace{{0.2cm}}
670+
671+
\small {global_minimum_text}
672+
\vspace{{0.2cm}}
673+
}}
674+
}}
675+
}};
676+
\end{{tikzpicture}}
677+
678+
\end{{document}}
679+
"""
680+
681+
682+
def plot_latex(
683+
func: "BaseTestFunction",
684+
output_path: Optional[str] = None,
685+
compile_pdf: bool = False,
686+
samples: int = 100,
687+
view_azimuth: int = -35,
688+
view_elevation: int = 25,
689+
zmin: Optional[float] = None,
690+
zmax: Optional[float] = None,
691+
title: Optional[str] = None,
692+
) -> str:
693+
"""Generate a publication-quality LaTeX file with pgfplots 3D surface.
694+
695+
Creates a standalone LaTeX document with a 3D surface plot and the
696+
mathematical formula in a box below. Requires the function to have
697+
`latex_formula` and `pgfmath_formula` attributes.
698+
699+
Args:
700+
func: A 2-dimensional algebraic test function with formula attributes.
701+
output_path: Path for the output .tex file. Defaults to
702+
'{function_name}.tex' in the current directory.
703+
compile_pdf: If True, compile the .tex to PDF using pdflatex.
704+
Requires pdflatex to be installed.
705+
samples: Number of samples per axis for the surface (default: 100).
706+
Higher values give smoother surfaces but slower compilation.
707+
view_azimuth: Horizontal viewing angle in degrees (default: -35).
708+
view_elevation: Vertical viewing angle in degrees (default: 25).
709+
zmin: Optional minimum z-axis value. Auto-scaled if None.
710+
zmax: Optional maximum z-axis value. Auto-scaled if None.
711+
title: Plot title. Defaults to function name.
712+
713+
Returns:
714+
Path to the generated .tex file (or .pdf if compile_pdf=True).
715+
716+
Raises:
717+
PlotCompatibilityError: If function is not 2D or lacks formula attributes.
718+
MissingDependencyError: If compile_pdf=True but pdflatex not found.
719+
RuntimeError: If PDF compilation fails.
720+
721+
Examples:
722+
>>> from surfaces.test_functions import AckleyFunction
723+
>>> from surfaces.visualize import plot_latex
724+
>>> func = AckleyFunction()
725+
>>> tex_path = plot_latex(func)
726+
>>> print(f"Generated: {tex_path}")
727+
Generated: ackley_function.tex
728+
729+
>>> # Compile to PDF
730+
>>> pdf_path = plot_latex(func, compile_pdf=True)
731+
>>> print(f"Generated: {pdf_path}")
732+
Generated: ackley_function.pdf
733+
"""
734+
import os
735+
736+
_validate_plot(func, "latex")
737+
738+
# Check for required attributes
739+
latex_formula = getattr(func, "latex_formula", None)
740+
pgfmath_formula = getattr(func, "pgfmath_formula", None)
741+
742+
if latex_formula is None:
743+
raise PlotCompatibilityError(
744+
plot_name="latex",
745+
reason="function has no 'latex_formula' attribute",
746+
func=func,
747+
suggestions=["Use algebraic test functions which have formula attributes"],
748+
)
749+
750+
if pgfmath_formula is None:
751+
raise PlotCompatibilityError(
752+
plot_name="latex",
753+
reason="function has no 'pgfmath_formula' attribute (some complex functions cannot be rendered in pgfplots)",
754+
func=func,
755+
suggestions=["Use plot_surface() for interactive visualization instead"],
756+
)
757+
758+
# Get function metadata
759+
func_name = getattr(func, "name", type(func).__name__)
760+
func_id = getattr(func, "_name_", type(func).__name__.lower())
761+
default_bounds = getattr(func, "default_bounds", (-5.0, 5.0))
762+
f_global = getattr(func, "f_global", None)
763+
x_global = getattr(func, "x_global", None)
764+
765+
# Build global minimum text
766+
if f_global is not None and x_global is not None:
767+
# Handle multiple global minima
768+
if hasattr(x_global, "ndim") and x_global.ndim == 2:
769+
x_str = r"\pm " + ", ".join(f"{abs(x_global[0, i]):.4g}" for i in range(x_global.shape[1]))
770+
global_minimum_text = f"Global minimum: $f({x_str}) = {f_global}$"
771+
else:
772+
x_str = ", ".join(f"{x:.4g}" for x in x_global)
773+
global_minimum_text = f"Global minimum: $f({x_str}) = {f_global}$"
774+
else:
775+
global_minimum_text = ""
776+
777+
# Build z-axis limits
778+
zmin_line = f"zmin={zmin}," if zmin is not None else ""
779+
zmax_line = f"zmax={zmax}," if zmax is not None else ""
780+
781+
# Generate LaTeX content
782+
latex_content = _LATEX_TEMPLATE.format(
783+
func_id=func_id,
784+
pgfmath_formula=pgfmath_formula,
785+
title=title or func_name,
786+
domain_min=default_bounds[0],
787+
domain_max=default_bounds[1],
788+
samples=samples,
789+
view_azimuth=view_azimuth,
790+
view_elevation=view_elevation,
791+
zmin_line=zmin_line,
792+
zmax_line=zmax_line,
793+
latex_formula=latex_formula,
794+
global_minimum_text=global_minimum_text,
795+
)
796+
797+
# Determine output path
798+
if output_path is None:
799+
output_path = f"{func_id}.tex"
800+
801+
# Ensure .tex extension
802+
if not output_path.endswith(".tex"):
803+
output_path = output_path + ".tex"
804+
805+
# Write the file
806+
with open(output_path, "w") as f:
807+
f.write(latex_content)
808+
809+
# Optionally compile to PDF
810+
if compile_pdf:
811+
import shutil
812+
import subprocess
813+
814+
if shutil.which("pdflatex") is None:
815+
raise MissingDependencyError(
816+
["pdflatex"],
817+
"PDF compilation requires pdflatex. Install TeX Live or MiKTeX.",
818+
)
819+
820+
# Get directory and filename
821+
tex_dir = os.path.dirname(output_path) or "."
822+
tex_file = os.path.basename(output_path)
823+
824+
try:
825+
result = subprocess.run(
826+
["pdflatex", "-interaction=nonstopmode", tex_file],
827+
cwd=tex_dir,
828+
capture_output=True,
829+
text=True,
830+
timeout=120,
831+
)
832+
if result.returncode != 0:
833+
raise RuntimeError(
834+
f"pdflatex compilation failed:\n{result.stdout}\n{result.stderr}"
835+
)
836+
837+
# Return PDF path
838+
pdf_path = output_path.replace(".tex", ".pdf")
839+
return pdf_path
840+
841+
except subprocess.TimeoutExpired:
842+
raise RuntimeError("pdflatex compilation timed out after 120 seconds")
843+
844+
return output_path
845+
846+
601847
# =============================================================================
602848
# Auto Plot: Automatically select best visualization
603849
# =============================================================================

0 commit comments

Comments
 (0)