euler314 commited on
Commit
3994d34
·
verified ·
1 Parent(s): 45cee47

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +264 -125
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
- coeffs = [a, b, c, d]
503
- roots = np.roots(coeffs)
 
 
 
 
 
 
 
 
 
 
 
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
- ims, res = [], []
 
 
519
  for z in z_points:
520
  roots = compute_cubic_roots(z, beta, z_a, y)
521
- roots = sorted(roots, key=lambda x: abs(x.imag))
522
- ims.append([root.imag for root in roots])
523
- res.append([root.real for root in roots])
524
- ims = np.array(ims)
525
- res = np.array(res)
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
- return fig_im, fig_re
 
 
 
 
 
 
 
 
 
 
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
- ims, res = [], []
 
 
593
  for beta in beta_points:
594
  roots = compute_cubic_roots(z, beta, z_a, y)
595
- roots = sorted(roots, key=lambda x: abs(x.imag))
596
- ims.append([root.imag for root in roots])
597
- res.append([root.real for root in roots])
598
- ims = np.array(ims)
599
- res = np.array(res)
600
-
601
- # Find transition points in root structure
602
- transition_points, structure_types = analyze_complex_root_structure(beta_points, z, z_a, y)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
 
604
- # Create traces for imaginary parts
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 transition points
611
- for beta in transition_points:
612
- fig_im.add_vline(x=beta, line=dict(color="red", width=1, dash="dash"))
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 traces for real parts
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 transition points
624
- for beta in transition_points:
625
- fig_re.add_vline(x=beta, line=dict(color="red", width=1, dash="dash"))
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 a plot showing the discriminant
631
  fig_disc = go.Figure()
632
-
633
- # Calculate discriminant as a function of beta
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
- xaxis_title="β", yaxis_title="Discriminant", hovermode="x unified")
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=1000, max_value=10000, value=5000, step=500, key="z_points_z")
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]: