Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -490,54 +490,177 @@ def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps,
|
|
490 |
|
491 |
def compute_cubic_roots(z, beta, z_a, y):
|
492 |
"""
|
493 |
-
Compute the roots of the cubic equation for given parameters.
|
494 |
"""
|
495 |
# Apply the condition for y
|
496 |
y_effective = y if y > 1 else 1/y
|
497 |
|
|
|
498 |
a = z * z_a
|
499 |
b = z * z_a + z + z_a - z_a*y_effective
|
500 |
c = z + z_a + 1 - y_effective*(beta*z_a + 1 - beta)
|
501 |
d = 1
|
502 |
-
|
503 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
504 |
return roots
|
505 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
506 |
def generate_root_plots(beta, y, z_a, z_min, z_max, n_points):
|
507 |
"""
|
508 |
-
Generate Im(s) and Re(s) vs. z plots.
|
509 |
"""
|
510 |
if z_a <= 0 or y <= 0 or z_min >= z_max:
|
511 |
st.error("Invalid input parameters.")
|
512 |
-
return None, None
|
513 |
|
514 |
# Apply the condition for y
|
515 |
y_effective = y if y > 1 else 1/y
|
516 |
|
517 |
z_points = np.linspace(z_min, z_max, n_points)
|
518 |
-
|
|
|
|
|
519 |
for z in z_points:
|
520 |
roots = compute_cubic_roots(z, beta, z_a, y)
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
527 |
fig_im = go.Figure()
|
528 |
for i in range(3):
|
529 |
fig_im.add_trace(go.Scatter(x=z_points, y=ims[:, i], mode="lines", name=f"Im{{s{i+1}}}",
|
530 |
line=dict(width=2)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
531 |
fig_im.update_layout(title=f"Im{{s}} vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
532 |
xaxis_title="z", yaxis_title="Im{s}", hovermode="x unified")
|
533 |
|
|
|
534 |
fig_re = go.Figure()
|
535 |
for i in range(3):
|
536 |
fig_re.add_trace(go.Scatter(x=z_points, y=res[:, i], mode="lines", name=f"Re{{s{i+1}}}",
|
537 |
line=dict(width=2)))
|
|
|
|
|
|
|
|
|
|
|
538 |
fig_re.update_layout(title=f"Re{{s}} vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
539 |
xaxis_title="z", yaxis_title="Re{s}", hovermode="x unified")
|
540 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
541 |
|
542 |
def analyze_complex_root_structure(beta_values, z, z_a, y):
|
543 |
"""
|
@@ -579,7 +702,7 @@ def analyze_complex_root_structure(beta_values, z, z_a, y):
|
|
579 |
|
580 |
def generate_roots_vs_beta_plots(z, y, z_a, beta_min, beta_max, n_points):
|
581 |
"""
|
582 |
-
Generate Im(s) and Re(s) vs. β plots.
|
583 |
"""
|
584 |
if z_a <= 0 or y <= 0 or beta_min >= beta_max:
|
585 |
st.error("Invalid input parameters.")
|
@@ -589,140 +712,79 @@ def generate_roots_vs_beta_plots(z, y, z_a, beta_min, beta_max, n_points):
|
|
589 |
y_effective = y if y > 1 else 1/y
|
590 |
|
591 |
beta_points = np.linspace(beta_min, beta_max, n_points)
|
592 |
-
|
|
|
|
|
593 |
for beta in beta_points:
|
594 |
roots = compute_cubic_roots(z, beta, z_a, y)
|
595 |
-
|
596 |
-
|
597 |
-
|
598 |
-
|
599 |
-
|
600 |
-
|
601 |
-
#
|
602 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
603 |
|
604 |
-
# Create
|
605 |
fig_im = go.Figure()
|
606 |
for i in range(3):
|
607 |
fig_im.add_trace(go.Scatter(x=beta_points, y=ims[:, i], mode="lines", name=f"Im{{s{i+1}}}",
|
608 |
line=dict(width=2)))
|
609 |
|
610 |
-
# Add vertical lines at
|
611 |
-
|
612 |
-
|
613 |
-
|
|
|
|
|
|
|
|
|
614 |
fig_im.update_layout(title=f"Im{{s}} vs. β (z={z:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
615 |
xaxis_title="β", yaxis_title="Im{s}", hovermode="x unified")
|
616 |
|
617 |
-
# Create
|
618 |
fig_re = go.Figure()
|
619 |
for i in range(3):
|
620 |
fig_re.add_trace(go.Scatter(x=beta_points, y=res[:, i], mode="lines", name=f"Re{{s{i+1}}}",
|
621 |
line=dict(width=2)))
|
622 |
|
623 |
-
# Add vertical lines at
|
624 |
-
for
|
625 |
-
fig_re.add_vline(x=
|
626 |
-
|
627 |
fig_re.update_layout(title=f"Re{{s}} vs. β (z={z:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
628 |
xaxis_title="β", yaxis_title="Re{s}", hovermode="x unified")
|
629 |
|
630 |
-
# Create
|
631 |
fig_disc = go.Figure()
|
632 |
-
|
633 |
-
|
634 |
-
discriminant_values = []
|
635 |
-
for beta in beta_points:
|
636 |
-
# For cubic ax^3 + bx^2 + cx + d, the discriminant is:
|
637 |
-
# Δ = 18abcd - 27a^2d^2 + b^2c^2 - 2b^3d - 9ac^3
|
638 |
-
|
639 |
-
a = z * z_a
|
640 |
-
b = z * z_a + z + z_a - z_a*y_effective
|
641 |
-
c = z + z_a + 1 - y_effective*(beta*z_a + 1 - beta)
|
642 |
-
d = 1
|
643 |
-
|
644 |
-
delta0 = b*b - 3*a*c
|
645 |
-
delta1 = 2*b*b*b - 9*a*b*c + 27*a*a*d
|
646 |
-
discriminant = delta1*delta1 - 4*delta0*delta0*delta0
|
647 |
-
|
648 |
-
discriminant_values.append(discriminant)
|
649 |
-
|
650 |
-
fig_disc.add_trace(go.Scatter(x=beta_points, y=discriminant_values, mode="lines",
|
651 |
-
name="Discriminant", line=dict(width=2, color="black")))
|
652 |
-
|
653 |
-
# Add horizontal line at y=0
|
654 |
fig_disc.add_hline(y=0, line=dict(color="red", width=1, dash="dash"))
|
655 |
|
656 |
-
# Add vertical lines at transition points
|
657 |
-
for beta in transition_points:
|
658 |
-
fig_disc.add_vline(x=beta, line=dict(color="red", width=1, dash="dash"))
|
659 |
-
|
660 |
fig_disc.update_layout(title=f"Cubic Discriminant vs. β (z={z:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
661 |
-
|
662 |
|
663 |
return fig_im, fig_re, fig_disc
|
664 |
|
665 |
-
@st.cache_data
|
666 |
-
def generate_eigenvalue_distribution(beta, y, z_a, n=1000, seed=42):
|
667 |
-
"""
|
668 |
-
Generate the eigenvalue distribution of B_n = S_n T_n as n→∞
|
669 |
-
"""
|
670 |
-
# Apply the condition for y
|
671 |
-
y_effective = y if y > 1 else 1/y
|
672 |
-
|
673 |
-
# Set random seed
|
674 |
-
np.random.seed(seed)
|
675 |
-
|
676 |
-
# Compute dimension p based on aspect ratio y
|
677 |
-
p = int(y_effective * n)
|
678 |
-
|
679 |
-
# Constructing T_n (Population / Shape Matrix) - using the approach from the second script
|
680 |
-
k = int(np.floor(beta * p))
|
681 |
-
diag_entries = np.concatenate([
|
682 |
-
np.full(k, z_a),
|
683 |
-
np.full(p - k, 1.0)
|
684 |
-
])
|
685 |
-
np.random.shuffle(diag_entries)
|
686 |
-
T_n = np.diag(diag_entries)
|
687 |
-
|
688 |
-
# Generate the data matrix X with i.i.d. standard normal entries
|
689 |
-
X = np.random.randn(p, n)
|
690 |
-
|
691 |
-
# Compute the sample covariance matrix S_n = (1/n) * XX^T
|
692 |
-
S_n = (1 / n) * (X @ X.T)
|
693 |
-
|
694 |
-
# Compute B_n = S_n T_n
|
695 |
-
B_n = S_n @ T_n
|
696 |
-
|
697 |
-
# Compute eigenvalues of B_n
|
698 |
-
eigenvalues = np.linalg.eigvalsh(B_n)
|
699 |
-
|
700 |
-
# Use KDE to compute a smooth density estimate
|
701 |
-
kde = gaussian_kde(eigenvalues)
|
702 |
-
x_vals = np.linspace(min(eigenvalues), max(eigenvalues), 500)
|
703 |
-
kde_vals = kde(x_vals)
|
704 |
-
|
705 |
-
# Create figure
|
706 |
-
fig = go.Figure()
|
707 |
-
|
708 |
-
# Add histogram trace
|
709 |
-
fig.add_trace(go.Histogram(x=eigenvalues, histnorm='probability density',
|
710 |
-
name="Histogram", marker=dict(color='blue', opacity=0.6)))
|
711 |
-
|
712 |
-
# Add KDE trace
|
713 |
-
fig.add_trace(go.Scatter(x=x_vals, y=kde_vals, mode="lines",
|
714 |
-
name="KDE", line=dict(color='red', width=2)))
|
715 |
-
|
716 |
-
fig.update_layout(
|
717 |
-
title=f"Eigenvalue Distribution for B_n = S_n T_n (y={y:.1f}, β={beta:.2f}, a={z_a:.1f})",
|
718 |
-
xaxis_title="Eigenvalue",
|
719 |
-
yaxis_title="Density",
|
720 |
-
hovermode="closest",
|
721 |
-
showlegend=True
|
722 |
-
)
|
723 |
-
|
724 |
-
return fig, eigenvalues
|
725 |
-
|
726 |
def generate_phase_diagram(z_a, y, beta_min=0.0, beta_max=1.0, z_min=-10.0, z_max=10.0,
|
727 |
beta_steps=100, z_steps=100):
|
728 |
"""
|
@@ -787,6 +849,67 @@ def generate_phase_diagram(z_a, y, beta_min=0.0, beta_max=1.0, z_min=-10.0, z_ma
|
|
787 |
|
788 |
return fig
|
789 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
790 |
# ----------------- Streamlit UI -----------------
|
791 |
st.title("Cubic Root Analysis")
|
792 |
|
@@ -925,13 +1048,29 @@ with tab2:
|
|
925 |
z_min_z = st.number_input("z_min", value=-10.0, key="z_min_tab2_z")
|
926 |
z_max_z = st.number_input("z_max", value=10.0, key="z_max_tab2_z")
|
927 |
with st.expander("Resolution Settings", expanded=False):
|
928 |
-
z_points = st.slider("z grid points", min_value=
|
929 |
if st.button("Compute Complex Roots vs. z", key="tab2_button_z"):
|
930 |
with col2:
|
931 |
-
fig_im, fig_re = generate_root_plots(beta_z, y_z, z_a_z, z_min_z, z_max_z, z_points)
|
932 |
-
if fig_im is not None and fig_re is not None:
|
933 |
st.plotly_chart(fig_im, use_container_width=True)
|
934 |
st.plotly_chart(fig_re, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
935 |
|
936 |
# New tab for Im{s} vs. β plot
|
937 |
with plot_tabs[1]:
|
|
|
490 |
|
491 |
def compute_cubic_roots(z, beta, z_a, y):
|
492 |
"""
|
493 |
+
Compute the roots of the cubic equation for given parameters with improved accuracy.
|
494 |
"""
|
495 |
# Apply the condition for y
|
496 |
y_effective = y if y > 1 else 1/y
|
497 |
|
498 |
+
# Coefficients in the form as^3 + bs^2 + cs + d = 0
|
499 |
a = z * z_a
|
500 |
b = z * z_a + z + z_a - z_a*y_effective
|
501 |
c = z + z_a + 1 - y_effective*(beta*z_a + 1 - beta)
|
502 |
d = 1
|
503 |
+
|
504 |
+
# Special handling for small leading coefficient
|
505 |
+
if abs(a) < 1e-10:
|
506 |
+
if abs(b) < 1e-10: # Linear case
|
507 |
+
roots = np.array([-d/c, 0, 0])
|
508 |
+
else: # Quadratic case
|
509 |
+
quadratic_roots = np.roots([b, c, d])
|
510 |
+
roots = np.append(quadratic_roots, 0)
|
511 |
+
else:
|
512 |
+
# Use high-precision computation for the cubic
|
513 |
+
coeffs = [a, b, c, d]
|
514 |
+
roots = np.roots(coeffs)
|
515 |
+
|
516 |
return roots
|
517 |
|
518 |
+
def track_roots_consistently(z_values, all_roots):
|
519 |
+
"""
|
520 |
+
Ensure consistent tracking of roots across z values by minimizing discontinuity.
|
521 |
+
"""
|
522 |
+
n_points = len(z_values)
|
523 |
+
n_roots = all_roots[0].shape[0]
|
524 |
+
tracked_roots = np.zeros((n_points, n_roots), dtype=complex)
|
525 |
+
tracked_roots[0] = all_roots[0]
|
526 |
+
|
527 |
+
for i in range(1, n_points):
|
528 |
+
prev_roots = tracked_roots[i-1]
|
529 |
+
current_roots = all_roots[i]
|
530 |
+
|
531 |
+
# For each current root, find the closest previous root
|
532 |
+
assignments = np.zeros(n_roots, dtype=int)
|
533 |
+
for j in range(n_roots):
|
534 |
+
distances = np.abs(current_roots - prev_roots[j])
|
535 |
+
best_match = np.argmin(distances)
|
536 |
+
|
537 |
+
# If this best match is already assigned, compare distances
|
538 |
+
if best_match in assignments:
|
539 |
+
prev_idx = np.where(assignments == best_match)[0][0]
|
540 |
+
if distances[best_match] < np.abs(current_roots[best_match] - prev_roots[prev_idx]):
|
541 |
+
# This root is a better match
|
542 |
+
assignments[j] = best_match
|
543 |
+
assignments[prev_idx] = -1 # Mark for reassignment
|
544 |
+
else:
|
545 |
+
assignments[j] = best_match
|
546 |
+
|
547 |
+
# Handle unassigned roots
|
548 |
+
unassigned = np.where(assignments == -1)[0]
|
549 |
+
available = np.setdiff1d(np.arange(n_roots), assignments[assignments >= 0])
|
550 |
+
|
551 |
+
for j, ua in enumerate(unassigned):
|
552 |
+
if j < len(available):
|
553 |
+
assignments[ua] = available[j]
|
554 |
+
else:
|
555 |
+
# Find the least bad assignment
|
556 |
+
all_distances = np.array([np.abs(current_roots[k] - prev_roots[ua]) for k in range(n_roots)])
|
557 |
+
assigned_indices = assignments[assignments >= 0]
|
558 |
+
mask = np.ones(n_roots, dtype=bool)
|
559 |
+
mask[assigned_indices] = False
|
560 |
+
if np.any(mask):
|
561 |
+
assignments[ua] = np.where(mask)[0][0]
|
562 |
+
|
563 |
+
# Reorder current roots based on assignments
|
564 |
+
reordered_roots = np.zeros_like(current_roots)
|
565 |
+
for j in range(n_roots):
|
566 |
+
if assignments[j] >= 0:
|
567 |
+
reordered_roots[j] = current_roots[assignments[j]]
|
568 |
+
else:
|
569 |
+
# Find any unassigned root
|
570 |
+
used = np.sort([assignments[k] for k in range(n_roots) if assignments[k] >= 0])
|
571 |
+
unused = np.setdiff1d(np.arange(n_roots), used)
|
572 |
+
if len(unused) > 0:
|
573 |
+
reordered_roots[j] = current_roots[unused[0]]
|
574 |
+
assignments[j] = unused[0]
|
575 |
+
|
576 |
+
tracked_roots[i] = reordered_roots
|
577 |
+
|
578 |
+
return tracked_roots
|
579 |
+
|
580 |
def generate_root_plots(beta, y, z_a, z_min, z_max, n_points):
|
581 |
"""
|
582 |
+
Generate Im(s) and Re(s) vs. z plots with improved accuracy and tracking.
|
583 |
"""
|
584 |
if z_a <= 0 or y <= 0 or z_min >= z_max:
|
585 |
st.error("Invalid input parameters.")
|
586 |
+
return None, None, None
|
587 |
|
588 |
# Apply the condition for y
|
589 |
y_effective = y if y > 1 else 1/y
|
590 |
|
591 |
z_points = np.linspace(z_min, z_max, n_points)
|
592 |
+
|
593 |
+
# Collect all roots first
|
594 |
+
all_roots = []
|
595 |
for z in z_points:
|
596 |
roots = compute_cubic_roots(z, beta, z_a, y)
|
597 |
+
# Initial sorting to have some consistency
|
598 |
+
roots = sorted(roots, key=lambda x: (abs(x.imag), x.real))
|
599 |
+
all_roots.append(roots)
|
600 |
+
|
601 |
+
all_roots = np.array(all_roots)
|
602 |
+
|
603 |
+
# Track roots consistently
|
604 |
+
tracked_roots = track_roots_consistently(z_points, all_roots)
|
605 |
+
|
606 |
+
# Extract imaginary and real parts
|
607 |
+
ims = np.imag(tracked_roots)
|
608 |
+
res = np.real(tracked_roots)
|
609 |
+
|
610 |
+
# Calculate discriminant for verification
|
611 |
+
discriminants = []
|
612 |
+
for z in z_points:
|
613 |
+
a = z * z_a
|
614 |
+
b = z * z_a + z + z_a - z_a*y_effective
|
615 |
+
c = z + z_a + 1 - y_effective*(beta*z_a + 1 - beta)
|
616 |
+
d = 1
|
617 |
+
|
618 |
+
# Cubic discriminant: 18abcd - 27a²d² + b²c² - 2b³d - 9ac³
|
619 |
+
disc = (18*a*b*c*d - 27*a*a*d*d + b*b*c*c - 2*b*b*b*d - 9*a*c*c*c)
|
620 |
+
discriminants.append(disc)
|
621 |
+
|
622 |
+
discriminants = np.array(discriminants)
|
623 |
+
|
624 |
+
# Create figure for imaginary parts
|
625 |
fig_im = go.Figure()
|
626 |
for i in range(3):
|
627 |
fig_im.add_trace(go.Scatter(x=z_points, y=ims[:, i], mode="lines", name=f"Im{{s{i+1}}}",
|
628 |
line=dict(width=2)))
|
629 |
+
|
630 |
+
# Add vertical lines at discriminant zero crossings
|
631 |
+
disc_zeros = []
|
632 |
+
for i in range(len(discriminants)-1):
|
633 |
+
if discriminants[i] * discriminants[i+1] <= 0: # Sign change
|
634 |
+
zero_pos = z_points[i] + (z_points[i+1] - z_points[i]) * (0 - discriminants[i]) / (discriminants[i+1] - discriminants[i])
|
635 |
+
disc_zeros.append(zero_pos)
|
636 |
+
fig_im.add_vline(x=zero_pos, line=dict(color="red", width=1, dash="dash"))
|
637 |
+
|
638 |
fig_im.update_layout(title=f"Im{{s}} vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
639 |
xaxis_title="z", yaxis_title="Im{s}", hovermode="x unified")
|
640 |
|
641 |
+
# Create figure for real parts
|
642 |
fig_re = go.Figure()
|
643 |
for i in range(3):
|
644 |
fig_re.add_trace(go.Scatter(x=z_points, y=res[:, i], mode="lines", name=f"Re{{s{i+1}}}",
|
645 |
line=dict(width=2)))
|
646 |
+
|
647 |
+
# Add vertical lines at discriminant zero crossings
|
648 |
+
for zero_pos in disc_zeros:
|
649 |
+
fig_re.add_vline(x=zero_pos, line=dict(color="red", width=1, dash="dash"))
|
650 |
+
|
651 |
fig_re.update_layout(title=f"Re{{s}} vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
652 |
xaxis_title="z", yaxis_title="Re{s}", hovermode="x unified")
|
653 |
+
|
654 |
+
# Create discriminant plot
|
655 |
+
fig_disc = go.Figure()
|
656 |
+
fig_disc.add_trace(go.Scatter(x=z_points, y=discriminants, mode="lines",
|
657 |
+
name="Cubic Discriminant", line=dict(color="black", width=2)))
|
658 |
+
fig_disc.add_hline(y=0, line=dict(color="red", width=1, dash="dash"))
|
659 |
+
|
660 |
+
fig_disc.update_layout(title=f"Cubic Discriminant vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
661 |
+
xaxis_title="z", yaxis_title="Discriminant", hovermode="x unified")
|
662 |
+
|
663 |
+
return fig_im, fig_re, fig_disc
|
664 |
|
665 |
def analyze_complex_root_structure(beta_values, z, z_a, y):
|
666 |
"""
|
|
|
702 |
|
703 |
def generate_roots_vs_beta_plots(z, y, z_a, beta_min, beta_max, n_points):
|
704 |
"""
|
705 |
+
Generate Im(s) and Re(s) vs. β plots with improved accuracy.
|
706 |
"""
|
707 |
if z_a <= 0 or y <= 0 or beta_min >= beta_max:
|
708 |
st.error("Invalid input parameters.")
|
|
|
712 |
y_effective = y if y > 1 else 1/y
|
713 |
|
714 |
beta_points = np.linspace(beta_min, beta_max, n_points)
|
715 |
+
|
716 |
+
# Collect all roots first
|
717 |
+
all_roots = []
|
718 |
for beta in beta_points:
|
719 |
roots = compute_cubic_roots(z, beta, z_a, y)
|
720 |
+
# Initial sorting to have some consistency
|
721 |
+
roots = sorted(roots, key=lambda x: (abs(x.imag), x.real))
|
722 |
+
all_roots.append(roots)
|
723 |
+
|
724 |
+
all_roots = np.array(all_roots)
|
725 |
+
|
726 |
+
# Track roots consistently
|
727 |
+
tracked_roots = track_roots_consistently(beta_points, all_roots)
|
728 |
+
|
729 |
+
# Extract imaginary and real parts
|
730 |
+
ims = np.imag(tracked_roots)
|
731 |
+
res = np.real(tracked_roots)
|
732 |
+
|
733 |
+
# Calculate discriminant for verification
|
734 |
+
discriminants = []
|
735 |
+
for beta in beta_points:
|
736 |
+
a = z * z_a
|
737 |
+
b = z * z_a + z + z_a - z_a*y_effective
|
738 |
+
c = z + z_a + 1 - y_effective*(beta*z_a + 1 - beta)
|
739 |
+
d = 1
|
740 |
+
|
741 |
+
# Cubic discriminant
|
742 |
+
disc = (18*a*b*c*d - 27*a*a*d*d + b*b*c*c - 2*b*b*b*d - 9*a*c*c*c)
|
743 |
+
discriminants.append(disc)
|
744 |
+
|
745 |
+
discriminants = np.array(discriminants)
|
746 |
|
747 |
+
# Create figure for imaginary parts
|
748 |
fig_im = go.Figure()
|
749 |
for i in range(3):
|
750 |
fig_im.add_trace(go.Scatter(x=beta_points, y=ims[:, i], mode="lines", name=f"Im{{s{i+1}}}",
|
751 |
line=dict(width=2)))
|
752 |
|
753 |
+
# Add vertical lines at discriminant zero crossings
|
754 |
+
disc_zeros = []
|
755 |
+
for i in range(len(discriminants)-1):
|
756 |
+
if discriminants[i] * discriminants[i+1] <= 0: # Sign change
|
757 |
+
zero_pos = beta_points[i] + (beta_points[i+1] - beta_points[i]) * (0 - discriminants[i]) / (discriminants[i+1] - discriminants[i])
|
758 |
+
disc_zeros.append(zero_pos)
|
759 |
+
fig_im.add_vline(x=zero_pos, line=dict(color="red", width=1, dash="dash"))
|
760 |
+
|
761 |
fig_im.update_layout(title=f"Im{{s}} vs. β (z={z:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
762 |
xaxis_title="β", yaxis_title="Im{s}", hovermode="x unified")
|
763 |
|
764 |
+
# Create figure for real parts
|
765 |
fig_re = go.Figure()
|
766 |
for i in range(3):
|
767 |
fig_re.add_trace(go.Scatter(x=beta_points, y=res[:, i], mode="lines", name=f"Re{{s{i+1}}}",
|
768 |
line=dict(width=2)))
|
769 |
|
770 |
+
# Add vertical lines at discriminant zero crossings
|
771 |
+
for zero_pos in disc_zeros:
|
772 |
+
fig_re.add_vline(x=zero_pos, line=dict(color="red", width=1, dash="dash"))
|
773 |
+
|
774 |
fig_re.update_layout(title=f"Re{{s}} vs. β (z={z:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
775 |
xaxis_title="β", yaxis_title="Re{s}", hovermode="x unified")
|
776 |
|
777 |
+
# Create discriminant plot
|
778 |
fig_disc = go.Figure()
|
779 |
+
fig_disc.add_trace(go.Scatter(x=beta_points, y=discriminants, mode="lines",
|
780 |
+
name="Cubic Discriminant", line=dict(color="black", width=2)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
781 |
fig_disc.add_hline(y=0, line=dict(color="red", width=1, dash="dash"))
|
782 |
|
|
|
|
|
|
|
|
|
783 |
fig_disc.update_layout(title=f"Cubic Discriminant vs. β (z={z:.3f}, y={y:.3f}, z_a={z_a:.3f})",
|
784 |
+
xaxis_title="β", yaxis_title="Discriminant", hovermode="x unified")
|
785 |
|
786 |
return fig_im, fig_re, fig_disc
|
787 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
788 |
def generate_phase_diagram(z_a, y, beta_min=0.0, beta_max=1.0, z_min=-10.0, z_max=10.0,
|
789 |
beta_steps=100, z_steps=100):
|
790 |
"""
|
|
|
849 |
|
850 |
return fig
|
851 |
|
852 |
+
@st.cache_data
|
853 |
+
def generate_eigenvalue_distribution(beta, y, z_a, n=1000, seed=42):
|
854 |
+
"""
|
855 |
+
Generate the eigenvalue distribution of B_n = S_n T_n as n→∞
|
856 |
+
"""
|
857 |
+
# Apply the condition for y
|
858 |
+
y_effective = y if y > 1 else 1/y
|
859 |
+
|
860 |
+
# Set random seed
|
861 |
+
np.random.seed(seed)
|
862 |
+
|
863 |
+
# Compute dimension p based on aspect ratio y
|
864 |
+
p = int(y_effective * n)
|
865 |
+
|
866 |
+
# Constructing T_n (Population / Shape Matrix) - using the approach from the second script
|
867 |
+
k = int(np.floor(beta * p))
|
868 |
+
diag_entries = np.concatenate([
|
869 |
+
np.full(k, z_a),
|
870 |
+
np.full(p - k, 1.0)
|
871 |
+
])
|
872 |
+
np.random.shuffle(diag_entries)
|
873 |
+
T_n = np.diag(diag_entries)
|
874 |
+
|
875 |
+
# Generate the data matrix X with i.i.d. standard normal entries
|
876 |
+
X = np.random.randn(p, n)
|
877 |
+
|
878 |
+
# Compute the sample covariance matrix S_n = (1/n) * XX^T
|
879 |
+
S_n = (1 / n) * (X @ X.T)
|
880 |
+
|
881 |
+
# Compute B_n = S_n T_n
|
882 |
+
B_n = S_n @ T_n
|
883 |
+
|
884 |
+
# Compute eigenvalues of B_n
|
885 |
+
eigenvalues = np.linalg.eigvalsh(B_n)
|
886 |
+
|
887 |
+
# Use KDE to compute a smooth density estimate
|
888 |
+
kde = gaussian_kde(eigenvalues)
|
889 |
+
x_vals = np.linspace(min(eigenvalues), max(eigenvalues), 500)
|
890 |
+
kde_vals = kde(x_vals)
|
891 |
+
|
892 |
+
# Create figure
|
893 |
+
fig = go.Figure()
|
894 |
+
|
895 |
+
# Add histogram trace
|
896 |
+
fig.add_trace(go.Histogram(x=eigenvalues, histnorm='probability density',
|
897 |
+
name="Histogram", marker=dict(color='blue', opacity=0.6)))
|
898 |
+
|
899 |
+
# Add KDE trace
|
900 |
+
fig.add_trace(go.Scatter(x=x_vals, y=kde_vals, mode="lines",
|
901 |
+
name="KDE", line=dict(color='red', width=2)))
|
902 |
+
|
903 |
+
fig.update_layout(
|
904 |
+
title=f"Eigenvalue Distribution for B_n = S_n T_n (y={y:.1f}, β={beta:.2f}, a={z_a:.1f})",
|
905 |
+
xaxis_title="Eigenvalue",
|
906 |
+
yaxis_title="Density",
|
907 |
+
hovermode="closest",
|
908 |
+
showlegend=True
|
909 |
+
)
|
910 |
+
|
911 |
+
return fig, eigenvalues
|
912 |
+
|
913 |
# ----------------- Streamlit UI -----------------
|
914 |
st.title("Cubic Root Analysis")
|
915 |
|
|
|
1048 |
z_min_z = st.number_input("z_min", value=-10.0, key="z_min_tab2_z")
|
1049 |
z_max_z = st.number_input("z_max", value=10.0, key="z_max_tab2_z")
|
1050 |
with st.expander("Resolution Settings", expanded=False):
|
1051 |
+
z_points = st.slider("z grid points", min_value=100, max_value=2000, value=500, step=100, key="z_points_z")
|
1052 |
if st.button("Compute Complex Roots vs. z", key="tab2_button_z"):
|
1053 |
with col2:
|
1054 |
+
fig_im, fig_re, fig_disc = generate_root_plots(beta_z, y_z, z_a_z, z_min_z, z_max_z, z_points)
|
1055 |
+
if fig_im is not None and fig_re is not None and fig_disc is not None:
|
1056 |
st.plotly_chart(fig_im, use_container_width=True)
|
1057 |
st.plotly_chart(fig_re, use_container_width=True)
|
1058 |
+
st.plotly_chart(fig_disc, use_container_width=True)
|
1059 |
+
|
1060 |
+
with st.expander("Root Structure Analysis", expanded=False):
|
1061 |
+
st.markdown("""
|
1062 |
+
### Root Structure Explanation
|
1063 |
+
|
1064 |
+
The red dashed vertical lines mark the points where the cubic discriminant equals zero.
|
1065 |
+
At these points, the cubic equation's root structure changes:
|
1066 |
+
|
1067 |
+
- When the discriminant is positive, the cubic has three distinct real roots.
|
1068 |
+
- When the discriminant is negative, the cubic has one real root and two complex conjugate roots.
|
1069 |
+
- When the discriminant is exactly zero, the cubic has at least two equal roots.
|
1070 |
+
|
1071 |
+
These transition points align perfectly with the z*(β) boundary curves from the first tab,
|
1072 |
+
which represent exactly these transitions in the (β,z) plane.
|
1073 |
+
""")
|
1074 |
|
1075 |
# New tab for Im{s} vs. β plot
|
1076 |
with plot_tabs[1]:
|