diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,3098 +1,649 @@ import streamlit as st -import tempfile -import os -import logging -from pathlib import Path -from PIL import Image -import io +import sympy as sp import numpy as np -import sys -import subprocess -import json -from pygments import highlight -from pygments.lexers import PythonLexer -from pygments.formatters import HtmlFormatter -import base64 -from transformers import pipeline -import re -import shutil -import time -from datetime import datetime, timedelta -import streamlit.components.v1 as components -import uuid -import platform -import pandas as pd -import plotly.express as px -import markdown -import zipfile -import contextlib -import threading -import traceback -from io import StringIO, BytesIO - -# Set up enhanced logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.StreamHandler() - ] +import plotly.graph_objects as go +from scipy.optimize import fsolve +from scipy.stats import gaussian_kde + +# Configure Streamlit for Hugging Face Spaces +st.set_page_config( + page_title="Cubic Root Analysis", + layout="wide", + initial_sidebar_state="collapsed" ) -logger = logging.getLogger(__name__) - -# Model configuration mapping for different API requirements and limits -MODEL_CONFIGS = { - "DeepSeek-V3-0324": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "DeepSeek", "warning": None}, - "DeepSeek-R1": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "DeepSeek", "warning": None}, - "gpt-4o": {"max_tokens": 16000, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None}, - "gpt-4.1": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None}, - "gpt-4.1-mini": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None}, - "gpt-4.1-nano": {"max_tokens": 32768, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None}, - "o3": {"max_tokens": 100000, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None}, - "o4-mini": {"max_tokens": 100000, "param_name": "max_tokens", "api_version": None, "category": "OpenAI", "warning": None}, - # Default configuration for other models - "default": {"max_tokens": 4000, "param_name": "max_tokens", "api_version": None, "category": "Other", "warning": None} -} -# Try to import Streamlit Ace -try: - from streamlit_ace import st_ace - ACE_EDITOR_AVAILABLE = True -except ImportError: - ACE_EDITOR_AVAILABLE = False - logger.warning("streamlit-ace not available, falling back to standard text editor") +def add_sqrt_support(expr_str): + """Replace 'sqrt(' with 'sp.sqrt(' for sympy compatibility""" + return expr_str.replace('sqrt(', 'sp.sqrt(') -def prepare_api_params(messages, model_name): - """Create appropriate API parameters based on model configuration""" - # Get model configuration - config = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["default"]) - - # Base parameters common to all models - api_params = { - "messages": messages, - "model": model_name - } - - # Add the appropriate token parameter based on model's parameter name - token_param = config["param_name"] - token_value = config[token_param] # Get the actual value from the config - - # Add the parameter to the API params - api_params[token_param] = token_value - - return api_params, config +############################# +# 1) Define the discriminant +############################# -# New functions for accessing secrets and password verification -def get_secret(github_token_api): - """Retrieve a secret from HuggingFace Spaces environment variables""" - secret_value = os.environ.get(github_token_api) - if not secret_value: - logger.warning(f"Secret '{github_token_api}' not found") - return None - return secret_value +# Symbolic variables for the cubic discriminant +z_sym, beta_sym, z_a_sym, y_sym = sp.symbols("z beta z_a y", real=True, positive=True) -def check_password(): - """Returns True if the user entered the correct password""" - # Get the password from secrets - correct_password = get_secret("password") - if not correct_password: - st.error("Admin password not configured in HuggingFace Spaces secrets") - return False - - # Password input - if "password_entered" not in st.session_state: - st.session_state.password_entered = False - - if not st.session_state.password_entered: - password = st.text_input("Enter password to access AI features", type="password") - if password: - if password == correct_password: - st.session_state.password_entered = True - return True - else: - st.error("Incorrect password") - return False - return False - return True - -def ensure_packages(): - required_packages = { - 'manim': '0.17.3', - 'Pillow': '9.0.0', - 'numpy': '1.22.0', - 'transformers': '4.30.0', - 'torch': '2.0.0', - 'pygments': '2.15.1', - 'streamlit-ace': '0.1.1', - 'pydub': '0.25.1', # For audio processing - 'plotly': '5.14.0', # For timeline editor - 'pandas': '2.0.0', # For data manipulation - 'python-pptx': '0.6.21', # For PowerPoint export - 'markdown': '3.4.3', # For markdown processing - 'fpdf': '1.7.2', # For PDF generation - 'matplotlib': '3.5.0', # For Python script runner - 'seaborn': '0.11.2', # For enhanced visualizations - 'scipy': '1.7.3', # For scientific computations - 'huggingface_hub': '0.16.0', # For Hugging Face API - } - - with st.spinner("Checking required packages..."): - # First, quickly check if packages are already installed - missing_packages = {} - for package, version in required_packages.items(): - try: - # Try to import the package to check if it's available - if package == 'manim': - import manim - elif package == 'Pillow': - import PIL - elif package == 'numpy': - import numpy - elif package == 'transformers': - import transformers - elif package == 'torch': - import torch - elif package == 'pygments': - import pygments - elif package == 'streamlit-ace': - # This one is trickier, we already handle it with ACE_EDITOR_AVAILABLE flag - pass - elif package == 'pydub': - import pydub - elif package == 'plotly': - import plotly - elif package == 'pandas': - import pandas - elif package == 'python-pptx': - import pptx - elif package == 'markdown': - import markdown - elif package == 'fpdf': - import fpdf - elif package == 'matplotlib': - import matplotlib - elif package == 'seaborn': - import seaborn - elif package == 'scipy': - import scipy - elif package == 'huggingface_hub': - import huggingface_hub - except ImportError: - missing_packages[package] = version - - # If no packages are missing, return success immediately - if not missing_packages: - logger.info("All required packages already installed.") - return True - - # If there are missing packages, install them with progress reporting - progress_bar = st.progress(0) - status_text = st.empty() - - for i, (package, version) in enumerate(missing_packages.items()): - try: - progress = (i / len(missing_packages)) - progress_bar.progress(progress) - status_text.text(f"Installing {package}...") - - result = subprocess.run( - [sys.executable, "-m", "pip", "install", f"{package}>={version}"], - capture_output=True, - text=True - ) - - if result.returncode != 0: - st.error(f"Failed to install {package}: {result.stderr}") - logger.error(f"Package installation failed: {package}") - return False - - except Exception as e: - st.error(f"Error installing {package}: {str(e)}") - logger.error(f"Package installation error: {str(e)}") - return False - - progress_bar.progress(1.0) - status_text.text("All packages installed successfully!") - time.sleep(0.5) - progress_bar.empty() - status_text.empty() - return True +# Define coefficients a, b, c, d in terms of z_sym, beta_sym, z_a_sym, y_sym +a_sym = z_sym * z_a_sym +b_sym = z_sym * z_a_sym + z_sym + z_a_sym - z_a_sym*y_sym +c_sym = z_sym + z_a_sym + 1 - y_sym*(beta_sym*z_a_sym + 1 - beta_sym) +d_sym = 1 -def install_custom_packages(package_list): - """Install custom packages specified by the user without page refresh""" - if not package_list.strip(): - return True, "No packages specified" - - # Split and clean package list - packages = [pkg.strip() for pkg in package_list.split(',') if pkg.strip()] - - if not packages: - return True, "No valid packages specified" - - status_placeholder = st.sidebar.empty() - progress_bar = st.sidebar.progress(0) - - results = [] - success = True - - for i, package in enumerate(packages): - try: - progress = (i / len(packages)) - progress_bar.progress(progress) - status_placeholder.text(f"Installing {package}...") - - result = subprocess.run( - [sys.executable, "-m", "pip", "install", package], - capture_output=True, - text=True - ) - - if result.returncode != 0: - error_msg = f"Failed to install {package}: {result.stderr}" - results.append(error_msg) - logger.error(error_msg) - success = False - else: - results.append(f"Successfully installed {package}") - logger.info(f"Successfully installed custom package: {package}") - - except Exception as e: - error_msg = f"Error installing {package}: {str(e)}" - results.append(error_msg) - logger.error(error_msg) - success = False - - progress_bar.progress(1.0) - status_placeholder.text("Installation complete!") - time.sleep(0.5) - progress_bar.empty() - status_placeholder.empty() - - return success, "\n".join(results) - -@st.cache_resource(ttl=3600) -def init_ai_models_direct(): - """Direct implementation using the exact pattern from the example code""" - try: - # Get token from secrets - token = get_secret("github_token_api") - if not token: - st.error("GitHub token not found in secrets. Please add 'github_token_api' to your HuggingFace Spaces secrets.") - return None - - # Log what we're doing - for debugging - logger.info(f"Initializing AI model with token: {token[:5]}...") - - # Use exact imports as in your example - import os - from azure.ai.inference import ChatCompletionsClient - from azure.ai.inference.models import SystemMessage, UserMessage - from azure.core.credentials import AzureKeyCredential - - # Use exact endpoint as in your example - endpoint = "https://models.inference.ai.azure.com" - - # Use default model - model_name = "gpt-4o" - - # Create client exactly as in your example - client = ChatCompletionsClient( - endpoint=endpoint, - credential=AzureKeyCredential(token), - ) - - # Return the necessary information - return { - "client": client, - "model_name": model_name, - "endpoint": endpoint - } - except ImportError as ie: - st.error(f"Import error: {str(ie)}. Please make sure azure-ai-inference is installed.") - logger.error(f"Import error: {str(ie)}") - return None - except Exception as e: - st.error(f"Error initializing AI model: {str(e)}") - logger.error(f"Initialization error: {str(e)}") - return None - -def suggest_code_completion(code_snippet, models): - """Generate code completion using the AI model""" - if not models: - st.error("AI models not properly initialized.") - return None - - try: - # Create the prompt - prompt = f"""Write a complete Manim animation scene based on this code or idea: -{code_snippet} - -The code should be a complete, working Manim animation that includes: -- Proper Scene class definition -- Constructor with animations -- Proper use of self.play() for animations -- Proper wait times between animations +# Symbolic expression for the cubic discriminant +Delta_expr = ( + ((b_sym*c_sym)/(6*a_sym**2) - (b_sym**3)/(27*a_sym**3) - d_sym/(2*a_sym))**2 + + (c_sym/(3*a_sym) - (b_sym**2)/(9*a_sym**2))**3 +) -Here's the complete Manim code: -""" - - with st.spinner("AI is generating your animation code..."): - # Get the current model name and base URL - model_name = models["model_name"] - - # Convert message to the appropriate format based on model category - config = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["default"]) - category = config.get("category", "Other") - - if category == "OpenAI": - # Import OpenAI client - from openai import OpenAI - - # Get token - token = get_secret("github_token_api") - - # Create or get client - if "openai_client" not in models: - client = OpenAI( - base_url="https://models.github.ai/inference", - api_key=token - ) - models["openai_client"] = client +# Fast numeric function for the discriminant +discriminant_func = sp.lambdify((z_sym, beta_sym, z_a_sym, y_sym), Delta_expr, "numpy") + +@st.cache_data +def find_z_at_discriminant_zero(z_a, y, beta, z_min, z_max, steps): + """ + Scan z in [z_min, z_max] for sign changes in the discriminant, + and return approximated roots (where the discriminant is zero). + """ + z_grid = np.linspace(z_min, z_max, steps) + disc_vals = discriminant_func(z_grid, beta, z_a, y) + roots_found = [] + for i in range(len(z_grid) - 1): + f1, f2 = disc_vals[i], disc_vals[i+1] + if np.isnan(f1) or np.isnan(f2): + continue + if f1 == 0.0: + roots_found.append(z_grid[i]) + elif f2 == 0.0: + roots_found.append(z_grid[i+1]) + elif f1 * f2 < 0: + zl, zr = z_grid[i], z_grid[i+1] + for _ in range(50): + mid = 0.5 * (zl + zr) + fm = discriminant_func(mid, beta, z_a, y) + if fm == 0: + zl = zr = mid + break + if np.sign(fm) == np.sign(f1): + zl, f1 = mid, fm else: - client = models["openai_client"] - - # For OpenAI models, we need role-based messages - messages = [ - {"role": "system", "content": "You are an expert in Manim animations."}, - {"role": "user", "content": prompt} - ] - - # Create params - params = { - "messages": messages, - "model": model_name - } - - # Add token parameter - token_param = config["param_name"] - params[token_param] = config[token_param] - - # Make API call - response = client.chat.completions.create(**params) - completed_code = response.choices[0].message.content - + zr, f2 = mid, fm + roots_found.append(0.5 * (zl + zr)) + return np.array(roots_found) + +@st.cache_data +def sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps): + """ + For each beta in [0,1] (with beta_steps points), find the minimum and maximum z + for which the discriminant is zero. + Returns: betas, lower z*(β) values, and upper z*(β) values. + """ + betas = np.linspace(0, 1, beta_steps) + z_min_values = [] + z_max_values = [] + for b in betas: + roots = find_z_at_discriminant_zero(z_a, y, b, z_min, z_max, z_steps) + if len(roots) == 0: + z_min_values.append(np.nan) + z_max_values.append(np.nan) + else: + z_min_values.append(np.min(roots)) + z_max_values.append(np.max(roots)) + return betas, np.array(z_min_values), np.array(z_max_values) + +@st.cache_data +def compute_high_y_curve(betas, z_a, y): + """ + Compute the "High y Expression" curve. + """ + a = z_a + betas = np.array(betas) + denominator = 1 - 2*a + if denominator == 0: + return np.full_like(betas, np.nan) + numerator = -4*a*(a-1)*y*betas - 2*a*y - 2*a*(2*a-1) + return numerator/denominator + +def compute_alternate_low_expr(betas, z_a, y): + """ + Compute the alternate low expression: + (z_a*y*beta*(z_a-1) - 2*z_a*(1-y) - 2*z_a**2) / (2+2*z_a) + """ + betas = np.array(betas) + return (z_a * y * betas * (z_a - 1) - 2*z_a*(1 - y) - 2*z_a**2) / (2 + 2*z_a) + +@st.cache_data +def compute_max_k_expression(betas, z_a, y, k_samples=1000): + """ + Compute max_{k ∈ (0,∞)} (y*beta*(a-1)*k + (a*k+1)*((y-1)*k-1)) / ((a*k+1)*(k^2+k)) + """ + a = z_a + # Sample k values on a logarithmic scale + k_values = np.logspace(-3, 3, k_samples) + + max_vals = np.zeros_like(betas) + for i, beta in enumerate(betas): + values = np.zeros_like(k_values) + for j, k in enumerate(k_values): + numerator = y*beta*(a-1)*k + (a*k+1)*((y-1)*k-1) + denominator = (a*k+1)*(k**2+k) + if abs(denominator) < 1e-10: + values[j] = np.nan else: - # Use Azure client - from azure.ai.inference.models import UserMessage - - # Convert message format for Azure - messages = [UserMessage(prompt)] - api_params, _ = prepare_api_params(messages, model_name) - - # Make API call with Azure client - response = models["client"].complete(**api_params) - completed_code = response.choices[0].message.content - - # Process the code - if "```python" in completed_code: - completed_code = completed_code.split("```python")[1].split("```")[0] - elif "```" in completed_code: - completed_code = completed_code.split("```")[1].split("```")[0] - - # Add Scene class if missing - if "Scene" not in completed_code: - completed_code = f"""from manim import * - -class MyScene(Scene): - def construct(self): - {completed_code}""" - - return completed_code - - except Exception as e: - st.error(f"Error generating code: {str(e)}") - st.code(traceback.format_exc()) - return None - -def check_model_freshness(): - """Check if models need to be reloaded based on TTL""" - if 'ai_models' not in st.session_state or st.session_state.ai_models is None: - return False - - if 'last_loaded' not in st.session_state.ai_models: - return False - - last_loaded = datetime.fromisoformat(st.session_state.ai_models['last_loaded']) - ttl_hours = 1 # 1 hour TTL - - return datetime.now() - last_loaded < timedelta(hours=ttl_hours) - -def extract_scene_class_name(python_code): - """Extract the scene class name from Python code.""" - import re - scene_classes = re.findall(r'class\s+(\w+)\s*\([^)]*Scene[^)]*\)', python_code) - - if scene_classes: - # Return the first scene class found - return scene_classes[0] - else: - # If no scene class is found, use a default name - return "MyScene" - -def suggest_code_completion(code_snippet, models): - if not models or "code_model" not in models: - st.error("AI models not properly initialized") - return None - - try: - prompt = f"""Write a complete Manim animation scene based on this code or idea: -{code_snippet} - -The code should be a complete, working Manim animation that includes: -- Proper Scene class definition -- Constructor with animations -- Proper use of self.play() for animations -- Proper wait times between animations - -Here's the complete Manim code: -```python -""" - with st.spinner("AI is generating your animation code..."): - response = models["code_model"]( - prompt, - max_length=1024, - do_sample=True, - temperature=0.2, - top_p=0.95, - top_k=50, - num_return_sequences=1, - truncation=True, - pad_token_id=50256 - ) - - if not response or not response[0].get('generated_text'): - st.error("No valid completion generated") - return None - - completed_code = response[0]['generated_text'] - if "```python" in completed_code: - completed_code = completed_code.split("```python")[1].split("```")[0] + values[j] = numerator/denominator - if "Scene" not in completed_code: - completed_code = f"""from manim import * - -class MyScene(Scene): - def construct(self): - {completed_code}""" - - return completed_code - except Exception as e: - st.error(f"Error suggesting code: {str(e)}") - logger.error(f"Code suggestion error: {str(e)}") - return None - -# Quality presets -QUALITY_PRESETS = { - "480p": {"resolution": "480p", "fps": "30"}, - "720p": {"resolution": "720p", "fps": "30"}, - "1080p": {"resolution": "1080p", "fps": "60"}, - "4K": {"resolution": "2160p", "fps": "60"}, - "8K": {"resolution": "4320p", "fps": "60"} # Added 8K option -} - -# Animation speeds -ANIMATION_SPEEDS = { - "Slow": 0.5, - "Normal": 1.0, - "Fast": 2.0, - "Very Fast": 3.0 -} - -# Export formats -EXPORT_FORMATS = { - "MP4 Video": "mp4", - "GIF Animation": "gif", - "WebM Video": "webm", - "PNG Image Sequence": "png_sequence", - "SVG Image": "svg" -} - -# FPS options -FPS_OPTIONS = [15, 24, 30, 60, 120] - -def highlight_code(code): - formatter = HtmlFormatter(style='monokai') - highlighted = highlight(code, PythonLexer(), formatter) - return highlighted, formatter.get_style_defs() - -def generate_manim_preview(python_code): - """Generate a lightweight preview of the Manim animation""" - try: - # Extract scene components for preview - scene_objects = [] - if "Circle" in python_code: - scene_objects.append("circle") - if "Square" in python_code: - scene_objects.append("square") - if "MathTex" in python_code or "Tex" in python_code: - scene_objects.append("equation") - if "Text" in python_code: - scene_objects.append("text") - if "Axes" in python_code: - scene_objects.append("graph") - if "ThreeDScene" in python_code or "ThreeDAxes" in python_code: - scene_objects.append("3D scene") - if "Sphere" in python_code: - scene_objects.append("sphere") - if "Cube" in python_code: - scene_objects.append("cube") - - # Generate a more detailed visual preview based on extracted objects - object_icons = { - "circle": "⭕", - "square": "🔲", - "equation": "📊", - "text": "📝", - "graph": "📈", - "3D scene": "🧊", - "sphere": "🌐", - "cube": "🧊" - } - - icon_html = "" - for obj in scene_objects: - if obj in object_icons: - icon_html += f'{object_icons[obj]}' - - preview_html = f""" -
-

