diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -11,54 +11,70 @@ import sys import importlib.util import time from datetime import timedelta -import mpmath +import mpmath # Direct import instead of through sympy + +# Set high precision for mpmath calculations +mpmath.mp.dps = 50 # 50 digits of precision + +# Initialize session state for persistent UI elements +if 'initialized' not in st.session_state: + st.session_state.initialized = True + st.session_state.progress_containers = {} + st.session_state.status_texts = {} + st.session_state.progress_values = {} + st.session_state.start_times = {} + # Configure Streamlit for Hugging Face Spaces st.set_page_config( page_title="Cubic Root Analysis (C++ Accelerated)", layout="wide", initial_sidebar_state="collapsed" ) -# Set decimal precision to 50 digits -mpmath.mp.dps = 50 -# Create a class for advanced progress tracking + +# Create containers for main UI elements that should persist +header_container = st.empty() +subtitle_container = st.empty() +cpp_status_container = st.empty() + +# Display header - won't be refreshed later +with header_container: + st.title("Cubic Root Analysis (C++ Accelerated)") + +with subtitle_container: + st.markdown("Analyze cubic equations with high-precision calculations and visualizations") + + +# Advanced Progress Bar Class that persists across refreshes class AdvancedProgressBar: - def __init__(self, total_steps, description="Processing", auto_refresh=True): + def __init__(self, key, total_steps, description="Processing"): + self.key = key self.total_steps = total_steps - self.current_step = 0 - self.start_time = time.time() self.description = description - self.auto_refresh = auto_refresh - - # Create UI elements - self.status_container = st.empty() - self.progress_bar = st.progress(0) - self.metrics_cols = st.columns(4) - self.step_metric = self.metrics_cols[0].empty() - self.percent_metric = self.metrics_cols[1].empty() - self.elapsed_metric = self.metrics_cols[2].empty() - self.eta_metric = self.metrics_cols[3].empty() - # Initialize with starting values - self.update_status(f"Starting {self.description}...") - self.update(0) + # Initialize in session state if not present + if key not in st.session_state.progress_containers: + st.session_state.progress_containers[key] = st.empty() + st.session_state.status_texts[key] = st.empty() + st.session_state.progress_values[key] = 0 + st.session_state.start_times[key] = time.time() def update(self, step=None, description=None): if step is not None: - self.current_step = step + st.session_state.progress_values[self.key] = step else: - self.current_step += 1 + st.session_state.progress_values[self.key] += 1 if description: self.description = description # Calculate progress percentage - progress = min(self.current_step / self.total_steps, 1.0) + progress = min(st.session_state.progress_values[self.key] / self.total_steps, 1.0) # Update progress bar - self.progress_bar.progress(progress) + st.session_state.progress_containers[self.key].progress(progress) - # Update metrics - elapsed = time.time() - self.start_time + # Calculate time metrics + elapsed = time.time() - st.session_state.start_times[self.key] elapsed_str = str(timedelta(seconds=int(elapsed))) if progress > 0: @@ -66,54 +82,41 @@ class AdvancedProgressBar: eta_str = str(timedelta(seconds=int(eta))) else: eta_str = "Calculating..." - - step_text = f"{self.current_step}/{self.total_steps}" - percent_text = f"{progress*100:.1f}%" - - self.step_metric.metric("Steps", step_text) - self.percent_metric.metric("Progress", percent_text) - self.elapsed_metric.metric("Elapsed", elapsed_str) - self.eta_metric.metric("ETA", eta_str) # Update status text - self.update_status(f"{self.description} - Step {self.current_step} of {self.total_steps}") - - def update_status(self, text): - self.status_container.text(text) - - def complete(self, success=True): - if success: - self.progress_bar.progress(1.0) - self.update_status(f"✅ {self.description} completed successfully!") - else: - self.update_status(f"❌ {self.description} failed or was interrupted.") - - elapsed = time.time() - self.start_time + status_text = (f"{self.description} - Step {st.session_state.progress_values[self.key]} of {self.total_steps} " + f"({progress*100:.1f}%) | Elapsed: {elapsed_str} | ETA: {eta_str}") + st.session_state.status_texts[self.key].info(status_text) + + def complete(self): + # Mark as completed + st.session_state.progress_containers[self.key].progress(1.0) + elapsed = time.time() - st.session_state.start_times[self.key] elapsed_str = str(timedelta(seconds=int(elapsed))) - self.elapsed_metric.metric("Total Time", elapsed_str) - self.eta_metric.metric("ETA", "Completed") - - # Add small delay to show completion - time.sleep(0.5) + st.session_state.status_texts[self.key].success(f"✅ {self.description} completed in {elapsed_str}") def clear(self): - self.status_container.empty() - self.progress_bar.empty() - self.step_metric.empty() - self.percent_metric.empty() - self.elapsed_metric.empty() - self.eta_metric.empty() + # Remove from session state + if self.key in st.session_state.progress_containers: + st.session_state.progress_containers[self.key].empty() + st.session_state.status_texts[self.key].empty() + del st.session_state.progress_containers[self.key] + del st.session_state.status_texts[self.key] + del st.session_state.progress_values[self.key] + del st.session_state.start_times[self.key] -# Initialize sympy precision settings for higher accuracy -# With these lines: -import mpmath -mpmath.mp.dps = 50 # Set decimal precision to 50 digits -# Check if C++ module is already compiled, otherwise compile it -cpp_compiled = False +def add_sqrt_support(expr_str): + """Replace 'sqrt(' with 'sp.sqrt(' for sympy compatibility""" + return expr_str.replace('sqrt(', 'sp.sqrt(') + +# C++ Module Compilation Function +@st.cache_resource def compile_cpp_module(): - progress = AdvancedProgressBar(5, "Compiling C++ module") + """Compile C++ acceleration module once and cache it""" + # Create a unique progress tracker for compilation + progress = AdvancedProgressBar("cpp_compilation", 5, "Compiling C++ acceleration module") # Define C++ code as a string cpp_code = """ @@ -132,7 +135,7 @@ def compile_cpp_module(): namespace py = pybind11; using namespace Eigen; - // Fast discriminant computation function + // Fast discriminant computation function using standard cubic form double compute_discriminant_fast(double z, double beta, double z_a, double y) { double a = z * z_a; double b = z * z_a + z + z_a - z_a*y; @@ -217,6 +220,7 @@ def compile_cpp_module(): } else if (f1 * f2 < 0) { // Binary search for more accurate root double zl = z_grid[i], zr = z_grid[i+1]; + double fl = f1, fr = f2; for (int j = 0; j < 50; j++) { double mid = 0.5 * (zl + zr); double fm = compute_discriminant_fast(mid, beta, z_a, y_effective); @@ -226,12 +230,12 @@ def compile_cpp_module(): break; } - if ((fm > 0 && f1 > 0) || (fm < 0 && f1 < 0)) { + if ((fm > 0 && fl > 0) || (fm < 0 && fl < 0)) { zl = mid; - f1 = fm; + fl = fm; } else { zr = mid; - f2 = fm; + fr = fm; } } roots_found.push_back(0.5 * (zl + zr)); @@ -280,7 +284,7 @@ def compile_cpp_module(): std::vector min_vals; std::vector max_vals; - // Run multiple trials with different seeds + // Run multiple trials with different seeds for more stable results for (int seed = 0; seed < seeds; seed++) { // Set random seed std::mt19937 gen(seed * 100 + i); @@ -599,6 +603,56 @@ def compile_cpp_module(): return result; } + // Generate phase diagram + py::array_t generate_phase_diagram_cpp( + double z_a, double y, double beta_min, double beta_max, + double z_min, double z_max, int beta_steps, int z_steps) { + + // Apply the condition for y + double y_effective = y > 1.0 ? y : 1.0/y; + + // Create result array + auto result = py::array_t({z_steps, beta_steps}); + auto result_buf = result.request(); + int* result_ptr = static_cast(result_buf.ptr); + + // Create beta and z grids + std::vector beta_values(beta_steps); + std::vector z_values(z_steps); + + for (int i = 0; i < beta_steps; i++) { + beta_values[i] = beta_min + (beta_max - beta_min) * static_cast(i) / (beta_steps - 1); + } + + for (int i = 0; i < z_steps; i++) { + z_values[i] = z_min + (z_max - z_min) * static_cast(i) / (z_steps - 1); + } + + // Analyze roots for each (z, beta) point + #pragma omp parallel for collapse(2) + for (int i = 0; i < z_steps; i++) { + for (int j = 0; j < beta_steps; j++) { + double z = z_values[i]; + double beta = beta_values[j]; + + // Coefficients for cubic equation + double a = z * z_a; + double b = z * z_a + z + z_a - z_a*y_effective; + double c = z + z_a + 1 - y_effective*(beta*z_a + 1 - beta); + double d = 1.0; + + // Calculate discriminant + double discriminant = 18*a*b*c*d - 27*a*a*d*d + b*b*c*c - 2*b*b*b*d - 9*a*c*c*c; + + // Set result based on sign of discriminant + // 1 for all real roots (discriminant > 0), -1 for complex roots (discriminant < 0) + result_ptr[i * beta_steps + j] = (discriminant > 0) ? 1 : -1; + } + } + + return result; + } + // Generate eigenvalue distribution std::tuple, py::array_t> generate_eigenvalue_distribution_cpp(double beta, double y, double z_a, int n, int seed) { @@ -671,56 +725,6 @@ def compile_cpp_module(): return std::make_tuple(result, x_grid); } - // Generate phase diagram - py::array_t generate_phase_diagram_cpp( - double z_a, double y, double beta_min, double beta_max, - double z_min, double z_max, int beta_steps, int z_steps) { - - // Apply the condition for y - double y_effective = y > 1.0 ? y : 1.0/y; - - // Create result array - auto result = py::array_t({z_steps, beta_steps}); - auto result_buf = result.request(); - int* result_ptr = static_cast(result_buf.ptr); - - // Create beta and z grids - std::vector beta_values(beta_steps); - std::vector z_values(z_steps); - - for (int i = 0; i < beta_steps; i++) { - beta_values[i] = beta_min + (beta_max - beta_min) * static_cast(i) / (beta_steps - 1); - } - - for (int i = 0; i < z_steps; i++) { - z_values[i] = z_min + (z_max - z_min) * static_cast(i) / (z_steps - 1); - } - - // Analyze roots for each (z, beta) point - #pragma omp parallel for collapse(2) - for (int i = 0; i < z_steps; i++) { - for (int j = 0; j < beta_steps; j++) { - double z = z_values[i]; - double beta = beta_values[j]; - - // Coefficients for cubic equation - double a = z * z_a; - double b = z * z_a + z + z_a - z_a*y_effective; - double c = z + z_a + 1 - y_effective*(beta*z_a + 1 - beta); - double d = 1.0; - - // Calculate discriminant - double discriminant = 18*a*b*c*d - 27*a*a*d*d + b*b*c*c - 2*b*b*b*d - 9*a*c*c*c; - - // Set result based on sign of discriminant - // 1 for all real roots (discriminant > 0), -1 for complex roots (discriminant < 0) - result_ptr[i * beta_steps + j] = (discriminant > 0) ? 1 : -1; - } - } - - return result; - } - PYBIND11_MODULE(cubic_cpp, m) { m.doc() = "C++ accelerated cubic root analysis"; @@ -793,11 +797,8 @@ setup( break if not module_path: - progress.complete(False) - st.error("Failed to find compiled module.") - st.code(result.stdout) - st.code(result.stderr) - return False + progress.complete() + return None, f"Failed to find compiled module. Output: {result.stdout}\n{result.stderr}" # Import the module progress.update(5, "Importing compiled module") @@ -805,119 +806,143 @@ setup( cubic_cpp = importlib.util.module_from_spec(spec) spec.loader.exec_module(cubic_cpp) - # Make functions available globally - globals()["discriminant_array"] = cubic_cpp.discriminant_array - globals()["find_discriminant_zeros"] = cubic_cpp.find_discriminant_zeros - globals()["compute_eigenvalue_boundaries"] = cubic_cpp.compute_eigenvalue_boundaries - globals()["compute_cubic_roots_cpp"] = cubic_cpp.compute_cubic_roots_cpp - globals()["compute_high_y_curve"] = cubic_cpp.compute_high_y_curve - globals()["compute_alternate_low_expr"] = cubic_cpp.compute_alternate_low_expr - globals()["compute_max_k_expression"] = cubic_cpp.compute_max_k_expression - globals()["compute_min_t_expression"] = cubic_cpp.compute_min_t_expression - globals()["generate_eigenvalue_distribution_cpp"] = cubic_cpp.generate_eigenvalue_distribution_cpp - globals()["generate_phase_diagram_cpp"] = cubic_cpp.generate_phase_diagram_cpp - - progress.complete(True) - return True + progress.complete() + return cubic_cpp, None except subprocess.CalledProcessError as e: - progress.complete(False) - st.error(f"Compilation failed: {e}") - st.code(e.stdout) - st.code(e.stderr) - return False + progress.complete() + return None, f"Compilation failed: {e.stdout}\n{e.stderr}" + except Exception as e: + progress.complete() + return None, f"Error: {str(e)}" # Try to compile the C++ module -cpp_compiled = compile_cpp_module() +cpp_module, cpp_error = compile_cpp_module() -# Python fallback functions if C++ compilation failed -def add_sqrt_support(expr_str): - """Replace 'sqrt(' with 'sp.sqrt(' for sympy compatibility""" - return expr_str.replace('sqrt(', 'sp.sqrt(') +# Display C++ status +with cpp_status_container: + if cpp_module: + st.success("✅ C++ acceleration module loaded successfully! Calculations will run faster.") + else: + st.warning("⚠️ C++ acceleration not available. Using Python fallback implementation.") + if cpp_error: + with st.expander("Error details"): + st.code(cpp_error) + +# ----- SymPy-based high-precision implementations ----- + +# 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) + +# 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 + +# Standard discriminant formula for cubic equations +# Δ = 18abcd - 27a²d² + b²c² - 2b³d - 9ac³ +Delta_expr = ( + 18*a_sym*b_sym*c_sym*d_sym - + 27*a_sym**2*d_sym**2 + + b_sym**2*c_sym**2 - + 2*b_sym**3*d_sym - + 9*a_sym*c_sym**3 +) + +# Fast numeric function for the discriminant +discriminant_func = sp.lambdify((z_sym, beta_sym, z_a_sym, y_sym), Delta_expr, modules=[{"sqrt": np.sqrt}, "numpy"]) -@st.cache_data + +# Function to find zeros of the discriminant with SymPy precision +@st.cache_data(show_spinner=False) 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). + Uses high-precision SymPy calculations. """ - # Create progress bar - progress = AdvancedProgressBar(steps, "Finding discriminant zeros") + # Create an advanced progress bar + progress_key = f"find_zeros_{z_a}_{y}_{beta}_{z_min}_{z_max}_{steps}" + progress = AdvancedProgressBar(progress_key, steps, "Finding discriminant zeros") # Apply the condition for y y_effective = y if y > 1 else 1/y - # Symbolic variables for the cubic discriminant with higher precision - z_sym, beta_sym, z_a_sym, y_sym = sp.symbols("z beta z_a y", real=True, positive=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 - - # Symbolic expression for the cubic discriminant using standard form - Delta_expr = 18*a_sym*b_sym*c_sym*d_sym - 27*a_sym**2*d_sym**2 + b_sym**2*c_sym**2 - 2*b_sym**3*d_sym - 9*a_sym*c_sym**3 - - # Fast numeric function for the discriminant - discriminant_func = sp.lambdify((z_sym, beta_sym, z_a_sym, y_sym), Delta_expr, "mpmath") + # Create SymPy lambdified function with high precision + mpmath_discriminant_func = sp.lambdify( + (z_sym, beta_sym, z_a_sym, y_sym), + Delta_expr, + modules="mpmath" + ) + # Create grid with high precision z_grid = np.linspace(z_min, z_max, steps) disc_vals = [] # Calculate discriminant values with progress tracking for i, z in enumerate(z_grid): progress.update(i+1, f"Computing discriminant at z = {z:.4f}") - disc_vals.append(float(discriminant_func(z, beta, z_a, y_effective))) + + # Use high precision mpmath for calculation + disc_vals.append(float(mpmath_discriminant_func(z, beta, z_a, y_effective))) disc_vals = np.array(disc_vals) roots_found = [] - # Find sign changes with high-precision refinement - progress.update_status("Finding and refining discriminant zero locations") + # Loop through and find sign changes 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 abs(f1) < 1e-12: roots_found.append(z_grid[i]) elif abs(f2) < 1e-12: roots_found.append(z_grid[i+1]) elif f1 * f2 < 0: - # High-precision binary search for more accurate root - zl, zr = z_grid[i], z_grid[i+1] - f1 = discriminant_func(zl, beta, z_a, y_effective) - f2 = discriminant_func(zr, beta, z_a, y_effective) + # Use high-precision binary search to refine root location + zl, zr = mpmath.mpf(str(z_grid[i])), mpmath.mpf(str(z_grid[i+1])) + fl = mpmath_discriminant_func(zl, beta, z_a, y_effective) + fr = mpmath_discriminant_func(zr, beta, z_a, y_effective) + # Binary search with high precision for _ in range(50): - mid = sp.Float(0.5) * (zl + zr) - fm = discriminant_func(mid, beta, z_a, y_effective) - if abs(fm) < 1e-15: + mid = (zl + zr) / 2 + fm = mpmath_discriminant_func(mid, beta, z_a, y_effective) + + if abs(fm) < 1e-20: zl = zr = mid break - if fm * f1 > 0: - zl, f1 = mid, fm + + if fm * fl > 0: + zl, fl = mid, fm else: - zr, f2 = mid, fm + zr, fr = mid, fm # Convert back to float for NumPy compatibility - roots_found.append(float(0.5 * (zl + zr))) + roots_found.append(float(zl)) progress.complete() return np.array(roots_found) -@st.cache_data + +@st.cache_data(show_spinner=False) def sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps): """ - Python fallback. 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. + For each beta in [0,1] (with beta_steps points), find the minimum and maximum z + for which the discriminant is zero. Uses C++ acceleration if available. """ - if cpp_compiled: - return find_discriminant_zeros(z_a, y, z_min, z_max, beta_steps, z_steps) + # Try to use C++ acceleration if available + if cpp_module is not None: + try: + return cpp_module.find_discriminant_zeros(z_a, y, z_min, z_max, beta_steps, z_steps) + except Exception as e: + st.warning(f"C++ acceleration failed, falling back to Python: {str(e)}") # Create progress tracking - progress = AdvancedProgressBar(beta_steps, "Computing discriminant zeros across β values") + progress_key = f"sweep_beta_{z_a}_{y}_{z_min}_{z_max}_{beta_steps}_{z_steps}" + progress = AdvancedProgressBar(progress_key, beta_steps, "Computing discriminant zeros across β values") betas = np.linspace(0, 1, beta_steps) z_min_values = [] @@ -925,7 +950,7 @@ def sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps): for i, b in enumerate(betas): progress.update(i+1, f"Processing β = {b:.4f}") - roots = find_z_at_discriminant_zero(z_a, y, b, z_min, z_max, z_steps) + roots = find_z_at_discriminant_zero(z_a, y, b, z_min, z_max, max(100, z_steps // beta_steps)) if len(roots) == 0: z_min_values.append(np.nan) z_max_values.append(np.nan) @@ -936,17 +961,23 @@ def sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps): progress.complete() return betas, np.array(z_min_values), np.array(z_max_values) -@st.cache_data + +@st.cache_data(show_spinner=False) def compute_eigenvalue_support_boundaries(z_a, y, beta_values, n_samples=100, seeds=5): """ - Python fallback. Compute the support boundaries of the eigenvalue distribution by directly - finding the minimum and maximum eigenvalues of B_n = S_n T_n for different beta values. + Compute the support boundaries of the eigenvalue distribution. + Uses C++ acceleration if available. """ - if cpp_compiled: - return compute_eigenvalue_boundaries(z_a, y, beta_values, n_samples, seeds) + # Try to use C++ acceleration if available + if cpp_module is not None: + try: + return cpp_module.compute_eigenvalue_boundaries(z_a, y, beta_values, n_samples, seeds) + except Exception as e: + st.warning(f"C++ acceleration failed, falling back to Python: {str(e)}") # Create progress tracking - progress = AdvancedProgressBar(len(beta_values), "Computing eigenvalue support boundaries") + progress_key = f"eigenval_bound_{z_a}_{y}_{len(beta_values)}_{n_samples}_{seeds}" + progress = AdvancedProgressBar(progress_key, len(beta_values), "Computing eigenvalue support boundaries") # Apply the condition for y y_effective = y if y > 1 else 1/y @@ -956,7 +987,7 @@ def compute_eigenvalue_support_boundaries(z_a, y, beta_values, n_samples=100, se for i, beta in enumerate(beta_values): # Update progress - progress.update(i+1, f"Processing β = {beta:.4f}") + progress.update(i+1, f"Processing β = {beta:.4f} (matrix size: {n_samples})") min_vals = [] max_vals = [] @@ -1002,66 +1033,89 @@ def compute_eigenvalue_support_boundaries(z_a, y, beta_values, n_samples=100, se progress.complete() return min_eigenvalues, max_eigenvalues -@st.cache_data + +@st.cache_data(show_spinner=False) def compute_high_y_curve(betas, z_a, y): """ Compute the "High y Expression" curve with high precision. + Uses C++ acceleration if available. """ - if cpp_compiled: - return compute_high_y_curve(betas, z_a, y) + # Try to use C++ acceleration if available + if cpp_module is not None: + try: + return cpp_module.compute_high_y_curve(betas, z_a, y) + except Exception as e: + st.warning(f"C++ acceleration failed, falling back to Python: {str(e)}") - # Create progress tracking - progress = AdvancedProgressBar(1, "Computing high y expression") + # Create progress bar + progress_key = f"high_y_{z_a}_{y}_{len(betas)}" + progress = AdvancedProgressBar(progress_key, 3, "Computing high y expression") + progress.update(1, "Setting up symbolic expression") # Apply the condition for y y_effective = y if y > 1 else 1/y # Use SymPy for higher precision - beta_sym, z_a_sym, y_sym = sp.symbols("beta z_a y", positive=True) a = z_a_sym denominator = 1 - 2*a numerator = -4*a*(a-1)*y_sym*beta_sym - 2*a*y_sym - 2*a*(2*a-1) - - # Create the high precision expression expr = numerator / denominator + progress.update(2, "Creating high-precision lambda function") + # Convert to a high-precision numeric function - func = sp.lambdify((beta_sym, z_a_sym, y_sym), expr, "mpmath") + func = sp.lambdify((beta_sym, z_a_sym, y_sym), expr, modules="mpmath") + + progress.update(3, "Computing values") + + # Check if denominator is near zero (division by zero case) + denom_val = float(sp.N(denominator.subs(z_a_sym, z_a))) + if abs(denom_val) < 1e-12: + progress.complete() + return np.full_like(betas, np.nan) - # Compute values with high precision + # Compute values with high precision for each beta result = np.zeros_like(betas) - if abs(float(denominator.subs(z_a_sym, z_a))) < 1e-12: - result.fill(np.nan) - else: - for i, beta in enumerate(betas): - result[i] = float(func(beta, z_a, y_effective)) + for i, beta in enumerate(betas): + result[i] = float(func(beta, z_a, y_effective)) progress.complete() return result -@st.cache_data + +@st.cache_data(show_spinner=False) def compute_alternate_low_expr(betas, z_a, y): """ Compute the alternate low expression with high precision. + Uses C++ acceleration if available. """ - if cpp_compiled: - return compute_alternate_low_expr(betas, z_a, y) + # Try to use C++ acceleration if available + if cpp_module is not None: + try: + return cpp_module.compute_alternate_low_expr(betas, z_a, y) + except Exception as e: + st.warning(f"C++ acceleration failed, falling back to Python: {str(e)}") - # Create progress tracking - progress = AdvancedProgressBar(1, "Computing low y expression") + # Create progress bar + progress_key = f"low_expr_{z_a}_{y}_{len(betas)}" + progress = AdvancedProgressBar(progress_key, 3, "Computing low y expression") + progress.update(1, "Setting up symbolic expression") # Apply the condition for y y_effective = y if y > 1 else 1/y # Use SymPy for higher precision - beta_sym, z_a_sym, y_sym = sp.symbols("beta z_a y", positive=True) expr = (z_a_sym * y_sym * beta_sym * (z_a_sym - 1) - 2*z_a_sym*(1 - y_sym) - 2*z_a_sym**2) / (2 + 2*z_a_sym) + progress.update(2, "Creating high-precision lambda function") + # Convert to a high-precision numeric function - func = sp.lambdify((beta_sym, z_a_sym, y_sym), expr, "mpmath") + func = sp.lambdify((beta_sym, z_a_sym, y_sym), expr, modules="mpmath") + + progress.update(3, "Computing values") - # Compute values with high precision + # Compute values with high precision for each beta result = np.zeros_like(betas) for i, beta in enumerate(betas): result[i] = float(func(beta, z_a, y_effective)) @@ -1069,30 +1123,36 @@ def compute_alternate_low_expr(betas, z_a, y): progress.complete() return result -@st.cache_data + +@st.cache_data(show_spinner=False) 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)) - with high precision. + with high precision. Uses C++ acceleration if available. """ - if cpp_compiled: - return compute_max_k_expression(betas, z_a, y, k_samples) + # Try to use C++ acceleration if available + if cpp_module is not None: + try: + return cpp_module.compute_max_k_expression(betas, z_a, y, k_samples) + except Exception as e: + st.warning(f"C++ acceleration failed, falling back to Python: {str(e)}") - # Create progress tracking - progress = AdvancedProgressBar(len(betas), "Computing max k expression") + # Create progress bar + progress_key = f"max_k_{z_a}_{y}_{len(betas)}_{k_samples}" + progress = AdvancedProgressBar(progress_key, len(betas), "Computing max k expression") # Apply the condition for y y_effective = y if y > 1 else 1/y # Use SymPy for symbolic expression - k_sym, beta_sym, z_a_sym, y_sym = sp.symbols("k beta z_a y", positive=True) + k_sym = sp.Symbol("k", positive=True) a = z_a_sym numerator = y_sym*beta_sym*(a-1)*k_sym + (a*k_sym+1)*((y_sym-1)*k_sym-1) denominator = (a*k_sym+1)*(k_sym**2+k_sym) expr = numerator / denominator # Convert to high-precision function - func = sp.lambdify((k_sym, beta_sym, z_a_sym, y_sym), expr, "mpmath") + func = sp.lambdify((k_sym, beta_sym, z_a_sym, y_sym), expr, modules="mpmath") # Sample k values on a logarithmic scale k_values = np.logspace(-3, 3, k_samples) @@ -1121,34 +1181,40 @@ def compute_max_k_expression(betas, z_a, y, k_samples=1000): progress.complete() return max_vals -@st.cache_data + +@st.cache_data(show_spinner=False) 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)) - with high precision. + with high precision. Uses C++ acceleration if available. """ - if cpp_compiled: - return compute_min_t_expression(betas, z_a, y, t_samples) + # Try to use C++ acceleration if available + if cpp_module is not None: + try: + return cpp_module.compute_min_t_expression(betas, z_a, y, t_samples) + except Exception as e: + st.warning(f"C++ acceleration failed, falling back to Python: {str(e)}") - # Create progress tracking - progress = AdvancedProgressBar(len(betas), "Computing min t expression") + # Create progress bar + progress_key = f"min_t_{z_a}_{y}_{len(betas)}_{t_samples}" + progress = AdvancedProgressBar(progress_key, len(betas), "Computing min t expression") # Apply the condition for y y_effective = y if y > 1 else 1/y # Use SymPy for symbolic expression - t_sym, beta_sym, z_a_sym, y_sym = sp.symbols("t beta z_a y") + t_sym = sp.Symbol("t") a = z_a_sym numerator = y_sym*beta_sym*(a-1)*t_sym + (a*t_sym+1)*((y_sym-1)*t_sym-1) denominator = (a*t_sym+1)*(t_sym**2+t_sym) expr = numerator / denominator # Convert to high-precision function - func = sp.lambdify((t_sym, beta_sym, z_a_sym, y_sym), expr, "mpmath") + func = sp.lambdify((t_sym, beta_sym, z_a_sym, y_sym), expr, modules="mpmath") a = z_a if a <= 0: - progress.complete(False) + progress.complete() return np.full_like(betas, np.nan) lower_bound = -1/a + 1e-10 # Avoid division by zero @@ -1178,10 +1244,10 @@ def compute_min_t_expression(betas, z_a, y, t_samples=1000): progress.complete() return min_vals -@st.cache_data + +@st.cache_data(show_spinner=False) def compute_derivatives(curve, betas): - """Compute first and second derivatives of a curve using SymPy for accuracy.""" - # Create a spline representation for smoother derivatives + """Compute first and second derivatives of a curve using smooth spline interpolation.""" from scipy.interpolate import CubicSpline # Filter out NaN values @@ -1215,9 +1281,12 @@ def compute_derivatives(curve, betas): return d1, d2 + 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""" - progress = AdvancedProgressBar(7, "Computing derivatives") + # Progress tracking + progress_key = f"derivatives_{len(betas)}" + progress = AdvancedProgressBar(progress_key, 7, "Computing derivatives") derivatives = {} @@ -1267,12 +1336,15 @@ def compute_all_derivatives(betas, z_mins, z_maxs, low_y_curve, high_y_curve, al progress.complete() return derivatives + def compute_custom_expression(betas, z_a, y, s_num_expr, s_denom_expr, is_s_based=True): """ Compute custom curve with high precision using SymPy. If is_s_based=True, compute using s substitution. Otherwise, compute direct z(β) expression. """ - progress = AdvancedProgressBar(4, "Computing custom expression") + # Progress tracking + progress_key = f"custom_expr_{z_a}_{y}_{len(betas)}" + progress = AdvancedProgressBar(progress_key, 4, "Computing custom expression") # Apply the condition for y y_effective = y if y > 1 else 1/y @@ -1304,16 +1376,16 @@ def compute_custom_expression(betas, z_a, y, s_num_expr, s_denom_expr, is_s_base final_expr = num_expr / denom_expr except sp.SympifyError as e: - progress.complete(False) + progress.complete() st.error(f"Error parsing expressions: {e}") return np.full_like(betas, np.nan) - progress.update(3, "Creating lambda function") + progress.update(3, "Creating lambda function with mpmath precision") # Convert to high-precision numeric function - final_func = sp.lambdify((beta_sym, z_a_sym, y_sym), final_expr, modules=["mpmath"]) + final_func = sp.lambdify((beta_sym, z_a_sym, y_sym), final_expr, modules="mpmath") - progress.update(4, "Evaluating expression") + progress.update(4, "Evaluating expression for all β values") # Compute values for each beta result = np.zeros_like(betas) @@ -1330,13 +1402,18 @@ def compute_custom_expression(betas, z_a, y, s_num_expr, s_denom_expr, is_s_base progress.complete() return result + def compute_cubic_roots(z, beta, z_a, y): """ - Compute the roots of the cubic equation for given parameters with high precision using SymPy. + Compute the roots of the cubic equation for given parameters with high precision. + Uses C++ acceleration if available. """ - if cpp_compiled: - roots = compute_cubic_roots_cpp(z, beta, z_a, y) - return roots + # Try to use C++ acceleration if available + if cpp_module is not None: + try: + return cpp_module.compute_cubic_roots_cpp(z, beta, z_a, y) + except Exception as e: + st.warning(f"C++ acceleration failed, falling back to Python: {str(e)}") # Apply the condition for y y_effective = y if y > 1 else 1/y @@ -1358,9 +1435,18 @@ def compute_cubic_roots(z, beta, z_a, y): # Use SymPy for higher precision quad_eq = b*s**2 + c*s + d symbolic_roots = sp.solve(quad_eq, s) - numerical_roots = [complex(float(sp.N(root.evalf(50)).real), - float(sp.N(root.evalf(50)).imag)) for root in symbolic_roots] - roots = np.array(numerical_roots + [0], dtype=complex) + + # Convert to complex numbers with proper precision + numerical_roots = [] + for root in symbolic_roots: + high_prec_root = complex(float(sp.re(root).evalf(50)), float(sp.im(root).evalf(50))) + numerical_roots.append(high_prec_root) + + # Pad to 3 roots + while len(numerical_roots) < 3: + numerical_roots.append(0j) + + roots = np.array(numerical_roots, dtype=complex) return roots try: @@ -1373,7 +1459,7 @@ def compute_cubic_roots(z, beta, z_a, y): # Convert to high-precision complex numbers numerical_roots = [] for root in symbolic_roots: - # Use SymPy's N function with high precision (50 digits) + # Use SymPy's evalf with high precision (50 digits) high_prec_root = root.evalf(50) numerical_root = complex(float(sp.re(high_prec_root)), float(sp.im(high_prec_root))) numerical_roots.append(numerical_root) @@ -1389,6 +1475,7 @@ def compute_cubic_roots(z, beta, z_a, y): coeffs = [a, b, c, d] return np.roots(coeffs) + def track_roots_consistently(z_values, all_roots): """ Ensure consistent tracking of roots across z values by minimizing discontinuity. @@ -1430,39 +1517,24 @@ def track_roots_consistently(z_values, all_roots): return tracked_roots + def generate_cubic_discriminant(z, beta, z_a, y_effective): """ - Calculate the cubic discriminant with high precision using SymPy's standard formula. + Calculate the cubic discriminant with high precision using the standard formula. For a cubic ax^3 + bx^2 + cx + d: Δ = 18abcd - 27a^2d^2 + b^2c^2 - 2b^3d - 9ac^3 """ - # Create symbolic variables for more accurate calculation - z_sym, beta_sym, z_a_sym, y_sym = sp.symbols("z beta z_a y", real=True) - - # Define coefficients with symbols - 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 - - # Standard formula for cubic discriminant - discriminant_expr = ( - 18*a_sym*b_sym*c_sym*d_sym - - 27*a_sym**2*d_sym**2 + - b_sym**2*c_sym**2 - - 2*b_sym**3*d_sym - - 9*a_sym*c_sym**3 - ) + # Use SymPy for high precision calculation + a = mpmath.mpf(str(z * z_a)) + b = mpmath.mpf(str(z * z_a + z + z_a - z_a*y_effective)) + c = mpmath.mpf(str(z + z_a + 1 - y_effective*(beta*z_a + 1 - beta))) + d = mpmath.mpf("1.0") - # Create a high-precision lambda function - discriminant_func = sp.lambdify( - (z_sym, beta_sym, z_a_sym, y_sym), - discriminant_expr, - modules="mpmath" - ) - - # Evaluate with high precision - return float(discriminant_func(z, beta, z_a, y_effective)) + # Standard formula for cubic discriminant with high precision + discriminant = (18*a*b*c*d - 27*a**2*d**2 + b**2*c**2 - 2*b**3*d - 9*a*c**3) + return float(discriminant) + +@st.cache_data(show_spinner=False) def generate_root_plots(beta, y, z_a, z_min, z_max, n_points): """ Generate Im(s) and Re(s) vs. z plots with improved accuracy using SymPy. @@ -1475,7 +1547,8 @@ def generate_root_plots(beta, y, z_a, z_min, z_max, n_points): y_effective = y if y > 1 else 1/y # Create progress bar - progress = AdvancedProgressBar(n_points, "Computing cubic roots vs. z") + progress_key = f"root_plots_{beta}_{y}_{z_a}_{z_min}_{z_max}_{n_points}" + progress = AdvancedProgressBar(progress_key, n_points + 1, "Computing cubic roots vs. z") z_points = np.linspace(z_min, z_max, n_points) @@ -1487,7 +1560,7 @@ def generate_root_plots(beta, y, z_a, z_min, z_max, n_points): # Update progress progress.update(i+1, f"Computing roots for z = {z:.3f}") - # Calculate roots using SymPy + # Calculate roots using high precision roots = compute_cubic_roots(z, beta, z_a, y) # Initial sorting to help with tracking @@ -1498,10 +1571,8 @@ def generate_root_plots(beta, y, z_a, z_min, z_max, n_points): disc = generate_cubic_discriminant(z, beta, z_a, y_effective) discriminants.append(disc) - progress.complete() - - # Create secondary progress bar for root tracking - track_progress = AdvancedProgressBar(1, "Tracking roots consistently across z values") + # Update for final tracking step + progress.update(n_points+1, "Tracking roots across z values") all_roots = np.array(all_roots) discriminants = np.array(discriminants) @@ -1509,7 +1580,7 @@ def generate_root_plots(beta, y, z_a, z_min, z_max, n_points): # Track roots consistently across z values tracked_roots = track_roots_consistently(z_points, all_roots) - track_progress.complete() + progress.complete() # Extract imaginary and real parts ims = np.imag(tracked_roots) @@ -1556,6 +1627,8 @@ def generate_root_plots(beta, y, z_a, z_min, z_max, n_points): return fig_im, fig_re, fig_disc + +@st.cache_data(show_spinner=False) def generate_roots_vs_beta_plots(z, y, z_a, beta_min, beta_max, n_points): """ Generate Im(s) and Re(s) vs. β plots with improved accuracy using SymPy. @@ -1568,7 +1641,8 @@ def generate_roots_vs_beta_plots(z, y, z_a, beta_min, beta_max, n_points): y_effective = y if y > 1 else 1/y # Create progress bar - progress = AdvancedProgressBar(n_points, "Computing cubic roots vs. β") + progress_key = f"roots_beta_{z}_{y}_{z_a}_{beta_min}_{beta_max}_{n_points}" + progress = AdvancedProgressBar(progress_key, n_points + 1, "Computing cubic roots vs. β") beta_points = np.linspace(beta_min, beta_max, n_points) @@ -1580,7 +1654,7 @@ def generate_roots_vs_beta_plots(z, y, z_a, beta_min, beta_max, n_points): # Update progress progress.update(i+1, f"Computing roots for β = {beta:.3f}") - # Calculate roots using SymPy for higher precision + # Calculate roots using high precision roots = compute_cubic_roots(z, beta, z_a, y) # Initial sorting to help with tracking @@ -1591,10 +1665,8 @@ def generate_roots_vs_beta_plots(z, y, z_a, beta_min, beta_max, n_points): disc = generate_cubic_discriminant(z, beta, z_a, y_effective) discriminants.append(disc) - progress.complete() - - # Create secondary progress bar for root tracking - track_progress = AdvancedProgressBar(1, "Tracking roots consistently across β values") + # Update for final tracking step + progress.update(n_points+1, "Tracking roots across β values") all_roots = np.array(all_roots) discriminants = np.array(discriminants) @@ -1602,7 +1674,7 @@ def generate_roots_vs_beta_plots(z, y, z_a, beta_min, beta_max, n_points): # Track roots consistently across beta values tracked_roots = track_roots_consistently(beta_points, all_roots) - track_progress.complete() + progress.complete() # Extract imaginary and real parts ims = np.imag(tracked_roots) @@ -1649,18 +1721,32 @@ def generate_roots_vs_beta_plots(z, y, z_a, beta_min, beta_max, n_points): return fig_im, fig_re, fig_disc + +@st.cache_data(show_spinner=False) def generate_phase_diagram(z_a, y, beta_min=0.0, beta_max=1.0, z_min=-10.0, z_max=10.0, beta_steps=100, z_steps=100): """ - Generate a phase diagram showing regions of complex and real roots with high precision. + Generate a phase diagram showing regions of complex and real roots. + Uses C++ acceleration if available. """ - if cpp_compiled: - phase_map = generate_phase_diagram_cpp(z_a, y, beta_min, beta_max, z_min, z_max, beta_steps, z_steps) - beta_values = np.linspace(beta_min, beta_max, beta_steps) - z_values = np.linspace(z_min, z_max, z_steps) + # Try to use C++ acceleration if available + if cpp_module is not None: + try: + phase_map = cpp_module.generate_phase_diagram_cpp(z_a, y, beta_min, beta_max, z_min, z_max, beta_steps, z_steps) + beta_values = np.linspace(beta_min, beta_max, beta_steps) + z_values = np.linspace(z_min, z_max, z_steps) + except Exception as e: + st.warning(f"C++ acceleration failed, falling back to Python: {str(e)}") + # Fall back to Python implementation below + phase_map = None else: + phase_map = None + + # If C++ failed or is not available, use Python implementation + if phase_map is None: # Create progress tracking - progress = AdvancedProgressBar(z_steps, "Generating phase diagram") + progress_key = f"phase_diagram_{z_a}_{y}_{beta_steps}_{z_steps}" + progress = AdvancedProgressBar(progress_key, z_steps, "Generating phase diagram") # Apply the condition for y y_effective = y if y > 1 else 1/y @@ -1673,10 +1759,10 @@ def generate_phase_diagram(z_a, y, beta_min=0.0, beta_max=1.0, z_min=-10.0, z_ma for i, z in enumerate(z_values): # Update progress - progress.update(i+1, f"Analyzing z = {z:.2f}") + progress.update(i+1, f"Analyzing phase at z = {z:.2f} ({i+1}/{len(z_values)})") for j, beta in enumerate(beta_values): - # Calculate discriminant with high precision + # Use high-precision discriminant calculation disc = generate_cubic_discriminant(z, beta, z_a, y_effective) # Set result based on sign of discriminant @@ -1710,19 +1796,32 @@ def generate_phase_diagram(z_a, y, beta_min=0.0, beta_max=1.0, z_min=-10.0, z_ma return fig + +@st.cache_data(show_spinner=False) 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→∞ - with high precision. + Uses C++ acceleration if available. """ # Create progress tracking - progress = AdvancedProgressBar(7, "Computing eigenvalue distribution") + progress_key = f"eigenval_dist_{beta}_{y}_{z_a}_{n}_{seed}" + progress = AdvancedProgressBar(progress_key, 7, "Generating eigenvalue distribution") - if cpp_compiled: - progress.update(1, "Using C++ accelerated implementation") - eigenvalues, x_vals = generate_eigenvalue_distribution_cpp(beta, y, z_a, n, seed) - progress.update(6, "Eigenvalues computed successfully") + # Try to use C++ acceleration if available + if cpp_module is not None: + try: + progress.update(1, "Using C++ accelerated eigenvalue calculation") + eigenvalues, x_vals = cpp_module.generate_eigenvalue_distribution_cpp(beta, y, z_a, n, seed) + progress.update(6, "Eigenvalues computed successfully") + except Exception as e: + st.warning(f"C++ acceleration failed, falling back to Python: {str(e)}") + # Fall back to Python implementation below + eigenvalues = None else: + eigenvalues = None + + # If C++ failed or is not available, use Python implementation + if eigenvalues is None: # Apply the condition for y y_effective = y if y > 1 else 1/y @@ -1755,11 +1854,11 @@ def generate_eigenvalue_distribution(beta, y, z_a, n=1000, seed=42): progress.update(5, "Computing B_n matrix") B_n = S_n @ T_n - # Compute eigenvalues of B_n with high precision + # Compute eigenvalues of B_n progress.update(6, "Computing eigenvalues") eigenvalues = np.linalg.eigvalsh(B_n) - # Generate x values for KDE + # Create x grid for KDE x_vals = np.linspace(min(eigenvalues), max(eigenvalues), 500) # Use KDE to compute a smooth density estimate @@ -1790,6 +1889,53 @@ def generate_eigenvalue_distribution(beta, y, z_a, n=1000, seed=42): return fig, eigenvalues + +@st.cache_data(show_spinner=False) +def analyze_complex_root_structure(beta_values, z, z_a, y): + """ + Analyze when the cubic equation switches between having all real roots + and having a complex conjugate pair plus one real root. + + Returns: + - transition_points: beta values where the root structure changes + - structure_types: list indicating whether each interval has all real roots or complex roots + """ + # Create progress tracking + progress_key = f"root_struct_{z}_{z_a}_{y}_{len(beta_values)}" + progress = AdvancedProgressBar(progress_key, len(beta_values), "Analyzing root structure") + + transition_points = [] + structure_types = [] + + previous_type = None + + for i, beta in enumerate(beta_values): + progress.update(i+1, f"Analyzing root structure at β = {beta:.4f}") + + # Calculate roots with high precision + roots = compute_cubic_roots(z, beta, z_a, y) + + # Check if all roots are real (imaginary parts close to zero) + is_all_real = all(abs(root.imag) < 1e-10 for root in roots) + + current_type = "real" if is_all_real else "complex" + + if previous_type is not None and current_type != previous_type: + # Found a transition point + transition_points.append(beta) + structure_types.append(previous_type) + + previous_type = current_type + + # Add the final interval type + if previous_type is not None: + structure_types.append(previous_type) + + progress.complete() + return transition_points, structure_types + + +@st.cache_data(show_spinner=False) 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, @@ -1805,27 +1951,28 @@ def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps, Generate z vs beta plot with high precision calculations. """ # Create main progress tracking - main_progress = AdvancedProgressBar(5, "Computing z*(β) curves") + progress_key = f"z_vs_beta_{z_a}_{y}_{beta_steps}" + progress = AdvancedProgressBar(progress_key, 5, "Computing z*(β) curves") if z_a <= 0 or y <= 0 or z_min >= z_max: - main_progress.complete(False) + progress.complete() st.error("Invalid input parameters.") return None - main_progress.update(1, "Creating β grid") + progress.update(1, "Creating β grid") betas = np.linspace(0, 1, beta_steps) if use_eigenvalue_method: # Use the eigenvalue method to compute boundaries - main_progress.update(2, "Computing eigenvalue support boundaries") + progress.update(2, "Computing eigenvalue support boundaries") min_eigs, max_eigs = compute_eigenvalue_support_boundaries(z_a, y, betas, n_samples, seeds) z_mins, z_maxs = min_eigs, max_eigs else: # Use the original discriminant method - main_progress.update(2, "Computing discriminant zeros") + progress.update(2, "Computing discriminant zeros") betas, z_mins, z_maxs = sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps) - main_progress.update(3, "Computing additional curves") + progress.update(3, "Computing additional curves") # Compute additional curves high_y_curve = compute_high_y_curve(betas, z_a, y) if show_high_y else None @@ -1845,7 +1992,7 @@ def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps, # Compute derivatives if needed if show_derivatives: - main_progress.update(4, "Computing derivatives") + progress.update(4, "Computing 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 if they exist @@ -1854,7 +2001,7 @@ def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps, if show_min_t and min_t_curve is not None: min_t_derivatives = compute_derivatives(min_t_curve, betas) - main_progress.update(5, "Creating plot") + progress.update(5, "Creating plot") fig = go.Figure() @@ -1958,135 +2105,105 @@ def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps, ) ) - main_progress.complete() + progress.complete() return fig -def analyze_complex_root_structure(beta_values, z, z_a, y): - """ - Analyze when the cubic equation switches between having all real roots - and having a complex conjugate pair plus one real root. - - Returns: - - transition_points: beta values where the root structure changes - - structure_types: list indicating whether each interval has all real roots or complex roots - """ - # Create progress tracking - progress = AdvancedProgressBar(len(beta_values), "Analyzing root structure") - - # Apply the condition for y - y_effective = y if y > 1 else 1/y - - transition_points = [] - structure_types = [] - - previous_type = None - - for i, beta in enumerate(beta_values): - progress.update(i+1, f"Analyzing root structure at β = {beta:.4f}") - - roots = compute_cubic_roots(z, beta, z_a, y) - - # Check if all roots are real (imaginary parts close to zero) - is_all_real = all(abs(root.imag) < 1e-10 for root in roots) - - current_type = "real" if is_all_real else "complex" - - if previous_type is not None and current_type != previous_type: - # Found a transition point - transition_points.append(beta) - structure_types.append(previous_type) - - previous_type = current_type - - # Add the final interval type - if previous_type is not None: - structure_types.append(previous_type) - - progress.complete() - return transition_points, structure_types # ----------------- Streamlit UI ----------------- -st.title("Cubic Root Analysis (C++ Accelerated)") - -# Add a note about C++ acceleration and high precision -if cpp_compiled: - st.success("✅ C++ acceleration module loaded successfully. Calculations will run faster!") -else: - st.warning("⚠️ C++ module compilation failed. Falling back to Python implementations with high precision SymPy calculations.") -st.info(f"Using high precision calculations with {mpmath.mp.dps} decimal digits for accurate results.") - -# Define three tabs -tab1, tab2, tab3 = st.tabs(["z*(β) Curves", "Complex Root Analysis", "Differential Analysis"]) +# Create tab containers that won't be refreshed +if 'tab_placeholders' not in st.session_state: + st.session_state.tab_placeholders = st.tabs(["z*(β) Curves", "Complex Root Analysis", "Differential Analysis"]) # ----- Tab 1: z*(β) Curves ----- -with tab1: - st.header("Eigenvalue Support Boundaries") - - # Cleaner layout with better column organization - col1, col2, col3 = st.columns([1, 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") +with st.session_state.tab_placeholders[0]: + # Create persistent containers for this tab + if 'tab1_header' not in st.session_state: + st.session_state.tab1_header = st.empty() + st.session_state.tab1_col1 = st.empty() + st.session_state.tab1_col2 = st.empty() + st.session_state.tab1_col3 = st.empty() + + # Fill header + with st.session_state.tab1_header: + st.header("Eigenvalue Support Boundaries") + + # Create column layout + with st.session_state.tab1_col1: + col1, col2, col3 = st.columns([1, 1, 2]) - with col2: - 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 col1: - method_type = st.radio( - "Calculation Method", - ["Eigenvalue Method", "Discriminant Method"], - index=0 # Default to eigenvalue method - ) - - # Advanced settings in collapsed expanders - with st.expander("Method Settings", expanded=False): - if method_type == "Eigenvalue Method": - beta_steps = st.slider("β steps", min_value=21, max_value=101, value=51, step=10, - key="beta_steps_eigen") - n_samples = st.slider("Matrix size (n)", min_value=100, max_value=2000, value=1000, - step=100) - seeds = st.slider("Number of seeds", min_value=1, max_value=10, value=5, step=1) - else: - 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") - - # Curve visibility options - with st.expander("Curve Visibility", expanded=False): - col_vis1, col_vis2 = st.columns(2) - with col_vis1: - show_high_y = st.checkbox("Show High y Expression", value=False, key="show_high_y") - show_max_k = st.checkbox("Show Max k Expression", value=True, key="show_max_k") - with col_vis2: - show_low_y = st.checkbox("Show Low y Expression", value=False, key="show_low_y") - show_min_t = st.checkbox("Show Min t Expression", value=True, key="show_min_t") - - # Custom expressions collapsed by default - with st.expander("Custom Expression 1 (s-based)", expanded=False): - 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") - - with st.expander("Custom Expression 2 (direct z(β))", expanded=False): - 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") - - # Move show_derivatives to main UI level for better visibility - with col2: - show_derivatives = st.checkbox("Show derivatives", value=False) - - # Compute button - if st.button("Compute Curves", key="tab1_button"): - with col3: + 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") + + with col2: + 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 col1: + method_type = st.radio( + "Calculation Method", + ["Eigenvalue Method", "Discriminant Method"], + index=0 # Default to eigenvalue method + ) + + # Advanced settings in collapsed expanders + with st.expander("Method Settings", expanded=False): + if method_type == "Eigenvalue Method": + beta_steps = st.slider("β steps", min_value=21, max_value=101, value=51, step=10, + key="beta_steps_eigen") + n_samples = st.slider("Matrix size (n)", min_value=100, max_value=2000, value=1000, + step=100) + seeds = st.slider("Number of seeds", min_value=1, max_value=10, value=5, step=1) + else: + 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") + + # Curve visibility options + with st.expander("Curve Visibility", expanded=False): + col_vis1, col_vis2 = st.columns(2) + with col_vis1: + show_high_y = st.checkbox("Show High y Expression", value=False, key="show_high_y") + show_max_k = st.checkbox("Show Max k Expression", value=True, key="show_max_k") + with col_vis2: + show_low_y = st.checkbox("Show Low y Expression", value=False, key="show_low_y") + show_min_t = st.checkbox("Show Min t Expression", value=True, key="show_min_t") + + # Custom expressions collapsed by default + with st.expander("Custom Expression 1 (s-based)", expanded=False): + 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") + + with st.expander("Custom Expression 2 (direct z(β))", expanded=False): + 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") + + # Move show_derivatives to main UI level for better visibility + with col2: + show_derivatives = st.checkbox("Show derivatives", value=False) + + # Compute button container + with st.session_state.tab1_col2: + compute_button = st.button("Compute Curves", key="tab1_button") + + # Results container + with st.session_state.tab1_col3: + result_container = st.empty() + + # Compute if button pressed + if compute_button: + with result_container: + plot_container = st.empty() + explanation_container = st.empty() + use_eigenvalue_method = (method_type == "Eigenvalue Method") if use_eigenvalue_method: fig = generate_z_vs_beta_plot(z_a_1, y_1, z_min_1, z_max_1, beta_steps, None, @@ -2101,10 +2218,10 @@ with tab1: use_eigenvalue_method=False) if fig is not None: - st.plotly_chart(fig, use_container_width=True) + plot_container.plotly_chart(fig, use_container_width=True) # Curve explanations in collapsed expander - with st.expander("Curve Explanations", expanded=False): + with explanation_container.expander("Curve Explanations", expanded=False): if use_eigenvalue_method: st.markdown(""" - **Upper/Lower Bounds** (Blue): Maximum/minimum eigenvalues of B_n = S_n T_n @@ -2134,263 +2251,331 @@ with tab1: - Dotted lines: Second derivatives (d²/dβ²) """) + # ----- Tab 2: Complex Root Analysis ----- -with tab2: - st.header("Complex Root Analysis") +with st.session_state.tab_placeholders[1]: + # Create persistent containers for this tab + if 'tab2_header' not in st.session_state: + st.session_state.tab2_header = st.empty() + st.session_state.tab2_subtabs = st.empty() - # Create tabs within the page for different plots - plot_tabs = st.tabs(["Im{s} vs. z", "Im{s} vs. β", "Phase Diagram", "Eigenvalue Distribution"]) + # Fill header + with st.session_state.tab2_header: + st.header("Complex Root Analysis") - # Tab for Im{s} vs. z plot - with plot_tabs[0]: - col1, col2 = st.columns([1, 2]) - with col1: - beta_z = st.number_input("β", value=0.5, min_value=0.0, max_value=1.0, key="beta_tab2_z") - y_z = st.number_input("y", value=1.0, key="y_tab2_z") - z_a_z = st.number_input("z_a", value=1.0, key="z_a_tab2_z") - z_min_z = st.number_input("z_min", value=-10.0, key="z_min_tab2_z") - z_max_z = st.number_input("z_max", value=10.0, key="z_max_tab2_z") - with st.expander("Resolution Settings", expanded=False): - z_points = st.slider("z grid points", min_value=100, max_value=2000, value=500, step=100, key="z_points_z") - if st.button("Compute Complex Roots vs. z", key="tab2_button_z"): - with col2: - fig_im, fig_re, fig_disc = generate_root_plots(beta_z, y_z, z_a_z, z_min_z, z_max_z, z_points) - if fig_im is not None and fig_re is not None and fig_disc is not None: - st.plotly_chart(fig_im, use_container_width=True) - st.plotly_chart(fig_re, use_container_width=True) - st.plotly_chart(fig_disc, use_container_width=True) + # Create tabs within the page for different plots + with st.session_state.tab2_subtabs: + plot_tabs = st.tabs(["Im{s} vs. z", "Im{s} vs. β", "Phase Diagram", "Eigenvalue Distribution"]) + + # Tab for Im{s} vs. z plot + with plot_tabs[0]: + if 'tab2_z_col1' not in st.session_state: + st.session_state.tab2_z_col1 = st.empty() + st.session_state.tab2_z_col2 = st.empty() + + with st.session_state.tab2_z_col1: + col1, col2 = st.columns([1, 2]) + with col1: + beta_z = st.number_input("β", value=0.5, min_value=0.0, max_value=1.0, key="beta_tab2_z") + y_z = st.number_input("y", value=1.0, key="y_tab2_z") + z_a_z = st.number_input("z_a", value=1.0, key="z_a_tab2_z") + z_min_z = st.number_input("z_min", value=-10.0, key="z_min_tab2_z") + z_max_z = st.number_input("z_max", value=10.0, key="z_max_tab2_z") + with st.expander("Resolution Settings", expanded=False): + z_points = st.slider("z grid points", min_value=100, max_value=2000, value=500, step=100, key="z_points_z") + compute_button_z = st.button("Compute Complex Roots vs. z", key="tab2_button_z") + + with st.session_state.tab2_z_col2: + result_container_z = st.empty() + + if compute_button_z: + with result_container_z: + fig_im, fig_re, fig_disc = generate_root_plots(beta_z, y_z, z_a_z, z_min_z, z_max_z, z_points) + if fig_im is not None and fig_re is not None and fig_disc is not None: + st.plotly_chart(fig_im, use_container_width=True) + st.plotly_chart(fig_re, use_container_width=True) + st.plotly_chart(fig_disc, use_container_width=True) + + with st.expander("Root Structure Analysis", expanded=False): + st.markdown(""" + ### Root Structure Explanation + + The red dashed vertical lines mark the points where the cubic discriminant equals zero. + At these points, the cubic equation's root structure changes: + + - When the discriminant is positive, the cubic has three distinct real roots. + - When the discriminant is negative, the cubic has one real root and two complex conjugate roots. + - When the discriminant is exactly zero, the cubic has at least two equal roots. + + These transition points align perfectly with the z*(β) boundary curves from the first tab, + which represent exactly these transitions in the (β,z) plane. + """) + + # Tab for Im{s} vs. β plot + with plot_tabs[1]: + if 'tab2_beta_col1' not in st.session_state: + st.session_state.tab2_beta_col1 = st.empty() + st.session_state.tab2_beta_col2 = st.empty() + + with st.session_state.tab2_beta_col1: + col1, col2 = st.columns([1, 2]) + with col1: + z_beta = st.number_input("z", value=1.0, key="z_tab2_beta") + y_beta = st.number_input("y", value=1.0, key="y_tab2_beta") + z_a_beta = st.number_input("z_a", value=1.0, key="z_a_tab2_beta") + beta_min = st.number_input("β_min", value=0.0, min_value=0.0, max_value=1.0, key="beta_min_tab2") + beta_max = st.number_input("β_max", value=1.0, min_value=0.0, max_value=1.0, key="beta_max_tab2") + with st.expander("Resolution Settings", expanded=False): + beta_points = st.slider("β grid points", min_value=100, max_value=1000, value=500, step=100, key="beta_points") + compute_button_beta = st.button("Compute Complex Roots vs. β", key="tab2_button_beta") + + with st.session_state.tab2_beta_col2: + result_container_beta = st.empty() + + if compute_button_beta: + with result_container_beta: + fig_im_beta, fig_re_beta, fig_disc = generate_roots_vs_beta_plots( + z_beta, y_beta, z_a_beta, beta_min, beta_max, beta_points) - with st.expander("Root Structure Analysis", expanded=False): - st.markdown(""" - ### Root Structure Explanation + if fig_im_beta is not None and fig_re_beta is not None and fig_disc is not None: + st.plotly_chart(fig_im_beta, use_container_width=True) + st.plotly_chart(fig_re_beta, use_container_width=True) + st.plotly_chart(fig_disc, use_container_width=True) - The red dashed vertical lines mark the points where the cubic discriminant equals zero. - At these points, the cubic equation's root structure changes: + # Add analysis of transition points + transition_points, structure_types = analyze_complex_root_structure( + np.linspace(beta_min, beta_max, beta_points), z_beta, z_a_beta, y_beta) - - When the discriminant is positive, the cubic has three distinct real roots. - - When the discriminant is negative, the cubic has one real root and two complex conjugate roots. - - When the discriminant is exactly zero, the cubic has at least two equal roots. + if transition_points: + st.subheader("Root Structure Transition Points") + for i, beta in enumerate(transition_points): + prev_type = structure_types[i] + next_type = structure_types[i+1] if i+1 < len(structure_types) else "unknown" + st.markdown(f"- At β = {beta:.6f}: Transition from {prev_type} roots to {next_type} roots") + else: + st.info("No transitions detected in root structure across this β range.") - These transition points align perfectly with the z*(β) boundary curves from the first tab, - which represent exactly these transitions in the (β,z) plane. - """) - - # New tab for Im{s} vs. β plot - with plot_tabs[1]: - col1, col2 = st.columns([1, 2]) - with col1: - z_beta = st.number_input("z", value=1.0, key="z_tab2_beta") - y_beta = st.number_input("y", value=1.0, key="y_tab2_beta") - z_a_beta = st.number_input("z_a", value=1.0, key="z_a_tab2_beta") - beta_min = st.number_input("β_min", value=0.0, min_value=0.0, max_value=1.0, key="beta_min_tab2") - beta_max = st.number_input("β_max", value=1.0, min_value=0.0, max_value=1.0, key="beta_max_tab2") - with st.expander("Resolution Settings", expanded=False): - beta_points = st.slider("β grid points", min_value=100, max_value=1000, value=500, step=100, key="beta_points") - if st.button("Compute Complex Roots vs. β", key="tab2_button_beta"): - with col2: - fig_im_beta, fig_re_beta, fig_disc = generate_roots_vs_beta_plots( - z_beta, y_beta, z_a_beta, beta_min, beta_max, beta_points) - - if fig_im_beta is not None and fig_re_beta is not None and fig_disc is not None: - st.plotly_chart(fig_im_beta, use_container_width=True) - st.plotly_chart(fig_re_beta, use_container_width=True) - st.plotly_chart(fig_disc, use_container_width=True) + # Explanation + with st.expander("Analysis Explanation", expanded=False): + st.markdown(""" + ### Interpreting the Plots + + - **Im{s} vs. β**: Shows how the imaginary parts of the roots change with β. When all curves are at Im{s}=0, all roots are real. + - **Re{s} vs. β**: Shows how the real parts of the roots change with β. + - **Discriminant Plot**: The cubic discriminant changes sign at points where the root structure changes. + - When discriminant < 0: The cubic has one real root and two complex conjugate roots. + - When discriminant > 0: The cubic has three distinct real roots. + - When discriminant = 0: The cubic has multiple roots (at least two roots are equal). + + The vertical red dashed lines mark the transition points where the root structure changes. + """) + + # Tab for Phase Diagram + with plot_tabs[2]: + if 'tab2_phase_col1' not in st.session_state: + st.session_state.tab2_phase_col1 = st.empty() + st.session_state.tab2_phase_col2 = st.empty() + + with st.session_state.tab2_phase_col1: + col1, col2 = st.columns([1, 2]) + with col1: + z_a_phase = st.number_input("z_a", value=1.0, key="z_a_phase") + y_phase = st.number_input("y", value=1.0, key="y_phase") + beta_min_phase = st.number_input("β_min", value=0.0, min_value=0.0, max_value=1.0, key="beta_min_phase") + beta_max_phase = st.number_input("β_max", value=1.0, min_value=0.0, max_value=1.0, key="beta_max_phase") + z_min_phase = st.number_input("z_min", value=-10.0, key="z_min_phase") + z_max_phase = st.number_input("z_max", value=10.0, key="z_max_phase") - # Add analysis of transition points - transition_points, structure_types = analyze_complex_root_structure( - np.linspace(beta_min, beta_max, beta_points), z_beta, z_a_beta, y_beta) + with st.expander("Resolution Settings", expanded=False): + beta_steps_phase = st.slider("β grid points", min_value=20, max_value=200, value=100, step=20, key="beta_steps_phase") + z_steps_phase = st.slider("z grid points", min_value=20, max_value=200, value=100, step=20, key="z_steps_phase") - if transition_points: - st.subheader("Root Structure Transition Points") - for i, beta in enumerate(transition_points): - prev_type = structure_types[i] - next_type = structure_types[i+1] if i+1 < len(structure_types) else "unknown" - st.markdown(f"- At β = {beta:.6f}: Transition from {prev_type} roots to {next_type} roots") - else: - st.info("No transitions detected in root structure across this β range.") + compute_button_phase = st.button("Generate Phase Diagram", key="tab2_button_phase") + + with st.session_state.tab2_phase_col2: + result_container_phase = st.empty() + + if compute_button_phase: + with result_container_phase: + st.info("Generating phase diagram. This may take a while depending on resolution...") + fig_phase = generate_phase_diagram( + z_a_phase, y_phase, beta_min_phase, beta_max_phase, z_min_phase, z_max_phase, + beta_steps_phase, z_steps_phase) - # Explanation - with st.expander("Analysis Explanation", expanded=False): - st.markdown(""" - ### Interpreting the Plots - - - **Im{s} vs. β**: Shows how the imaginary parts of the roots change with β. When all curves are at Im{s}=0, all roots are real. - - **Re{s} vs. β**: Shows how the real parts of the roots change with β. - - **Discriminant Plot**: The cubic discriminant changes sign at points where the root structure changes. - - When discriminant < 0: The cubic has one real root and two complex conjugate roots. - - When discriminant > 0: The cubic has three distinct real roots. - - When discriminant = 0: The cubic has multiple roots (at least two roots are equal). + if fig_phase is not None: + st.plotly_chart(fig_phase, use_container_width=True) - The vertical red dashed lines mark the transition points where the root structure changes. - """) - - # Tab for Phase Diagram - with plot_tabs[2]: - col1, col2 = st.columns([1, 2]) - with col1: - z_a_phase = st.number_input("z_a", value=1.0, key="z_a_phase") - y_phase = st.number_input("y", value=1.0, key="y_phase") - beta_min_phase = st.number_input("β_min", value=0.0, min_value=0.0, max_value=1.0, key="beta_min_phase") - beta_max_phase = st.number_input("β_max", value=1.0, min_value=0.0, max_value=1.0, key="beta_max_phase") - z_min_phase = st.number_input("z_min", value=-10.0, key="z_min_phase") - z_max_phase = st.number_input("z_max", value=10.0, key="z_max_phase") + with st.expander("Phase Diagram Explanation", expanded=False): + st.markdown(""" + ### Understanding the Phase Diagram + + This heatmap shows the regions in the (β, z) plane where: + + - **Red Regions**: The cubic equation has all real roots + - **Blue Regions**: The cubic equation has one real root and two complex conjugate roots + + The boundaries between these regions represent values where the discriminant is zero, + which are the exact same curves as the z*(β) boundaries in the first tab. This phase + diagram provides a comprehensive view of the eigenvalue support structure. + """) + + # Eigenvalue distribution tab + with plot_tabs[3]: + if 'tab2_eigen_col1' not in st.session_state: + st.session_state.tab2_eigen_col1 = st.empty() + st.session_state.tab2_eigen_col2 = st.empty() - with st.expander("Resolution Settings", expanded=False): - beta_steps_phase = st.slider("β grid points", min_value=20, max_value=200, value=100, step=20, key="beta_steps_phase") - z_steps_phase = st.slider("z grid points", min_value=20, max_value=200, value=100, step=20, key="z_steps_phase") - - if st.button("Generate Phase Diagram", key="tab2_button_phase"): - with col2: - st.info("Generating phase diagram. This may take a while depending on resolution...") - fig_phase = generate_phase_diagram( - z_a_phase, y_phase, beta_min_phase, beta_max_phase, z_min_phase, z_max_phase, - beta_steps_phase, z_steps_phase) + with st.session_state.tab2_eigen_col1: + st.subheader("Eigenvalue Distribution for B_n = S_n T_n") + with st.expander("Simulation Information", expanded=False): + st.markdown(""" + This simulation generates the eigenvalue distribution of B_n as n→∞, where: + - B_n = (1/n)XX^T with X being a p×n matrix + - p/n → y as n→∞ + - The diagonal entries of T_n follow distribution β·δ(z_a) + (1-β)·δ(1) + """) - if fig_phase is not None: - st.plotly_chart(fig_phase, use_container_width=True) + col_eigen1, col_eigen2 = st.columns([1, 2]) + with col_eigen1: + beta_eigen = st.number_input("β", value=0.5, min_value=0.0, max_value=1.0, key="beta_eigen") + y_eigen = st.number_input("y", value=1.0, key="y_eigen") + z_a_eigen = st.number_input("z_a", value=1.0, key="z_a_eigen") + 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) - with st.expander("Phase Diagram Explanation", expanded=False): - st.markdown(""" - ### Understanding the Phase Diagram - - This heatmap shows the regions in the (β, z) plane where: - - - **Red Regions**: The cubic equation has all real roots - - **Blue Regions**: The cubic equation has one real root and two complex conjugate roots + # Add comparison option + show_theoretical = st.checkbox("Show theoretical boundaries", value=True) + show_empirical_stats = st.checkbox("Show empirical statistics", value=True) + + compute_button_eigen = st.button("Generate Eigenvalue Distribution", key="tab2_eigen_button") + + with st.session_state.tab2_eigen_col2: + result_container_eigen = st.empty() + + if compute_button_eigen: + with result_container_eigen: + # Generate the eigenvalue distribution + fig_eigen, eigenvalues = generate_eigenvalue_distribution(beta_eigen, y_eigen, z_a_eigen, n=n_samples, seed=sim_seed) + + # If requested, compute and add theoretical boundaries + if show_theoretical: + # Calculate min and max eigenvalues using the support boundary functions + betas = np.array([beta_eigen]) + min_eig, max_eig = compute_eigenvalue_support_boundaries(z_a_eigen, y_eigen, betas, n_samples=n_samples, seeds=5) - The boundaries between these regions represent values where the discriminant is zero, - which are the exact same curves as the z*(β) boundaries in the first tab. This phase - diagram provides a comprehensive view of the eigenvalue support structure. - """) - - # Eigenvalue distribution tab - with plot_tabs[3]: - st.subheader("Eigenvalue Distribution for B_n = S_n T_n") - with st.expander("Simulation Information", expanded=False): - st.markdown(""" - This simulation generates the eigenvalue distribution of B_n as n→∞, where: - - B_n = (1/n)XX^T with X being a p×n matrix - - p/n → y as n→∞ - - The diagonal entries of T_n follow distribution β·δ(z_a) + (1-β)·δ(1) - """) - - col_eigen1, col_eigen2 = st.columns([1, 2]) - with col_eigen1: - beta_eigen = st.number_input("β", value=0.5, min_value=0.0, max_value=1.0, key="beta_eigen") - y_eigen = st.number_input("y", value=1.0, key="y_eigen") - z_a_eigen = st.number_input("z_a", value=1.0, key="z_a_eigen") - 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) - - # Add comparison option - show_theoretical = st.checkbox("Show theoretical boundaries", value=True) - show_empirical_stats = st.checkbox("Show empirical statistics", value=True) - - if st.button("Generate Eigenvalue Distribution", key="tab2_eigen_button"): - with col_eigen2: - # Generate the eigenvalue distribution - fig_eigen, eigenvalues = generate_eigenvalue_distribution(beta_eigen, y_eigen, z_a_eigen, n=n_samples, seed=sim_seed) - - # If requested, compute and add theoretical boundaries - if show_theoretical: - # Calculate min and max eigenvalues using the support boundary functions - betas = np.array([beta_eigen]) - min_eig, max_eig = compute_eigenvalue_support_boundaries(z_a_eigen, y_eigen, betas, n_samples=n_samples, seeds=5) + # Add vertical lines for boundaries + fig_eigen.add_vline( + x=min_eig[0], + line=dict(color="red", width=2, dash="dash"), + annotation_text="Min theoretical", + annotation_position="top right" + ) + fig_eigen.add_vline( + x=max_eig[0], + line=dict(color="red", width=2, dash="dash"), + annotation_text="Max theoretical", + annotation_position="top left" + ) - # Add vertical lines for boundaries - fig_eigen.add_vline( - x=min_eig[0], - line=dict(color="red", width=2, dash="dash"), - annotation_text="Min theoretical", - annotation_position="top right" - ) - fig_eigen.add_vline( - x=max_eig[0], - line=dict(color="red", width=2, dash="dash"), - annotation_text="Max theoretical", - annotation_position="top left" - ) - - # Display the plot - st.plotly_chart(fig_eigen, use_container_width=True) - - # Add comparison of empirical vs theoretical bounds - if show_theoretical and show_empirical_stats: - empirical_min = eigenvalues.min() - empirical_max = eigenvalues.max() + # Display the plot + st.plotly_chart(fig_eigen, use_container_width=True) - st.markdown("### Comparison of Empirical vs Theoretical Bounds") - col1, col2, col3 = st.columns(3) - with col1: - st.metric("Theoretical Min", f"{min_eig[0]:.6f}") - st.metric("Theoretical Max", f"{max_eig[0]:.6f}") - st.metric("Theoretical Width", f"{max_eig[0] - min_eig[0]:.6f}") - with col2: - st.metric("Empirical Min", f"{empirical_min:.6f}") - st.metric("Empirical Max", f"{empirical_max:.6f}") - st.metric("Empirical Width", f"{empirical_max - empirical_min:.6f}") - with col3: - st.metric("Min Difference", f"{empirical_min - min_eig[0]:.6f}") - st.metric("Max Difference", f"{empirical_max - max_eig[0]:.6f}") - st.metric("Width Difference", f"{(empirical_max - empirical_min) - (max_eig[0] - min_eig[0]):.6f}") - - # Display additional statistics - if show_empirical_stats: - st.markdown("### Eigenvalue Statistics") - col1, col2 = st.columns(2) - with col1: - st.metric("Mean", f"{np.mean(eigenvalues):.6f}") - st.metric("Median", f"{np.median(eigenvalues):.6f}") - with col2: - st.metric("Standard Deviation", f"{np.std(eigenvalues):.6f}") - st.metric("Interquartile Range", f"{np.percentile(eigenvalues, 75) - np.percentile(eigenvalues, 25):.6f}") + # Add comparison of empirical vs theoretical bounds + if show_theoretical and show_empirical_stats: + empirical_min = eigenvalues.min() + empirical_max = eigenvalues.max() + + st.markdown("### Comparison of Empirical vs Theoretical Bounds") + col1, col2, col3 = st.columns(3) + with col1: + st.metric("Theoretical Min", f"{min_eig[0]:.6f}") + st.metric("Theoretical Max", f"{max_eig[0]:.6f}") + st.metric("Theoretical Width", f"{max_eig[0] - min_eig[0]:.6f}") + with col2: + st.metric("Empirical Min", f"{empirical_min:.6f}") + st.metric("Empirical Max", f"{empirical_max:.6f}") + st.metric("Empirical Width", f"{empirical_max - empirical_min:.6f}") + with col3: + st.metric("Min Difference", f"{empirical_min - min_eig[0]:.6f}") + st.metric("Max Difference", f"{empirical_max - max_eig[0]:.6f}") + st.metric("Width Difference", f"{(empirical_max - empirical_min) - (max_eig[0] - min_eig[0]):.6f}") + + # Display additional statistics + if show_empirical_stats: + st.markdown("### Eigenvalue Statistics") + col1, col2 = st.columns(2) + with col1: + st.metric("Mean", f"{np.mean(eigenvalues):.6f}") + st.metric("Median", f"{np.median(eigenvalues):.6f}") + with col2: + st.metric("Standard Deviation", f"{np.std(eigenvalues):.6f}") + st.metric("Interquartile Range", f"{np.percentile(eigenvalues, 75) - np.percentile(eigenvalues, 25):.6f}") + # ----- Tab 3: Differential Analysis ----- -with tab3: - st.header("Differential Analysis vs. β") - with st.expander("Description", expanded=False): - 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") - - diff_method_type = st.radio( - "Boundary Calculation Method", - ["Eigenvalue Method", "Discriminant Method"], - index=0, - key="diff_method_type" - ) - - with st.expander("Resolution Settings", expanded=False): - if diff_method_type == "Eigenvalue Method": - beta_steps_diff = st.slider("β steps", min_value=21, max_value=101, value=51, step=10, - key="beta_steps_diff_eigen") - diff_n_samples = st.slider("Matrix size (n)", min_value=100, max_value=2000, value=1000, - step=100, key="diff_n_samples") - diff_seeds = st.slider("Number of seeds", min_value=1, max_value=10, value=5, step=1, - key="diff_seeds") - else: - 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("Low y Expression", value=False) - - if st.button("Compute Differentials", key="tab3_button"): - with col2: +with st.session_state.tab_placeholders[2]: + # Create persistent containers for this tab + if 'tab3_header' not in st.session_state: + st.session_state.tab3_header = st.empty() + st.session_state.tab3_col1 = st.empty() + st.session_state.tab3_col2 = st.empty() + + # Fill header + with st.session_state.tab3_header: + st.header("Differential Analysis vs. β") + with st.expander("Description", expanded=False): + 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 β.") + + # Create column layout + with st.session_state.tab3_col1: + 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") + + diff_method_type = st.radio( + "Boundary Calculation Method", + ["Eigenvalue Method", "Discriminant Method"], + index=0, + key="diff_method_type" + ) + + with st.expander("Resolution Settings", expanded=False): + if diff_method_type == "Eigenvalue Method": + beta_steps_diff = st.slider("β steps", min_value=21, max_value=101, value=51, step=10, + key="beta_steps_diff_eigen") + diff_n_samples = st.slider("Matrix size (n)", min_value=100, max_value=2000, value=1000, + step=100, key="diff_n_samples") + diff_seeds = st.slider("Number of seeds", min_value=1, max_value=10, value=5, step=1, + key="diff_seeds") + else: + 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("Low y Expression", value=False) + + compute_button_diff = st.button("Compute Differentials", key="tab3_button") + + # Results container + with st.session_state.tab3_col2: + result_container_diff = st.empty() + + # Compute if button pressed + if compute_button_diff: + with result_container_diff: use_eigenvalue_method_diff = (diff_method_type == "Eigenvalue Method") # Create a progress tracker - progress = AdvancedProgressBar(5, "Computing differential analysis") + progress_key = f"diff_analysis_{z_a_diff}_{y_diff}" + progress = AdvancedProgressBar(progress_key, 5, "Computing differential analysis") progress.update(1, "Setting up β grid") if use_eigenvalue_method_diff: @@ -2468,4 +2653,24 @@ with tab3: - Solid lines: Original curves - Dashed lines: First derivatives (d/dβ) - Dotted lines: Second derivatives (d²/dβ²) - """) \ No newline at end of file + """) + + +# Show high precision info +st.sidebar.markdown(f"#### Calculation Settings") +st.sidebar.info(f"Using high precision calculations with {mpmath.mp.dps} digits for accurate results.") + +# Add OpenMP thread control if C++ acceleration is available +if cpp_module is not None: + st.sidebar.markdown("#### C++ Acceleration Settings") + omp_threads = st.sidebar.slider("OpenMP Threads", min_value=1, max_value=12, value=4, step=1) + + # Set OpenMP threads + try: + # Try to import OpenMP control module + import ctypes + omp_lib = ctypes.CDLL("libgomp.so.1") + omp_lib.omp_set_num_threads(ctypes.c_int(omp_threads)) + st.sidebar.success(f"Set OpenMP to use {omp_threads} threads.") + except Exception as e: + st.sidebar.warning(f"Could not set OpenMP threads: {str(e)}") \ No newline at end of file