euler314's picture
Update app.py
af5cab7 verified
raw
history blame
21.4 kB
import streamlit as st
import subprocess
import os
import json
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
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
# 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
return quadratic_solver(b, c, d)
# Special case for d=0 (one root is zero)
if abs(d) < epsilon:
return handle_zero_d_case(a, b, c)
# 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
return force_root_pattern(roots, zero_threshold)
# Helper function to solve quadratic equations
def quadratic_solver(b, c, d, epsilon=1e-40):
if abs(b) < epsilon: # Linear 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'))]
# Helper function to handle the case when d=0 (one root is zero)
def handle_zero_d_case(a, b, c):
# 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
# Helper function to force the root pattern
def force_root_pattern(roots, zero_threshold=1e-20):
# 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
# Create high-quality Dash-like visualizations for cubic equation analysis
def create_dash_style_visualizations(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,
width=900,
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