Animation Preview

-
- {icon_html if icon_html else '🎬'} -
-

Scene contains: {', '.join(scene_objects) if scene_objects else 'No detected objects'}

-
Full rendering required for accurate preview
-
- """ - return preview_html - except Exception as e: - logger.error(f"Preview generation error: {str(e)}") - return f""" -
-
-

Preview Error

-

{str(e)}

-
-
- """ - -def prepare_audio_for_manim(audio_file, target_dir): - """Process audio file and return path for use in Manim""" - try: - # Create audio directory if it doesn't exist - audio_dir = os.path.join(target_dir, "audio") - os.makedirs(audio_dir, exist_ok=True) - - # Generate a unique filename - filename = f"audio_{int(time.time())}.mp3" - output_path = os.path.join(audio_dir, filename) - - # Save audio file - with open(output_path, "wb") as f: - f.write(audio_file.getvalue()) - - return output_path - except Exception as e: - logger.error(f"Audio processing error: {str(e)}") - return None - -def mp4_to_gif(mp4_path, output_path, fps=15): - """Convert MP4 to GIF using ffmpeg as a backup when Manim fails""" - try: - # Use ffmpeg for conversion with optimized settings - command = [ - "ffmpeg", - "-i", mp4_path, - "-vf", f"fps={fps},scale=640:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse", - "-loop", "0", - output_path - ] - - # Run the conversion - result = subprocess.run(command, capture_output=True, text=True) - - if result.returncode != 0: - logger.error(f"FFmpeg conversion error: {result.stderr}") - return None - - return output_path - - except Exception as e: - logger.error(f"GIF conversion error: {str(e)}") - return None - -def generate_manim_video(python_code, format_type, quality_preset, animation_speed=1.0, audio_path=None, fps=None): - temp_dir = None - progress_placeholder = st.empty() - status_placeholder = st.empty() - log_placeholder = st.empty() - video_data = None # Initialize video data variable - - try: - if not python_code or not format_type: - raise ValueError("Missing required parameters") - - # Create temporary directory - temp_dir = tempfile.mkdtemp(prefix="manim_render_") - - # Extract the scene class name from the code - scene_class = extract_scene_class_name(python_code) - logger.info(f"Detected scene class: {scene_class}") - - # If audio is provided, we need to modify the code to include it - if audio_path: - # Check if the code already has a with_sound decorator - if "with_sound" not in python_code: - # Add the necessary import - if "from manim.scene.scene_file_writer import SceneFileWriter" not in python_code: - python_code = "from manim.scene.scene_file_writer import SceneFileWriter\n" + python_code - - # Add sound to the scene - scene_def_pattern = f"class {scene_class}\\(.*?\\):" - scene_def_match = re.search(scene_def_pattern, python_code) - - if scene_def_match: - scene_def = scene_def_match.group(0) - scene_def_with_sound = f"@with_sound(\"{audio_path}\")\n{scene_def}" - python_code = python_code.replace(scene_def, scene_def_with_sound) - else: - logger.warning("Could not find scene definition to add audio") - - # Write the code to a file - scene_file = os.path.join(temp_dir, "scene.py") - with open(scene_file, "w", encoding="utf-8") as f: - f.write(python_code) - - # Map quality preset to Manim quality flag - quality_map = { - "480p": "-ql", # Low quality - "720p": "-qm", # Medium quality - "1080p": "-qh", # High quality - "4K": "-qk", # 4K quality - "8K": "-qp" # 8K quality (production quality) - } - quality_flag = quality_map.get(quality_preset, "-qm") - - # Handle special formats - if format_type == "png_sequence": - # For PNG sequence, we need additional flags - format_arg = "--format=png" - extra_args = ["--save_pngs"] - elif format_type == "svg": - # For SVG, we need a different format - format_arg = "--format=svg" - extra_args = [] + valid_indices = ~np.isnan(values) + if np.any(valid_indices): + max_vals[i] = np.max(values[valid_indices]) else: - # Standard video formats - format_arg = f"--format={format_type}" - extra_args = [] - - # Add custom FPS if specified - if fps is not None: - extra_args.append(f"--fps={fps}") - - # Show status and create progress bar - status_placeholder.info(f"Rendering {scene_class} with {quality_preset} quality...") - progress_bar = progress_placeholder.progress(0) - - # Build command - command = [ - "manim", - scene_file, - scene_class, - quality_flag, - format_arg - ] - command.extend(extra_args) - - logger.info(f"Running command: {' '.join(command)}") - - # Execute the command - process = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True - ) - - # Track output - full_output = [] - output_file_path = None - mp4_output_path = None # Track MP4 output for GIF fallback - - # Animation tracking variables - total_animations = None - current_animation = 0 - total_frames = None - current_frame = 0 - - while True: - line = process.stdout.readline() - if not line and process.poll() is not None: - break - - full_output.append(line) - log_placeholder.code("".join(full_output[-10:])) - - # Try to detect total animations - if "Rendering animation number" in line or "Processing animation" in line: - try: - # Extract current animation number - anim_match = re.search(r"(?:Rendering animation number|Processing animation) (\d+) (?:out of|/) (\d+)", line) - if anim_match: - current_animation = int(anim_match.group(1)) - total_animations = int(anim_match.group(2)) - logger.info(f"Animation progress: {current_animation}/{total_animations}") - - # Calculate progress based on animations - animation_progress = current_animation / total_animations - progress_bar.progress(animation_progress) - status_placeholder.info(f"Rendering {scene_class}: Animation {current_animation}/{total_animations} ({int(animation_progress*100)}%)") - except Exception as e: - logger.error(f"Error parsing animation progress: {str(e)}") - - # Try to extract total frames information as fallback - elif "Render animations with total frames:" in line and not total_animations: - try: - total_frames = int(line.split("Render animations with total frames:")[1].strip().split()[0]) - logger.info(f"Total frames to render: {total_frames}") - except Exception as e: - logger.error(f"Error parsing total frames: {str(e)}") - - # Update progress bar based on frame information if animation count not available - elif "Rendering frame" in line and total_frames and not total_animations: - try: - # Extract current frame number - frame_match = re.search(r"Rendering frame (\d+)", line) - if frame_match: - current_frame = int(frame_match.group(1)) - # Calculate progress as current frame / total frames - frame_progress = min(0.99, current_frame / total_frames) - progress_bar.progress(frame_progress) - # Update status with frame information - status_placeholder.info(f"Rendering {scene_class}: Frame {current_frame}/{total_frames} ({int(frame_progress*100)}%)") - except Exception as e: - logger.error(f"Error parsing frame progress: {str(e)}") - elif "%" in line and not total_animations and not total_frames: - try: - # Fallback to percentage if available - percent = float(line.split("%")[0].strip().split()[-1]) - progress_bar.progress(min(0.99, percent / 100)) - except: - pass - - # Try to capture the output file path from Manim's output - if "File ready at" in line: - try: - # Combine next few lines to get the full path - path_parts = [] - path_parts.append(line.split("File ready at")[-1].strip()) - - # Read up to 5 more lines to get the complete path - for _ in range(5): - additional_line = process.stdout.readline() - if additional_line: - full_output.append(additional_line) - path_parts.append(additional_line.strip()) - if additional_line.strip().endswith(('.mp4', '.gif', '.webm', '.svg')): - break - - # Join all parts and clean up - potential_path = ''.join(path_parts).replace("'", "").strip() - # Look for path pattern surrounded by quotes - path_match = re.search(r'([\'"]?)((?:/|[a-zA-Z]:\\).*?\.(?:mp4|gif|webm|svg))(\1)', potential_path) - if path_match: - output_file_path = path_match.group(2) - logger.info(f"Found output path in logs: {output_file_path}") - - # Track MP4 file for potential GIF fallback - if output_file_path.endswith('.mp4'): - mp4_output_path = output_file_path - except Exception as e: - logger.error(f"Error parsing output path: {str(e)}") - - # Wait for the process to complete - process.wait() - progress_bar.progress(1.0) - - # IMPORTANT: Wait a moment for file system to catch up - time.sleep(3) - - # Rest of the function remains the same - - # Special handling for GIF format - if Manim failed to generate a GIF but we have an MP4 - if format_type == "gif" and (not output_file_path or not os.path.exists(output_file_path)) and mp4_output_path and os.path.exists(mp4_output_path): - status_placeholder.info("GIF generation via Manim failed. Trying FFmpeg conversion...") - - # Generate a GIF using FFmpeg - gif_output_path = os.path.join(temp_dir, f"{scene_class}_converted.gif") - gif_path = mp4_to_gif(mp4_output_path, gif_output_path, fps=fps if fps else 15) - - if gif_path and os.path.exists(gif_path): - output_file_path = gif_path - logger.info(f"Successfully converted MP4 to GIF using FFmpeg: {gif_path}") - - # For PNG sequence, we need to collect the PNGs - if format_type == "png_sequence": - # Find the PNG directory - png_dirs = [] - search_dirs = [ - os.path.join(os.getcwd(), "media", "images", scene_class, "Animations"), - os.path.join(temp_dir, "media", "images", scene_class, "Animations"), - "/tmp/media/images", - ] - - for search_dir in search_dirs: - if os.path.exists(search_dir): - for root, dirs, _ in os.walk(search_dir): - for d in dirs: - if os.path.exists(os.path.join(root, d)): - png_dirs.append(os.path.join(root, d)) - - if png_dirs: - # Get the newest directory - newest_dir = max(png_dirs, key=os.path.getctime) - - # Create a zip file with all PNGs - png_files = [f for f in os.listdir(newest_dir) if f.endswith('.png')] - if png_files: - zip_path = os.path.join(temp_dir, f"{scene_class}_pngs.zip") - - with zipfile.ZipFile(zip_path, 'w') as zipf: - for png in png_files: - png_path = os.path.join(newest_dir, png) - zipf.write(png_path, os.path.basename(png_path)) - - with open(zip_path, 'rb') as f: - video_data = f.read() - - logger.info(f"Created PNG sequence zip: {zip_path}") - else: - logger.error("No PNG files found in directory") + max_vals[i] = np.nan + + return max_vals + +@st.cache_data +def compute_min_t_expression(betas, z_a, y, t_samples=1000): + """ + Compute min_{t ∈ (-1/a, 0)} (y*beta*(a-1)*t + (a*t+1)*((y-1)*t-1)) / ((a*t+1)*(t^2+t)) + """ + a = z_a + if a <= 0: + return np.full_like(betas, np.nan) + + lower_bound = -1/a + 1e-10 # Avoid division by zero + t_values = np.linspace(lower_bound, -1e-10, t_samples) + + min_vals = np.zeros_like(betas) + for i, beta in enumerate(betas): + values = np.zeros_like(t_values) + for j, t in enumerate(t_values): + numerator = y*beta*(a-1)*t + (a*t+1)*((y-1)*t-1) + denominator = (a*t+1)*(t**2+t) + if abs(denominator) < 1e-10: + values[j] = np.nan else: - logger.error("No PNG directories found") - elif output_file_path and os.path.exists(output_file_path): - # For other formats, read the output file directly - with open(output_file_path, 'rb') as f: - video_data = f.read() - logger.info(f"Read output file from path: {output_file_path}") - else: - # If we didn't find the output path, search for files - search_paths = [ - os.path.join(os.getcwd(), "media", "videos"), - os.path.join(os.getcwd(), "media", "videos", "scene"), - os.path.join(os.getcwd(), "media", "videos", scene_class), - "/tmp/media/videos", - temp_dir, - os.path.join(temp_dir, "media", "videos"), - ] - - # Add quality-specific paths - for quality in ["480p30", "720p30", "1080p60", "2160p60", "4320p60"]: - search_paths.append(os.path.join(os.getcwd(), "media", "videos", "scene", quality)) - search_paths.append(os.path.join(os.getcwd(), "media", "videos", scene_class, quality)) - - # For SVG format - if format_type == "svg": - search_paths.extend([ - os.path.join(os.getcwd(), "media", "designs"), - os.path.join(os.getcwd(), "media", "designs", scene_class), - ]) - - # Find all output files in the search paths - output_files = [] - for search_path in search_paths: - if os.path.exists(search_path): - for root, _, files in os.walk(search_path): - for file in files: - if file.endswith(f".{format_type}") and "partial" not in file: - file_path = os.path.join(root, file) - if os.path.exists(file_path): - output_files.append(file_path) - logger.info(f"Found output file: {file_path}") - - if output_files: - # Get the newest file - latest_file = max(output_files, key=os.path.getctime) - with open(latest_file, 'rb') as f: - video_data = f.read() - logger.info(f"Read output from file search: {latest_file}") - - # If the format is GIF but we got an MP4, try to convert it - if format_type == "gif" and latest_file.endswith('.mp4'): - gif_output_path = os.path.join(temp_dir, f"{scene_class}_converted.gif") - gif_path = mp4_to_gif(latest_file, gif_output_path, fps=fps if fps else 15) - - if gif_path and os.path.exists(gif_path): - with open(gif_path, 'rb') as f: - video_data = f.read() - logger.info(f"Successfully converted MP4 to GIF using FFmpeg: {gif_path}") + values[j] = numerator/denominator - # If we got output data, return it - if video_data: - file_size_mb = len(video_data) / (1024 * 1024) - - # Clear placeholders - progress_placeholder.empty() - status_placeholder.empty() - log_placeholder.empty() - - return video_data, f"✅ Animation generated successfully! ({file_size_mb:.1f} MB)" + valid_indices = ~np.isnan(values) + if np.any(valid_indices): + min_vals[i] = np.min(values[valid_indices]) else: - output_str = ''.join(full_output) - logger.error(f"No output files found. Full output: {output_str}") - - # Check if we have an MP4 but need a GIF (special handling for GIF issues) - if format_type == "gif": - # Try one more aggressive search for any MP4 file - mp4_files = [] - for search_path in [os.getcwd(), temp_dir, "/tmp"]: - for root, _, files in os.walk(search_path): - for file in files: - if file.endswith('.mp4') and scene_class.lower() in file.lower(): - mp4_path = os.path.join(root, file) - if os.path.exists(mp4_path) and os.path.getsize(mp4_path) > 0: - mp4_files.append(mp4_path) - - if mp4_files: - newest_mp4 = max(mp4_files, key=os.path.getctime) - logger.info(f"Found MP4 for GIF conversion: {newest_mp4}") - - # Convert to GIF - gif_output_path = os.path.join(temp_dir, f"{scene_class}_converted.gif") - gif_path = mp4_to_gif(newest_mp4, gif_output_path, fps=fps if fps else 15) - - if gif_path and os.path.exists(gif_path): - with open(gif_path, 'rb') as f: - video_data = f.read() - - # Clear placeholders - progress_placeholder.empty() - status_placeholder.empty() - log_placeholder.empty() - - file_size_mb = len(video_data) / (1024 * 1024) - return video_data, f"✅ Animation converted to GIF successfully! ({file_size_mb:.1f} MB)" + min_vals[i] = np.nan - return None, f"❌ Error: No output files were generated.\n\nMakim output:\n{output_str[:500]}..." - - except Exception as e: - logger.error(f"Error: {str(e)}") - import traceback - logger.error(traceback.format_exc()) - - if progress_placeholder: - progress_placeholder.empty() - if status_placeholder: - status_placeholder.error(f"Rendering Error: {str(e)}") - if log_placeholder: - log_placeholder.empty() - - return None, f"❌ Error: {str(e)}" - - finally: - # CRITICAL: Only cleanup after we've captured the output data - if temp_dir and os.path.exists(temp_dir) and video_data is not None: - try: - shutil.rmtree(temp_dir) - logger.info(f"Cleaned up temp dir: {temp_dir}") - except Exception as e: - logger.error(f"Failed to clean temp dir: {str(e)}") + return min_vals -def detect_input_calls(code): - """Detect input() calls in Python code to prepare for handling""" - input_calls = [] - lines = code.split('\n') - for i, line in enumerate(lines): - if 'input(' in line and not line.strip().startswith('#'): - # Try to extract the prompt if available - prompt_match = re.search(r'input\([\'"](.+?)[\'"]\)', line) - prompt = prompt_match.group(1) if prompt_match else f"Input for line {i+1}" - input_calls.append({"line": i+1, "prompt": prompt}) - return input_calls +@st.cache_data +def compute_derivatives(curve, betas): + """Compute first and second derivatives of a curve""" + d1 = np.gradient(curve, betas) + d2 = np.gradient(d1, betas) + return d1, d2 -def run_python_script(code, inputs=None, timeout=60): - """Execute a Python script and capture output, handling input calls""" - result = { - "stdout": "", - "stderr": "", - "exception": None, - "plots": [], - "dataframes": [], - "execution_time": 0 - } +def compute_all_derivatives(betas, z_mins, z_maxs, low_y_curve, high_y_curve, alt_low_expr, custom_curve1=None, custom_curve2=None): + """Compute derivatives for all curves""" + derivatives = {} - # Replace input() calls with predefined values if provided - if inputs and len(inputs) > 0: - # Modify the code to use predefined inputs instead of waiting for user input - modified_code = """ -# Input values provided by the user -__INPUT_VALUES = {} -__INPUT_INDEX = 0 - -# Override the built-in input function -def input(prompt=''): - global __INPUT_INDEX - print(prompt, end='') - if __INPUT_INDEX < len(__INPUT_VALUES): - value = __INPUT_VALUES[__INPUT_INDEX] - __INPUT_INDEX += 1 - print(value) # Echo the input - return value - else: - print("\\n[WARNING] No more predefined inputs available, using empty string") - return "" - -""".format(inputs) - - code = modified_code + code + # Upper z*(β) + derivatives['upper'] = compute_derivatives(z_maxs, betas) - # Create a tempdir for script execution - with tempfile.TemporaryDirectory() as temp_dir: - # Path for saving plots - plot_dir = os.path.join(temp_dir, 'plots') - os.makedirs(plot_dir, exist_ok=True) - - # Files for capturing stdout and stderr - stdout_file = os.path.join(temp_dir, 'stdout.txt') - stderr_file = os.path.join(temp_dir, 'stderr.txt') - - # Add plot saving code - if 'matplotlib' in code or 'plt' in code: - if 'import matplotlib.pyplot as plt' not in code and 'from matplotlib import pyplot as plt' not in code: - code = "import matplotlib.pyplot as plt\n" + code - - # Add code to save plots - save_plots_code = """ -# Save all figures -import matplotlib.pyplot as plt -import os - -__figures = plt.get_fignums() -for __i, __num in enumerate(__figures): - __fig = plt.figure(__num) - __fig.savefig(os.path.join('{}', f'plot_{{__i}}.png')) -""".format(plot_dir.replace('\\', '\\\\')) - - code += "\n" + save_plots_code - - # Add dataframe display code if pandas is used - if 'pandas' in code or 'pd.' in code or 'DataFrame' in code: - if 'import pandas as pd' not in code and 'from pandas import' not in code: - code = "import pandas as pd\n" + code - - # Add code to save dataframe info - dataframes_code = """ -# Capture DataFrames -import pandas as pd -import json -import io -import os - -__globals_dict = globals() -__dataframes = [] -for __var_name, __var_val in __globals_dict.items(): - if isinstance(__var_val, pd.DataFrame) and not __var_name.startswith('__'): - try: - # Save basic info - __df_info = { - "name": __var_name, - "shape": __var_val.shape, - "columns": list(__var_val.columns), - "preview_html": __var_val.head().to_html() - } - with open(os.path.join('{}', f'df_{{__var_name}}.json'), 'w') as __f: - json.dump(__df_info, __f) - except: - pass -""".format(temp_dir.replace('\\', '\\\\')) - - code += "\n" + dataframes_code - - # Create the script file - script_path = os.path.join(temp_dir, 'script.py') - with open(script_path, 'w') as f: - f.write(code) - - # Execute with timeout - start_time = time.time() - try: - # Run the script with stdout and stderr redirection - with open(stdout_file, 'w') as stdout_f, open(stderr_file, 'w') as stderr_f: - process = subprocess.Popen( - [sys.executable, script_path], - stdout=stdout_f, - stderr=stderr_f, - cwd=temp_dir - ) - - try: - process.wait(timeout=timeout) - except subprocess.TimeoutExpired: - process.kill() - result["stderr"] += f"\nScript execution timed out after {timeout} seconds." - result["exception"] = "TimeoutError" - return result - - # Read the output - with open(stdout_file, 'r') as f: - result["stdout"] = f.read() - - with open(stderr_file, 'r') as f: - result["stderr"] = f.read() - - # Collect plots - if os.path.exists(plot_dir): - plot_files = sorted([f for f in os.listdir(plot_dir) if f.endswith('.png')]) - for plot_file in plot_files: - with open(os.path.join(plot_dir, plot_file), 'rb') as f: - result["plots"].append(f.read()) - - # Collect dataframes - df_files = [f for f in os.listdir(temp_dir) if f.startswith('df_') and f.endswith('.json')] - for df_file in df_files: - with open(os.path.join(temp_dir, df_file), 'r') as f: - result["dataframes"].append(json.load(f)) - - # Calculate execution time - result["execution_time"] = time.time() - start_time - - except Exception as e: - result["exception"] = str(e) - result["stderr"] += f"\nError executing script: {str(e)}" - - return result - -def display_python_script_results(result): - """Display the results from the Python script execution""" - if not result: - st.error("No results to display.") - return + # Lower z*(β) + derivatives['lower'] = compute_derivatives(z_mins, betas) - # Display execution time - st.info(f"Execution completed in {result['execution_time']:.2f} seconds") + # Low y Expression (only if provided) + if low_y_curve is not None: + derivatives['low_y'] = compute_derivatives(low_y_curve, betas) - # Display any errors - if result["exception"]: - st.error(f"Exception occurred: {result['exception']}") + # High y Expression + derivatives['high_y'] = compute_derivatives(high_y_curve, betas) - if result["stderr"]: - st.error("Errors:") - st.code(result["stderr"], language="bash") + # Alternate Low Expression + derivatives['alt_low'] = compute_derivatives(alt_low_expr, betas) - # Display plots if any - if result["plots"]: - st.markdown("### Plots") - cols = st.columns(min(3, len(result["plots"]))) - for i, plot_data in enumerate(result["plots"]): - cols[i % len(cols)].image(plot_data, use_column_width=True) - - # Display dataframes if any - if result["dataframes"]: - st.markdown("### DataFrames") - for df_info in result["dataframes"]: - with st.expander(f"{df_info['name']} - {df_info['shape'][0]} rows × {df_info['shape'][1]} columns"): - st.markdown(df_info["preview_html"], unsafe_allow_html=True) - - # Display standard output - if result["stdout"]: - st.markdown("### Standard Output") - st.code(result["stdout"], language="bash") + # Custom Expression 1 (if provided) + if custom_curve1 is not None: + derivatives['custom1'] = compute_derivatives(custom_curve1, betas) -def parse_animation_steps(python_code): - """Parse Manim code to extract animation steps for timeline editor""" - animation_steps = [] - - # Look for self.play calls in the code - play_calls = re.findall(r'self\.play\((.*?)\)', python_code, re.DOTALL) - wait_calls = re.findall(r'self\.wait\((.*?)\)', python_code, re.DOTALL) - - # Extract animation objects from play calls - for i, play_call in enumerate(play_calls): - # Parse the arguments to self.play() - animations = [arg.strip() for arg in play_call.split(',')] + # Custom Expression 2 (if provided) + if custom_curve2 is not None: + derivatives['custom2'] = compute_derivatives(custom_curve2, betas) - # Get wait time after this animation if available - wait_time = 1.0 # Default wait time - if i < len(wait_calls): - wait_match = re.search(r'(\d+\.?\d*)', wait_calls[i]) - if wait_match: - wait_time = float(wait_match.group(1)) - - # Add to animation steps - animation_steps.append({ - "id": i+1, - "type": "play", - "animations": animations, - "duration": wait_time, - "start_time": sum([step.get("duration", 1.0) for step in animation_steps]), - "code": f"self.play({play_call})" - }) - - return animation_steps + return derivatives -def generate_code_from_timeline(animation_steps, original_code): - """Generate Manim code from the timeline data""" - # Extract the class definition and setup - class_match = re.search(r'(class\s+\w+\s*\([^)]*\)\s*:.*?def\s+construct\s*\(\s*self\s*\)\s*:)', original_code, re.DOTALL) +def compute_custom_expression(betas, z_a, y, s_num_expr, s_denom_expr, is_s_based=True): + """ + Compute custom curve. If is_s_based=True, compute using s substitution. + Otherwise, compute direct z(β) expression. + """ + beta_sym, z_a_sym, y_sym = sp.symbols("beta z_a y", positive=True) + local_dict = {"beta": beta_sym, "z_a": z_a_sym, "y": y_sym, "sp": sp} - if not class_match: - return original_code # Can't find proper structure to modify - - setup_code = class_match.group(1) - - # Build the new construct method - new_code = [setup_code] - indent = " " # Standard Manim indentation - - # Add each animation step in order - for step in sorted(animation_steps, key=lambda x: x["id"]): - new_code.append(f"{indent}{step['code']}") - if "duration" in step and step["duration"] > 0: - new_code.append(f"{indent}self.wait({step['duration']})") - - # Add any code that might come after animations - end_match = re.search(r'(#\s*End\s+of\s+animations.*?$)', original_code, re.DOTALL) - if end_match: - new_code.append(end_match.group(1)) + try: + # Add sqrt support + s_num_expr = add_sqrt_support(s_num_expr) + s_denom_expr = add_sqrt_support(s_denom_expr) + + num_expr = sp.sympify(s_num_expr, locals=local_dict) + denom_expr = sp.sympify(s_denom_expr, locals=local_dict) + + if is_s_based: + # Compute s and substitute into main expression + s_expr = num_expr / denom_expr + a = z_a_sym + numerator = y_sym*beta_sym*(z_a_sym-1)*s_expr + (a*s_expr+1)*((y_sym-1)*s_expr-1) + denominator = (a*s_expr+1)*(s_expr**2 + s_expr) + final_expr = numerator/denominator + else: + # Direct z(β) expression + final_expr = num_expr / denom_expr + + except sp.SympifyError as e: + st.error(f"Error parsing expressions: {e}") + return np.full_like(betas, np.nan) + + final_func = sp.lambdify((beta_sym, z_a_sym, y_sym), final_expr, modules=["numpy"]) + with np.errstate(divide='ignore', invalid='ignore'): + result = final_func(betas, z_a, y) + if np.isscalar(result): + result = np.full_like(betas, result) + return result + +def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps, + s_num_expr=None, s_denom_expr=None, + z_num_expr=None, z_denom_expr=None, + show_derivatives=False): + if z_a <= 0 or y <= 0 or z_min >= z_max: + st.error("Invalid input parameters.") + return None + + betas = np.linspace(0, 1, beta_steps) + betas, z_mins, z_maxs = sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps) + # Removed low_y_curve computation + high_y_curve = compute_high_y_curve(betas, z_a, y) + alt_low_expr = compute_alternate_low_expr(betas, z_a, y) + + # Compute the max/min expressions + max_k_curve = compute_max_k_expression(betas, z_a, y) + min_t_curve = compute_min_t_expression(betas, z_a, y) + + # Compute both custom curves + custom_curve1 = None + custom_curve2 = None + if s_num_expr and s_denom_expr: + custom_curve1 = compute_custom_expression(betas, z_a, y, s_num_expr, s_denom_expr, True) + if z_num_expr and z_denom_expr: + custom_curve2 = compute_custom_expression(betas, z_a, y, z_num_expr, z_denom_expr, False) + + # Compute derivatives if needed + if show_derivatives: + derivatives = compute_all_derivatives(betas, z_mins, z_maxs, None, high_y_curve, + alt_low_expr, custom_curve1, custom_curve2) + # Calculate derivatives for max_k and min_t curves + max_k_derivatives = compute_derivatives(max_k_curve, betas) + min_t_derivatives = compute_derivatives(min_t_curve, betas) + + fig = go.Figure() + + # Original curves + fig.add_trace(go.Scatter(x=betas, y=z_maxs, mode="markers+lines", + name="Upper z*(β)", line=dict(color='blue'))) + fig.add_trace(go.Scatter(x=betas, y=z_mins, mode="markers+lines", + name="Lower z*(β)", line=dict(color='blue'))) + # Removed the Low y Expression trace + fig.add_trace(go.Scatter(x=betas, y=high_y_curve, mode="markers+lines", + name="High y Expression", line=dict(color='green'))) + fig.add_trace(go.Scatter(x=betas, y=alt_low_expr, mode="markers+lines", + name="Low Expression", line=dict(color='green'))) + + # Add the new max/min curves + fig.add_trace(go.Scatter(x=betas, y=max_k_curve, mode="lines", + name="Max k Expression", line=dict(color='red', width=2))) + fig.add_trace(go.Scatter(x=betas, y=min_t_curve, mode="lines", + name="Min t Expression", line=dict(color='orange', width=2))) + + if custom_curve1 is not None: + fig.add_trace(go.Scatter(x=betas, y=custom_curve1, mode="markers+lines", + name="Custom 1 (s-based)", line=dict(color='purple'))) + if custom_curve2 is not None: + fig.add_trace(go.Scatter(x=betas, y=custom_curve2, mode="markers+lines", + name="Custom 2 (direct)", line=dict(color='magenta'))) + + if show_derivatives: + # First derivatives + curve_info = [ + ('upper', 'Upper z*(β)', 'blue'), + ('lower', 'Lower z*(β)', 'lightblue'), + # Removed low_y curve + ('high_y', 'High y', 'green'), + ('alt_low', 'Alt Low', 'orange') + ] - # Combine the code parts with proper indentation - return "\n".join(new_code) + if custom_curve1 is not None: + curve_info.append(('custom1', 'Custom 1', 'purple')) + if custom_curve2 is not None: + curve_info.append(('custom2', 'Custom 2', 'magenta')) + + for key, name, color in curve_info: + fig.add_trace(go.Scatter(x=betas, y=derivatives[key][0], mode="lines", + name=f"{name} d/dβ", line=dict(color=color, dash='dash'))) + fig.add_trace(go.Scatter(x=betas, y=derivatives[key][1], mode="lines", + name=f"{name} d²/dβ²", line=dict(color=color, dash='dot'))) + + # Add derivatives for max_k and min_t curves + fig.add_trace(go.Scatter(x=betas, y=max_k_derivatives[0], mode="lines", + name="Max k d/dβ", line=dict(color='red', dash='dash'))) + fig.add_trace(go.Scatter(x=betas, y=max_k_derivatives[1], mode="lines", + name="Max k d²/dβ²", line=dict(color='red', dash='dot'))) + fig.add_trace(go.Scatter(x=betas, y=min_t_derivatives[0], mode="lines", + name="Min t d/dβ", line=dict(color='orange', dash='dash'))) + fig.add_trace(go.Scatter(x=betas, y=min_t_derivatives[1], mode="lines", + name="Min t d²/dβ²", line=dict(color='orange', dash='dot'))) -def create_timeline_editor(code): - """Create an interactive timeline editor for animation sequences""" - st.markdown("### 🎞️ Animation Timeline Editor") - - if not code: - st.warning("Add animation code first to use the timeline editor.") - return code - - # Parse animation steps from the code - animation_steps = parse_animation_steps(code) - - if not animation_steps: - st.warning("No animation steps detected in your code.") - return code - - # Convert to DataFrame for easier manipulation - df = pd.DataFrame(animation_steps) - - # Create an interactive Gantt chart with plotly - st.markdown("#### Animation Timeline") - st.markdown("Drag timeline elements to reorder or resize to change duration") - - # Create the Gantt chart - fig = px.timeline( - df, - x_start="start_time", - x_end=df["start_time"] + df["duration"], - y="id", - color="type", - hover_name="animations", - labels={"id": "Step", "start_time": "Time (seconds)"} - ) - - # Make it interactive fig.update_layout( - height=400, - xaxis=dict( - title="Time (seconds)", - rangeslider_visible=True - ) - ) - - # Add buttons and interactivity - timeline_chart = st.plotly_chart(fig, use_container_width=True) - - # Control panel - st.markdown("#### Timeline Controls") - controls_col1, controls_col2, controls_col3 = st.columns(3) - - with controls_col1: - selected_step = st.selectbox( - "Select Step to Edit:", - options=list(range(1, len(animation_steps) + 1)), - format_func=lambda x: f"Step {x}" - ) - - with controls_col2: - new_duration = st.number_input( - "Duration (seconds):", - min_value=0.1, - max_value=10.0, - value=float(df[df["id"] == selected_step]["duration"].values[0]), - step=0.1 - ) - - with controls_col3: - step_action = st.selectbox( - "Action:", - options=["Update Duration", "Move Up", "Move Down", "Delete Step"] + title="Curves vs β: z*(β) Boundaries and Asymptotic Expressions", + xaxis_title="β", + yaxis_title="Value", + hovermode="x unified", + showlegend=True, + legend=dict( + yanchor="top", + y=0.99, + xanchor="left", + x=0.01 ) - - apply_btn = st.button("Apply Change", key="apply_timeline_change") - - # Handle timeline modifications - if apply_btn: - modified = False - - if step_action == "Update Duration": - # Update the duration of the selected step - idx = df[df["id"] == selected_step].index[0] - df.at[idx, "duration"] = new_duration - modified = True - - elif step_action == "Move Up" and selected_step > 1: - # Swap with the step above - idx1 = df[df["id"] == selected_step].index[0] - idx2 = df[df["id"] == selected_step - 1].index[0] - - # Swap IDs to maintain order - df.at[idx1, "id"], df.at[idx2, "id"] = selected_step - 1, selected_step - modified = True - - elif step_action == "Move Down" and selected_step < len(animation_steps): - # Swap with the step below - idx1 = df[df["id"] == selected_step].index[0] - idx2 = df[df["id"] == selected_step + 1].index[0] - - # Swap IDs to maintain order - df.at[idx1, "id"], df.at[idx2, "id"] = selected_step + 1, selected_step - modified = True - - elif step_action == "Delete Step": - # Remove the selected step - df = df[df["id"] != selected_step] - # Reindex remaining steps - new_ids = list(range(1, len(df) + 1)) - df["id"] = new_ids - modified = True - - if modified: - # Recalculate start times - df = df.sort_values("id") - cumulative_time = 0 - for idx, row in df.iterrows(): - df.at[idx, "start_time"] = cumulative_time - cumulative_time += row["duration"] - - # Regenerate animation code - animation_steps = df.to_dict('records') - new_code = generate_code_from_timeline(animation_steps, code) - - st.success("Timeline updated! Code has been regenerated.") - return new_code - - # Visual keyframe editor - st.markdown("#### Visual Keyframe Editor") - st.markdown("Add keyframes for smooth property transitions") - - keyframe_obj = st.selectbox( - "Select object to animate:", - options=[f"Object {i+1}" for i in range(5)] # Placeholder for actual objects - ) - - keyframe_prop = st.selectbox( - "Select property:", - options=["position", "scale", "rotation", "opacity", "color"] - ) - - # Keyframe timeline visualization - keyframe_times = [0, 1, 2, 3, 4] # Placeholder - keyframe_values = [0, 0.5, 0.8, 0.2, 1.0] # Placeholder - - keyframe_df = pd.DataFrame({ - "time": keyframe_times, - "value": keyframe_values - }) - - keyframe_fig = px.line( - keyframe_df, - x="time", - y="value", - markers=True, - title=f"{keyframe_prop.capitalize()} Keyframes" ) + return fig + +def compute_cubic_roots(z, beta, z_a, y): + """ + Compute the roots of the cubic equation for given parameters. + """ + a = z * z_a + b = z * z_a + z + z_a - z_a*y + c = z + z_a + 1 - y*(beta*z_a + 1 - beta) + d = 1 + coeffs = [a, b, c, d] + roots = np.roots(coeffs) + return roots + +def generate_root_plots(beta, y, z_a, z_min, z_max, n_points): + """ + Generate Im(s) and Re(s) vs. z plots. + """ + if z_a <= 0 or y <= 0 or z_min >= z_max: + st.error("Invalid input parameters.") + return None, None + + z_points = np.linspace(z_min, z_max, n_points) + ims, res = [], [] + for z in z_points: + roots = compute_cubic_roots(z, beta, z_a, y) + roots = sorted(roots, key=lambda x: abs(x.imag)) + ims.append([root.imag for root in roots]) + res.append([root.real for root in roots]) + ims = np.array(ims) + res = np.array(res) + + fig_im = go.Figure() + for i in range(3): + fig_im.add_trace(go.Scatter(x=z_points, y=ims[:, i], mode="lines", name=f"Im{{s{i+1}}}", + line=dict(width=2))) + fig_im.update_layout(title=f"Im{{s}} vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})", + xaxis_title="z", yaxis_title="Im{s}", hovermode="x unified") + + fig_re = go.Figure() + for i in range(3): + fig_re.add_trace(go.Scatter(x=z_points, y=res[:, i], mode="lines", name=f"Re{{s{i+1}}}", + line=dict(width=2))) + fig_re.update_layout(title=f"Re{{s}} vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})", + xaxis_title="z", yaxis_title="Re{s}", hovermode="x unified") + return fig_im, fig_re + +@st.cache_data +def generate_eigenvalue_distribution(beta, y, z_a, n=1000, seed=42): + """ + Generate the eigenvalue distribution of B_n = S_n T_n as n→∞ + + Parameters: + ----------- + beta : float + Fraction of components equal to z_a + y : float + Aspect ratio p/n + z_a : float + Value for the delta mass at z_a + n : int + Number of samples + seed : int + Random seed for reproducibility + """ + # Set random seed + np.random.seed(seed) + + # Compute dimension p based on aspect ratio y + p = int(y * n) + + # Constructing T_n (Population / Shape Matrix) + T_diag = np.where(np.random.rand(p) < beta, z_a, 1.0) + T_n = np.diag(T_diag) + + # Generate the data matrix X with i.i.d. standard normal entries + X = np.random.randn(p, n) + + # Compute the sample covariance matrix S_n = (1/n) * XX^T + S_n = (1 / n) * (X @ X.T) + + # Compute B_n = S_n T_n + B_n = S_n @ T_n + + # Compute eigenvalues of B_n + eigenvalues = np.linalg.eigvalsh(B_n) + + # Use KDE to compute a smooth density estimate + kde = gaussian_kde(eigenvalues) + x_vals = np.linspace(min(eigenvalues), max(eigenvalues), 500) + kde_vals = kde(x_vals) + + # Create figure + fig = go.Figure() + + # Add histogram trace + fig.add_trace(go.Histogram(x=eigenvalues, histnorm='probability density', + name="Histogram", marker=dict(color='blue', opacity=0.6))) + + # Add KDE trace + fig.add_trace(go.Scatter(x=x_vals, y=kde_vals, mode="lines", + name="KDE", line=dict(color='red', width=2))) - keyframe_fig.update_layout( - xaxis_title="Time (seconds)", - yaxis_title="Value", - height=250 + fig.update_layout( + title=f"Eigenvalue Distribution for B_n = S_n T_n (y={y:.1f}, β={beta:.2f}, a={z_a:.1f})", + xaxis_title="Eigenvalue", + yaxis_title="Density", + hovermode="closest", + showlegend=True ) - st.plotly_chart(keyframe_fig, use_container_width=True) + return fig + +# ----------------- Streamlit UI ----------------- +st.title("Cubic Root Analysis") + +# Define three tabs (removed "Curve Intersections") +tab1, tab2, tab3 = st.tabs(["z*(β) Curves", "Im{s} vs. z", "Differential Analysis"]) + +# ----- Tab 1: z*(β) Curves ----- +with tab1: + st.header("Find z Values where Cubic Roots Transition Between Real and Complex") + col1, col2 = st.columns([1, 2]) + with col1: + z_a_1 = st.number_input("z_a", value=1.0, key="z_a_1") + y_1 = st.number_input("y", value=1.0, key="y_1") + z_min_1 = st.number_input("z_min", value=-10.0, key="z_min_1") + z_max_1 = st.number_input("z_max", value=10.0, key="z_max_1") + with st.expander("Resolution Settings"): + beta_steps = st.slider("β steps", min_value=51, max_value=501, value=201, step=50, key="beta_steps") + z_steps = st.slider("z grid steps", min_value=1000, max_value=100000, value=50000, step=1000, key="z_steps") + + st.subheader("Custom Expression 1 (s-based)") + st.markdown("""Enter expressions for s = numerator/denominator + (using variables `y`, `beta`, `z_a`, and `sqrt()`)""") + st.latex(r"\text{This s will be inserted into:}") + st.latex(r"\frac{y\beta(z_a-1)\underline{s}+(a\underline{s}+1)((y-1)\underline{s}-1)}{(a\underline{s}+1)(\underline{s}^2 + \underline{s})}") + s_num = st.text_input("s numerator", value="", key="s_num") + s_denom = st.text_input("s denominator", value="", key="s_denom") + + st.subheader("Custom Expression 2 (direct z(β))") + st.markdown("""Enter direct expression for z(β) = numerator/denominator + (using variables `y`, `beta`, `z_a`, and `sqrt()`)""") + z_num = st.text_input("z(β) numerator", value="", key="z_num") + z_denom = st.text_input("z(β) denominator", value="", key="z_denom") + + show_derivatives = st.checkbox("Show derivatives", value=False) + + if st.button("Compute z vs. β Curves", key="tab1_button"): + with col2: + fig = generate_z_vs_beta_plot(z_a_1, y_1, z_min_1, z_max_1, beta_steps, z_steps, + s_num, s_denom, z_num, z_denom, show_derivatives) + if fig is not None: + st.plotly_chart(fig, use_container_width=True) + st.markdown("### Curve Explanations") + st.markdown(""" + - **Upper z*(β)** (Blue): Maximum z value where discriminant is zero + - **Lower z*(β)** (Light Blue): Minimum z value where discriminant is zero + - **High y Expression** (Green): Asymptotic approximation for high y values + - **Low Expression** (Orange): Alternative asymptotic expression + - **Max k Expression** (Red): $\\max_{k \\in (0,\\infty)} \\frac{y\\beta (a-1)k + \\bigl(ak+1\\bigr)\\bigl((y-1)k-1\\bigr)}{(ak+1)(k^2+k)}$ + - **Min t Expression** (Orange): $\\min_{t \\in \\left(-\\frac{1}{a},\\, 0\\right)} \\frac{y\\beta (a-1)t + \\bigl(at+1\\bigr)\\bigl((y-1)t-1\\bigr)}{(at+1)(t^2+t)}$ + - **Custom Expression 1** (Purple): Result from user-defined s substituted into the main formula + - **Custom Expression 2** (Magenta): Direct z(β) expression + """) + if show_derivatives: + st.markdown(""" + Derivatives are shown as: + - Dashed lines: First derivatives (d/dβ) + - Dotted lines: Second derivatives (d²/dβ²) + """) + +# ----- Tab 2: Im{s} vs. z ----- +with tab2: + st.header("Plot Complex Roots vs. z") + col1, col2 = st.columns([1, 2]) + with col1: + beta = st.number_input("β", value=0.5, min_value=0.0, max_value=1.0, key="beta_tab2") + y_2 = st.number_input("y", value=1.0, key="y_tab2") + z_a_2 = st.number_input("z_a", value=1.0, key="z_a_tab2") + z_min_2 = st.number_input("z_min", value=-10.0, key="z_min_tab2") + z_max_2 = st.number_input("z_max", value=10.0, key="z_max_tab2") + with st.expander("Resolution Settings"): + z_points = st.slider("z grid points", min_value=1000, max_value=10000, value=5000, step=500, key="z_points") + if st.button("Compute Complex Roots vs. z", key="tab2_button"): + with col2: + fig_im, fig_re = generate_root_plots(beta, y_2, z_a_2, z_min_2, z_max_2, z_points) + if fig_im is not None and fig_re is not None: + st.plotly_chart(fig_im, use_container_width=True) + st.plotly_chart(fig_re, use_container_width=True) - keyframe_col1, keyframe_col2, keyframe_col3 = st.columns(3) - with keyframe_col1: - keyframe_time = st.number_input("Time (s)", min_value=0.0, max_value=10.0, value=0.0, step=0.1) - with keyframe_col2: - keyframe_value = st.number_input("Value", min_value=0.0, max_value=1.0, value=0.0, step=0.1) - with keyframe_col3: - add_keyframe = st.button("Add Keyframe") + # Add a separator + st.markdown("---") - # Return the original code or modified code - return code - -def export_to_educational_format(video_data, format_type, animation_title, explanation_text, temp_dir): - """Export animation to various educational formats""" - try: - if format_type == "powerpoint": - # Make sure python-pptx is installed - try: - import pptx - from pptx.util import Inches - except ImportError: - logger.error("python-pptx not installed") - subprocess.run([sys.executable, "-m", "pip", "install", "python-pptx"], check=True) - import pptx - from pptx.util import Inches - - # Create PowerPoint presentation - prs = pptx.Presentation() - - # Title slide - title_slide = prs.slides.add_slide(prs.slide_layouts[0]) - title_slide.shapes.title.text = animation_title - title_slide.placeholders[1].text = "Created with Manim Animation Studio" - - # Video slide - video_slide = prs.slides.add_slide(prs.slide_layouts[5]) - video_slide.shapes.title.text = "Animation" - - # Save video to temp file - video_path = os.path.join(temp_dir, "animation.mp4") - with open(video_path, "wb") as f: - f.write(video_data) - - # Add video to slide - try: - left = Inches(1) - top = Inches(1.5) - width = Inches(8) - height = Inches(4.5) - video_slide.shapes.add_movie(video_path, left, top, width, height) - except Exception as e: - logger.error(f"Error adding video to PowerPoint: {str(e)}") - # Fallback to adding a picture with link - img_path = os.path.join(temp_dir, "thumbnail.png") - # Generate thumbnail with ffmpeg - subprocess.run([ - "ffmpeg", "-i", video_path, "-ss", "00:00:01.000", - "-vframes", "1", img_path - ], check=True) - - if os.path.exists(img_path): - pic = video_slide.shapes.add_picture(img_path, left, top, width, height) - video_slide.shapes.add_textbox(left, top + height + Inches(0.5), width, Inches(0.5)).text_frame.text = "Click to play video (exported separately)" - - # Explanation slide - if explanation_text: - text_slide = prs.slides.add_slide(prs.slide_layouts[1]) - text_slide.shapes.title.text = "Explanation" - text_slide.placeholders[1].text = explanation_text - - # Save presentation - output_path = os.path.join(temp_dir, f"{animation_title.replace(' ', '_')}.pptx") - prs.save(output_path) - - # Read the file to return it - with open(output_path, "rb") as f: - return f.read(), "powerpoint" - - elif format_type == "html": - # Create interactive HTML animation - html_template = """ - - - - {title} - - - - -

