@@ -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