Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -8,9 +8,26 @@ from scipy.optimize import fsolve
|
|
8 |
st.set_page_config(
|
9 |
page_title="Cubic Root Analysis",
|
10 |
layout="wide",
|
11 |
-
initial_sidebar_state="
|
12 |
)
|
13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
#############################
|
15 |
# 1) Define the discriminant
|
16 |
#############################
|
@@ -73,7 +90,6 @@ def find_z_at_discriminant_zero(z_a, y, beta, z_min, z_max, steps):
|
|
73 |
roots_found.append(root_approx)
|
74 |
|
75 |
return np.array(roots_found)
|
76 |
-
|
77 |
@st.cache_data
|
78 |
def sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps):
|
79 |
"""
|
@@ -127,6 +143,23 @@ def compute_high_y_curve(betas, z_a, y):
|
|
127 |
numerator = -4*a*(a-1)*y*betas - 2*a*y - 2*a*(2*a-1)
|
128 |
return numerator/denominator
|
129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
130 |
def compute_custom_expression(betas, z_a, y, num_expr_str, denom_expr_str):
|
131 |
"""
|
132 |
Compute a custom curve given numerator and denominator expressions
|
@@ -186,8 +219,7 @@ def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps,
|
|
186 |
line=dict(color='lightblue'),
|
187 |
)
|
188 |
)
|
189 |
-
|
190 |
-
fig.add_trace(
|
191 |
go.Scatter(
|
192 |
x=betas,
|
193 |
y=low_y_curve,
|
@@ -231,8 +263,7 @@ def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps,
|
|
231 |
hovermode="x unified",
|
232 |
)
|
233 |
|
234 |
-
#
|
235 |
-
# Use numpy.gradient assuming betas is evenly spaced.
|
236 |
dzmax_dbeta = np.gradient(z_maxs, betas)
|
237 |
dzmin_dbeta = np.gradient(z_mins, betas)
|
238 |
dlowy_dbeta = np.gradient(low_y_curve, betas)
|
@@ -251,8 +282,7 @@ def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps,
|
|
251 |
line=dict(color='blue'),
|
252 |
)
|
253 |
)
|
254 |
-
|
255 |
-
fig_deriv.add_trace(
|
256 |
go.Scatter(
|
257 |
x=betas,
|
258 |
y=dzmin_dbeta,
|
@@ -318,7 +348,6 @@ def compute_cubic_roots(z, beta, z_a, y):
|
|
318 |
coeffs = [a, b, c, d]
|
319 |
roots = np.roots(coeffs)
|
320 |
return roots
|
321 |
-
|
322 |
def generate_root_plots(beta, y, z_a, z_min, z_max, n_points):
|
323 |
"""Generate both Im(s) and Re(s) vs. z plots"""
|
324 |
if z_a <= 0 or y <= 0 or z_min >= z_max:
|
@@ -377,126 +406,11 @@ def generate_root_plots(beta, y, z_a, z_min, z_max, n_points):
|
|
377 |
)
|
378 |
|
379 |
return fig_im, fig_re
|
380 |
-
|
381 |
-
def curve1(s, z, y):
|
382 |
-
"""First curve: z*s^2 + (z-y+1)*s + 1"""
|
383 |
-
return z*s**2 + (z-y+1)*s + 1
|
384 |
-
|
385 |
-
def curve2(s, y, beta, a):
|
386 |
-
"""Second curve: y*β*((a-1)*s)/(a*s+1)"""
|
387 |
-
return y*beta*((a-1)*s)/(a*s+1)
|
388 |
-
|
389 |
-
def find_intersections(z, y, beta, a, s_range, n_guesses, tolerance):
|
390 |
-
"""Find intersections between the two curves with improved accuracy"""
|
391 |
-
def equation(s):
|
392 |
-
return curve1(s, z, y) - curve2(s, y, beta, a)
|
393 |
-
|
394 |
-
# Create a finer grid of initial guesses
|
395 |
-
s_guesses = np.linspace(s_range[0], s_range[1], n_guesses)
|
396 |
-
intersections = []
|
397 |
-
|
398 |
-
# First pass: find all potential intersections
|
399 |
-
for s_guess in s_guesses:
|
400 |
-
try:
|
401 |
-
s_sol = fsolve(equation, s_guess, full_output=True, xtol=tolerance)
|
402 |
-
if s_sol[2] == 1: # Check if convergence was achieved
|
403 |
-
s_val = s_sol[0][0]
|
404 |
-
if (s_range[0] <= s_val <= s_range[1] and
|
405 |
-
not any(abs(s_val - s_prev) < tolerance for s_prev in intersections)):
|
406 |
-
if abs(equation(s_val)) < tolerance:
|
407 |
-
intersections.append(s_val)
|
408 |
-
except:
|
409 |
-
continue
|
410 |
-
|
411 |
-
# Sort intersections
|
412 |
-
intersections = np.sort(np.array(intersections))
|
413 |
-
|
414 |
-
# Ensure even number of intersections by checking for missed ones
|
415 |
-
if len(intersections) % 2 != 0:
|
416 |
-
refined_intersections = []
|
417 |
-
for i in range(len(intersections)-1):
|
418 |
-
mid_point = (intersections[i] + intersections[i+1])/2
|
419 |
-
try:
|
420 |
-
s_sol = fsolve(equation, mid_point, full_output=True, xtol=tolerance)
|
421 |
-
if s_sol[2] == 1:
|
422 |
-
s_val = s_sol[0][0]
|
423 |
-
if (intersections[i] < s_val < intersections[i+1] and
|
424 |
-
abs(equation(s_val)) < tolerance):
|
425 |
-
refined_intersections.append(s_val)
|
426 |
-
except:
|
427 |
-
continue
|
428 |
-
|
429 |
-
intersections = np.sort(np.append(intersections, refined_intersections))
|
430 |
-
|
431 |
-
return intersections
|
432 |
-
|
433 |
-
def generate_curves_plot(z, y, beta, a, s_range, n_points, n_guesses, tolerance):
|
434 |
-
s = np.linspace(s_range[0], s_range[1], n_points)
|
435 |
-
|
436 |
-
# Compute curves
|
437 |
-
y1 = curve1(s, z, y)
|
438 |
-
y2 = curve2(s, y, beta, a)
|
439 |
-
|
440 |
-
# Find intersections with improved accuracy
|
441 |
-
intersections = find_intersections(z, y, beta, a, s_range, n_guesses, tolerance)
|
442 |
-
|
443 |
-
fig = go.Figure()
|
444 |
-
|
445 |
-
fig.add_trace(
|
446 |
-
go.Scatter(
|
447 |
-
x=s, y=y1,
|
448 |
-
mode='lines',
|
449 |
-
name='z*s² + (z-y+1)*s + 1',
|
450 |
-
line=dict(color='blue', width=2)
|
451 |
-
)
|
452 |
-
)
|
453 |
-
|
454 |
-
fig.add_trace(
|
455 |
-
go.Scatter(
|
456 |
-
x=s, y=y2,
|
457 |
-
mode='lines',
|
458 |
-
name='y*β*((a-1)*s)/(a*s+1)',
|
459 |
-
line=dict(color='red', width=2)
|
460 |
-
)
|
461 |
-
)
|
462 |
-
|
463 |
-
if len(intersections) > 0:
|
464 |
-
fig.add_trace(
|
465 |
-
go.Scatter(
|
466 |
-
x=intersections,
|
467 |
-
y=curve1(intersections, z, y),
|
468 |
-
mode='markers',
|
469 |
-
name='Intersections',
|
470 |
-
marker=dict(
|
471 |
-
size=12,
|
472 |
-
color='green',
|
473 |
-
symbol='x',
|
474 |
-
line=dict(width=2)
|
475 |
-
)
|
476 |
-
)
|
477 |
-
)
|
478 |
-
|
479 |
-
fig.update_layout(
|
480 |
-
title=f"Curve Intersection Analysis (y={y:.4f}, β={beta:.4f}, a={a:.4f})",
|
481 |
-
xaxis_title="s",
|
482 |
-
yaxis_title="Value",
|
483 |
-
hovermode="closest",
|
484 |
-
showlegend=True,
|
485 |
-
legend=dict(
|
486 |
-
yanchor="top",
|
487 |
-
y=0.99,
|
488 |
-
xanchor="left",
|
489 |
-
x=0.01
|
490 |
-
)
|
491 |
-
)
|
492 |
-
|
493 |
-
return fig, intersections
|
494 |
-
|
495 |
# ------------------- Streamlit UI -------------------
|
496 |
|
497 |
st.title("Cubic Root Analysis")
|
498 |
|
499 |
-
tab1, tab2, tab3 = st.tabs(["z*(β) Curves", "Im{s} vs. z", "Curve Intersections"])
|
500 |
|
501 |
with tab1:
|
502 |
st.header("Find z Values where Cubic Roots Transition Between Real and Complex")
|
@@ -513,13 +427,6 @@ with tab1:
|
|
513 |
beta_steps = st.slider("β steps", min_value=51, max_value=501, value=201, step=50)
|
514 |
z_steps = st.slider("z grid steps", min_value=1000, max_value=100000, value=50000, step=1000)
|
515 |
|
516 |
-
st.subheader("Custom Expression")
|
517 |
-
st.markdown("Enter a **numerator** and a **denominator** expression as functions of `z_a`, `beta`, and `y`.")
|
518 |
-
default_num = "(y - 2)*((-1 + sqrt(y*beta*(z_a - 1)))/z_a) + y*beta*((z_a-1)/z_a) - 1/z_a - 1"
|
519 |
-
default_denom = "((-1 + sqrt(y*beta*(z_a - 1)))/z_a)**2 + ((-1 + sqrt(y*beta*(z_a - 1)))/z_a)"
|
520 |
-
custom_num_expr = st.text_input("Numerator Expression", value=default_num)
|
521 |
-
custom_denom_expr = st.text_input("Denominator Expression", value=default_denom)
|
522 |
-
|
523 |
if st.button("Compute z vs. β Curves"):
|
524 |
with col2:
|
525 |
fig_main, fig_deriv = generate_z_vs_beta_plot(z_a_1, y_1, z_min_1, z_max_1,
|
@@ -527,24 +434,8 @@ with tab1:
|
|
527 |
custom_num_expr, custom_denom_expr)
|
528 |
if fig_main is not None and fig_deriv is not None:
|
529 |
st.plotly_chart(fig_main, use_container_width=True)
|
530 |
-
st.markdown("### Derivative of Each Curve vs. β")
|
531 |
st.plotly_chart(fig_deriv, use_container_width=True)
|
532 |
-
|
533 |
-
st.markdown("### Additional Expressions")
|
534 |
-
st.markdown("""
|
535 |
-
**Low y Expression (Red):**
|
536 |
-
```
|
537 |
-
((y - 2)*(-1 + sqrt(y*β*(z_a-1)))/z_a + y*β*((z_a-1)/z_a) - 1/z_a - 1) /
|
538 |
-
(((-1 + sqrt(y*β*(z_a-1)))/z_a)**2 + ((-1 + sqrt(y*β*(z_a-1)))/z_a))
|
539 |
-
```
|
540 |
-
|
541 |
-
**High y Expression (Green):**
|
542 |
-
```
|
543 |
-
(- 4 z_a*(z_a-1)*y*β - 2z_a*y + 2z_a*(2z_a-1))/(1-2z_a)
|
544 |
-
```
|
545 |
-
where z_a is the input parameter.
|
546 |
-
""")
|
547 |
-
|
548 |
with tab2:
|
549 |
st.header("Plot Complex Roots vs. z")
|
550 |
|
@@ -567,40 +458,80 @@ with tab2:
|
|
567 |
st.plotly_chart(fig_im, use_container_width=True)
|
568 |
st.plotly_chart(fig_re, use_container_width=True)
|
569 |
|
570 |
-
with
|
571 |
-
st.header("
|
572 |
|
573 |
col1, col2 = st.columns([1, 2])
|
574 |
|
575 |
with col1:
|
576 |
-
|
577 |
-
|
578 |
-
|
579 |
-
|
580 |
-
|
581 |
-
st.subheader("s Range")
|
582 |
-
s_min = st.number_input("s_min", value=-5.0)
|
583 |
-
s_max = st.number_input("s_max", value=5.0)
|
584 |
|
585 |
with st.expander("Resolution Settings"):
|
586 |
-
|
587 |
-
|
588 |
-
|
589 |
-
|
590 |
-
options=[1e-6, 1e-8, 1e-10, 1e-12, 1e-14, 1e-16, 1e-18, 1e-20],
|
591 |
-
value=1e-10
|
592 |
-
)
|
593 |
-
|
594 |
-
if st.button("Compute Intersections"):
|
595 |
with col2:
|
596 |
-
|
597 |
-
|
598 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
599 |
|
600 |
-
|
601 |
-
|
602 |
-
|
603 |
-
|
604 |
-
|
605 |
-
|
606 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
st.set_page_config(
|
9 |
page_title="Cubic Root Analysis",
|
10 |
layout="wide",
|
11 |
+
initial_sidebar_state="expanded" # Changed to expanded
|
12 |
)
|
13 |
|
14 |
+
# Move custom expression inputs to sidebar
|
15 |
+
with st.sidebar:
|
16 |
+
st.header("Custom Expression Settings")
|
17 |
+
expression_type = st.radio(
|
18 |
+
"Select Expression Type",
|
19 |
+
["Original Low y", "Alternative Low y"]
|
20 |
+
)
|
21 |
+
|
22 |
+
if expression_type == "Original Low y":
|
23 |
+
default_num = "(y - 2)*((-1 + sqrt(y*beta*(z_a - 1)))/z_a) + y*beta*((z_a-1)/z_a) - 1/z_a - 1"
|
24 |
+
default_denom = "((-1 + sqrt(y*beta*(z_a - 1)))/z_a)**2 + ((-1 + sqrt(y*beta*(z_a - 1)))/z_a)"
|
25 |
+
else:
|
26 |
+
default_num = "1*z_a*y*beta*(z_a-1) - 2*z_a*(1 - y) - 2*z_a**2"
|
27 |
+
default_denom = "2+2*z_a"
|
28 |
+
|
29 |
+
custom_num_expr = st.text_input("Numerator Expression", value=default_num)
|
30 |
+
custom_denom_expr = st.text_input("Denominator Expression", value=default_denom)
|
31 |
#############################
|
32 |
# 1) Define the discriminant
|
33 |
#############################
|
|
|
90 |
roots_found.append(root_approx)
|
91 |
|
92 |
return np.array(roots_found)
|
|
|
93 |
@st.cache_data
|
94 |
def sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps):
|
95 |
"""
|
|
|
143 |
numerator = -4*a*(a-1)*y*betas - 2*a*y - 2*a*(2*a-1)
|
144 |
return numerator/denominator
|
145 |
|
146 |
+
@st.cache_data
|
147 |
+
def compute_z_difference_and_derivatives(z_a, y, z_min, z_max, beta_steps, z_steps):
|
148 |
+
"""
|
149 |
+
Compute the difference between upper and lower z*(β) curves and their derivatives
|
150 |
+
"""
|
151 |
+
betas, z_mins, z_maxs = sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps)
|
152 |
+
|
153 |
+
# Compute difference
|
154 |
+
z_difference = z_maxs - z_mins
|
155 |
+
|
156 |
+
# First derivatives
|
157 |
+
dz_diff_dbeta = np.gradient(z_difference, betas)
|
158 |
+
|
159 |
+
# Second derivatives
|
160 |
+
d2z_diff_dbeta2 = np.gradient(dz_diff_dbeta, betas)
|
161 |
+
|
162 |
+
return betas, z_difference, dz_diff_dbeta, d2z_diff_dbeta2
|
163 |
def compute_custom_expression(betas, z_a, y, num_expr_str, denom_expr_str):
|
164 |
"""
|
165 |
Compute a custom curve given numerator and denominator expressions
|
|
|
219 |
line=dict(color='lightblue'),
|
220 |
)
|
221 |
)
|
222 |
+
fig.add_trace(
|
|
|
223 |
go.Scatter(
|
224 |
x=betas,
|
225 |
y=low_y_curve,
|
|
|
263 |
hovermode="x unified",
|
264 |
)
|
265 |
|
266 |
+
# Compute Derivatives with Respect to β
|
|
|
267 |
dzmax_dbeta = np.gradient(z_maxs, betas)
|
268 |
dzmin_dbeta = np.gradient(z_mins, betas)
|
269 |
dlowy_dbeta = np.gradient(low_y_curve, betas)
|
|
|
282 |
line=dict(color='blue'),
|
283 |
)
|
284 |
)
|
285 |
+
fig_deriv.add_trace(
|
|
|
286 |
go.Scatter(
|
287 |
x=betas,
|
288 |
y=dzmin_dbeta,
|
|
|
348 |
coeffs = [a, b, c, d]
|
349 |
roots = np.roots(coeffs)
|
350 |
return roots
|
|
|
351 |
def generate_root_plots(beta, y, z_a, z_min, z_max, n_points):
|
352 |
"""Generate both Im(s) and Re(s) vs. z plots"""
|
353 |
if z_a <= 0 or y <= 0 or z_min >= z_max:
|
|
|
406 |
)
|
407 |
|
408 |
return fig_im, fig_re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
409 |
# ------------------- Streamlit UI -------------------
|
410 |
|
411 |
st.title("Cubic Root Analysis")
|
412 |
|
413 |
+
tab1, tab2, tab3, tab4 = st.tabs(["z*(β) Curves", "Im{s} vs. z", "Curve Intersections", "z*(β) Difference Analysis"])
|
414 |
|
415 |
with tab1:
|
416 |
st.header("Find z Values where Cubic Roots Transition Between Real and Complex")
|
|
|
427 |
beta_steps = st.slider("β steps", min_value=51, max_value=501, value=201, step=50)
|
428 |
z_steps = st.slider("z grid steps", min_value=1000, max_value=100000, value=50000, step=1000)
|
429 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
430 |
if st.button("Compute z vs. β Curves"):
|
431 |
with col2:
|
432 |
fig_main, fig_deriv = generate_z_vs_beta_plot(z_a_1, y_1, z_min_1, z_max_1,
|
|
|
434 |
custom_num_expr, custom_denom_expr)
|
435 |
if fig_main is not None and fig_deriv is not None:
|
436 |
st.plotly_chart(fig_main, use_container_width=True)
|
|
|
437 |
st.plotly_chart(fig_deriv, use_container_width=True)
|
438 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
439 |
with tab2:
|
440 |
st.header("Plot Complex Roots vs. z")
|
441 |
|
|
|
458 |
st.plotly_chart(fig_im, use_container_width=True)
|
459 |
st.plotly_chart(fig_re, use_container_width=True)
|
460 |
|
461 |
+
with tab4:
|
462 |
+
st.header("z*(β) Difference Analysis")
|
463 |
|
464 |
col1, col2 = st.columns([1, 2])
|
465 |
|
466 |
with col1:
|
467 |
+
z_a_4 = st.number_input("z_a", value=1.0, key="z_a_4")
|
468 |
+
y_4 = st.number_input("y", value=1.0, key="y_4")
|
469 |
+
z_min_4 = st.number_input("z_min", value=-10.0, key="z_min_4")
|
470 |
+
z_max_4 = st.number_input("z_max", value=10.0, key="z_max_4")
|
|
|
|
|
|
|
|
|
471 |
|
472 |
with st.expander("Resolution Settings"):
|
473 |
+
beta_steps_4 = st.slider("β steps", min_value=51, max_value=501, value=201, step=50, key="beta_steps_4")
|
474 |
+
z_steps_4 = st.slider("z grid steps", min_value=1000, max_value=100000, value=50000, step=1000, key="z_steps_4")
|
475 |
+
|
476 |
+
if st.button("Compute Difference Analysis"):
|
|
|
|
|
|
|
|
|
|
|
477 |
with col2:
|
478 |
+
betas, z_diff, dz_diff, d2z_diff = compute_z_difference_and_derivatives(
|
479 |
+
z_a_4, y_4, z_min_4, z_max_4, beta_steps_4, z_steps_4
|
480 |
+
)
|
481 |
+
|
482 |
+
# Plot difference
|
483 |
+
fig_diff = go.Figure()
|
484 |
+
fig_diff.add_trace(
|
485 |
+
go.Scatter(
|
486 |
+
x=betas,
|
487 |
+
y=z_diff,
|
488 |
+
mode="lines",
|
489 |
+
name="z*(β) Difference",
|
490 |
+
line=dict(color='purple', width=2)
|
491 |
+
)
|
492 |
+
)
|
493 |
+
fig_diff.update_layout(
|
494 |
+
title="Difference between Upper and Lower z*(β)",
|
495 |
+
xaxis_title="β",
|
496 |
+
yaxis_title="z_max - z_min",
|
497 |
+
hovermode="x unified"
|
498 |
+
)
|
499 |
+
st.plotly_chart(fig_diff, use_container_width=True)
|
500 |
|
501 |
+
# Plot first derivative
|
502 |
+
fig_first_deriv = go.Figure()
|
503 |
+
fig_first_deriv.add_trace(
|
504 |
+
go.Scatter(
|
505 |
+
x=betas,
|
506 |
+
y=dz_diff,
|
507 |
+
mode="lines",
|
508 |
+
name="First Derivative",
|
509 |
+
line=dict(color='blue', width=2)
|
510 |
+
)
|
511 |
+
)
|
512 |
+
fig_first_deriv.update_layout(
|
513 |
+
title="First Derivative of z*(β) Difference",
|
514 |
+
xaxis_title="β",
|
515 |
+
yaxis_title="d(z_max - z_min)/dβ",
|
516 |
+
hovermode="x unified"
|
517 |
+
)
|
518 |
+
st.plotly_chart(fig_first_deriv, use_container_width=True)
|
519 |
+
|
520 |
+
# Plot second derivative
|
521 |
+
fig_second_deriv = go.Figure()
|
522 |
+
fig_second_deriv.add_trace(
|
523 |
+
go.Scatter(
|
524 |
+
x=betas,
|
525 |
+
y=d2z_diff,
|
526 |
+
mode="lines",
|
527 |
+
name="Second Derivative",
|
528 |
+
line=dict(color='green', width=2)
|
529 |
+
)
|
530 |
+
)
|
531 |
+
fig_second_deriv.update_layout(
|
532 |
+
title="Second Derivative of z*(β) Difference",
|
533 |
+
xaxis_title="β",
|
534 |
+
yaxis_title="d²(z_max - z_min)/dβ²",
|
535 |
+
hovermode="x unified"
|
536 |
+
)
|
537 |
+
st.plotly_chart(fig_second_deriv, use_container_width=True)
|