{title}

- -
- - -
- - - - - - -
-
- -
-

Explanation

- {explanation_html} -
- - - - - """ - - # Convert video data to base64 - video_base64 = base64.b64encode(video_data).decode('utf-8') - - # Convert markdown explanation to HTML - explanation_html = markdown.markdown(explanation_text) if explanation_text else "

No explanation provided.

" - - # Format the HTML template - html_content = html_template.format( - title=animation_title, - video_base64=video_base64, - explanation_html=explanation_html - ) - - # Save to file - output_path = os.path.join(temp_dir, f"{animation_title.replace(' ', '_')}.html") - with open(output_path, "w", encoding="utf-8") as f: - f.write(html_content) - - # Read the file to return it - with open(output_path, "rb") as f: - return f.read(), "html" - - elif format_type == "sequence": - # Generate animation sequence with explanatory text - # Make sure FPDF is installed - try: - from fpdf import FPDF - except ImportError: - logger.error("fpdf not installed") - subprocess.run([sys.executable, "-m", "pip", "install", "fpdf"], check=True) - from fpdf import FPDF - - # Save video temporarily - temp_video_path = os.path.join(temp_dir, "temp_video.mp4") - with open(temp_video_path, "wb") as f: - f.write(video_data) - - # Create frames directory - frames_dir = os.path.join(temp_dir, "frames") - os.makedirs(frames_dir, exist_ok=True) - - # Extract frames using ffmpeg (assuming it's installed) - frame_count = 5 # Number of key frames to extract - try: - subprocess.run([ - "ffmpeg", - "-i", temp_video_path, - "-vf", f"select=eq(n\\,0)+eq(n\\,{frame_count//4})+eq(n\\,{frame_count//2})+eq(n\\,{frame_count*3//4})+eq(n\\,{frame_count-1})", - "-vsync", "0", - os.path.join(frames_dir, "frame_%03d.png") - ], check=True) - except Exception as e: - logger.error(f"Error extracting frames: {str(e)}") - # Try a simpler approach - subprocess.run([ - "ffmpeg", - "-i", temp_video_path, - "-r", "1", # 1 frame per second - os.path.join(frames_dir, "frame_%03d.png") - ], check=True) - - # Parse explanation text into segments (assuming sections divided by ##) - explanation_segments = explanation_text.split("##") if explanation_text else ["No explanation provided."] - - # Create a PDF with frames and explanations - pdf = FPDF() - pdf.set_auto_page_break(auto=True, margin=15) - - # Title page - pdf.add_page() - pdf.set_font("Arial", "B", 20) - pdf.cell(190, 10, animation_title, ln=True, align="C") - pdf.ln(10) - pdf.set_font("Arial", "", 12) - pdf.cell(190, 10, "Animation Sequence with Explanations", ln=True, align="C") - - # Add each frame with explanation - frame_files = sorted([f for f in os.listdir(frames_dir) if f.endswith('.png')]) - - for i, frame_file in enumerate(frame_files): - pdf.add_page() - - # Add frame image - frame_path = os.path.join(frames_dir, frame_file) - pdf.image(frame_path, x=10, y=10, w=190) - - # Add explanation text - pdf.ln(140) # Move below the image - pdf.set_font("Arial", "B", 12) - pdf.cell(190, 10, f"Step {i+1}", ln=True) - pdf.set_font("Arial", "", 10) - - # Use the corresponding explanation segment if available - explanation = explanation_segments[min(i, len(explanation_segments)-1)] - pdf.multi_cell(190, 5, explanation.strip()) - - # Save PDF - output_path = os.path.join(temp_dir, f"{animation_title.replace(' ', '_')}_sequence.pdf") - pdf.output(output_path) - - # Read the file to return it - with open(output_path, "rb") as f: - return f.read(), "pdf" - - return None, None - - except Exception as e: - logger.error(f"Educational export error: {str(e)}") - import traceback - logger.error(traceback.format_exc()) - return None, None - -def main(): - # Initialize session state variables if they don't exist - if 'init' not in st.session_state: - st.session_state.init = True - st.session_state.video_data = None - st.session_state.status = None - st.session_state.ai_models = None - st.session_state.generated_code = "" - st.session_state.code = "" - st.session_state.temp_code = "" - st.session_state.editor_key = str(uuid.uuid4()) - st.session_state.packages_checked = False # Track if packages were already checked - st.session_state.audio_path = None - st.session_state.image_paths = [] - st.session_state.custom_library_result = "" - st.session_state.python_script = "import matplotlib.pyplot as plt\nimport numpy as np\n\n# Example: Create a simple plot\nx = np.linspace(0, 10, 100)\ny = np.sin(x)\n\nplt.figure(figsize=(10, 6))\nplt.plot(x, y, 'b-', label='sin(x)')\nplt.title('Sine Wave')\nplt.xlabel('x')\nplt.ylabel('sin(x)')\nplt.grid(True)\nplt.legend()\n" - st.session_state.python_result = None - st.session_state.active_tab = 0 # Track currently active tab - st.session_state.settings = { - "quality": "720p", - "format_type": "mp4", - "animation_speed": "Normal", - "fps": 30 # Default FPS - } - st.session_state.password_entered = False # Track password authentication - st.session_state.custom_model = "gpt-4o" # Default model - st.session_state.first_load_complete = False # Prevent refreshes on first load - st.session_state.pending_tab_switch = None # Track pending tab switches - - # Page configuration with improved layout - st.set_page_config( - page_title="Manim Animation Studio", - page_icon="🎬", - layout="wide", - initial_sidebar_state="expanded" - ) - - # Custom CSS for improved UI - st.markdown(""" - - """, unsafe_allow_html=True) - - # Header + # Add eigenvalue distribution section + st.header("Eigenvalue Distribution for B_n = S_n T_n") st.markdown(""" -
- 🎬 Manim Animation Studio -
-

