import gradio as gr import matplotlib.pyplot as plt from matplotlib.colors import hsv_to_rgb import numpy as np import time import inspect # Import from your V2 script (make sure these functions are not hidden inside 'if __name__ == "__main__":') from gp_art_v2 import evolve_two_trees, render_samila, make_point_grid, safe_eval, norm01, evolve_two_trees_stream def decode_raw_stack(model): """Translates the StackGP array into readable algebra.""" if not model or len(model) < 2: return "Empty" ops = model[0] vars_stack = model[1] stack1 = vars_stack[:] stack3 = [] probe = ["x", "y", "r", "theta", "theta*r", "sin(theta)*r"] def resolve_var(v): if callable(v): try: return str(v(probe)) except: try: return str(round(float(v()), 3)) except: return "c" else: return str(round(float(v), 3)) for op in ops: if op == "pop": if len(stack1) > 0: stack3.insert(0, resolve_var(stack1[0])) stack1 = stack1[1:] else: stack3.insert(0, "c") elif hasattr(op, '__name__'): try: patt = len(inspect.signature(op).parameters) except ValueError: patt = 1 while patt > len(stack3): if len(stack1) > 0: stack3.insert(0, resolve_var(stack1[0])) stack1 = stack1[1:] else: stack3.insert(0, "c") args = stack3[:patt] stack3 = stack3[patt:] args.reverse() op_name = op.__name__ if op_name == "add": op_str = f"({args[0]} + {args[1]})" elif op_name == "sub": op_str = f"({args[0]} - {args[1]})" elif op_name == "mult": op_str = f"({args[0]} * {args[1]})" elif op_name in ["div_safe", "protectDiv"]: op_str = f"({args[0]} / {args[1]})" elif op_name == "sqrd": op_str = f"({args[0]}^2)" elif patt == 1: op_str = f"{op_name}({args[0]})" elif patt == 2: op_str = f"{op_name}({args[0]}, {args[1]})" else: op_str = f"{op_name}(...)" stack3.insert(0, op_str) return stack3[0] if len(stack3) > 0 else "Empty" # --- NEW: The Classic Pixel Grid Renderer (from gp_art.py) --- def render_pixel_grid(model_f1, model_f2, mode, resolution, figsize=(8, 8)): """Evaluates the equations directly into a static pixel grid.""" grid = make_point_grid(resolution) f1 = safe_eval(model_f1, grid['input']) f1n = norm01(f1).reshape(resolution, resolution) fig, ax = plt.subplots(figsize=figsize, facecolor='#050505') ax.axis('off') if mode == "PIXEL_MONO": # Matches your gp_art.py monochrome olive/green style hue = 0.17 sat = 0.45 hsv = np.stack([ np.full_like(f1n, hue), np.full_like(f1n, sat), f1n # F1 equation drives the brightness/value ], axis=-1) rgb = hsv_to_rgb(hsv) ax.imshow(rgb, interpolation='bilinear') elif mode == "PIXEL_COLOR": # Full HSV evolution: F1 drives Hue, F2 drives Saturation f2 = safe_eval(model_f2, grid['input']) f2n = norm01(f2).reshape(resolution, resolution) hsv = np.stack([ f1n, # Hue np.clip(f2n + 0.2, 0, 1), # Saturation np.clip(f1n + 0.5, 0, 1) # Value (Brightness) ], axis=-1) rgb = hsv_to_rgb(hsv) ax.imshow(rgb, interpolation='bilinear') plt.tight_layout(pad=0) return fig, ax def live_evolution_stream(pop_size, total_gens, mode, cmap): yield None, "Initializing Population...", "Starting evolutionary engine..." current_gen = 0 # Loop over your TRUE intermediate equations as they evolve! for model_f1, model_f2 in evolve_two_trees_stream(resolution=80, generations=total_gens, pop_size=pop_size): current_gen += 1 # Decode the actual equations for THIS specific generation eq1 = decode_raw_stack(model_f1) eq2 = decode_raw_stack(model_f2) # Only render to the UI every 5 generations to keep the browser from crashing if current_gen % 5 == 0 or current_gen == total_gens: if mode.startswith("PIXEL"): fig, ax = render_pixel_grid(model_f1, model_f2, mode, resolution=150) else: fig, ax = render_samila( model_f1, model_f2, mode=mode, resolution=150, cmap=cmap, point_size=1.5, alpha=0.85, color_by='f1', bgcolor='#050505', figsize=(8, 8) ) stats = f"**Generation:** {current_gen} / {total_gens} | **Status:** Actively Evolving..." eq_text = f"**F1 (Equation 1):**\n`{eq1}`\n\n**F2 (Equation 2):**\n`{eq2}`" # Stream the actual intermediate math and image to the screen yield fig, stats, eq_text # Final completion state final_stats = f"**Generation:** {total_gens} (Complete) | **F1 Complexity:** {model_f1[2][1]} | **F2 Complexity:** {model_f2[2][1]}" yield fig, final_stats, eq_text # --- The Generator Function --- # def live_evolution_stream(pop_size, total_gens, mode, cmap): # yield None, "Initializing Population...", "Starting evolutionary engine..." # # Trigger StackGP backend # model_f1, model_f2 = evolve_two_trees( # resolution=64, # generations=total_gens, # pop_size=pop_size # ) # eq1 = decode_raw_stack(model_f1) # eq2 = decode_raw_stack(model_f2) # # Stream the rendering to visualize emergence # steps = 5 # for step in range(1, steps + 1): # current_gen = int((total_gens / steps) * step) # # Branch based on Vector Displacement vs Pixel Grid # if mode.startswith("PIXEL"): # current_res = int(128 + (64 * step)) # Pixel grids need higher res # fig, ax = render_pixel_grid(model_f1, model_f2, mode, current_res) # else: # current_res = int(80 + (40 * step)) # fig, ax = render_samila( # model_f1, model_f2, # mode=mode, # resolution=current_res, # cmap=cmap, # point_size=1.5, # alpha=0.4 + (0.1 * step), # color_by='f1', # bgcolor='#050505', # figsize=(8, 8) # ) # stats = f"**Generation:** {current_gen} / {total_gens} | **Status:** Evolving Geometry..." # eq_text = f"**F1 (Equation 1):**\n`{eq1}`\n\n**F2 (Equation 2):**\n`{eq2}`" # yield fig, stats, eq_text # time.sleep(0.3) # final_stats = f"**Generation:** {total_gens} (Complete) | **F1 Complexity:** {model_f1[2][1]} | **F2 Complexity:** {model_f2[2][1]}" # yield fig, final_stats, eq_text # ============================================================================= # GRADIO UI ARCHITECTURE # ============================================================================= with gr.Blocks(theme=gr.themes.Monochrome(text_size="lg")) as demo: gr.Markdown("# StackGPArt: Live Evolution Engine") gr.Markdown("*Evolving generative structures using pure Stack-based Genetic Programming.*") with gr.Row(): with gr.Column(scale=1): gr.Markdown("### 1. Evolution Parameters") pop_slider = gr.Slider(minimum=20, maximum=300, value=100, step=10, label="Population Size") gen_slider = gr.Slider(minimum=10, maximum=300, value=80, step=10, label="Target Generations") # The Unified Dropdown! mode_dropdown = gr.Dropdown( choices=[ "F1_VS_X1", "F1_VS_F2", "POLAR", "X2_VS_F2", # Vector Modes (gp_art_v2) "PIXEL_MONO", "PIXEL_COLOR" # Pixel Modes (gp_art) ], value="F1_VS_X1", label="Rendering Paradigm" ) cmap_dropdown = gr.Dropdown( choices=["turbo", "plasma", "magma", "viridis"], value="turbo", label="Colormap (Ignored for Pixel Modes)" ) evolve_btn = gr.Button("🧬 Start Evolution", variant="primary") gr.Markdown("### 2. Live Telemetry") stats_display = gr.Markdown("**Generation:** 0 | **Status:** Idle") eq_display = gr.Markdown("**F1:** —\n\n**F2:** —") with gr.Column(scale=2): art_output = gr.Plot(label="Emergent Geometry") evolve_btn.click( fn=live_evolution_stream, inputs=[pop_slider, gen_slider, mode_dropdown, cmap_dropdown], outputs=[art_output, stats_display, eq_display] ) if __name__ == "__main__": demo.queue().launch()