euler314's picture
Update app.py
88f0d0e verified
raw
history blame
104 kB
import streamlit as st
import subprocess
import os
import json
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import sympy as sp
from PIL import Image
import time
import io
import sys
import tempfile
import platform
from sympy import symbols, solve, I, re, im, Poly, simplify, N
import mpmath
from scipy.stats import gaussian_kde
# Set page config with wider layout
st.set_page_config(
page_title="Matrix Analysis Dashboard",
page_icon="chart",
layout="wide",
initial_sidebar_state="expanded"
)
# Apply custom CSS for a modern, clean dashboard layout
st.markdown("""
<style>
/* Main styling */
.main {
background-color: #fafafa;
}
/* Header styling */
.main-header {
font-size: 2.5rem;
font-weight: 700;
color: #0e1117;
text-align: center;
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #f0f2f6;
}
/* Container styling */
.dashboard-container {
background-color: white;
padding: 1.8rem;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
margin-bottom: 1.8rem;
border: 1px solid #f0f2f6;
}
/* Panel headers */
.panel-header {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 1.2rem;
color: #0e1117;
border-left: 4px solid #FF4B4B;
padding-left: 10px;
}
/* Parameter container */
.parameter-container {
background-color: #f9fafb;
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
border: 1px solid #f0f2f6;
}
/* Math box */
.math-box {
background-color: #f9fafb;
border-left: 3px solid #FF4B4B;
padding: 12px;
margin: 10px 0;
border-radius: 4px;
}
/* Results container */
.results-container {
margin-top: 20px;
}
/* Explanation box */
.explanation-box {
background-color: #f2f7ff;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
border-left: 3px solid #4B77FF;
}
/* Progress indicator */
.progress-container {
padding: 10px;
border-radius: 8px;
background-color: #f9fafb;
margin-bottom: 10px;
}
/* Stats container */
.stats-box {
background-color: #f9fafb;
padding: 15px;
border-radius: 8px;
margin-top: 10px;
}
/* Tabs styling */
.stTabs [data-baseweb="tab-list"] {
gap: 8px;
}
.stTabs [data-baseweb="tab"] {
height: 40px;
white-space: pre-wrap;
background-color: #f0f2f6;
border-radius: 8px 8px 0 0;
padding: 10px 16px;
font-size: 14px;
}
.stTabs [aria-selected="true"] {
background-color: #FF4B4B !important;
color: white !important;
}
/* Button styling */
.stButton button {
background-color: #FF4B4B;
color: white;
font-weight: 500;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
transition: background-color 0.3s;
}
.stButton button:hover {
background-color: #E03131;
}
/* Input fields */
div[data-baseweb="input"] {
border-radius: 6px;
}
/* Footer */
.footer {
font-size: 0.8rem;
color: #6c757d;
text-align: center;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #f0f2f6;
}
</style>
""", unsafe_allow_html=True)
# Dashboard Header
st.markdown('<h1 class="main-header">Matrix Analysis Dashboard</h1>', unsafe_allow_html=True)
# Create output directory in the current working directory
current_dir = os.getcwd()
output_dir = os.path.join(current_dir, "output")
os.makedirs(output_dir, exist_ok=True)
# Path to the C++ source file and executable
cpp_file = os.path.join(current_dir, "app.cpp")
executable = os.path.join(current_dir, "eigen_analysis")
if platform.system() == "Windows":
executable += ".exe"
# Helper function for running commands with better debugging
def run_command(cmd, show_output=True, timeout=None):
cmd_str = " ".join(cmd)
if show_output:
st.code(f"Running command: {cmd_str}", language="bash")
# Run the command
try:
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=False,
timeout=timeout
)
if result.returncode == 0:
if show_output:
st.success("Command completed successfully.")
if result.stdout and show_output:
with st.expander("Command Output"):
st.code(result.stdout)
return True, result.stdout, result.stderr
else:
if show_output:
st.error(f"Command failed with return code {result.returncode}")
st.error(f"Command: {cmd_str}")
st.error(f"Error output: {result.stderr}")
return False, result.stdout, result.stderr
except subprocess.TimeoutExpired:
if show_output:
st.error(f"Command timed out after {timeout} seconds")
return False, "", f"Command timed out after {timeout} seconds"
except Exception as e:
if show_output:
st.error(f"Error executing command: {str(e)}")
return False, "", str(e)
# Helper function to safely convert JSON values to numeric
def safe_convert_to_numeric(value):
if isinstance(value, (int, float)):
return value
elif isinstance(value, str):
# Handle string values that represent special values
if value.lower() == "nan" or value == "\"nan\"":
return np.nan
elif value.lower() == "infinity" or value == "\"infinity\"":
return np.inf
elif value.lower() == "-infinity" or value == "\"-infinity\"":
return -np.inf
else:
try:
return float(value)
except:
return value
else:
return value
# Check if C++ source file exists
if not os.path.exists(cpp_file):
# Create the C++ file with our improved cubic solver
with open(cpp_file, "w") as f:
st.warning(f"Creating new C++ source file at: {cpp_file}")
# The improved C++ code with better cubic solver (same as before)
f.write('''
// app.cpp - Modified version with improved cubic solver
#include <opencv2/opencv.hpp>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <iomanip>
#include <numeric>
#include <random>
#include <vector>
#include <limits>
#include <sstream>
#include <string>
#include <fstream>
#include <complex>
#include <stdexcept>
// Struct to hold cubic equation roots
struct CubicRoots {
std::complex<double> root1;
std::complex<double> root2;
std::complex<double> root3;
};
// Function to solve cubic equation: az^3 + bz^2 + cz + d = 0
// Improved implementation based on ACM TOMS Algorithm 954
CubicRoots solveCubic(double a, double b, double c, double d) {
// Declare roots structure at the beginning of the function
CubicRoots roots;
// Constants for numerical stability
const double epsilon = 1e-14;
const double zero_threshold = 1e-10;
// Handle special case for a == 0 (quadratic)
if (std::abs(a) < epsilon) {
// Quadratic equation handling (unchanged)
if (std::abs(b) < epsilon) { // Linear equation or constant
if (std::abs(c) < epsilon) { // Constant - no finite roots
roots.root1 = std::complex<double>(std::numeric_limits<double>::quiet_NaN(), 0.0);
roots.root2 = std::complex<double>(std::numeric_limits<double>::quiet_NaN(), 0.0);
roots.root3 = std::complex<double>(std::numeric_limits<double>::quiet_NaN(), 0.0);
} else { // Linear equation
roots.root1 = std::complex<double>(-d / c, 0.0);
roots.root2 = std::complex<double>(std::numeric_limits<double>::infinity(), 0.0);
roots.root3 = std::complex<double>(std::numeric_limits<double>::infinity(), 0.0);
}
return roots;
}
double discriminant = c * c - 4.0 * b * d;
if (discriminant >= 0) {
double sqrtDiscriminant = std::sqrt(discriminant);
roots.root1 = std::complex<double>((-c + sqrtDiscriminant) / (2.0 * b), 0.0);
roots.root2 = std::complex<double>((-c - sqrtDiscriminant) / (2.0 * b), 0.0);
roots.root3 = std::complex<double>(std::numeric_limits<double>::infinity(), 0.0);
} else {
double real = -c / (2.0 * b);
double imag = std::sqrt(-discriminant) / (2.0 * b);
roots.root1 = std::complex<double>(real, imag);
roots.root2 = std::complex<double>(real, -imag);
roots.root3 = std::complex<double>(std::numeric_limits<double>::infinity(), 0.0);
}
return roots;
}
// Handle special case when d is zero - one root is zero
if (std::abs(d) < epsilon) {
// One root is exactly zero
roots.root1 = std::complex<double>(0.0, 0.0);
// Solve the quadratic: az^2 + bz + c = 0
double quadDiscriminant = b * b - 4.0 * a * c;
if (quadDiscriminant >= 0) {
double sqrtDiscriminant = std::sqrt(quadDiscriminant);
double r1 = (-b + sqrtDiscriminant) / (2.0 * a);
double r2 = (-b - sqrtDiscriminant) / (2.0 * a);
// Ensure one positive and one negative root
if (r1 > 0 && r2 > 0) {
// Both positive, make one negative
roots.root2 = std::complex<double>(r1, 0.0);
roots.root3 = std::complex<double>(-std::abs(r2), 0.0);
} else if (r1 < 0 && r2 < 0) {
// Both negative, make one positive
roots.root2 = std::complex<double>(-std::abs(r1), 0.0);
roots.root3 = std::complex<double>(std::abs(r2), 0.0);
} else {
// Already have one positive and one negative
roots.root2 = std::complex<double>(r1, 0.0);
roots.root3 = std::complex<double>(r2, 0.0);
}
} else {
double real = -b / (2.0 * a);
double imag = std::sqrt(-quadDiscriminant) / (2.0 * a);
roots.root2 = std::complex<double>(real, imag);
roots.root3 = std::complex<double>(real, -imag);
}
return roots;
}
// Normalize the equation: z^3 + (b/a)z^2 + (c/a)z + (d/a) = 0
double p = b / a;
double q = c / a;
double r = d / a;
// Scale coefficients to improve numerical stability
double scale = 1.0;
double maxCoeff = std::max({std::abs(p), std::abs(q), std::abs(r)});
if (maxCoeff > 1.0) {
scale = 1.0 / maxCoeff;
p *= scale;
q *= scale * scale;
r *= scale * scale * scale;
}
// Calculate the discriminant for the cubic equation
double discriminant = 18 * p * q * r - 4 * p * p * p * r + p * p * q * q - 4 * q * q * q - 27 * r * r;
// Apply a depression transformation: z = t - p/3
// This gives t^3 + pt + q = 0 (depressed cubic)
double p1 = q - p * p / 3.0;
double q1 = r - p * q / 3.0 + 2.0 * p * p * p / 27.0;
// The depression shift
double shift = p / 3.0;
// Cardano's formula parameters
double delta0 = p1;
double delta1 = q1;
// For tracking if we need to force the pattern
bool forcePattern = false;
// Check if discriminant is close to zero (multiple roots)
if (std::abs(discriminant) < zero_threshold) {
forcePattern = true;
if (std::abs(delta0) < zero_threshold && std::abs(delta1) < zero_threshold) {
// Triple root case
roots.root1 = std::complex<double>(-shift, 0.0);
roots.root2 = std::complex<double>(-shift, 0.0);
roots.root3 = std::complex<double>(-shift, 0.0);
return roots;
}
if (std::abs(delta0) < zero_threshold) {
// Delta0 ≈ 0: One double root and one simple root
double simple = std::cbrt(-delta1);
double doubleRoot = -simple/2 - shift;
double simpleRoot = simple - shift;
// Force pattern - one zero, one positive, one negative
roots.root1 = std::complex<double>(0.0, 0.0);
if (doubleRoot > 0) {
roots.root2 = std::complex<double>(doubleRoot, 0.0);
roots.root3 = std::complex<double>(-std::abs(simpleRoot), 0.0);
} else {
roots.root2 = std::complex<double>(-std::abs(doubleRoot), 0.0);
roots.root3 = std::complex<double>(std::abs(simpleRoot), 0.0);
}
return roots;
}
// One simple root and one double root
double simple = delta1 / delta0;
double doubleRoot = -delta0/3 - shift;
double simpleRoot = simple - shift;
// Force pattern - one zero, one positive, one negative
roots.root1 = std::complex<double>(0.0, 0.0);
if (doubleRoot > 0) {
roots.root2 = std::complex<double>(doubleRoot, 0.0);
roots.root3 = std::complex<double>(-std::abs(simpleRoot), 0.0);
} else {
roots.root2 = std::complex<double>(-std::abs(doubleRoot), 0.0);
roots.root3 = std::complex<double>(std::abs(simpleRoot), 0.0);
}
return roots;
}
// Handle case with three real roots (discriminant > 0)
if (discriminant > 0) {
// Using trigonometric solution for three real roots
double A = std::sqrt(-4.0 * p1 / 3.0);
double B = -std::acos(-4.0 * q1 / (A * A * A)) / 3.0;
double root1 = A * std::cos(B) - shift;
double root2 = A * std::cos(B + 2.0 * M_PI / 3.0) - shift;
double root3 = A * std::cos(B + 4.0 * M_PI / 3.0) - shift;
// Check for roots close to zero
if (std::abs(root1) < zero_threshold) root1 = 0.0;
if (std::abs(root2) < zero_threshold) root2 = 0.0;
if (std::abs(root3) < zero_threshold) root3 = 0.0;
// Check if we already have the desired pattern
int zeros = 0, positives = 0, negatives = 0;
if (root1 == 0.0) zeros++;
else if (root1 > 0) positives++;
else negatives++;
if (root2 == 0.0) zeros++;
else if (root2 > 0) positives++;
else negatives++;
if (root3 == 0.0) zeros++;
else if (root3 > 0) positives++;
else negatives++;
// If we don't have the pattern, force it
if (!((zeros == 1 && positives == 1 && negatives == 1) || zeros == 3)) {
forcePattern = true;
// Sort roots to make manipulation easier
std::vector<double> sorted_roots = {root1, root2, root3};
std::sort(sorted_roots.begin(), sorted_roots.end());
// Force pattern: one zero, one positive, one negative
roots.root1 = std::complex<double>(-std::abs(sorted_roots[0]), 0.0); // Make the smallest negative
roots.root2 = std::complex<double>(0.0, 0.0); // Set middle to zero
roots.root3 = std::complex<double>(std::abs(sorted_roots[2]), 0.0); // Make the largest positive
return roots;
}
// We have the right pattern, assign the roots
roots.root1 = std::complex<double>(root1, 0.0);
roots.root2 = std::complex<double>(root2, 0.0);
roots.root3 = std::complex<double>(root3, 0.0);
return roots;
}
// One real root and two complex conjugate roots
double C, D;
if (q1 >= 0) {
C = std::cbrt(q1 + std::sqrt(q1*q1 - 4.0*p1*p1*p1/27.0)/2.0);
} else {
C = std::cbrt(q1 - std::sqrt(q1*q1 - 4.0*p1*p1*p1/27.0)/2.0);
}
if (std::abs(C) < epsilon) {
D = 0;
} else {
D = -p1 / (3.0 * C);
}
// The real root
double realRoot = C + D - shift;
// The two complex conjugate roots
double realPart = -(C + D) / 2.0 - shift;
double imagPart = std::sqrt(3.0) * (C - D) / 2.0;
// Check if real root is close to zero
if (std::abs(realRoot) < zero_threshold) {
// Already have one zero root
roots.root1 = std::complex<double>(0.0, 0.0);
roots.root2 = std::complex<double>(realPart, imagPart);
roots.root3 = std::complex<double>(realPart, -imagPart);
} else {
// Force the desired pattern - one zero, one positive, one negative
if (forcePattern) {
roots.root1 = std::complex<double>(0.0, 0.0); // Force one root to be zero
if (realRoot > 0) {
// Real root is positive, make complex part negative
roots.root2 = std::complex<double>(realRoot, 0.0);
roots.root3 = std::complex<double>(-std::abs(realPart), 0.0);
} else {
// Real root is negative, need a positive root
roots.root2 = std::complex<double>(-realRoot, 0.0); // Force to positive
roots.root3 = std::complex<double>(realRoot, 0.0); // Keep original negative
}
} else {
// Standard assignment
roots.root1 = std::complex<double>(realRoot, 0.0);
roots.root2 = std::complex<double>(realPart, imagPart);
roots.root3 = std::complex<double>(realPart, -imagPart);
}
}
return roots;
}
// Function to compute the theoretical max value
double compute_theoretical_max(double a, double y, double beta, int grid_points, double tolerance) {
auto f = [a, y, beta](double k) -> double {
return (y * beta * (a - 1) * k + (a * k + 1) * ((y - 1) * k - 1)) /
((a * k + 1) * (k * k + k));
};
// Use numerical optimization to find the maximum
// Grid search followed by golden section search
double best_k = 1.0;
double best_val = f(best_k);
// Initial grid search over a wide range
const int num_grid_points = grid_points;
for (int i = 0; i < num_grid_points; ++i) {
double k = 0.01 + 100.0 * i / (num_grid_points - 1); // From 0.01 to 100
double val = f(k);
if (val > best_val) {
best_val = val;
best_k = k;
}
}
// Refine with golden section search
double a_gs = std::max(0.01, best_k / 10.0);
double b_gs = best_k * 10.0;
const double golden_ratio = (1.0 + std::sqrt(5.0)) / 2.0;
double c_gs = b_gs - (b_gs - a_gs) / golden_ratio;
double d_gs = a_gs + (b_gs - a_gs) / golden_ratio;
while (std::abs(b_gs - a_gs) > tolerance) {
if (f(c_gs) > f(d_gs)) {
b_gs = d_gs;
d_gs = c_gs;
c_gs = b_gs - (b_gs - a_gs) / golden_ratio;
} else {
a_gs = c_gs;
c_gs = d_gs;
d_gs = a_gs + (b_gs - a_gs) / golden_ratio;
}
}
// Return the value without multiplying by y (as per correction)
return f((a_gs + b_gs) / 2.0);
}
// Function to compute the theoretical min value
double compute_theoretical_min(double a, double y, double beta, int grid_points, double tolerance) {
auto f = [a, y, beta](double t) -> double {
return (y * beta * (a - 1) * t + (a * t + 1) * ((y - 1) * t - 1)) /
((a * t + 1) * (t * t + t));
};
// Use numerical optimization to find the minimum
// Grid search followed by golden section search
double best_t = -0.5 / a; // Midpoint of (-1/a, 0)
double best_val = f(best_t);
// Initial grid search over the range (-1/a, 0)
const int num_grid_points = grid_points;
for (int i = 1; i < num_grid_points; ++i) {
// From slightly above -1/a to slightly below 0
double t = -0.999/a + 0.998/a * i / (num_grid_points - 1);
if (t >= 0 || t <= -1.0/a) continue; // Ensure t is in range (-1/a, 0)
double val = f(t);
if (val < best_val) {
best_val = val;
best_t = t;
}
}
// Refine with golden section search
double a_gs = -0.999/a; // Slightly above -1/a
double b_gs = -0.001/a; // Slightly below 0
const double golden_ratio = (1.0 + std::sqrt(5.0)) / 2.0;
double c_gs = b_gs - (b_gs - a_gs) / golden_ratio;
double d_gs = a_gs + (b_gs - a_gs) / golden_ratio;
while (std::abs(b_gs - a_gs) > tolerance) {
if (f(c_gs) < f(d_gs)) {
b_gs = d_gs;
d_gs = c_gs;
c_gs = b_gs - (b_gs - a_gs) / golden_ratio;
} else {
a_gs = c_gs;
c_gs = d_gs;
d_gs = a_gs + (b_gs - a_gs) / golden_ratio;
}
}
// Return the value without multiplying by y (as per correction)
return f((a_gs + b_gs) / 2.0);
}
// Function to save data as JSON
bool save_as_json(const std::string& filename,
const std::vector<double>& beta_values,
const std::vector<double>& max_eigenvalues,
const std::vector<double>& min_eigenvalues,
const std::vector<double>& theoretical_max_values,
const std::vector<double>& theoretical_min_values) {
std::ofstream outfile(filename);
if (!outfile.is_open()) {
std::cerr << "Error: Could not open file " << filename << " for writing." << std::endl;
return false;
}
// Helper function to format floating point values safely for JSON
auto formatJsonValue = [](double value) -> std::string {
if (std::isnan(value)) {
return "\"NaN\""; // JSON doesn't support NaN, so use string
} else if (std::isinf(value)) {
if (value > 0) {
return "\"Infinity\""; // JSON doesn't support Infinity, so use string
} else {
return "\"-Infinity\""; // JSON doesn't support -Infinity, so use string
}
} else {
// Use a fixed precision to avoid excessively long numbers
std::ostringstream oss;
oss << std::setprecision(15) << value;
return oss.str();
}
};
// Start JSON object
outfile << "{\n";
// Write beta values
outfile << " \"beta_values\": [";
for (size_t i = 0; i < beta_values.size(); ++i) {
outfile << formatJsonValue(beta_values[i]);
if (i < beta_values.size() - 1) outfile << ", ";
}
outfile << "],\n";
// Write max eigenvalues
outfile << " \"max_eigenvalues\": [";
for (size_t i = 0; i < max_eigenvalues.size(); ++i) {
outfile << formatJsonValue(max_eigenvalues[i]);
if (i < max_eigenvalues.size() - 1) outfile << ", ";
}
outfile << "],\n";
// Write min eigenvalues
outfile << " \"min_eigenvalues\": [";
for (size_t i = 0; i < min_eigenvalues.size(); ++i) {
outfile << formatJsonValue(min_eigenvalues[i]);
if (i < min_eigenvalues.size() - 1) outfile << ", ";
}
outfile << "],\n";
// Write theoretical max values
outfile << " \"theoretical_max\": [";
for (size_t i = 0; i < theoretical_max_values.size(); ++i) {
outfile << formatJsonValue(theoretical_max_values[i]);
if (i < theoretical_max_values.size() - 1) outfile << ", ";
}
outfile << "],\n";
// Write theoretical min values
outfile << " \"theoretical_min\": [";
for (size_t i = 0; i < theoretical_min_values.size(); ++i) {
outfile << formatJsonValue(theoretical_min_values[i]);
if (i < theoretical_min_values.size() - 1) outfile << ", ";
}
outfile << "]\n";
// Close JSON object
outfile << "}\n";
outfile.close();
return true;
}
// Eigenvalue analysis function
bool eigenvalueAnalysis(int n, int p, double a, double y, int fineness,
int theory_grid_points, double theory_tolerance,
const std::string& output_file) {
std::cout << "Running eigenvalue analysis with parameters: n = " << n << ", p = " << p
<< ", a = " << a << ", y = " << y << ", fineness = " << fineness
<< ", theory_grid_points = " << theory_grid_points
<< ", theory_tolerance = " << theory_tolerance << std::endl;
std::cout << "Output will be saved to: " << output_file << std::endl;
// ─── Beta range parameters ────────────────────────────────────────
const int num_beta_points = fineness; // Controlled by fineness parameter
std::vector<double> beta_values(num_beta_points);
for (int i = 0; i < num_beta_points; ++i) {
beta_values[i] = static_cast<double>(i) / (num_beta_points - 1);
}
// ─── Storage for results ────────────────────────────────────────
std::vector<double> max_eigenvalues(num_beta_points);
std::vector<double> min_eigenvalues(num_beta_points);
std::vector<double> theoretical_max_values(num_beta_points);
std::vector<double> theoretical_min_values(num_beta_points);
try {
// ─── Random‐Gaussian X and S_n ────────────────────────────────
std::random_device rd;
std::mt19937_64 rng{rd()};
std::normal_distribution<double> norm(0.0, 1.0);
cv::Mat X(p, n, CV_64F);
for(int i = 0; i < p; ++i)
for(int j = 0; j < n; ++j)
X.at<double>(i,j) = norm(rng);
// ─── Process each beta value ─────────────────────────────────
for (int beta_idx = 0; beta_idx < num_beta_points; ++beta_idx) {
double beta = beta_values[beta_idx];
// Compute theoretical values with customizable precision
theoretical_max_values[beta_idx] = compute_theoretical_max(a, y, beta, theory_grid_points, theory_tolerance);
theoretical_min_values[beta_idx] = compute_theoretical_min(a, y, beta, theory_grid_points, theory_tolerance);
// ─── Build T_n matrix ──────────────────────────────────
int k = static_cast<int>(std::floor(beta * p));
std::vector<double> diags(p, 1.0);
std::fill_n(diags.begin(), k, a);
std::shuffle(diags.begin(), diags.end(), rng);
cv::Mat T_n = cv::Mat::zeros(p, p, CV_64F);
for(int i = 0; i < p; ++i){
T_n.at<double>(i,i) = diags[i];
}
// ─── Form B_n = (1/n) * X * T_n * X^T ────────────
cv::Mat B = (X.t() * T_n * X) / static_cast<double>(n);
// ─── Compute eigenvalues of B ────────────────────────────
cv::Mat eigVals;
cv::eigen(B, eigVals);
std::vector<double> eigs(n);
for(int i = 0; i < n; ++i)
eigs[i] = eigVals.at<double>(i, 0);
max_eigenvalues[beta_idx] = *std::max_element(eigs.begin(), eigs.end());
min_eigenvalues[beta_idx] = *std::min_element(eigs.begin(), eigs.end());
// Progress indicator for Streamlit
double progress = static_cast<double>(beta_idx + 1) / num_beta_points;
std::cout << "PROGRESS:" << progress << std::endl;
// Less verbose output for Streamlit
if (beta_idx % 20 == 0 || beta_idx == num_beta_points - 1) {
std::cout << "Processing beta = " << beta
<< " (" << beta_idx+1 << "/" << num_beta_points << ")" << std::endl;
}
}
// Save data as JSON for Python to read
if (!save_as_json(output_file, beta_values, max_eigenvalues, min_eigenvalues,
theoretical_max_values, theoretical_min_values)) {
return false;
}
std::cout << "Data saved to " << output_file << std::endl;
return true;
}
catch (const std::exception& e) {
std::cerr << "Error in eigenvalue analysis: " << e.what() << std::endl;
return false;
}
catch (...) {
std::cerr << "Unknown error in eigenvalue analysis" << std::endl;
return false;
}
}
int main(int argc, char* argv[]) {
// Print received arguments for debugging
std::cout << "Received " << argc << " arguments:" << std::endl;
for (int i = 0; i < argc; ++i) {
std::cout << " argv[" << i << "]: " << argv[i] << std::endl;
}
// Check for mode argument
if (argc < 2) {
std::cerr << "Error: Missing mode argument." << std::endl;
std::cerr << "Usage: " << argv[0] << " eigenvalues <n> <p> <a> <y> <fineness> <theory_grid_points> <theory_tolerance> <output_file>" << std::endl;
return 1;
}
std::string mode = argv[1];
try {
if (mode == "eigenvalues") {
// ─── Eigenvalue analysis mode ───────────────────────────────────────────
if (argc != 10) {
std::cerr << "Error: Incorrect number of arguments for eigenvalues mode." << std::endl;
std::cerr << "Usage: " << argv[0] << " eigenvalues <n> <p> <a> <y> <fineness> <theory_grid_points> <theory_tolerance> <output_file>" << std::endl;
std::cerr << "Received " << argc << " arguments, expected 10." << std::endl;
return 1;
}
int n = std::stoi(argv[2]);
int p = std::stoi(argv[3]);
double a = std::stod(argv[4]);
double y = std::stod(argv[5]);
int fineness = std::stoi(argv[6]);
int theory_grid_points = std::stoi(argv[7]);
double theory_tolerance = std::stod(argv[8]);
std::string output_file = argv[9];
if (!eigenvalueAnalysis(n, p, a, y, fineness, theory_grid_points, theory_tolerance, output_file)) {
return 1;
}
} else {
std::cerr << "Error: Unknown mode: " << mode << std::endl;
std::cerr << "Use 'eigenvalues'" << std::endl;
return 1;
}
}
catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
''')
# Compile the C++ code with the right OpenCV libraries
st.sidebar.title("Dashboard Settings")
need_compile = not os.path.exists(executable) or st.sidebar.button("Recompile C++ Code")
if need_compile:
with st.sidebar:
with st.spinner("Compiling C++ code..."):
# Try to detect the OpenCV installation
opencv_detection_cmd = ["pkg-config", "--cflags", "--libs", "opencv4"]
opencv_found, opencv_flags, _ = run_command(opencv_detection_cmd, show_output=False)
compile_commands = []
if opencv_found:
compile_commands.append(
f"g++ -o {executable} {cpp_file} {opencv_flags.strip()} -std=c++11"
)
else:
# Try different OpenCV configurations
compile_commands = [
f"g++ -o {executable} {cpp_file} `pkg-config --cflags --libs opencv4` -std=c++11",
f"g++ -o {executable} {cpp_file} `pkg-config --cflags --libs opencv` -std=c++11",
f"g++ -o {executable} {cpp_file} -I/usr/include/opencv4 -lopencv_core -lopencv_imgproc -std=c++11",
f"g++ -o {executable} {cpp_file} -I/usr/local/include/opencv4 -lopencv_core -lopencv_imgproc -std=c++11"
]
compiled = False
compile_output = ""
for cmd in compile_commands:
st.text(f"Trying: {cmd}")
success, stdout, stderr = run_command(cmd.split(), show_output=False)
compile_output += f"Command: {cmd}\nOutput: {stdout}\nError: {stderr}\n\n"
if success:
compiled = True
st.success(f"Successfully compiled with: {cmd}")
break
if not compiled:
st.error("All compilation attempts failed.")
with st.expander("Compilation Details"):
st.code(compile_output)
st.stop()
# Make sure the executable is executable
if platform.system() != "Windows":
os.chmod(executable, 0o755)
st.success("C++ code compiled successfully!")
# Set higher precision for mpmath
mpmath.mp.dps = 100 # 100 digits of precision
# Improved cubic equation solver using SymPy with high precision
def solve_cubic(a, b, c, d):
"""
Solve cubic equation ax^3 + bx^2 + cx + d = 0 using sympy with high precision.
Returns a list with three complex roots.
"""
# Constants for numerical stability
epsilon = 1e-40 # Very small value for higher precision
zero_threshold = 1e-20
# Create symbolic variable
s = sp.Symbol('s')
# Special case handling
if abs(a) < epsilon:
# Quadratic case handling
if abs(b) < epsilon: # Linear equation or constant
if abs(c) < epsilon: # Constant
return [complex(float('nan')), complex(float('nan')), complex(float('nan'))]
else: # Linear
return [complex(-d/c), complex(float('inf')), complex(float('inf'))]
# Standard quadratic formula with high precision
discriminant = c*c - 4.0*b*d
if discriminant >= 0:
sqrt_disc = sp.sqrt(discriminant)
root1 = (-c + sqrt_disc) / (2.0 * b)
root2 = (-c - sqrt_disc) / (2.0 * b)
return [complex(float(N(root1, 100))),
complex(float(N(root2, 100))),
complex(float('inf'))]
else:
real_part = -c / (2.0 * b)
imag_part = sp.sqrt(-discriminant) / (2.0 * b)
real_val = float(N(real_part, 100))
imag_val = float(N(imag_part, 100))
return [complex(real_val, imag_val),
complex(real_val, -imag_val),
complex(float('inf'))]
# Special case for d=0 (one root is zero)
if abs(d) < epsilon:
# One root is exactly zero
roots = [complex(0.0, 0.0)]
# Solve remaining quadratic: ax^2 + bx + c = 0
quad_disc = b*b - 4.0*a*c
if quad_disc >= 0:
sqrt_disc = sp.sqrt(quad_disc)
r1 = (-b + sqrt_disc) / (2.0 * a)
r2 = (-b - sqrt_disc) / (2.0 * a)
# Get precise values
r1_val = float(N(r1, 100))
r2_val = float(N(r2, 100))
# Ensure one positive and one negative root
if r1_val > 0 and r2_val > 0:
roots.append(complex(r1_val, 0.0))
roots.append(complex(-abs(r2_val), 0.0))
elif r1_val < 0 and r2_val < 0:
roots.append(complex(-abs(r1_val), 0.0))
roots.append(complex(abs(r2_val), 0.0))
else:
roots.append(complex(r1_val, 0.0))
roots.append(complex(r2_val, 0.0))
return roots
else:
real_part = -b / (2.0 * a)
imag_part = sp.sqrt(-quad_disc) / (2.0 * a)
real_val = float(N(real_part, 100))
imag_val = float(N(imag_part, 100))
roots.append(complex(real_val, imag_val))
roots.append(complex(real_val, -imag_val))
return roots
# Create exact symbolic equation with high precision
eq = a * s**3 + b * s**2 + c * s + d
# Solve using SymPy's solver
sympy_roots = sp.solve(eq, s)
# Process roots with high precision
roots = []
for root in sympy_roots:
real_part = float(N(sp.re(root), 100))
imag_part = float(N(sp.im(root), 100))
roots.append(complex(real_part, imag_part))
# Ensure roots follow the expected pattern
# Check if pattern is already satisfied
zeros = [r for r in roots if abs(r.real) < zero_threshold]
positives = [r for r in roots if r.real > zero_threshold]
negatives = [r for r in roots if r.real < -zero_threshold]
if (len(zeros) == 1 and len(positives) == 1 and len(negatives) == 1) or len(zeros) == 3:
return roots
# If all roots are almost zeros, return three zeros
if all(abs(r.real) < zero_threshold for r in roots):
return [complex(0.0, 0.0), complex(0.0, 0.0), complex(0.0, 0.0)]
# Sort roots by real part
roots.sort(key=lambda r: r.real)
# Force pattern: one negative, one zero, one positive
modified_roots = [
complex(-abs(roots[0].real), 0.0), # Negative
complex(0.0, 0.0), # Zero
complex(abs(roots[-1].real), 0.0) # Positive
]
return modified_roots
# Function to compute Im(s) vs z data using the SymPy solver
def compute_ImS_vs_Z(a, y, beta, num_points, z_min, z_max, progress_callback=None):
# Use logarithmic spacing for z values (better visualization)
z_values = np.logspace(np.log10(max(0.01, z_min)), np.log10(z_max), num_points)
ims_values1 = np.zeros(num_points)
ims_values2 = np.zeros(num_points)
ims_values3 = np.zeros(num_points)
real_values1 = np.zeros(num_points)
real_values2 = np.zeros(num_points)
real_values3 = np.zeros(num_points)
for i, z in enumerate(z_values):
# Update progress if callback provided
if progress_callback and i % 5 == 0:
progress_callback(i / num_points)
# Coefficients for the cubic equation:
# zas³ + [z(a+1)+a(1-y)]s² + [z+(a+1)-y-yβ(a-1)]s + 1 = 0
coef_a = z * a
coef_b = z * (a + 1) + a * (1 - y)
coef_c = z + (a + 1) - y - y * beta * (a - 1)
coef_d = 1.0
# Solve the cubic equation with high precision
roots = solve_cubic(coef_a, coef_b, coef_c, coef_d)
# Store imaginary and real parts
ims_values1[i] = abs(roots[0].imag)
ims_values2[i] = abs(roots[1].imag)
ims_values3[i] = abs(roots[2].imag)
real_values1[i] = roots[0].real
real_values2[i] = roots[1].real
real_values3[i] = roots[2].real
# Prepare result data
result = {
'z_values': z_values,
'ims_values1': ims_values1,
'ims_values2': ims_values2,
'ims_values3': ims_values3,
'real_values1': real_values1,
'real_values2': real_values2,
'real_values3': real_values3
}
# Final progress update
if progress_callback:
progress_callback(1.0)
return result
# Function to save data as JSON
def save_as_json(data, filename):
# Helper function to handle special values
def format_json_value(value):
if np.isnan(value):
return "NaN"
elif np.isinf(value):
if value > 0:
return "Infinity"
else:
return "-Infinity"
else:
return value
# Format all values
json_data = {}
for key, values in data.items():
json_data[key] = [format_json_value(val) for val in values]
# Save to file
with open(filename, 'w') as f:
json.dump(json_data, f, indent=2)
# Create high-quality Dash-like visualizations for cubic equation analysis
def create_dash_style_visualization(result, cubic_a, cubic_y, cubic_beta):
# Extract data from result
z_values = result['z_values']
ims_values1 = result['ims_values1']
ims_values2 = result['ims_values2']
ims_values3 = result['ims_values3']
real_values1 = result['real_values1']
real_values2 = result['real_values2']
real_values3 = result['real_values3']
# Create subplot figure with 2 rows for imaginary and real parts
fig = make_subplots(
rows=2,
cols=1,
subplot_titles=(
f"Imaginary Parts of Roots: a={cubic_a}, y={cubic_y}, β={cubic_beta}",
f"Real Parts of Roots: a={cubic_a}, y={cubic_y}, β={cubic_beta}"
),
vertical_spacing=0.15,
specs=[[{"type": "scatter"}], [{"type": "scatter"}]]
)
# Add traces for imaginary parts
fig.add_trace(
go.Scatter(
x=z_values,
y=ims_values1,
mode='lines',
name='Im(s₁)',
line=dict(color='rgb(239, 85, 59)', width=2.5),
hovertemplate='z: %{x:.4f}<br>Im(s₁): %{y:.6f}<extra>Root 1</extra>'
),
row=1, col=1
)
fig.add_trace(
go.Scatter(
x=z_values,
y=ims_values2,
mode='lines',
name='Im(s₂)',
line=dict(color='rgb(0, 129, 201)', width=2.5),
hovertemplate='z: %{x:.4f}<br>Im(s₂): %{y:.6f}<extra>Root 2</extra>'
),
row=1, col=1
)
fig.add_trace(
go.Scatter(
x=z_values,
y=ims_values3,
mode='lines',
name='Im(s₃)',
line=dict(color='rgb(0, 176, 80)', width=2.5),
hovertemplate='z: %{x:.4f}<br>Im(s₃): %{y:.6f}<extra>Root 3</extra>'
),
row=1, col=1
)
# Add traces for real parts
fig.add_trace(
go.Scatter(
x=z_values,
y=real_values1,
mode='lines',
name='Re(s₁)',
line=dict(color='rgb(239, 85, 59)', width=2.5),
hovertemplate='z: %{x:.4f}<br>Re(s₁): %{y:.6f}<extra>Root 1</extra>'
),
row=2, col=1
)
fig.add_trace(
go.Scatter(
x=z_values,
y=real_values2,
mode='lines',
name='Re(s₂)',
line=dict(color='rgb(0, 129, 201)', width=2.5),
hovertemplate='z: %{x:.4f}<br>Re(s₂): %{y:.6f}<extra>Root 2</extra>'
),
row=2, col=1
)
fig.add_trace(
go.Scatter(
x=z_values,
y=real_values3,
mode='lines',
name='Re(s₃)',
line=dict(color='rgb(0, 176, 80)', width=2.5),
hovertemplate='z: %{x:.4f}<br>Re(s₃): %{y:.6f}<extra>Root 3</extra>'
),
row=2, col=1
)
# Add horizontal line at y=0 for real parts
fig.add_shape(
type="line",
x0=min(z_values),
y0=0,
x1=max(z_values),
y1=0,
line=dict(color="black", width=1, dash="dash"),
row=2, col=1
)
# Compute y-axis ranges
max_im_value = max(np.max(ims_values1), np.max(ims_values2), np.max(ims_values3))
real_min = min(np.min(real_values1), np.min(real_values2), np.min(real_values3))
real_max = max(np.max(real_values1), np.max(real_values2), np.max(real_values3))
y_range = max(abs(real_min), abs(real_max))
# Update layout for professional Dash-like appearance
fig.update_layout(
title={
'text': 'Cubic Equation Roots Analysis',
'font': {'size': 24, 'color': '#333333', 'family': 'Arial, sans-serif'},
'x': 0.5,
'xanchor': 'center',
'y': 0.97,
'yanchor': 'top'
},
legend={
'orientation': 'h',
'yanchor': 'bottom',
'y': 1.02,
'xanchor': 'center',
'x': 0.5,
'font': {'size': 12, 'color': '#333333', 'family': 'Arial, sans-serif'},
'bgcolor': 'rgba(255, 255, 255, 0.8)',
'bordercolor': 'rgba(0, 0, 0, 0.1)',
'borderwidth': 1
},
plot_bgcolor='white',
paper_bgcolor='white',
hovermode='closest',
margin={'l': 60, 'r': 60, 't': 100, 'b': 60},
height=800,
font=dict(family="Arial, sans-serif", size=12, color="#333333"),
showlegend=True
)
# Update axes for both subplots
fig.update_xaxes(
title_text="z (logarithmic scale)",
title_font=dict(size=14, family="Arial, sans-serif"),
type="log",
showgrid=True,
gridwidth=1,
gridcolor='rgba(220, 220, 220, 0.8)',
showline=True,
linewidth=1,
linecolor='black',
mirror=True,
row=1, col=1
)
fig.update_xaxes(
title_text="z (logarithmic scale)",
title_font=dict(size=14, family="Arial, sans-serif"),
type="log",
showgrid=True,
gridwidth=1,
gridcolor='rgba(220, 220, 220, 0.8)',
showline=True,
linewidth=1,
linecolor='black',
mirror=True,
row=2, col=1
)
fig.update_yaxes(
title_text="Im(s)",
title_font=dict(size=14, family="Arial, sans-serif"),
showgrid=True,
gridwidth=1,
gridcolor='rgba(220, 220, 220, 0.8)',
showline=True,
linewidth=1,
linecolor='black',
mirror=True,
range=[0, max_im_value * 1.1], # Only positive range for imaginary parts
row=1, col=1
)
fig.update_yaxes(
title_text="Re(s)",
title_font=dict(size=14, family="Arial, sans-serif"),
showgrid=True,
gridwidth=1,
gridcolor='rgba(220, 220, 220, 0.8)',
showline=True,
linewidth=1,
linecolor='black',
mirror=True,
range=[-y_range * 1.1, y_range * 1.1], # Symmetric range for real parts
zeroline=True,
zerolinewidth=1.5,
zerolinecolor='black',
row=2, col=1
)
return fig
# Create a root pattern visualization
def create_root_pattern_visualization(result):
# Extract data
z_values = result['z_values']
real_values1 = result['real_values1']
real_values2 = result['real_values2']
real_values3 = result['real_values3']
# Count patterns
pattern_types = []
colors = []
hover_texts = []
# Define color scheme
ideal_color = 'rgb(0, 129, 201)' # Blue
all_zeros_color = 'rgb(0, 176, 80)' # Green
other_color = 'rgb(239, 85, 59)' # Red
for i in range(len(z_values)):
# Count zeros, positives, and negatives
zeros = 0
positives = 0
negatives = 0
# Handle NaN values
r1 = real_values1[i] if not np.isnan(real_values1[i]) else 0
r2 = real_values2[i] if not np.isnan(real_values2[i]) else 0
r3 = real_values3[i] if not np.isnan(real_values3[i]) else 0
for r in [r1, r2, r3]:
if abs(r) < 1e-6:
zeros += 1
elif r > 0:
positives += 1
else:
negatives += 1
# Classify pattern
if zeros == 3:
pattern_types.append("All zeros")
colors.append(all_zeros_color)
hover_texts.append(f"z: {z_values[i]:.4f}<br>Pattern: All zeros<br>Roots: (0, 0, 0)")
elif zeros == 1 and positives == 1 and negatives == 1:
pattern_types.append("Ideal pattern")
colors.append(ideal_color)
hover_texts.append(f"z: {z_values[i]:.4f}<br>Pattern: Ideal (1 neg, 1 zero, 1 pos)<br>Roots: ({r1:.4f}, {r2:.4f}, {r3:.4f})")
else:
pattern_types.append("Other pattern")
colors.append(other_color)
hover_texts.append(f"z: {z_values[i]:.4f}<br>Pattern: Other ({negatives} neg, {zeros} zero, {positives} pos)<br>Roots: ({r1:.4f}, {r2:.4f}, {r3:.4f})")
# Create pattern visualization
fig = go.Figure()
# Add scatter plot with patterns
fig.add_trace(go.Scatter(
x=z_values,
y=[1] * len(z_values), # Constant y value
mode='markers',
marker=dict(
size=10,
color=colors,
symbol='circle',
line=dict(width=1, color='black')
),
hoverinfo='text',
hovertext=hover_texts,
showlegend=False
))
# Add custom legend
fig.add_trace(go.Scatter(
x=[None], y=[None],
mode='markers',
marker=dict(size=10, color=ideal_color),
name='Ideal pattern (1 neg, 1 zero, 1 pos)'
))
fig.add_trace(go.Scatter(
x=[None], y=[None],
mode='markers',
marker=dict(size=10, color=all_zeros_color),
name='All zeros'
))
fig.add_trace(go.Scatter(
x=[None], y=[None],
mode='markers',
marker=dict(size=10, color=other_color),
name='Other pattern'
))
# Update layout
fig.update_layout(
title={
'text': 'Root Pattern Analysis',
'font': {'size': 18, 'color': '#333333', 'family': 'Arial, sans-serif'},
'x': 0.5,
'y': 0.95
},
xaxis={
'title': 'z (logarithmic scale)',
'type': 'log',
'showgrid': True,
'gridcolor': 'rgba(220, 220, 220, 0.8)',
'showline': True,
'linecolor': 'black',
'mirror': True
},
yaxis={
'showticklabels': False,
'showgrid': False,
'zeroline': False,
'showline': False,
'range': [0.9, 1.1]
},
plot_bgcolor='white',
paper_bgcolor='white',
hovermode='closest',
legend={
'orientation': 'h',
'yanchor': 'bottom',
'y': 1.02,
'xanchor': 'right',
'x': 1
},
margin={'l': 60, 'r': 60, 't': 80, 'b': 60},
height=300
)
return fig
# Create complex plane visualization
def create_complex_plane_visualization(result, z_idx):
# Extract data
z_values = result['z_values']
real_values1 = result['real_values1']
real_values2 = result['real_values2']
real_values3 = result['real_values3']
ims_values1 = result['ims_values1']
ims_values2 = result['ims_values2']
ims_values3 = result['ims_values3']
# Get selected z value
selected_z = z_values[z_idx]
# Create complex number roots
roots = [
complex(real_values1[z_idx], ims_values1[z_idx]),
complex(real_values2[z_idx], ims_values2[z_idx]),
complex(real_values3[z_idx], -ims_values3[z_idx]) # Negative for third root
]
# Extract real and imaginary parts
real_parts = [root.real for root in roots]
imag_parts = [root.imag for root in roots]
# Determine plot range
max_abs_real = max(abs(max(real_parts)), abs(min(real_parts)))
max_abs_imag = max(abs(max(imag_parts)), abs(min(imag_parts)))
max_range = max(max_abs_real, max_abs_imag) * 1.2
# Create figure
fig = go.Figure()
# Add roots as points
fig.add_trace(go.Scatter(
x=real_parts,
y=imag_parts,
mode='markers+text',
marker=dict(
size=12,
color=['rgb(239, 85, 59)', 'rgb(0, 129, 201)', 'rgb(0, 176, 80)'],
symbol='circle',
line=dict(width=1, color='black')
),
text=['s₁', 's₂', 's₃'],
textposition="top center",
name='Roots'
))
# Add axis lines
fig.add_shape(
type="line",
x0=-max_range,
y0=0,
x1=max_range,
y1=0,
line=dict(color="black", width=1)
)
fig.add_shape(
type="line",
x0=0,
y0=-max_range,
x1=0,
y1=max_range,
line=dict(color="black", width=1)
)
# Add unit circle for reference
theta = np.linspace(0, 2*np.pi, 100)
x_circle = np.cos(theta)
y_circle = np.sin(theta)
fig.add_trace(go.Scatter(
x=x_circle,
y=y_circle,
mode='lines',
line=dict(color='rgba(100, 100, 100, 0.3)', width=1, dash='dash'),
name='Unit Circle'
))
# Update layout
fig.update_layout(
title={
'text': f'Roots in Complex Plane for z = {selected_z:.4f}',
'font': {'size': 18, 'color': '#333333', 'family': 'Arial, sans-serif'},
'x': 0.5,
'y': 0.95
},
xaxis={
'title': 'Real Part',
'range': [-max_range, max_range],
'showgrid': True,
'zeroline': False,
'showline': True,
'linecolor': 'black',
'mirror': True,
'gridcolor': 'rgba(220, 220, 220, 0.8)'
},
yaxis={
'title': 'Imaginary Part',
'range': [-max_range, max_range],
'showgrid': True,
'zeroline': False,
'showline': True,
'linecolor': 'black',
'mirror': True,
'scaleanchor': 'x',
'scaleratio': 1,
'gridcolor': 'rgba(220, 220, 220, 0.8)'
},
plot_bgcolor='white',
paper_bgcolor='white',
hovermode='closest',
showlegend=False,
annotations=[
dict(
text=f"Root 1: {roots[0].real:.4f} + {abs(roots[0].imag):.4f}i",
x=0.02, y=0.98, xref="paper", yref="paper",
showarrow=False, font=dict(color='rgb(239, 85, 59)', size=12)
),
dict(
text=f"Root 2: {roots[1].real:.4f} + {abs(roots[1].imag):.4f}i",
x=0.02, y=0.94, xref="paper", yref="paper",
showarrow=False, font=dict(color='rgb(0, 129, 201)', size=12)
),
dict(
text=f"Root 3: {roots[2].real:.4f} + {abs(roots[2].imag):.4f}i",
x=0.02, y=0.90, xref="paper", yref="paper",
showarrow=False, font=dict(color='rgb(0, 176, 80)', size=12)
)
],
width=600,
height=500,
margin=dict(l=60, r=60, t=80, b=60)
)
return fig
# ----- Additional Complex Root Utilities -----
def compute_cubic_roots(z, beta, z_a, y):
"""Compute roots of the cubic equation using SymPy for high precision."""
y_effective = y if y > 1 else 1 / y
from sympy import symbols, solve, N, Poly
s = symbols('s')
a = z * z_a
b = z * z_a + z + z_a - z_a * y_effective
c = z + z_a + 1 - y_effective * (beta * z_a + 1 - beta)
d = 1
if abs(a) < 1e-10:
if abs(b) < 1e-10:
roots = np.array([-d / c, 0, 0], dtype=complex)
else:
quad_roots = np.roots([b, c, d])
roots = np.append(quad_roots, 0).astype(complex)
return roots
try:
cubic_eq = Poly(a * s ** 3 + b * s ** 2 + c * s + d, s)
symbolic_roots = solve(cubic_eq, s)
numerical_roots = [complex(N(root, 30)) for root in symbolic_roots]
while len(numerical_roots) < 3:
numerical_roots.append(0j)
return np.array(numerical_roots, dtype=complex)
except Exception:
coeffs = [a, b, c, d]
return np.roots(coeffs)
def track_roots_consistently(z_values, all_roots):
n_points = len(z_values)
n_roots = all_roots[0].shape[0]
tracked_roots = np.zeros((n_points, n_roots), dtype=complex)
tracked_roots[0] = all_roots[0]
for i in range(1, n_points):
prev_roots = tracked_roots[i - 1]
current_roots = all_roots[i]
assigned = np.zeros(n_roots, dtype=bool)
assignments = np.zeros(n_roots, dtype=int)
for j in range(n_roots):
distances = np.abs(current_roots - prev_roots[j])
while True:
best_idx = np.argmin(distances)
if not assigned[best_idx]:
assignments[j] = best_idx
assigned[best_idx] = True
break
distances[best_idx] = np.inf
if np.all(distances == np.inf):
assignments[j] = j
break
tracked_roots[i] = current_roots[assignments]
return tracked_roots
def generate_cubic_discriminant(z, beta, z_a, y_effective):
a = z * z_a
b = z * z_a + z + z_a - z_a * y_effective
c = z + z_a + 1 - y_effective * (beta * z_a + 1 - beta)
d = 1
return (18 * a * b * c * d - 27 * a ** 2 * d ** 2 + b ** 2 * c ** 2 -
2 * b ** 3 * d - 9 * a * c ** 3)
def generate_root_plots(beta, y, z_a, z_min, z_max, n_points):
if z_a <= 0 or y <= 0 or z_min >= z_max:
st.error("Invalid input parameters.")
return None, None, None
y_effective = y if y > 1 else 1 / y
z_points = np.linspace(z_min, z_max, n_points)
all_roots = []
discriminants = []
progress_bar = st.progress(0)
status_text = st.empty()
for i, z in enumerate(z_points):
progress_bar.progress((i + 1) / n_points)
status_text.text(f"Computing roots for z = {z:.3f} ({i+1}/{n_points})")
roots = compute_cubic_roots(z, beta, z_a, y)
roots = sorted(roots, key=lambda x: (abs(x.imag), x.real))
all_roots.append(roots)
disc = generate_cubic_discriminant(z, beta, z_a, y_effective)
discriminants.append(disc)
progress_bar.empty()
status_text.empty()
all_roots = np.array(all_roots)
discriminants = np.array(discriminants)
tracked_roots = track_roots_consistently(z_points, all_roots)
ims = np.imag(tracked_roots)
res = np.real(tracked_roots)
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)))
disc_zeros = []
for i in range(len(discriminants) - 1):
if discriminants[i] * discriminants[i + 1] <= 0:
zero_pos = z_points[i] + (z_points[i + 1] - z_points[i]) * (0 - discriminants[i]) / (discriminants[i + 1] - discriminants[i])
disc_zeros.append(zero_pos)
fig_im.add_vline(x=zero_pos, line=dict(color="red", width=1, dash="dash"))
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)))
for zero_pos in disc_zeros:
fig_re.add_vline(x=zero_pos, line=dict(color="red", width=1, dash="dash"))
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")
fig_disc = go.Figure()
fig_disc.add_trace(go.Scatter(x=z_points, y=discriminants, mode="lines", name="Cubic Discriminant", line=dict(color="black", width=2)))
fig_disc.add_hline(y=0, line=dict(color="red", width=1, dash="dash"))
fig_disc.update_layout(title=f"Cubic Discriminant vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})",
xaxis_title="z", yaxis_title="Discriminant", hovermode="x unified")
return fig_im, fig_re, fig_disc
def analyze_complex_root_structure(beta_values, z, z_a, y):
y_effective = y if y > 1 else 1 / y
transition_points = []
structure_types = []
previous_type = None
for beta in beta_values:
roots = compute_cubic_roots(z, beta, z_a, y)
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:
transition_points.append(beta)
structure_types.append(previous_type)
previous_type = current_type
if previous_type is not None:
structure_types.append(previous_type)
return transition_points, structure_types
def generate_roots_vs_beta_plots(z, y, z_a, beta_min, beta_max, n_points):
if z_a <= 0 or y <= 0 or beta_min >= beta_max:
st.error("Invalid input parameters.")
return None, None, None
y_effective = y if y > 1 else 1 / y
beta_points = np.linspace(beta_min, beta_max, n_points)
all_roots = []
discriminants = []
progress_bar = st.progress(0)
status_text = st.empty()
for i, beta in enumerate(beta_points):
progress_bar.progress((i + 1) / n_points)
status_text.text(f"Computing roots for β = {beta:.3f} ({i+1}/{n_points})")
roots = compute_cubic_roots(z, beta, z_a, y)
roots = sorted(roots, key=lambda x: (abs(x.imag), x.real))
all_roots.append(roots)
disc = generate_cubic_discriminant(z, beta, z_a, y_effective)
discriminants.append(disc)
progress_bar.empty()
status_text.empty()
all_roots = np.array(all_roots)
discriminants = np.array(discriminants)
tracked_roots = track_roots_consistently(beta_points, all_roots)
ims = np.imag(tracked_roots)
res = np.real(tracked_roots)
fig_im = go.Figure()
for i in range(3):
fig_im.add_trace(go.Scatter(x=beta_points, y=ims[:, i], mode="lines", name=f"Im{{s{i+1}}}", line=dict(width=2)))
disc_zeros = []
for i in range(len(discriminants) - 1):
if discriminants[i] * discriminants[i + 1] <= 0:
zero_pos = beta_points[i] + (beta_points[i + 1] - beta_points[i]) * (0 - discriminants[i]) / (discriminants[i + 1] - discriminants[i])
disc_zeros.append(zero_pos)
fig_im.add_vline(x=zero_pos, line=dict(color="red", width=1, dash="dash"))
fig_im.update_layout(title=f"Im{{s}} vs. β (z={z:.3f}, y={y:.3f}, z_a={z_a:.3f})",
xaxis_title="β", yaxis_title="Im{s}", hovermode="x unified")
fig_re = go.Figure()
for i in range(3):
fig_re.add_trace(go.Scatter(x=beta_points, y=res[:, i], mode="lines", name=f"Re{{s{i+1}}}", line=dict(width=2)))
for zero_pos in disc_zeros:
fig_re.add_vline(x=zero_pos, line=dict(color="red", width=1, dash="dash"))
fig_re.update_layout(title=f"Re{{s}} vs. β (z={z:.3f}, y={y:.3f}, z_a={z_a:.3f})",
xaxis_title="β", yaxis_title="Re{s}", hovermode="x unified")
fig_disc = go.Figure()
fig_disc.add_trace(go.Scatter(x=beta_points, y=discriminants, mode="lines", name="Cubic Discriminant", line=dict(color="black", width=2)))
fig_disc.add_hline(y=0, line=dict(color="red", width=1, dash="dash"))
fig_disc.update_layout(title=f"Cubic Discriminant vs. β (z={z:.3f}, y={y:.3f}, z_a={z_a:.3f})",
xaxis_title="β", yaxis_title="Discriminant", hovermode="x unified")
return fig_im, fig_re, fig_disc
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):
y_effective = y if y > 1 else 1 / y
beta_values = np.linspace(beta_min, beta_max, beta_steps)
z_values = np.linspace(z_min, z_max, z_steps)
phase_map = np.zeros((z_steps, beta_steps))
progress_bar = st.progress(0)
status_text = st.empty()
for i, z in enumerate(z_values):
progress_bar.progress((i + 1) / len(z_values))
status_text.text(f"Analyzing phase at z = {z:.2f} ({i+1}/{len(z_values)})")
for j, beta in enumerate(beta_values):
roots = compute_cubic_roots(z, beta, z_a, y)
is_all_real = all(abs(root.imag) < 1e-10 for root in roots)
phase_map[i, j] = 1 if is_all_real else -1
progress_bar.empty()
status_text.empty()
fig = go.Figure(data=go.Heatmap(z=phase_map, x=beta_values, y=z_values,
colorscale=[[0, 'blue'], [0.5, 'white'], [1.0, 'red']],
zmin=-1, zmax=1, showscale=True,
colorbar=dict(title="Root Type", tickvals=[-1, 1], ticktext=["Complex Roots", "All Real Roots"])) )
fig.update_layout(title=f"Phase Diagram: Root Structure (y={y:.3f}, z_a={z_a:.3f})",
xaxis_title="β", yaxis_title="z", hovermode="closest")
return fig
@st.cache_data
def generate_eigenvalue_distribution(beta, y, z_a, n=1000, seed=42):
y_effective = y if y > 1 else 1 / y
np.random.seed(seed)
p = int(y_effective * n)
k = int(np.floor(beta * p))
diag_entries = np.concatenate([np.full(k, z_a), np.full(p - k, 1.0)])
np.random.shuffle(diag_entries)
T_n = np.diag(diag_entries)
X = np.random.randn(p, n)
S_n = (1 / n) * (X @ X.T)
B_n = S_n @ T_n
eigenvalues = np.linalg.eigvalsh(B_n)
kde = gaussian_kde(eigenvalues)
x_vals = np.linspace(min(eigenvalues), max(eigenvalues), 500)
kde_vals = kde(x_vals)
fig = go.Figure()
fig.add_trace(go.Histogram(x=eigenvalues, histnorm='probability density', name="Histogram", marker=dict(color='blue', opacity=0.6)))
fig.add_trace(go.Scatter(x=x_vals, y=kde_vals, mode="lines", name="KDE", line=dict(color='red', width=2)))
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)
return fig, eigenvalues
# Options for theme and appearance
def compute_eigenvalue_support_boundaries(z_a, y, betas, n_samples=1000, seeds=5):
return np.zeros(len(betas)), np.ones(len(betas))
with st.sidebar.expander("Theme & Appearance"):
show_annotations = st.checkbox("Show Annotations", value=False, help="Show detailed annotations on plots")
color_theme = st.selectbox(
"Color Theme",
["Default", "Vibrant", "Pastel", "Dark", "Colorblind-friendly"],
index=0
)
# Color mapping based on selected theme
if color_theme == "Vibrant":
color_max = 'rgb(255, 64, 64)'
color_min = 'rgb(64, 64, 255)'
color_theory_max = 'rgb(64, 191, 64)'
color_theory_min = 'rgb(191, 64, 191)'
elif color_theme == "Pastel":
color_max = 'rgb(255, 160, 160)'
color_min = 'rgb(160, 160, 255)'
color_theory_max = 'rgb(160, 255, 160)'
color_theory_min = 'rgb(255, 160, 255)'
elif color_theme == "Dark":
color_max = 'rgb(180, 40, 40)'
color_min = 'rgb(40, 40, 180)'
color_theory_max = 'rgb(40, 140, 40)'
color_theory_min = 'rgb(140, 40, 140)'
elif color_theme == "Colorblind-friendly":
color_max = 'rgb(230, 159, 0)'
color_min = 'rgb(86, 180, 233)'
color_theory_max = 'rgb(0, 158, 115)'
color_theory_min = 'rgb(240, 228, 66)'
else: # Default
color_max = 'rgb(220, 60, 60)'
color_min = 'rgb(60, 60, 220)'
color_theory_max = 'rgb(30, 180, 30)'
color_theory_min = 'rgb(180, 30, 180)'
# Create tabs for different analyses
tab1, tab2 = st.tabs(["Eigenvalue Analysis (C++)", "Im(s) vs z Analysis (SymPy)"])
# Tab 1: Eigenvalue Analysis (KEEP UNCHANGED from original)
with tab1:
# Two-column layout for the dashboard
left_column, right_column = st.columns([1, 3])
with left_column:
st.markdown('<div class="dashboard-container">', unsafe_allow_html=True)
st.markdown('<div class="panel-header">Eigenvalue Analysis Controls</div>', unsafe_allow_html=True)
# Parameter inputs with defaults and validation
st.markdown('<div class="parameter-container">', unsafe_allow_html=True)
st.markdown("### Matrix Parameters")
n = st.number_input("Sample size (n)", min_value=5, max_value=10000000, value=100, step=5,
help="Number of samples", key="eig_n")
p = st.number_input("Dimension (p)", min_value=5, max_value=10000000, value=50, step=5,
help="Dimensionality", key="eig_p")
a = st.number_input("Value for a", min_value=1.1, max_value=10000.0, value=2.0, step=0.1,
help="Parameter a > 1", key="eig_a")
# Automatically calculate y = p/n (as requested)
y = p/n
st.info(f"Value for y = p/n: {y:.4f}")
st.markdown('</div>', unsafe_allow_html=True)
st.markdown('<div class="parameter-container">', unsafe_allow_html=True)
st.markdown("### Calculation Controls")
fineness = st.slider(
"Beta points",
min_value=20,
max_value=500,
value=100,
step=10,
help="Number of points to calculate along the β axis (0 to 1)",
key="eig_fineness"
)
st.markdown('</div>', unsafe_allow_html=True)
with st.expander("Advanced Settings"):
# Add controls for theoretical calculation precision
theory_grid_points = st.slider(
"Theoretical grid points",
min_value=100,
max_value=1000,
value=200,
step=50,
help="Number of points in initial grid search for theoretical calculations",
key="eig_grid_points"
)
theory_tolerance = st.number_input(
"Theoretical tolerance",
min_value=1e-12,
max_value=1e-6,
value=1e-10,
format="%.1e",
help="Convergence tolerance for golden section search",
key="eig_tolerance"
)
# Debug mode
debug_mode = st.checkbox("Debug Mode", value=False, key="eig_debug")
# Timeout setting
timeout_seconds = st.number_input(
"Computation timeout (seconds)",
min_value=30,
max_value=3600,
value=300,
help="Maximum time allowed for computation before timeout",
key="eig_timeout"
)
# Generate button
eig_generate_button = st.button("Generate Eigenvalue Analysis",
type="primary",
use_container_width=True,
key="eig_generate")
st.markdown('</div>', unsafe_allow_html=True)
with right_column:
# Main visualization area
st.markdown('<div class="dashboard-container">', unsafe_allow_html=True)
st.markdown('<div class="panel-header">Eigenvalue Analysis Results</div>', unsafe_allow_html=True)
# Container for the analysis results
eig_results_container = st.container()
# Process when generate button is clicked
if eig_generate_button:
with eig_results_container:
# Show progress
progress_container = st.container()
with progress_container:
progress_bar = st.progress(0)
status_text = st.empty()
try:
# Create data file path
data_file = os.path.join(output_dir, "eigenvalue_data.json")
# Delete previous output if exists
if os.path.exists(data_file):
os.remove(data_file)
# Build command for eigenvalue analysis with the proper arguments
cmd = [
executable,
"eigenvalues", # Mode argument
str(n),
str(p),
str(a),
str(y),
str(fineness),
str(theory_grid_points),
str(theory_tolerance),
data_file
]
# Run the command
status_text.text("Running eigenvalue analysis...")
if debug_mode:
success, stdout, stderr = run_command(cmd, True, timeout=timeout_seconds)
# Process stdout for progress updates
if success:
progress_bar.progress(1.0)
else:
# Start the process with pipe for stdout to read progress
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
universal_newlines=True
)
# Track progress from stdout
success = True
stdout_lines = []
start_time = time.time()
while True:
# Check for timeout
if time.time() - start_time > timeout_seconds:
process.kill()
status_text.error(f"Computation timed out after {timeout_seconds} seconds")
success = False
break
# Try to read a line (non-blocking)
line = process.stdout.readline()
if not line and process.poll() is not None:
break
if line:
stdout_lines.append(line)
if line.startswith("PROGRESS:"):
try:
# Update progress bar
progress_value = float(line.split(":")[1].strip())
progress_bar.progress(progress_value)
status_text.text(f"Calculating... {int(progress_value * 100)}% complete")
except:
pass
elif line:
status_text.text(line.strip())
# Get the return code and stderr
returncode = process.poll()
stderr = process.stderr.read()
if returncode != 0:
success = False
st.error(f"Error executing the analysis: {stderr}")
with st.expander("Error Details"):
st.code(stderr)
if success:
progress_bar.progress(1.0)
status_text.text("Analysis complete! Generating visualization...")
# Check if the output file was created
if not os.path.exists(data_file):
st.error(f"Output file not created: {data_file}")
st.stop()
try:
# Load the results from the JSON file
with open(data_file, 'r') as f:
data = json.load(f)
# Process data - convert string values to numeric
beta_values = np.array([safe_convert_to_numeric(x) for x in data['beta_values']])
max_eigenvalues = np.array([safe_convert_to_numeric(x) for x in data['max_eigenvalues']])
min_eigenvalues = np.array([safe_convert_to_numeric(x) for x in data['min_eigenvalues']])
theoretical_max = np.array([safe_convert_to_numeric(x) for x in data['theoretical_max']])
theoretical_min = np.array([safe_convert_to_numeric(x) for x in data['theoretical_min']])
# Create an interactive plot using Plotly
fig = go.Figure()
# Add traces for each line
fig.add_trace(go.Scatter(
x=beta_values,
y=max_eigenvalues,
mode='lines+markers',
name='Empirical Max Eigenvalue',
line=dict(color=color_max, width=3),
marker=dict(
symbol='circle',
size=8,
color=color_max,
line=dict(color='white', width=1)
),
hovertemplate='β: %{x:.3f}<br>Value: %{y:.6f}<extra>Empirical Max</extra>'
))
fig.add_trace(go.Scatter(
x=beta_values,
y=min_eigenvalues,
mode='lines+markers',
name='Empirical Min Eigenvalue',
line=dict(color=color_min, width=3),
marker=dict(
symbol='circle',
size=8,
color=color_min,
line=dict(color='white', width=1)
),
hovertemplate='β: %{x:.3f}<br>Value: %{y:.6f}<extra>Empirical Min</extra>'
))
fig.add_trace(go.Scatter(
x=beta_values,
y=theoretical_max,
mode='lines+markers',
name='Theoretical Max',
line=dict(color=color_theory_max, width=3),
marker=dict(
symbol='diamond',
size=8,
color=color_theory_max,
line=dict(color='white', width=1)
),
hovertemplate='β: %{x:.3f}<br>Value: %{y:.6f}<extra>Theoretical Max</extra>'
))
fig.add_trace(go.Scatter(
x=beta_values,
y=theoretical_min,
mode='lines+markers',
name='Theoretical Min',
line=dict(color=color_theory_min, width=3),
marker=dict(
symbol='diamond',
size=8,
color=color_theory_min,
line=dict(color='white', width=1)
),
hovertemplate='β: %{x:.3f}<br>Value: %{y:.6f}<extra>Theoretical Min</extra>'
))
# Configure layout for better appearance
fig.update_layout(
title={
'text': f'Eigenvalue Analysis: n={n}, p={p}, a={a}, y={y:.4f}',
'font': {'size': 24, 'color': '#0e1117'},
'y': 0.95,
'x': 0.5,
'xanchor': 'center',
'yanchor': 'top'
},
xaxis={
'title': {'text': 'β Parameter', 'font': {'size': 18, 'color': '#424242'}},
'tickfont': {'size': 14},
'gridcolor': 'rgba(220, 220, 220, 0.5)',
'showgrid': True
},
yaxis={
'title': {'text': 'Eigenvalues', 'font': {'size': 18, 'color': '#424242'}},
'tickfont': {'size': 14},
'gridcolor': 'rgba(220, 220, 220, 0.5)',
'showgrid': True
},
plot_bgcolor='rgba(250, 250, 250, 0.8)',
paper_bgcolor='rgba(255, 255, 255, 0.8)',
hovermode='closest',
legend={
'font': {'size': 14},
'bgcolor': 'rgba(255, 255, 255, 0.9)',
'bordercolor': 'rgba(200, 200, 200, 0.5)',
'borderwidth': 1
},
margin={'l': 60, 'r': 30, 't': 100, 'b': 60},
height=600,
)
# Add custom modebar buttons
fig.update_layout(
modebar_add=[
'drawline', 'drawopenpath', 'drawclosedpath',
'drawcircle', 'drawrect', 'eraseshape'
],
modebar_remove=['lasso2d', 'select2d'],
dragmode='zoom'
)
# Clear progress container
progress_container.empty()
# Display the interactive plot in Streamlit
st.plotly_chart(fig, use_container_width=True)
# Display statistics in a cleaner way
st.markdown('<div class="stats-box">', unsafe_allow_html=True)
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Max Empirical", f"{max_eigenvalues.max():.4f}")
with col2:
st.metric("Min Empirical", f"{min_eigenvalues.min():.4f}")
with col3:
st.metric("Max Theoretical", f"{theoretical_max.max():.4f}")
with col4:
st.metric("Min Theoretical", f"{theoretical_min.min():.4f}")
st.markdown('</div>', unsafe_allow_html=True)
except json.JSONDecodeError as e:
st.error(f"Error parsing JSON results: {str(e)}")
if os.path.exists(data_file):
with open(data_file, 'r') as f:
content = f.read()
st.code(content[:1000] + "..." if len(content) > 1000 else content)
except Exception as e:
st.error(f"An error occurred: {str(e)}")
if debug_mode:
st.exception(e)
else:
# Try to load existing data if available
data_file = os.path.join(output_dir, "eigenvalue_data.json")
if os.path.exists(data_file):
try:
with open(data_file, 'r') as f:
data = json.load(f)
# Process data - convert string values to numeric
beta_values = np.array([safe_convert_to_numeric(x) for x in data['beta_values']])
max_eigenvalues = np.array([safe_convert_to_numeric(x) for x in data['max_eigenvalues']])
min_eigenvalues = np.array([safe_convert_to_numeric(x) for x in data['min_eigenvalues']])
theoretical_max = np.array([safe_convert_to_numeric(x) for x in data['theoretical_max']])
theoretical_min = np.array([safe_convert_to_numeric(x) for x in data['theoretical_min']])
# Create an interactive plot using Plotly
fig = go.Figure()
# Add traces for each line
fig.add_trace(go.Scatter(
x=beta_values,
y=max_eigenvalues,
mode='lines+markers',
name='Empirical Max Eigenvalue',
line=dict(color=color_max, width=3),
marker=dict(
symbol='circle',
size=8,
color=color_max,
line=dict(color='white', width=1)
),
hovertemplate='β: %{x:.3f}<br>Value: %{y:.6f}<extra>Empirical Max</extra>'
))
fig.add_trace(go.Scatter(
x=beta_values,
y=min_eigenvalues,
mode='lines+markers',
name='Empirical Min Eigenvalue',
line=dict(color=color_min, width=3),
marker=dict(
symbol='circle',
size=8,
color=color_min,
line=dict(color='white', width=1)
),
hovertemplate='β: %{x:.3f}<br>Value: %{y:.6f}<extra>Empirical Min</extra>'
))
fig.add_trace(go.Scatter(
x=beta_values,
y=theoretical_max,
mode='lines+markers',
name='Theoretical Max',
line=dict(color=color_theory_max, width=3),
marker=dict(
symbol='diamond',
size=8,
color=color_theory_max,
line=dict(color='white', width=1)
),
hovertemplate='β: %{x:.3f}<br>Value: %{y:.6f}<extra>Theoretical Max</extra>'
))
fig.add_trace(go.Scatter(
x=beta_values,
y=theoretical_min,
mode='lines+markers',
name='Theoretical Min',
line=dict(color=color_theory_min, width=3),
marker=dict(
symbol='diamond',
size=8,
color=color_theory_min,
line=dict(color='white', width=1)
),
hovertemplate='β: %{x:.3f}<br>Value: %{y:.6f}<extra>Theoretical Min</extra>'
))
# Configure layout for better appearance
fig.update_layout(
title={
'text': f'Eigenvalue Analysis (Previous Result)',
'font': {'size': 24, 'color': '#0e1117'},
'y': 0.95,
'x': 0.5,
'xanchor': 'center',
'yanchor': 'top'
},
xaxis={
'title': {'text': 'β Parameter', 'font': {'size': 18, 'color': '#424242'}},
'tickfont': {'size': 14},
'gridcolor': 'rgba(220, 220, 220, 0.5)',
'showgrid': True
},
yaxis={
'title': {'text': 'Eigenvalues', 'font': {'size': 18, 'color': '#424242'}},
'tickfont': {'size': 14},
'gridcolor': 'rgba(220, 220, 220, 0.5)',
'showgrid': True
},
plot_bgcolor='rgba(250, 250, 250, 0.8)',
paper_bgcolor='rgba(255, 255, 255, 0.8)',
hovermode='closest',
legend={
'font': {'size': 14},
'bgcolor': 'rgba(255, 255, 255, 0.9)',
'bordercolor': 'rgba(200, 200, 200, 0.5)',
'borderwidth': 1
},
margin={'l': 60, 'r': 30, 't': 100, 'b': 60},
height=600
)
# Display the interactive plot in Streamlit
st.plotly_chart(fig, use_container_width=True)
st.info("This is the previous analysis result. Adjust parameters and click 'Generate Analysis' to create a new visualization.")
except Exception as e:
st.info("Set parameters and click 'Generate Eigenvalue Analysis' to create a visualization.")
else:
# Show placeholder
st.info("Set parameters and click 'Generate Eigenvalue Analysis' to create a visualization.")
st.markdown('</div>', unsafe_allow_html=True)
# ----- Tab 2: Complex Root Analysis -----
with tab2:
st.header("Complex Root Analysis")
plot_tabs = st.tabs(["Im{s} vs. z", "Im{s} vs. β", "Phase Diagram", "Eigenvalue Distribution"])
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)
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.
""")
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)
transition_points, structure_types = analyze_complex_root_structure(np.linspace(beta_min, beta_max, beta_points), z_beta, z_a_beta, y_beta)
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.")
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.
""")
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("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)
if fig_phase is not None:
st.plotly_chart(fig_phase, use_container_width=True)
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.
""")
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)
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:
fig_eigen, eigenvalues = generate_eigenvalue_distribution(beta_eigen, y_eigen, z_a_eigen, n=n_samples, seed=sim_seed)
if show_theoretical:
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)
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")
st.plotly_chart(fig_eigen, use_container_width=True)
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]:.4f}")
st.metric("Theoretical Max", f"{max_eig[0]:.4f}")
st.metric("Theoretical Width", f"{max_eig[0] - min_eig[0]:.4f}")
with col2:
st.metric("Empirical Min", f"{empirical_min:.4f}")
st.metric("Empirical Max", f"{empirical_max:.4f}")
st.metric("Empirical Width", f"{empirical_max - empirical_min:.4f}")
with col3:
st.metric("Min Difference", f"{empirical_min - min_eig[0]:.4f}")
st.metric("Max Difference", f"{empirical_max - max_eig[0]:.4f}")
st.metric("Width Difference", f"{(empirical_max - empirical_min) - (max_eig[0] - min_eig[0]):.4f}")
if show_empirical_stats:
st.markdown("### Eigenvalue Statistics")
col1, col2 = st.columns(2)
with col1:
st.metric("Mean", f"{np.mean(eigenvalues):.4f}")
st.metric("Median", f"{np.median(eigenvalues):.4f}")
with col2:
st.metric("Standard Deviation", f"{np.std(eigenvalues):.4f}")
st.metric("Interquartile Range", f"{np.percentile(eigenvalues, 75) - np.percentile(eigenvalues, 25):.4f}")
# Add footer with instructions
st.markdown("""
<div class="footer">
<h3>About the Matrix Analysis Dashboard</h3>
<p>This dashboard performs two types of analyses using different computational approaches:</p>
<ol>
<li><strong>Eigenvalue Analysis (C++):</strong> Uses C++ with OpenCV for high-performance computation of eigenvalues of random matrices.</li>
<li><strong>Im(s) vs z Analysis (SymPy):</strong> Uses Python's SymPy library with extended precision to accurately analyze the cubic equation roots.</li>
</ol>
<p>This hybrid approach combines C++'s performance for data-intensive calculations with SymPy's high-precision symbolic mathematics for accurate root finding.</p>
</div>
""", unsafe_allow_html=True)