Create mathematical animations with Manim

- """, unsafe_allow_html=True) - - # Check for packages ONLY ONCE per session - if not st.session_state.packages_checked: - if ensure_packages(): - st.session_state.packages_checked = True - else: - st.error("Failed to install required packages. Please try again.") - st.stop() - - # Create main tabs - LaTeX tab removed - tab_names = ["✨ Editor", "🤖 AI Assistant", "🎨 Assets", "🎞️ Timeline", "🎓 Educational Export", "🐍 Python Runner"] - tabs = st.tabs(tab_names) - - # Sidebar for rendering settings and custom libraries - with st.sidebar: - # Rendering settings section - st.markdown("## ⚙️ Rendering Settings") - - col1, col2 = st.columns(2) - with col1: - quality = st.selectbox( - "🎯 Quality", - options=list(QUALITY_PRESETS.keys()), - index=list(QUALITY_PRESETS.keys()).index(st.session_state.settings["quality"]), - key="quality_select" - ) - + This simulation generates the eigenvalue distribution of B_n as n→∞, where: + - B_n = (1/n)XX* with X being a p×n matrix + - p/n → y as n→∞ + - All elements of X are i.i.d with distribution β·δ(z_a) + (1-β)·δ(1) + """) + + col_eigen1, col_eigen2 = st.columns([1, 2]) + with col_eigen1: + n_samples = st.slider("Number of samples (n)", min_value=100, max_value=2000, value=1000, step=100) + sim_seed = st.number_input("Random seed", min_value=1, max_value=1000, value=42, step=1) + + if st.button("Generate Eigenvalue Distribution", key="tab2_eigen_button"): + with col_eigen2: + fig_eigen = generate_eigenvalue_distribution(beta, y_2, z_a_2, n=n_samples, seed=sim_seed) + if fig_eigen is not None: + st.plotly_chart(fig_eigen, use_container_width=True) + +# ----- Tab 3: Differential Analysis ----- +with tab3: + st.header("Differential Analysis vs. β") + st.markdown("This page shows the difference between the Upper (blue) and Lower (lightblue) z*(β) curves, along with their first and second derivatives with respect to β.") + col1, col2 = st.columns([1, 2]) + with col1: + z_a_diff = st.number_input("z_a", value=1.0, key="z_a_diff") + y_diff = st.number_input("y", value=1.0, key="y_diff") + z_min_diff = st.number_input("z_min", value=-10.0, key="z_min_diff") + z_max_diff = st.number_input("z_max", value=10.0, key="z_max_diff") + with st.expander("Resolution Settings"): + beta_steps_diff = st.slider("β steps", min_value=51, max_value=501, value=201, step=50, key="beta_steps_diff") + z_steps_diff = st.slider("z grid steps", min_value=1000, max_value=100000, value=50000, step=1000, key="z_steps_diff") + + # Add options for curve selection + st.subheader("Curves to Analyze") + analyze_upper_lower = st.checkbox("Upper-Lower Difference", value=True) + analyze_high_y = st.checkbox("High y Expression", value=False) + analyze_alt_low = st.checkbox("Alternate Low Expression", value=False) + + if st.button("Compute Differentials", key="tab3_button"): with col2: - format_type_display = st.selectbox( - "📦 Format", - options=list(EXPORT_FORMATS.keys()), - index=list(EXPORT_FORMATS.values()).index(st.session_state.settings["format_type"]) - if st.session_state.settings["format_type"] in EXPORT_FORMATS.values() else 0, - key="format_select_display" - ) - # Convert display name to actual format value - format_type = EXPORT_FORMATS[format_type_display] - - # Add FPS control - fps = st.selectbox( - "🎞️ FPS", - options=FPS_OPTIONS, - index=FPS_OPTIONS.index(st.session_state.settings["fps"]) if st.session_state.settings["fps"] in FPS_OPTIONS else 2, # Default to 30 FPS (index 2) - key="fps_select" - ) - - animation_speed = st.selectbox( - "⚡ Speed", - options=list(ANIMATION_SPEEDS.keys()), - index=list(ANIMATION_SPEEDS.keys()).index(st.session_state.settings["animation_speed"]), - key="speed_select" - ) - - # Apply the settings without requiring a button - st.session_state.settings = { - "quality": quality, - "format_type": format_type, - "animation_speed": animation_speed, - "fps": fps - } - - # Custom libraries section - st.markdown("## 📚 Custom Libraries") - st.markdown("Enter additional Python packages needed for your animations (comma-separated):") - - custom_libraries = st.text_area( - "Libraries to install", - placeholder="e.g., scipy, networkx, matplotlib", - key="custom_libraries" - ) - - if st.button("Install Libraries", key="install_libraries_btn"): - success, result = install_custom_packages(custom_libraries) - st.session_state.custom_library_result = result - - if success: - st.success("Installation complete!") - else: - st.error("Installation failed for some packages.") - - if st.session_state.custom_library_result: - with st.expander("Installation Results"): - st.code(st.session_state.custom_library_result) - - # EDITOR TAB - with tabs[0]: - col1, col2 = st.columns([3, 2]) - - with col1: - st.markdown("### 📝 Animation Editor") - - # Toggle between upload and type - editor_mode = st.radio( - "Choose how to input your code:", - ["Type Code", "Upload File"], - key="editor_mode" - ) - - if editor_mode == "Upload File": - uploaded_file = st.file_uploader("Upload Manim Python File", type=["py"], key="code_uploader") - if uploaded_file: - code_content = uploaded_file.getvalue().decode("utf-8") - if code_content.strip(): # Only update if file has content - st.session_state.code = code_content - st.session_state.temp_code = code_content - - # Code editor - if ACE_EDITOR_AVAILABLE: - current_code = st.session_state.code if hasattr(st.session_state, 'code') and st.session_state.code else "" - st.session_state.temp_code = st_ace( - value=current_code, - language="python", - theme="monokai", - min_lines=20, - key=f"ace_editor_{st.session_state.editor_key}" + betas_diff, lower_vals, upper_vals = sweep_beta_and_find_z_bounds(z_a_diff, y_diff, z_min_diff, z_max_diff, beta_steps_diff, z_steps_diff) + + # Create figure + fig_diff = go.Figure() + + if analyze_upper_lower: + diff_curve = upper_vals - lower_vals + d1 = np.gradient(diff_curve, betas_diff) + d2 = np.gradient(d1, betas_diff) + + fig_diff.add_trace(go.Scatter(x=betas_diff, y=diff_curve, mode="lines", + name="Upper-Lower Difference", line=dict(color="magenta", width=2))) + fig_diff.add_trace(go.Scatter(x=betas_diff, y=d1, mode="lines", + name="Upper-Lower d/dβ", line=dict(color="magenta", dash='dash'))) + fig_diff.add_trace(go.Scatter(x=betas_diff, y=d2, mode="lines", + name="Upper-Lower d²/dβ²", line=dict(color="magenta", dash='dot'))) + + if analyze_high_y: + high_y_curve = compute_high_y_curve(betas_diff, z_a_diff, y_diff) + d1 = np.gradient(high_y_curve, betas_diff) + d2 = np.gradient(d1, betas_diff) + + fig_diff.add_trace(go.Scatter(x=betas_diff, y=high_y_curve, mode="lines", + name="High y", line=dict(color="green", width=2))) + fig_diff.add_trace(go.Scatter(x=betas_diff, y=d1, mode="lines", + name="High y d/dβ", line=dict(color="green", dash='dash'))) + fig_diff.add_trace(go.Scatter(x=betas_diff, y=d2, mode="lines", + name="High y d²/dβ²", line=dict(color="green", dash='dot'))) + + if analyze_alt_low: + alt_low_curve = compute_alternate_low_expr(betas_diff, z_a_diff, y_diff) + d1 = np.gradient(alt_low_curve, betas_diff) + d2 = np.gradient(d1, betas_diff) + + fig_diff.add_trace(go.Scatter(x=betas_diff, y=alt_low_curve, mode="lines", + name="Alt Low", line=dict(color="orange", width=2))) + fig_diff.add_trace(go.Scatter(x=betas_diff, y=d1, mode="lines", + name="Alt Low d/dβ", line=dict(color="orange", dash='dash'))) + fig_diff.add_trace(go.Scatter(x=betas_diff, y=d2, mode="lines", + name="Alt Low d²/dβ²", line=dict(color="orange", dash='dot'))) + + fig_diff.update_layout( + title="Differential Analysis vs. β", + xaxis_title="β", + yaxis_title="Value", + hovermode="x unified", + showlegend=True, + legend=dict( + yanchor="top", + y=0.99, + xanchor="left", + x=0.01 ) - else: - current_code = st.session_state.code if hasattr(st.session_state, 'code') and st.session_state.code else "" - st.session_state.temp_code = st.text_area( - "Manim Python Code", - value=current_code, - height=400, - key=f"code_textarea_{st.session_state.editor_key}" - ) - - # Update code in session state if it changed - if st.session_state.temp_code != st.session_state.code: - st.session_state.code = st.session_state.temp_code - - # Generate button (use a form to prevent page reloads) - generate_btn = st.button("🚀 Generate Animation", use_container_width=True, key="generate_btn") - if generate_btn: - if not st.session_state.code: - st.error("Please enter some code before generating animation") - else: - # Extract scene class name - scene_class = extract_scene_class_name(st.session_state.code) - - # If no valid scene class found, add a basic one - if scene_class == "MyScene" and "class MyScene" not in st.session_state.code: - default_scene = """ -class MyScene(Scene): - def construct(self): - text = Text("Default Scene") - self.play(Write(text)) - self.wait(2) -""" - st.session_state.code += default_scene - st.session_state.temp_code = st.session_state.code - st.warning("No scene class found. Added a default scene.") - - with st.spinner("Generating animation..."): - video_data, status = generate_manim_video( - st.session_state.code, - st.session_state.settings["format_type"], - st.session_state.settings["quality"], - ANIMATION_SPEEDS[st.session_state.settings["animation_speed"]], - st.session_state.audio_path, - st.session_state.settings["fps"] - ) - st.session_state.video_data = video_data - st.session_state.status = status - - with col2: - st.markdown("### 🖥️ Preview & Output") - - # Preview container - if st.session_state.code: - with st.container(): - st.markdown("
", unsafe_allow_html=True) - preview_html = generate_manim_preview(st.session_state.code) - components.html(preview_html, height=250) - st.markdown("
", unsafe_allow_html=True) - - # Generated output display - if st.session_state.video_data: - # Different handling based on format type - format_type = st.session_state.settings["format_type"] - - if format_type == "png_sequence": - st.info("PNG sequence generated successfully. Use the download button to get the ZIP file.") - - # Add download button for ZIP - st.download_button( - label="⬇️ Download PNG Sequence (ZIP)", - data=st.session_state.video_data, - file_name=f"manim_pngs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip", - mime="application/zip", - use_container_width=True - ) - elif format_type == "svg": - # Display SVG preview - try: - svg_data = st.session_state.video_data.decode('utf-8') - components.html(svg_data, height=400) - except Exception as e: - st.error(f"Error displaying SVG: {str(e)}") - - # Download button for SVG - st.download_button( - label="⬇️ Download SVG", - data=st.session_state.video_data, - file_name=f"manim_animation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.svg", - mime="image/svg+xml", - use_container_width=True - ) - else: - # Standard video display for MP4, GIF, WebM - try: - st.video(st.session_state.video_data, format=format_type) - except Exception as e: - st.error(f"Error displaying video: {str(e)}") - # Fallback for GIF if st.video fails - if format_type == "gif": - st.markdown("GIF preview:") - gif_b64 = base64.b64encode(st.session_state.video_data).decode() - st.markdown(f'animation', unsafe_allow_html=True) - - # Add download button - st.download_button( - label=f"⬇️ Download {format_type.upper()}", - data=st.session_state.video_data, - file_name=f"manim_animation_{datetime.now().strftime('%Y%m%d_%H%M%S')}.{format_type}", - mime=f"{'image' if format_type == 'gif' else 'video'}/{format_type}", - use_container_width=True - ) - - if st.session_state.status: - if "Error" in st.session_state.status: - st.error(st.session_state.status) - - # Show troubleshooting tips - with st.expander("🔍 Troubleshooting Tips"): - st.markdown(""" - ### Common Issues: - 1. **Syntax Errors**: Check your Python code for any syntax issues - 2. **Missing Scene Class**: Ensure your code contains a scene class that extends Scene - 3. **High Resolution Issues**: Try a lower quality preset for complex animations - 4. **Memory Issues**: For 4K animations, reduce complexity or try again - 5. **Format Issues**: Some formats require specific Manim configurations - 6. **GIF Generation**: If GIF doesn't work, try MP4 and we'll convert it automatically - - ### Example Code: - ```python - from manim import * - - class MyScene(Scene): - def construct(self): - circle = Circle(color=RED) - self.play(Create(circle)) - self.wait(1) - ``` - """) - else: - st.success(st.session_state.status) - - # AI ASSISTANT TAB - with tabs[1]: - st.markdown("### 🤖 AI Animation Assistant") - - # Check password before allowing access - if check_password(): - # Debug section - with st.expander("🔧 Debug Connection"): - st.markdown("Test the AI model connection directly") - - if st.button("Test API Connection", key="test_api_btn"): - with st.spinner("Testing API connection..."): - try: - # Get token from secrets - token = get_secret("github_token_api") - if not token: - st.error("GitHub token not found in secrets") - st.stop() - - # Get model details - model_name = st.session_state.custom_model - config = MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["default"]) - category = config.get("category", "Other") - - if category == "OpenAI": - # Use OpenAI client for GitHub AI models - try: - from openai import OpenAI - except ImportError: - st.error("OpenAI package not installed. Please run 'pip install openai'") - st.stop() - - # Create OpenAI client with GitHub AI endpoint - client = OpenAI( - base_url="https://models.github.ai/inference", - api_key=token, - ) - - # For GitHub AI models, ensure the model_name includes the publisher - # If it doesn't have a publisher prefix, add "openai/" - if "/" not in model_name: - full_model_name = f"openai/{model_name}" - st.info(f"Using full model name: {full_model_name}") - else: - full_model_name = model_name - - # Prepare parameters based on model configuration - params = { - "messages": [ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Hello, this is a connection test."} - ], - "model": full_model_name - } - - # Add appropriate token parameter - token_param = config["param_name"] - params[token_param] = config[token_param] - - # Make API call - response = client.chat.completions.create(**params) - - # Check if response is valid - if response and response.choices and len(response.choices) > 0: - test_response = response.choices[0].message.content - st.success(f"✅ Connection successful! Response: {test_response[:50]}...") - - # Save working connection to session state - st.session_state.ai_models = { - "openai_client": client, - "model_name": full_model_name, # Store the full model name - "endpoint": "https://models.github.ai/inference", - "last_loaded": datetime.now().isoformat(), - "category": category - } - else: - st.error("❌ API returned an empty response") - - elif category == "Azure" or category in ["DeepSeek", "Meta", "Microsoft", "Mistral", "Other"]: - # Use Azure client for Azure API models - try: - from azure.ai.inference import ChatCompletionsClient - from azure.ai.inference.models import SystemMessage, UserMessage - from azure.core.credentials import AzureKeyCredential - except ImportError: - st.error("Azure AI packages not installed. Please run 'pip install azure-ai-inference azure-core'") - st.stop() - - # Define endpoint - endpoint = "https://models.inference.ai.azure.com" - - # Prepare API parameters - messages = [UserMessage("Hello, this is a connection test.")] - api_params, config = prepare_api_params(messages, model_name) - - # Create client with appropriate API version - api_version = config.get("api_version") - if api_version: - client = ChatCompletionsClient( - endpoint=endpoint, - credential=AzureKeyCredential(token), - api_version=api_version - ) - else: - client = ChatCompletionsClient( - endpoint=endpoint, - credential=AzureKeyCredential(token), - ) - - # Test with the prepared parameters - response = client.complete(**api_params) - - # Check if response is valid - if response and response.choices and len(response.choices) > 0: - test_response = response.choices[0].message.content - st.success(f"✅ Connection successful! Response: {test_response[:50]}...") - - # Save working connection to session state - st.session_state.ai_models = { - "client": client, - "model_name": model_name, - "endpoint": endpoint, - "last_loaded": datetime.now().isoformat(), - "category": category, - "api_version": api_version - } - else: - st.error("❌ API returned an empty response") - - else: - st.error(f"Unsupported model category: {category}") - - except ImportError as ie: - st.error(f"Module import error: {str(ie)}") - st.info("Try installing required packages: openai, azure-ai-inference and azure-core") - except Exception as e: - st.error(f"❌ API test failed: {str(e)}") - import traceback - st.code(traceback.format_exc()) - - # Model selection with enhanced UI - st.markdown("### 🤖 Model Selection") - st.markdown("Select an AI model for generating animation code:") - - # Group models by category for better organization - model_categories = {} - for model_name in MODEL_CONFIGS: - if model_name != "default": - category = MODEL_CONFIGS[model_name].get("category", "Other") - if category not in model_categories: - model_categories[category] = [] - model_categories[category].append(model_name) - - # Create tabbed interface for model categories - category_tabs = st.tabs(sorted(model_categories.keys())) - - for i, category in enumerate(sorted(model_categories.keys())): - with category_tabs[i]: - for model_name in sorted(model_categories[category]): - config = MODEL_CONFIGS[model_name] - is_selected = model_name == st.session_state.custom_model - warning = config.get("warning") - - # Create styled card for each model - warning_html = f'

⚠️ {warning}

' if warning else "" - - st.markdown(f""" -
-

{model_name}

-
-

Max Tokens: {config.get(config['param_name'], 'Unknown')}

-

Category: {config['category']}

-

API Version: {config['api_version'] if config['api_version'] else 'Default'}

- {warning_html} -
-
- """, unsafe_allow_html=True) - - # Button to select this model - button_label = "Selected ✓" if is_selected else "Select Model" - if st.button(button_label, key=f"model_{model_name}", disabled=is_selected): - st.session_state.custom_model = model_name - if st.session_state.ai_models and 'model_name' in st.session_state.ai_models: - st.session_state.ai_models['model_name'] = model_name - st.rerun() - - # Display current model selection - st.info(f"🤖 **Currently using: {st.session_state.custom_model}**") - - # Add a refresh button to update model connection - if st.button("🔄 Refresh Model Connection", key="refresh_model_connection"): - if st.session_state.ai_models and 'client' in st.session_state.ai_models: - try: - # Test connection with minimal prompt - from azure.ai.inference.models import UserMessage - model_name = st.session_state.custom_model - - # Prepare parameters - messages = [UserMessage("Hello")] - api_params, config = prepare_api_params(messages, model_name) - - # Check if we need a new client with specific API version - if config["api_version"] and config["api_version"] != st.session_state.ai_models.get("api_version"): - # Create version-specific client if needed - token = get_secret("github_token_api") - from azure.ai.inference import ChatCompletionsClient - from azure.core.credentials import AzureKeyCredential - - client = ChatCompletionsClient( - endpoint=st.session_state.ai_models["endpoint"], - credential=AzureKeyCredential(token), - api_version=config["api_version"] - ) - response = client.complete(**api_params) - - # Update session state with the new client - st.session_state.ai_models["client"] = client - st.session_state.ai_models["api_version"] = config["api_version"] - else: - response = st.session_state.ai_models["client"].complete(**api_params) - - st.success(f"✅ Connection to {model_name} successful!") - st.session_state.ai_models["model_name"] = model_name - - except Exception as e: - st.error(f"❌ Connection error: {str(e)}") - st.info("Please try the Debug Connection section to re-initialize the API connection.") - - # AI code generation - if st.session_state.ai_models and "client" in st.session_state.ai_models: - st.markdown("
", unsafe_allow_html=True) - st.markdown("#### Generate Animation from Description") - st.write("Describe the animation you want to create, or provide partial code to complete.") - - # Predefined animation ideas dropdown - animation_ideas = [ - "Select an idea...", - "Create a 3D animation showing a sphere morphing into a torus", - "Show a visual proof of the Pythagorean theorem", - "Visualize a Fourier transform converting a signal from time domain to frequency domain", - "Create an animation explaining neural network forward propagation", - "Illustrate the concept of integration with area under a curve" - ] - - selected_idea = st.selectbox( - "Try one of these ideas", - options=animation_ideas - ) - - prompt_value = selected_idea if selected_idea != "Select an idea..." else "" - - code_input = st.text_area( - "Your Prompt or Code", - value=prompt_value, - placeholder="Example: Create an animation that shows a circle morphing into a square while changing color from red to blue", - height=150 - ) - - if st.button("Generate Animation Code", key="gen_ai_code"): - if code_input: - with st.spinner("AI is generating your animation code..."): - try: - # Get the client and model name - client = st.session_state.ai_models["client"] - model_name = st.session_state.ai_models["model_name"] - - # Create the prompt - prompt = f"""Write a complete Manim animation scene based on this code or idea: - {code_input} - - The code should be a complete, working Manim animation that includes: - - Proper Scene class definition - - Constructor with animations - - Proper use of self.play() for animations - - Proper wait times between animations - - Here's the complete Manim code: - """ - - # Prepare API parameters - from azure.ai.inference.models import UserMessage - messages = [UserMessage(prompt)] - api_params, config = prepare_api_params(messages, model_name) - - # Make the API call with proper parameters - response = client.complete(**api_params) - - # Process the response - if response and response.choices and len(response.choices) > 0: - completed_code = response.choices[0].message.content - - # Extract code from markdown if present - if "```python" in completed_code: - completed_code = completed_code.split("```python")[1].split("```")[0] - elif "```" in completed_code: - completed_code = completed_code.split("```")[1].split("```")[0] - - # Add Scene class if missing - if "Scene" not in completed_code: - completed_code = f"""from manim import * - - class MyScene(Scene): - def construct(self): - {completed_code}""" - - # Store the generated code - st.session_state.generated_code = completed_code - else: - st.error("Failed to generate code. API returned an empty response.") - except Exception as e: - st.error(f"Error generating code: {str(e)}") - import traceback - st.code(traceback.format_exc()) - else: - st.warning("Please enter a description or prompt first") - - - # AI generated code display and actions - if "generated_code" in st.session_state and st.session_state.generated_code: - st.markdown("
", unsafe_allow_html=True) - st.markdown("#### Generated Animation Code") - st.code(st.session_state.generated_code, language="python") - - col_ai1, col_ai2 = st.columns(2) - with col_ai1: - if st.button("Use This Code", key="use_gen_code"): - st.session_state.code = st.session_state.generated_code - st.session_state.temp_code = st.session_state.generated_code - # Set pending tab switch to editor tab - st.session_state.pending_tab_switch = 0 - st.rerun() - - with col_ai2: - if st.button("Render Preview", key="render_preview"): - with st.spinner("Rendering preview..."): - video_data, status = generate_manim_video( - st.session_state.generated_code, - "mp4", - "480p", # Use lowest quality for preview - ANIMATION_SPEEDS["Normal"], - fps=st.session_state.settings["fps"] - ) - - if video_data: - st.video(video_data) - st.download_button( - label="Download Preview", - data=video_data, - file_name=f"manim_preview_{int(time.time())}.mp4", - mime="video/mp4" - ) - else: - st.error(f"Failed to generate preview: {status}") - st.markdown("
", unsafe_allow_html=True) - else: - st.warning("AI models not initialized. Please use the Debug Connection section to test API connectivity.") - else: - st.info("Please enter the correct password to access AI features") - - # ASSETS TAB - with tabs[2]: - st.markdown("### 🎨 Asset Management") - - asset_col1, asset_col2 = st.columns([1, 1]) - - with asset_col1: - # Image uploader section - st.markdown("#### 📸 Image Assets") - st.markdown("Upload images to use in your animations:") - - # Allow multiple image uploads - uploaded_images = st.file_uploader( - "Upload Images", - type=["jpg", "png", "jpeg", "svg"], - accept_multiple_files=True, - key="image_uploader_tab" ) + st.plotly_chart(fig_diff, use_container_width=True) - if uploaded_images: - # Create a unique image directory if it doesn't exist - image_dir = os.path.join(os.getcwd(), "manim_assets", "images") - os.makedirs(image_dir, exist_ok=True) - - # Process each uploaded image - for uploaded_image in uploaded_images: - # Generate a unique filename and save the image - file_extension = uploaded_image.name.split(".")[-1] - unique_filename = f"image_{int(time.time())}_{uuid.uuid4().hex[:8]}.{file_extension}" - image_path = os.path.join(image_dir, unique_filename) - - with open(image_path, "wb") as f: - f.write(uploaded_image.getvalue()) - - # Store the path in session state - if "image_paths" not in st.session_state: - st.session_state.image_paths = [] - - # Check if this image was already added - image_already_added = False - for img in st.session_state.image_paths: - if img["name"] == uploaded_image.name: - image_already_added = True - break - - if not image_already_added: - st.session_state.image_paths.append({ - "name": uploaded_image.name, - "path": image_path - }) - - # Display uploaded images in a grid - st.markdown("##### Uploaded Images:") - image_cols = st.columns(3) - - for i, img_info in enumerate(st.session_state.image_paths[-len(uploaded_images):]): - with image_cols[i % 3]: - try: - img = Image.open(img_info["path"]) - st.image(img, caption=img_info["name"], width=150) - - # Show code snippet for this specific image - if st.button(f"Use {img_info['name']}", key=f"use_img_{i}"): - image_code = f""" -# Load and display image -image = ImageMobject(r"{img_info['path']}") -image.scale(2) # Adjust size as needed -self.play(FadeIn(image)) -self.wait(1) -""" - if not st.session_state.code: - base_code = """from manim import * - -class ImageScene(Scene): - def construct(self): -""" - st.session_state.code = base_code + "\n " + image_code.replace("\n", "\n ") - else: - st.session_state.code += "\n" + image_code - - st.session_state.temp_code = st.session_state.code - st.success(f"Added {img_info['name']} to your code!") - - # Set pending tab switch to editor tab - st.session_state.pending_tab_switch = 0 - st.rerun() - except Exception as e: - st.error(f"Error loading image {img_info['name']}: {e}") - - # Display previously uploaded images - if st.session_state.image_paths: - with st.expander("Previously Uploaded Images"): - # Group images by 3 in each row - for i in range(0, len(st.session_state.image_paths), 3): - prev_cols = st.columns(3) - for j in range(3): - if i+j < len(st.session_state.image_paths): - img_info = st.session_state.image_paths[i+j] - with prev_cols[j]: - try: - img = Image.open(img_info["path"]) - st.image(img, caption=img_info["name"], width=100) - st.markdown(f"
Path: {img_info['path']}
", unsafe_allow_html=True) - except: - st.markdown(f"**{img_info['name']}**") - st.markdown(f"
Path: {img_info['path']}
", unsafe_allow_html=True) - - with asset_col2: - # Audio uploader section - st.markdown("#### 🎵 Audio Assets") - st.markdown("Upload audio files for background or narration:") - - uploaded_audio = st.file_uploader("Upload Audio", type=["mp3", "wav", "ogg"], key="audio_uploader") - - if uploaded_audio: - # Create a unique audio directory if it doesn't exist - audio_dir = os.path.join(os.getcwd(), "manim_assets", "audio") - os.makedirs(audio_dir, exist_ok=True) - - # Generate a unique filename and save the audio - file_extension = uploaded_audio.name.split(".")[-1] - unique_filename = f"audio_{int(time.time())}.{file_extension}" - audio_path = os.path.join(audio_dir, unique_filename) - - with open(audio_path, "wb") as f: - f.write(uploaded_audio.getvalue()) - - # Store the path in session state - st.session_state.audio_path = audio_path - - # Display audio player - st.audio(uploaded_audio) - - st.markdown(f""" -
-

Audio: {uploaded_audio.name}

-

Path: {audio_path}

-
- """, unsafe_allow_html=True) - - # Two options for audio usage - st.markdown("#### Add Audio to Your Animation") - - option = st.radio( - "Choose how to use audio:", - ["Background Audio", "Generate Audio from Text"] - ) - - if option == "Background Audio": - st.markdown("##### Code to add background audio:") - - # For with_sound decorator - audio_code1 = f""" -# Add this import at the top of your file -from manim.scene.scene_file_writer import SceneFileWriter - -# Add this decorator before your scene class -@with_sound("{audio_path}") -class YourScene(Scene): - def construct(self): - # Your animation code here -""" - st.code(audio_code1, language="python") - - if st.button("Use This Audio in Animation", key="use_audio_btn"): - st.success("Audio set for next render!") - - elif option == "Generate Audio from Text": - # Text-to-speech input - tts_text = st.text_area( - "Enter text for narration", - placeholder="Type the narration text here...", - height=100 - ) - - if st.button("Create Narration", key="create_narration_btn"): - try: - # Use basic TTS (placeholder for actual implementation) - st.warning("Text-to-speech feature requires additional setup. Using uploaded audio instead.") - st.session_state.audio_path = audio_path - st.success("Audio set for next render!") - except Exception as e: - st.error(f"Error creating narration: {str(e)}") - - # TIMELINE EDITOR TAB - with tabs[3]: - # New code for reordering animation steps - updated_code = create_timeline_editor(st.session_state.code) - - # If code was modified by the timeline editor, update the session state - if updated_code != st.session_state.code: - st.session_state.code = updated_code - st.session_state.temp_code = updated_code - - # EDUCATIONAL EXPORT TAB - with tabs[4]: - st.markdown("### 🎓 Educational Export Options") - - # Check if we have an animation to export - if not st.session_state.video_data: - st.warning("Generate an animation first before using educational export features.") - else: - st.markdown("Create various educational assets from your animation:") - - # Animation title and explanation - animation_title = st.text_input("Animation Title", value="Manim Animation", key="edu_title") - - st.markdown("#### Explanation Text") - st.markdown("Add explanatory text to accompany your animation. Use markdown formatting.") - st.markdown("Use ## to separate explanation sections for step-by-step sequence export.") - - explanation_text = st.text_area( - "Explanation (markdown supported)", - height=150, - placeholder="Explain your animation here...\n\n## Step 1\nIntroduction to the concept...\n\n## Step 2\nNext, we demonstrate..." - ) - - # Export format selection - edu_format = st.selectbox( - "Export Format", - options=["PowerPoint Presentation", "Interactive HTML", "Explanation Sequence PDF"] - ) - - # Format-specific options - if edu_format == "PowerPoint Presentation": - st.info("Creates a PowerPoint file with your animation and explanation text.") - - elif edu_format == "Interactive HTML": - st.info("Creates an interactive HTML webpage with playback controls and explanation.") - include_controls = st.checkbox("Include interactive controls", value=True) - - elif edu_format == "Explanation Sequence PDF": - st.info("Creates a PDF with key frames and step-by-step explanations.") - frame_count = st.slider("Number of key frames", min_value=3, max_value=10, value=5) - - # Export button - if st.button("Export Educational Material", key="export_edu_btn"): - with st.spinner(f"Creating {edu_format}..."): - # Map selected format to internal format type - format_map = { - "PowerPoint Presentation": "powerpoint", - "Interactive HTML": "html", - "Explanation Sequence PDF": "sequence" - } - - # Create a temporary directory for export - temp_export_dir = tempfile.mkdtemp(prefix="manim_edu_export_") - - # Process the export - exported_data, file_type = export_to_educational_format( - st.session_state.video_data, - format_map[edu_format], - animation_title, - explanation_text, - temp_export_dir - ) - - if exported_data: - # File extension mapping - ext_map = { - "powerpoint": "pptx", - "html": "html", - "pdf": "pdf" - } - - # Download button - ext = ext_map.get(file_type, "zip") - filename = f"{animation_title.replace(' ', '_')}.{ext}" - - st.success(f"{edu_format} created successfully!") - st.download_button( - label=f"⬇️ Download {edu_format}", - data=exported_data, - file_name=filename, - mime=f"application/{ext}", - use_container_width=True - ) - - # For HTML, also offer to open in browser - if file_type == "html": - html_path = os.path.join(temp_export_dir, filename) - st.markdown(f"[🌐 Open in browser](file://{html_path})", unsafe_allow_html=True) - else: - st.error(f"Failed to create {edu_format}. Check logs for details.") - - # Show usage examples and tips - with st.expander("Usage Tips"): - st.markdown(""" - ### Educational Export Tips - - **PowerPoint Presentations** - - Great for lectures and classroom presentations - - Animation will autoplay when clicked - - Add detailed explanations in notes section - - **Interactive HTML** - - Perfect for websites and online learning platforms - - Students can control playback speed and navigation - - Mobile-friendly for learning on any device - - **Explanation Sequence** - - Ideal for printed materials and study guides - - Use ## headers to mark different explanation sections - - Each section will be paired with a key frame - """) - - # PYTHON RUNNER TAB - with tabs[5]: - st.markdown("### 🐍 Python Script Runner") - st.markdown("Execute Python scripts and visualize the results directly.") - - # Predefined example scripts - example_scripts = { - "Select an example...": "", - "Basic Matplotlib Plot": """import matplotlib.pyplot as plt -import numpy as np - -# Create data -x = np.linspace(0, 10, 100) -y = np.sin(x) - -# Create plot -plt.figure(figsize=(10, 6)) -plt.plot(x, y, 'b-', label='sin(x)') -plt.title('Sine Wave') -plt.xlabel('x') -plt.ylabel('sin(x)') -plt.grid(True) -plt.legend() -""", - "User Input Example": """# This example demonstrates how to handle user input -name = input("Enter your name: ") -age = int(input("Enter your age: ")) - -print(f"Hello, {name}! In 10 years, you'll be {age + 10} years old.") - -# Let's get some numbers and calculate the average -num_count = int(input("How many numbers would you like to average? ")) -total = 0 - -for i in range(num_count): - num = float(input(f"Enter number {i+1}: ")) - total += num - -average = total / num_count -print(f"The average of your {num_count} numbers is: {average}") -""", - "Pandas DataFrame": """import pandas as pd -import numpy as np - -# Create a sample dataframe -data = { - 'Name': ['Alice', 'Bob', 'Charlie', 'David', 'Emma'], - 'Age': [25, 30, 35, 40, 45], - 'Salary': [50000, 60000, 70000, 80000, 90000], - 'Department': ['HR', 'IT', 'Finance', 'Marketing', 'Engineering'] -} - -df = pd.DataFrame(data) - -# Display the dataframe -print("Sample DataFrame:") -print(df) - -# Basic statistics -print("\\nSummary Statistics:") -print(df.describe()) - -# Filtering -print("\\nEmployees older than 30:") -print(df[df['Age'] > 30]) -""", - "Seaborn Visualization": """import matplotlib.pyplot as plt -import seaborn as sns -import numpy as np -import pandas as pd - -# Set the style -sns.set_style("whitegrid") - -# Create sample data -np.random.seed(42) -data = np.random.randn(100, 3) -df = pd.DataFrame(data, columns=['A', 'B', 'C']) -df['category'] = pd.Categorical(['Group 1'] * 50 + ['Group 2'] * 50) - -# Create a paired plot -sns.pairplot(df, hue='category', palette='viridis') - -# Create another plot -plt.figure(figsize=(10, 6)) -sns.violinplot(x='category', y='A', data=df, palette='magma') -plt.title('Distribution of A by Category') -""" - } - - # Select example script - selected_example = st.selectbox("Select an example script:", options=list(example_scripts.keys())) - - # Python code editor - if selected_example != "Select an example..." and selected_example in example_scripts: - python_code = example_scripts[selected_example] - else: - python_code = st.session_state.python_script - - if ACE_EDITOR_AVAILABLE: - python_code = st_ace( - value=python_code, - language="python", - theme="monokai", - min_lines=15, - key=f"python_editor_{st.session_state.editor_key}" - ) - else: - python_code = st.text_area( - "Python Code", - value=python_code, - height=400, - key=f"python_textarea_{st.session_state.editor_key}" - ) - - # Store script in session state (without clearing existing code) - st.session_state.python_script = python_code - - # Check for input() calls - input_calls = detect_input_calls(python_code) - user_inputs = [] - - if input_calls: - st.markdown("### Input Values") - st.info(f"This script contains {len(input_calls)} input() calls. Please provide values below:") - - for i, input_call in enumerate(input_calls): - user_input = st.text_input( - f"{input_call['prompt']} (Line {input_call['line']})", - key=f"input_{i}" - ) - user_inputs.append(user_input) - - # Options and execution - col1, col2 = st.columns([2, 1]) - - with col1: - timeout_seconds = st.slider("Execution Timeout (seconds)", 5, 3600, 30) - - with col2: - run_btn = st.button("▶️ Run Script", use_container_width=True) - - if run_btn: - with st.spinner("Executing Python script..."): - result = run_python_script(python_code, inputs=user_inputs, timeout=timeout_seconds) - st.session_state.python_result = result - - # Display results - if st.session_state.python_result: - display_python_script_results(st.session_state.python_result) - - # Option to insert plots into Manim animation - if st.session_state.python_result["plots"]: - with st.expander("Add Plots to Manim Animation"): - st.markdown("Select a plot to include in your Manim animation:") - - plot_cols = st.columns(min(3, len(st.session_state.python_result["plots"]))) - - for i, plot_data in enumerate(st.session_state.python_result["plots"]): - # Create a unique temporary file for each plot - with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp: - tmp.write(plot_data) - plot_path = tmp.name - - # Display the plot with selection button - with plot_cols[i % len(plot_cols)]: - st.image(plot_data, use_column_width=True) - if st.button(f"Use Plot {i+1}", key=f"use_plot_{i}"): - # Create code to include this plot in Manim - plot_code = f""" -# Import the plot image -plot_image = ImageMobject(r"{plot_path}") -plot_image.scale(2) # Adjust size as needed -self.play(FadeIn(plot_image)) -self.wait(1) -""" - # Insert into editor code - if st.session_state.code: - st.session_state.code += "\n" + plot_code - st.session_state.temp_code = st.session_state.code - st.success(f"Plot {i+1} added to your animation code!") - # Set pending tab switch to editor tab - st.session_state.pending_tab_switch = 0 - st.rerun() - else: - basic_scene = f"""from manim import * - -class PlotScene(Scene): - def construct(self): - {plot_code} -""" - st.session_state.code = basic_scene - st.session_state.temp_code = basic_scene - st.success(f"Created new scene with Plot {i+1}!") - # Set pending tab switch to editor tab - st.session_state.pending_tab_switch = 0 - st.rerun() - - # Provide option to save the script - if st.button("📄 Save This Script", key="save_script_btn"): - # Generate a unique filename - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - script_filename = f"script_{timestamp}.py" - - # Offer download button for the script - st.download_button( - label="⬇️ Download Script", - data=python_code, - file_name=script_filename, - mime="text/plain" - ) - - # Show advanced examples and tips - with st.expander("Python Script Runner Tips"): st.markdown(""" - ### Python Script Runner Tips - - **What can I run?** - - Any Python code that doesn't require direct UI interaction - - Libraries like Matplotlib, NumPy, Pandas, SciPy, etc. - - Data processing and visualization code - - Scripts that ask for user input (now supported!) - - **What can't I run?** - - Streamlit, Gradio, Dash, or other web UIs - - Long-running operations (timeout will occur) - - Code that requires file access outside the temporary environment - - **Working with visualizations:** - - All Matplotlib/Seaborn plots will be automatically captured - - Pandas DataFrames are detected and displayed as tables - - Use `print()` to show text output - - **Handling user input:** - - The app detects input() calls and automatically creates text fields - - Input values you provide will be passed to the script when it runs - - Type conversion (like int(), float()) is preserved - - **Adding to animations:** - - Charts and plots can be directly added to your Manim animations - - Generated images will be properly scaled for your animation - - Perfect for educational content combining data and animations - """) - - # Help section - with st.sidebar.expander("ℹ️ Help & Info"): - st.markdown(""" - ### About Manim Animation Studio - - This app allows you to create mathematical animations using Manim, - an animation engine for explanatory math videos. - - ### Example Code - - ```python - from manim import * - - class SimpleExample(Scene): - def construct(self): - circle = Circle(color=BLUE) - self.play(Create(circle)) - - square = Square(color=RED).next_to(circle, RIGHT) - self.play(Create(square)) - - text = Text("Manim Animation").next_to(VGroup(circle, square), DOWN) - self.play(Write(text)) - - self.wait(2) - ``` - """) - - # Handle tab switching with session state to prevent refresh loop - if st.session_state.pending_tab_switch is not None: - st.session_state.active_tab = st.session_state.pending_tab_switch - st.session_state.pending_tab_switch = None - - # Set tabs active state - for i, tab in enumerate(tabs): - if i == st.session_state.active_tab: - tab.active = True - - # Mark first load as complete to prevent unnecessary refreshes - if not st.session_state.first_load_complete: - st.session_state.first_load_complete = True - -if __name__ == "__main__": - main() \ No newline at end of file + ### Curve Types + - Solid lines: Original curves + - Dashed lines: First derivatives (d/dβ) + - Dotted lines: Second derivatives (d²/dβ²) + """) \ No newline at end of file