euler314 commited on
Commit
19ef319
·
verified ·
1 Parent(s): 913575d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +310 -403
app.py CHANGED
@@ -6,144 +6,131 @@ from scipy.optimize import fsolve
6
 
7
  # Configure Streamlit for Hugging Face Spaces
8
  st.set_page_config(
9
- page_title="Cubic Root Analysis",
10
- layout="wide",
11
- initial_sidebar_state="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
  #############################
33
  # 1) Define the discriminant
34
  #############################
35
 
36
- # Symbolic variables to build a symbolic expression of discriminant
37
  z_sym, beta_sym, z_a_sym, y_sym = sp.symbols("z beta z_a y", real=True, positive=True)
38
 
39
- # Define a, b, c, d in terms of z_sym, beta_sym, z_a_sym, y_sym
40
  a_sym = z_sym * z_a_sym
41
  b_sym = z_sym * z_a_sym + z_sym + z_a_sym - z_a_sym*y_sym
42
  c_sym = z_sym + z_a_sym + 1 - y_sym*(beta_sym*z_a_sym + 1 - beta_sym)
43
  d_sym = 1
44
 
45
- # Symbolic expression for the standard cubic discriminant
46
  Delta_expr = (
47
- ((b_sym*c_sym)/(6*a_sym**2) - (b_sym**3)/(27*a_sym**3) - d_sym/(2*a_sym))**2
48
- + (c_sym/(3*a_sym) - (b_sym**2)/(9*a_sym**2))**3
49
  )
50
 
51
- # Turn that into a fast numeric function:
52
  discriminant_func = sp.lambdify((z_sym, beta_sym, z_a_sym, y_sym), Delta_expr, "numpy")
53
 
54
  @st.cache_data
55
  def find_z_at_discriminant_zero(z_a, y, beta, z_min, z_max, steps):
56
- z_grid = np.linspace(z_min, z_max, steps)
57
- disc_vals = discriminant_func(z_grid, beta, z_a, y)
58
-
59
- roots_found = []
60
-
61
- # Scan for sign changes
62
- for i in range(len(z_grid) - 1):
63
- f1, f2 = disc_vals[i], disc_vals[i+1]
64
- if np.isnan(f1) or np.isnan(f2):
65
- continue
66
-
67
- if f1 == 0.0:
68
- roots_found.append(z_grid[i])
69
- elif f2 == 0.0:
70
- roots_found.append(z_grid[i+1])
71
- elif f1*f2 < 0:
72
- zl = z_grid[i]
73
- zr = z_grid[i+1]
74
- for _ in range(50):
75
- mid = 0.5*(zl + zr)
76
- fm = discriminant_func(mid, beta, z_a, y)
77
- if fm == 0:
78
- zl = zr = mid
79
- break
80
- if np.sign(fm) == np.sign(f1):
81
- zl = mid
82
- f1 = fm
83
- else:
84
- zr = mid
85
- f2 = fm
86
- root_approx = 0.5*(zl + zr)
87
- roots_found.append(root_approx)
88
-
89
- return np.array(roots_found)
90
 
91
  @st.cache_data
92
  def sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps):
93
- betas = np.linspace(0, 1, beta_steps)
94
- z_min_values = []
95
- z_max_values = []
96
-
97
- for b in betas:
98
- roots = find_z_at_discriminant_zero(z_a, y, b, z_min, z_max, z_steps)
99
- if len(roots) == 0:
100
- z_min_values.append(np.nan)
101
- z_max_values.append(np.nan)
102
- else:
103
- z_min_values.append(np.min(roots))
104
- z_max_values.append(np.max(roots))
105
-
106
- return betas, np.array(z_min_values), np.array(z_max_values)
 
 
 
107
 
108
  @st.cache_data
109
  def compute_low_y_curve(betas, z_a, y):
110
- betas = np.array(betas)
111
- with np.errstate(invalid='ignore', divide='ignore'):
112
- sqrt_term = y * betas * (z_a - 1)
113
- sqrt_term = np.where(sqrt_term < 0, np.nan, np.sqrt(sqrt_term))
114
-
115
- term = (-1 + sqrt_term)/z_a
116
- numerator = (y - 2)*term + y * betas * ((z_a - 1)/z_a) - 1/z_a - 1
117
- denominator = term**2 + term
118
- mask = (denominator != 0) & ~np.isnan(denominator) & ~np.isnan(numerator)
119
- return np.where(mask, numerator/denominator, np.nan)
 
 
 
120
 
121
  @st.cache_data
122
  def compute_high_y_curve(betas, z_a, y):
 
 
 
123
  a = z_a
124
  betas = np.array(betas)
125
  denominator = 1 - 2*a
126
-
127
  if denominator == 0:
128
  return np.full_like(betas, np.nan)
129
-
130
  numerator = -4*a*(a-1)*y*betas - 2*a*y - 2*a*(2*a-1)
131
  return numerator/denominator
132
 
133
- @st.cache_data
134
- def compute_z_difference_and_derivatives(z_a, y, z_min, z_max, beta_steps, z_steps):
135
- betas, z_mins, z_maxs = sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps)
136
-
137
- z_difference = z_maxs - z_mins
138
- dz_diff_dbeta = np.gradient(z_difference, betas)
139
- d2z_diff_dbeta2 = np.gradient(dz_diff_dbeta, betas)
140
-
141
- return betas, z_difference, dz_diff_dbeta, d2z_diff_dbeta2
142
 
143
- def compute_custom_expression(betas, z_a, y, num_expr_str, denom_expr_str):
144
- beta_sym, z_a_sym, y_sym, a_sym = sp.symbols("beta z_a y a", positive=True)
145
- local_dict = {"beta": beta_sym, "z_a": z_a_sym, "y": y_sym, "a": z_a_sym}
146
-
 
 
 
 
 
147
  try:
148
  num_expr = sp.sympify(num_expr_str, locals=local_dict)
149
  denom_expr = sp.sympify(denom_expr_str, locals=local_dict)
@@ -151,356 +138,276 @@ def compute_custom_expression(betas, z_a, y, num_expr_str, denom_expr_str):
151
  st.error(f"Error parsing expressions: {e}")
152
  return np.full_like(betas, np.nan)
153
 
154
- num_func = sp.lambdify((beta_sym, z_a_sym, y_sym), num_expr, modules=["numpy"])
155
- denom_func = sp.lambdify((beta_sym, z_a_sym, y_sym), denom_expr, modules=["numpy"])
156
-
157
  with np.errstate(divide='ignore', invalid='ignore'):
158
- result = num_func(betas, z_a, y) / denom_func(betas, z_a, y)
159
  return result
160
 
161
  def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps,
162
- custom_num_expr=None, custom_denom_expr=None):
163
- if z_a <= 0 or y <= 0 or z_min >= z_max:
164
- st.error("Invalid input parameters.")
165
- return None, None
166
-
167
- betas = np.linspace(0, 1, beta_steps)
168
- betas, z_mins, z_maxs = sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps)
169
- low_y_curve = compute_low_y_curve(betas, z_a, y)
170
- high_y_curve = compute_high_y_curve(betas, z_a, y)
171
-
172
- fig = go.Figure()
173
-
174
- fig.add_trace(
175
- go.Scatter(
176
- x=betas,
177
- y=z_maxs,
178
- mode="markers+lines",
179
- name="Upper z*(β)",
180
- marker=dict(size=5, color='blue'),
181
- line=dict(color='blue'),
182
- )
183
- )
184
-
185
- fig.add_trace(
186
- go.Scatter(
187
- x=betas,
188
- y=z_mins,
189
- mode="markers+lines",
190
- name="Lower z*(β)",
191
- marker=dict(size=5, color='lightblue'),
192
- line=dict(color='lightblue'),
193
- )
194
- )
195
-
196
- fig.add_trace(
197
- go.Scatter(
198
- x=betas,
199
- y=low_y_curve,
200
- mode="markers+lines",
201
- name="Low y Expression",
202
- marker=dict(size=5, color='red'),
203
- line=dict(color='red'),
204
- )
205
- )
206
-
207
- fig.add_trace(
208
- go.Scatter(
209
- x=betas,
210
- y=high_y_curve,
211
- mode="markers+lines",
212
- name="High y Expression",
213
- marker=dict(size=5, color='green'),
214
- line=dict(color='green'),
215
- )
216
- )
217
-
218
- custom_curve = None
219
- if custom_num_expr and custom_denom_expr:
220
- custom_curve = compute_custom_expression(betas, z_a, y, custom_num_expr, custom_denom_expr)
221
- fig.add_trace(
222
- go.Scatter(
223
- x=betas,
224
- y=custom_curve,
225
- mode="markers+lines",
226
- name="Custom Expression",
227
- marker=dict(size=5, color='purple'),
228
- line=dict(color='purple'),
229
- )
230
- )
231
-
232
- fig.update_layout(
233
- title="Curves vs β: z*(β) Boundaries and Asymptotic Expressions",
234
- xaxis_title="β",
235
- yaxis_title="Value",
236
- hovermode="x unified",
237
- )
238
-
239
- dzmax_dbeta = np.gradient(z_maxs, betas)
240
- dzmin_dbeta = np.gradient(z_mins, betas)
241
- dlowy_dbeta = np.gradient(low_y_curve, betas)
242
- dhighy_dbeta = np.gradient(high_y_curve, betas)
243
- dcustom_dbeta = np.gradient(custom_curve, betas) if custom_curve is not None else None
244
-
245
- fig_deriv = go.Figure()
246
-
247
- fig_deriv.add_trace(
248
- go.Scatter(
249
- x=betas,
250
- y=dzmax_dbeta,
251
- mode="markers+lines",
252
- name="d/dβ Upper z*(β)",
253
- marker=dict(size=5, color='blue'),
254
- line=dict(color='blue'),
255
- )
256
- )
257
-
258
- fig_deriv.add_trace(
259
- go.Scatter(
260
- x=betas,
261
- y=dzmin_dbeta,
262
- mode="markers+lines",
263
- name="d/dβ Lower z*(β)",
264
- marker=dict(size=5, color='lightblue'),
265
- line=dict(color='lightblue'),
266
- )
267
- )
268
-
269
- fig_deriv.add_trace(
270
- go.Scatter(
271
- x=betas,
272
- y=dlowy_dbeta,
273
- mode="markers+lines",
274
- name="d/dβ Low y Expression",
275
- marker=dict(size=5, color='red'),
276
- line=dict(color='red'),
277
- )
278
- )
279
-
280
- fig_deriv.add_trace(
281
- go.Scatter(
282
- x=betas,
283
- y=dhighy_dbeta,
284
- mode="markers+lines",
285
- name="d/dβ High y Expression",
286
- marker=dict(size=5, color='green'),
287
- line=dict(color='green'),
288
- )
289
- )
290
-
291
- if dcustom_dbeta is not None:
292
- fig_deriv.add_trace(
293
- go.Scatter(
294
- x=betas,
295
- y=dcustom_dbeta,
296
- mode="markers+lines",
297
- name="d/dβ Custom Expression",
298
- marker=dict(size=5, color='purple'),
299
- line=dict(color='purple'),
300
- )
301
- )
302
-
303
- fig_deriv.update_layout(
304
- title="Derivatives vs β of Each Curve",
305
- xaxis_title="β",
306
- yaxis_title="d(Value)/dβ",
307
- hovermode="x unified",
308
- )
309
-
310
- return fig, fig_deriv
311
 
312
  def compute_cubic_roots(z, beta, z_a, y):
313
- a = z * z_a
314
- b = z * z_a + z + z_a - z_a*y
315
- c = z + z_a + 1 - y*(beta*z_a + 1 - beta)
316
- d = 1
317
-
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
- if z_a <= 0 or y <= 0 or z_min >= z_max:
324
- st.error("Invalid input parameters.")
325
- return None, None
326
-
327
- z_points = np.linspace(z_min, z_max, n_points)
328
- ims = []
329
- res = []
330
-
331
- for z in z_points:
332
- roots = compute_cubic_roots(z, beta, z_a, y)
333
- roots = sorted(roots, key=lambda x: abs(x.imag))
334
- ims.append([root.imag for root in roots])
335
- res.append([root.real for root in roots])
336
-
337
- ims = np.array(ims)
338
- res = np.array(res)
339
-
340
- fig_im = go.Figure()
341
- for i in range(3):
342
- fig_im.add_trace(
343
- go.Scatter(
344
- x=z_points,
345
- y=ims[:,i],
346
- mode="lines",
347
- name=f"Im{{s{i+1}}}",
348
- line=dict(width=2),
349
- )
350
- )
351
- fig_im.update_layout(
352
- title=f"Im{{s}} vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})",
353
- xaxis_title="z",
354
- yaxis_title="Im{s}",
355
- hovermode="x unified",
356
- )
357
-
358
- fig_re = go.Figure()
359
- for i in range(3):
360
- fig_re.add_trace(
361
- go.Scatter(
362
- x=z_points,
363
- y=res[:,i],
364
- mode="lines",
365
- name=f"Re{{s{i+1}}}",
366
- line=dict(width=2),
367
- )
368
- )
369
- fig_re.update_layout(
370
- title=f"Re{{s}} vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})",
371
- xaxis_title="z",
372
- yaxis_title="Re{s}",
373
- hovermode="x unified",
374
- )
375
-
376
- return fig_im, fig_re
377
-
378
- # ------------------- Streamlit UI -------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
 
 
380
  st.title("Cubic Root Analysis")
381
 
382
- tab1, tab2, tab3 = st.tabs(["z*(β) Curves", "Im{s} vs. z", "z*(β) Difference Analysis"])
 
383
 
 
384
  with tab1:
385
  st.header("Find z Values where Cubic Roots Transition Between Real and Complex")
386
-
387
  col1, col2 = st.columns([1, 2])
388
-
389
  with col1:
390
  z_a_1 = st.number_input("z_a", value=1.0, key="z_a_1")
391
  y_1 = st.number_input("y", value=1.0, key="y_1")
392
  z_min_1 = st.number_input("z_min", value=-10.0, key="z_min_1")
393
  z_max_1 = st.number_input("z_max", value=10.0, key="z_max_1")
394
-
395
  with st.expander("Resolution Settings"):
396
  beta_steps = st.slider("β steps", min_value=51, max_value=501, value=201, step=50)
397
  z_steps = st.slider("z grid steps", min_value=1000, max_value=100000, value=50000, step=1000)
398
-
399
- if st.button("Compute z vs. β Curves"):
 
 
 
 
 
 
 
 
400
  with col2:
401
- fig_main, fig_deriv = generate_z_vs_beta_plot(z_a_1, y_1, z_min_1, z_max_1,
402
- beta_steps, z_steps,
403
- custom_num_expr, custom_denom_expr)
404
- if fig_main is not None and fig_deriv is not None:
405
- st.plotly_chart(fig_main, use_container_width=True)
406
- st.plotly_chart(fig_deriv, use_container_width=True)
 
 
 
 
 
 
 
 
 
 
407
 
 
 
 
 
 
 
 
408
  with tab2:
409
  st.header("Plot Complex Roots vs. z")
410
-
411
  col1, col2 = st.columns([1, 2])
412
-
413
  with col1:
414
  beta = st.number_input("β", value=0.5, min_value=0.0, max_value=1.0)
415
  y_2 = st.number_input("y", value=1.0, key="y_2")
416
  z_a_2 = st.number_input("z_a", value=1.0, key="z_a_2")
417
  z_min_2 = st.number_input("z_min", value=-10.0, key="z_min_2")
418
  z_max_2 = st.number_input("z_max", value=10.0, key="z_max_2")
419
-
420
  with st.expander("Resolution Settings"):
421
  z_points = st.slider("z grid points", min_value=1000, max_value=10000, value=5000, step=500)
422
-
423
- if st.button("Compute Complex Roots vs. z"):
424
  with col2:
425
  fig_im, fig_re = generate_root_plots(beta, y_2, z_a_2, z_min_2, z_max_2, z_points)
426
  if fig_im is not None and fig_re is not None:
427
  st.plotly_chart(fig_im, use_container_width=True)
428
  st.plotly_chart(fig_re, use_container_width=True)
429
 
 
430
  with tab3:
431
- st.header("z*(β) Difference Analysis")
432
-
433
  col1, col2 = st.columns([1, 2])
434
-
435
  with col1:
436
- z_a_4 = st.number_input("z_a", value=1.0, key="z_a_4")
437
- y_4 = st.number_input("y", value=1.0, key="y_4")
438
- z_min_4 = st.number_input("z_min", value=-10.0, key="z_min_4")
439
- z_max_4 = st.number_input("z_max", value=10.0, key="z_max_4")
440
-
 
 
441
  with st.expander("Resolution Settings"):
442
- beta_steps_4 = st.slider("β steps", min_value=51, max_value=501, value=201, step=50, key="beta_steps_4")
443
- z_steps_4 = st.slider("z grid steps", min_value=1000, max_value=100000, value=50000, step=1000, key="z_steps_4")
444
-
445
- if st.button("Compute Difference Analysis"):
446
- with col2:
447
- betas, z_diff, dz_diff, d2z_diff = compute_z_difference_and_derivatives(
448
- z_a_4, y_4, z_min_4, z_max_4, beta_steps_4, z_steps_4
449
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
 
451
- # Plot difference
452
  fig_diff = go.Figure()
453
- fig_diff.add_trace(
454
- go.Scatter(
455
- x=betas,
456
- y=z_diff,
457
- mode="lines",
458
- name="z*(β) Difference",
459
- line=dict(color='purple', width=2)
460
- )
461
- )
462
- fig_diff.update_layout(
463
- title="Difference between Upper and Lower z*(β)",
464
- xaxis_title="β",
465
- yaxis_title="z_max - z_min",
466
- hovermode="x unified"
467
- )
468
  st.plotly_chart(fig_diff, use_container_width=True)
469
-
470
- # Plot first derivative
471
- fig_first_deriv = go.Figure()
472
- fig_first_deriv.add_trace(
473
- go.Scatter(
474
- x=betas,
475
- y=dz_diff,
476
- mode="lines",
477
- name="First Derivative",
478
- line=dict(color='blue', width=2)
479
- )
480
- )
481
- fig_first_deriv.update_layout(
482
- title="First Derivative of z*(β) Difference",
483
- xaxis_title="β",
484
- yaxis_title="d(z_max - z_min)/dβ",
485
- hovermode="x unified"
486
- )
487
- st.plotly_chart(fig_first_deriv, use_container_width=True)
488
-
489
- # Plot second derivative
490
- fig_second_deriv = go.Figure()
491
- fig_second_deriv.add_trace(
492
- go.Scatter(
493
- x=betas,
494
- y=d2z_diff,
495
- mode="lines",
496
- name="Second Derivative",
497
- line=dict(color='green', width=2)
498
- )
499
- )
500
- fig_second_deriv.update_layout(
501
- title="Second Derivative of z*(β) Difference",
502
- xaxis_title="β",
503
- yaxis_title="d²(z_max - z_min)/dβ²",
504
- hovermode="x unified"
505
- )
506
- st.plotly_chart(fig_second_deriv, use_container_width=True)
 
6
 
7
  # Configure Streamlit for Hugging Face Spaces
8
  st.set_page_config(
9
+ page_title="Cubic Root Analysis",
10
+ layout="wide",
11
+ initial_sidebar_state="collapsed"
12
  )
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  #############################
15
  # 1) Define the discriminant
16
  #############################
17
 
18
+ # Symbolic variables for the cubic discriminant
19
  z_sym, beta_sym, z_a_sym, y_sym = sp.symbols("z beta z_a y", real=True, positive=True)
20
 
21
+ # Define coefficients a, b, c, d in terms of z_sym, beta_sym, z_a_sym, y_sym
22
  a_sym = z_sym * z_a_sym
23
  b_sym = z_sym * z_a_sym + z_sym + z_a_sym - z_a_sym*y_sym
24
  c_sym = z_sym + z_a_sym + 1 - y_sym*(beta_sym*z_a_sym + 1 - beta_sym)
25
  d_sym = 1
26
 
27
+ # Symbolic expression for the cubic discriminant
28
  Delta_expr = (
29
+ ((b_sym*c_sym)/(6*a_sym**2) - (b_sym**3)/(27*a_sym**3) - d_sym/(2*a_sym))**2
30
+ + (c_sym/(3*a_sym) - (b_sym**2)/(9*a_sym**2))**3
31
  )
32
 
33
+ # Fast numeric function for the discriminant
34
  discriminant_func = sp.lambdify((z_sym, beta_sym, z_a_sym, y_sym), Delta_expr, "numpy")
35
 
36
  @st.cache_data
37
  def find_z_at_discriminant_zero(z_a, y, beta, z_min, z_max, steps):
38
+ """
39
+ Scan z in [z_min, z_max] for sign changes in the discriminant,
40
+ and return approximated roots (where the discriminant is zero).
41
+ """
42
+ z_grid = np.linspace(z_min, z_max, steps)
43
+ disc_vals = discriminant_func(z_grid, beta, z_a, y)
44
+ roots_found = []
45
+ for i in range(len(z_grid) - 1):
46
+ f1, f2 = disc_vals[i], disc_vals[i+1]
47
+ if np.isnan(f1) or np.isnan(f2):
48
+ continue
49
+ if f1 == 0.0:
50
+ roots_found.append(z_grid[i])
51
+ elif f2 == 0.0:
52
+ roots_found.append(z_grid[i+1])
53
+ elif f1 * f2 < 0:
54
+ zl, zr = z_grid[i], z_grid[i+1]
55
+ for _ in range(50):
56
+ mid = 0.5 * (zl + zr)
57
+ fm = discriminant_func(mid, beta, z_a, y)
58
+ if fm == 0:
59
+ zl = zr = mid
60
+ break
61
+ if np.sign(fm) == np.sign(f1):
62
+ zl, f1 = mid, fm
63
+ else:
64
+ zr, f2 = mid, fm
65
+ roots_found.append(0.5 * (zl + zr))
66
+ return np.array(roots_found)
 
 
 
 
 
67
 
68
  @st.cache_data
69
  def sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps):
70
+ """
71
+ For each beta in [0,1] (with beta_steps points), find the minimum and maximum z
72
+ for which the discriminant is zero.
73
+ Returns: betas, lower z*(β) values, and upper z*(β) values.
74
+ """
75
+ betas = np.linspace(0, 1, beta_steps)
76
+ z_min_values = []
77
+ z_max_values = []
78
+ for b in betas:
79
+ roots = find_z_at_discriminant_zero(z_a, y, b, z_min, z_max, z_steps)
80
+ if len(roots) == 0:
81
+ z_min_values.append(np.nan)
82
+ z_max_values.append(np.nan)
83
+ else:
84
+ z_min_values.append(np.min(roots))
85
+ z_max_values.append(np.max(roots))
86
+ return betas, np.array(z_min_values), np.array(z_max_values)
87
 
88
  @st.cache_data
89
  def compute_low_y_curve(betas, z_a, y):
90
+ """
91
+ Compute the "Low y Expression" curve.
92
+ """
93
+ betas = np.array(betas)
94
+ with np.errstate(invalid='ignore', divide='ignore'):
95
+ sqrt_term = y * betas * (z_a - 1)
96
+ sqrt_term = np.where(sqrt_term < 0, np.nan, np.sqrt(sqrt_term))
97
+ term = (-1 + sqrt_term) / z_a
98
+ numerator = (y - 2)*term + y * betas * ((z_a - 1)/z_a) - 1/z_a - 1
99
+ denominator = term**2 + term
100
+ mask = (denominator != 0) & ~np.isnan(denominator) & ~np.isnan(numerator)
101
+ result = np.where(mask, numerator/denominator, np.nan)
102
+ return result
103
 
104
  @st.cache_data
105
  def compute_high_y_curve(betas, z_a, y):
106
+ """
107
+ Compute the "High y Expression" curve.
108
+ """
109
  a = z_a
110
  betas = np.array(betas)
111
  denominator = 1 - 2*a
 
112
  if denominator == 0:
113
  return np.full_like(betas, np.nan)
 
114
  numerator = -4*a*(a-1)*y*betas - 2*a*y - 2*a*(2*a-1)
115
  return numerator/denominator
116
 
117
+ def compute_alternate_low_expr(betas, z_a, y):
118
+ """
119
+ Compute the alternate low expression:
120
+ (z_a*y*beta*(z_a-1) - 2*z_a*(1-y) - 2*z_a**2) / (2+2*z_a)
121
+ """
122
+ betas = np.array(betas)
123
+ return (z_a * y * betas * (z_a - 1) - 2*z_a*(1 - y) - 2*z_a**2) / (2 + 2*z_a)
 
 
124
 
125
+ def compute_custom_expression(betas, z_a, y, s, num_expr_str, denom_expr_str):
126
+ """
127
+ Compute a custom curve from numerator and denominator expressions given as strings.
128
+ This version allows the expressions to depend on s (an extra parameter).
129
+ Allowed variables: z_a, beta, y, s.
130
+ Also, 'a' is accepted as an alias for z_a.
131
+ """
132
+ beta_sym, z_a_sym, y_sym, s_sym, a_sym = sp.symbols("beta z_a y s a", positive=True)
133
+ local_dict = {"beta": beta_sym, "z_a": z_a_sym, "y": y_sym, "s": s_sym, "a": z_a_sym}
134
  try:
135
  num_expr = sp.sympify(num_expr_str, locals=local_dict)
136
  denom_expr = sp.sympify(denom_expr_str, locals=local_dict)
 
138
  st.error(f"Error parsing expressions: {e}")
139
  return np.full_like(betas, np.nan)
140
 
141
+ num_func = sp.lambdify((beta_sym, z_a_sym, y_sym, s_sym), num_expr, modules=["numpy"])
142
+ denom_func = sp.lambdify((beta_sym, z_a_sym, y_sym, s_sym), denom_expr, modules=["numpy"])
 
143
  with np.errstate(divide='ignore', invalid='ignore'):
144
+ result = num_func(betas, z_a, y, s) / denom_func(betas, z_a, y, s)
145
  return result
146
 
147
  def generate_z_vs_beta_plot(z_a, y, z_min, z_max, beta_steps, z_steps,
148
+ custom_num_expr=None, custom_denom_expr=None, s_custom=None):
149
+ if z_a <= 0 or y <= 0 or z_min >= z_max:
150
+ st.error("Invalid input parameters.")
151
+ return None
152
+
153
+ betas = np.linspace(0, 1, beta_steps)
154
+ betas, z_mins, z_maxs = sweep_beta_and_find_z_bounds(z_a, y, z_min, z_max, beta_steps, z_steps)
155
+ low_y_curve = compute_low_y_curve(betas, z_a, y)
156
+ high_y_curve = compute_high_y_curve(betas, z_a, y)
157
+ alt_low_expr = compute_alternate_low_expr(betas, z_a, y)
158
+
159
+ fig = go.Figure()
160
+ fig.add_trace(go.Scatter(x=betas, y=z_maxs, mode="markers+lines", name="Upper z*(β)",
161
+ marker=dict(size=5, color='blue'), line=dict(color='blue')))
162
+ fig.add_trace(go.Scatter(x=betas, y=z_mins, mode="markers+lines", name="Lower z*(β)",
163
+ marker=dict(size=5, color='lightblue'), line=dict(color='lightblue')))
164
+ fig.add_trace(go.Scatter(x=betas, y=low_y_curve, mode="markers+lines", name="Low y Expression",
165
+ marker=dict(size=5, color='red'), line=dict(color='red')))
166
+ fig.add_trace(go.Scatter(x=betas, y=high_y_curve, mode="markers+lines", name="High y Expression",
167
+ marker=dict(size=5, color='green'), line=dict(color='green')))
168
+ fig.add_trace(go.Scatter(x=betas, y=alt_low_expr, mode="markers+lines", name="Alternate Low Expression",
169
+ marker=dict(size=5, color='orange'), line=dict(color='orange')))
170
+
171
+ if custom_num_expr and custom_denom_expr and s_custom is not None:
172
+ custom_curve = compute_custom_expression(betas, z_a, y, s_custom, custom_num_expr, custom_denom_expr)
173
+ fig.add_trace(go.Scatter(x=betas, y=custom_curve, mode="markers+lines", name="Custom Expression",
174
+ marker=dict(size=5, color='purple'), line=dict(color='purple')))
175
+
176
+ fig.update_layout(title="Curves vs β: z*(β) Boundaries and Asymptotic Expressions",
177
+ xaxis_title="β", yaxis_title="Value", hovermode="x unified")
178
+ return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
  def compute_cubic_roots(z, beta, z_a, y):
181
+ """
182
+ Compute the roots of the cubic equation for given parameters.
183
+ """
184
+ a = z * z_a
185
+ b = z * z_a + z + z_a - z_a*y
186
+ c = z + z_a + 1 - y*(beta*z_a + 1 - beta)
187
+ d = 1
188
+ coeffs = [a, b, c, d]
189
+ roots = np.roots(coeffs)
190
+ return roots
191
 
192
  def generate_root_plots(beta, y, z_a, z_min, z_max, n_points):
193
+ """
194
+ Generate Im(s) and Re(s) vs. z plots.
195
+ """
196
+ if z_a <= 0 or y <= 0 or z_min >= z_max:
197
+ st.error("Invalid input parameters.")
198
+ return None, None
199
+
200
+ z_points = np.linspace(z_min, z_max, n_points)
201
+ ims, res = [], []
202
+ for z in z_points:
203
+ roots = compute_cubic_roots(z, beta, z_a, y)
204
+ roots = sorted(roots, key=lambda x: abs(x.imag))
205
+ ims.append([root.imag for root in roots])
206
+ res.append([root.real for root in roots])
207
+ ims = np.array(ims)
208
+ res = np.array(res)
209
+
210
+ fig_im = go.Figure()
211
+ for i in range(3):
212
+ fig_im.add_trace(go.Scatter(x=z_points, y=ims[:, i], mode="lines", name=f"Im{{s{i+1}}}",
213
+ line=dict(width=2)))
214
+ fig_im.update_layout(title=f"Im{{s}} vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})",
215
+ xaxis_title="z", yaxis_title="Im{s}", hovermode="x unified")
216
+
217
+ fig_re = go.Figure()
218
+ for i in range(3):
219
+ fig_re.add_trace(go.Scatter(x=z_points, y=res[:, i], mode="lines", name=f"Re{{s{i+1}}}",
220
+ line=dict(width=2)))
221
+ fig_re.update_layout(title=f"Re{{s}} vs. z (β={beta:.3f}, y={y:.3f}, z_a={z_a:.3f})",
222
+ xaxis_title="z", yaxis_title="Re{s}", hovermode="x unified")
223
+ return fig_im, fig_re
224
+
225
+ def curve1(s, z, y):
226
+ """First curve: z*s^2 + (z-y+1)*s + 1"""
227
+ return z*s**2 + (z-y+1)*s + 1
228
+
229
+ def curve2(s, y, beta, a):
230
+ """Second curve: y*β*((a-1)*s)/(a*s+1)"""
231
+ return y*beta*((a-1)*s)/(a*s+1)
232
+
233
+ def find_intersections(z, y, beta, a, s_range, n_guesses, tolerance):
234
+ """Find intersections between curve1 and curve2."""
235
+ def equation(s):
236
+ return curve1(s, z, y) - curve2(s, y, beta, a)
237
+ s_guesses = np.linspace(s_range[0], s_range[1], n_guesses)
238
+ intersections = []
239
+ for s_guess in s_guesses:
240
+ try:
241
+ s_sol = fsolve(equation, s_guess, full_output=True, xtol=tolerance)
242
+ if s_sol[2] == 1:
243
+ s_val = s_sol[0][0]
244
+ if (s_range[0] <= s_val <= s_range[1] and
245
+ not any(abs(s_val - s_prev) < tolerance for s_prev in intersections)):
246
+ if abs(equation(s_val)) < tolerance:
247
+ intersections.append(s_val)
248
+ except:
249
+ continue
250
+ intersections = np.sort(np.array(intersections))
251
+ if len(intersections) % 2 != 0:
252
+ refined_intersections = []
253
+ for i in range(len(intersections)-1):
254
+ mid_point = (intersections[i] + intersections[i+1]) / 2
255
+ try:
256
+ s_sol = fsolve(equation, mid_point, full_output=True, xtol=tolerance)
257
+ if s_sol[2] == 1:
258
+ s_val = s_sol[0][0]
259
+ if (intersections[i] < s_val < intersections[i+1] and
260
+ abs(equation(s_val)) < tolerance):
261
+ refined_intersections.append(s_val)
262
+ except:
263
+ continue
264
+ intersections = np.sort(np.append(intersections, refined_intersections))
265
+ return intersections
266
+
267
+ def generate_curves_plot(z, y, beta, a, s_range, n_points, n_guesses, tolerance):
268
+ s = np.linspace(s_range[0], s_range[1], n_points)
269
+ y1 = curve1(s, z, y)
270
+ y2 = curve2(s, y, beta, a)
271
+ intersections = find_intersections(z, y, beta, a, s_range, n_guesses, tolerance)
272
+ fig = go.Figure()
273
+ fig.add_trace(go.Scatter(x=s, y=y1, mode='lines', name='z*s² + (z-y+1)*s + 1', line=dict(color='blue', width=2)))
274
+ fig.add_trace(go.Scatter(x=s, y=y2, mode='lines', name='y*β*((a-1)*s)/(a*s+1)', line=dict(color='red', width=2)))
275
+ if len(intersections) > 0:
276
+ fig.add_trace(go.Scatter(x=intersections, y=curve1(intersections, z, y),
277
+ mode='markers', name='Intersections',
278
+ marker=dict(size=12, color='green', symbol='x', line=dict(width=2))))
279
+ fig.update_layout(title=f"Curve Intersection Analysis (y={y:.4f}, β={beta:.4f}, a={a:.4f})",
280
+ xaxis_title="s", yaxis_title="Value", hovermode="closest",
281
+ showlegend=True, legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01))
282
+ return fig, intersections
283
 
284
+ # ----------------- Streamlit UI -----------------
285
  st.title("Cubic Root Analysis")
286
 
287
+ # Define four tabs
288
+ tab1, tab2, tab3, tab4 = st.tabs(["z*(β) Curves", "Im{s} vs. z", "Curve Intersections", "Differential Analysis"])
289
 
290
+ # ----- Tab 1: z*(β) Curves -----
291
  with tab1:
292
  st.header("Find z Values where Cubic Roots Transition Between Real and Complex")
 
293
  col1, col2 = st.columns([1, 2])
 
294
  with col1:
295
  z_a_1 = st.number_input("z_a", value=1.0, key="z_a_1")
296
  y_1 = st.number_input("y", value=1.0, key="y_1")
297
  z_min_1 = st.number_input("z_min", value=-10.0, key="z_min_1")
298
  z_max_1 = st.number_input("z_max", value=10.0, key="z_max_1")
 
299
  with st.expander("Resolution Settings"):
300
  beta_steps = st.slider("β steps", min_value=51, max_value=501, value=201, step=50)
301
  z_steps = st.slider("z grid steps", min_value=1000, max_value=100000, value=50000, step=1000)
302
+ st.subheader("Custom Expression")
303
+ st.markdown("Enter the **numerator** and **denominator** expressions (as functions of `z_a`, `beta`, `y`, and `s`) for the custom curve. Here, 'a' is an alias for `z_a`.")
304
+ st.markdown("The default expressions yield:")
305
+ st.latex(r"\frac{y\beta(z_a-1)s+(a\,s+1)((y-1)s-1)}{(a\,s+1)(s^2+s)}")
306
+ default_num = "y*beta*(z_a-1)*s + (a*s+1)*((y-1)*s-1)"
307
+ default_denom = "(a*s+1)*(s**2+s)"
308
+ custom_num_expr = st.text_input("Numerator Expression", value=default_num)
309
+ custom_denom_expr = st.text_input("Denominator Expression", value=default_denom)
310
+ s_custom = st.number_input("Custom s value", value=1.0, step=0.1)
311
+ if st.button("Compute z vs. β Curves", key="tab1_button"):
312
  with col2:
313
+ fig = generate_z_vs_beta_plot(z_a_1, y_1, z_min_1, z_max_1, beta_steps, z_steps,
314
+ custom_num_expr, custom_denom_expr, s_custom)
315
+ if fig is not None:
316
+ st.plotly_chart(fig, use_container_width=True)
317
+ st.markdown("### Additional Expressions")
318
+ st.markdown("""
319
+ **Low y Expression (Red):**
320
+ ```
321
+ ((y - 2)*((-1 + sqrt(y*beta*(z_a - 1)))/z_a) + y*beta*((z_a-1)/z_a) - 1/z_a - 1) /
322
+ (((-1 + sqrt(y*beta*(z_a - 1)))/z_a)**2 + ((-1 + sqrt(y*beta*(z_a - 1)))/z_a))
323
+ ```
324
+
325
+ **High y Expression (Green):**
326
+ ```
327
+ (-4*z_a*(z_a-1)*y*beta - 2*z_a*y + 2*z_a*(2*z_a-1))/(1-2*z_a)
328
+ ```
329
 
330
+ **Alternate Low Expression (Orange):**
331
+ ```
332
+ (z_a*y*beta*(z_a-1) - 2*z_a*(1-y) - 2*z_a**2)/(2+2*z_a)
333
+ ```
334
+ """)
335
+
336
+ # ----- Tab 2: Im{s} vs. z -----
337
  with tab2:
338
  st.header("Plot Complex Roots vs. z")
 
339
  col1, col2 = st.columns([1, 2])
 
340
  with col1:
341
  beta = st.number_input("β", value=0.5, min_value=0.0, max_value=1.0)
342
  y_2 = st.number_input("y", value=1.0, key="y_2")
343
  z_a_2 = st.number_input("z_a", value=1.0, key="z_a_2")
344
  z_min_2 = st.number_input("z_min", value=-10.0, key="z_min_2")
345
  z_max_2 = st.number_input("z_max", value=10.0, key="z_max_2")
 
346
  with st.expander("Resolution Settings"):
347
  z_points = st.slider("z grid points", min_value=1000, max_value=10000, value=5000, step=500)
348
+ if st.button("Compute Complex Roots vs. z", key="tab2_button"):
 
349
  with col2:
350
  fig_im, fig_re = generate_root_plots(beta, y_2, z_a_2, z_min_2, z_max_2, z_points)
351
  if fig_im is not None and fig_re is not None:
352
  st.plotly_chart(fig_im, use_container_width=True)
353
  st.plotly_chart(fig_re, use_container_width=True)
354
 
355
+ # ----- Tab 3: Curve Intersections -----
356
  with tab3:
357
+ st.header("Curve Intersection Analysis")
 
358
  col1, col2 = st.columns([1, 2])
 
359
  with col1:
360
+ z = st.slider("z", min_value=-10.0, max_value=10000.0, value=1.0, step=0.1)
361
+ y_3 = st.slider("y", min_value=0.1, max_value=1000.0, value=1.0, step=0.1, key="y_3")
362
+ beta_3 = st.slider("β", min_value=0.0, max_value=1.0, value=0.5, step=0.01, key="beta_3")
363
+ a = st.slider("a", min_value=0.1, max_value=1000.0, value=1.0, step=0.1)
364
+ st.subheader("s Range")
365
+ s_min = st.number_input("s_min", value=-5.0)
366
+ s_max = st.number_input("s_max", value=5.0)
367
  with st.expander("Resolution Settings"):
368
+ s_points = st.slider("s grid points", min_value=1000, max_value=10000, value=5000, step=500)
369
+ intersection_guesses = st.slider("Intersection search points", min_value=200, max_value=2000, value=1000, step=100)
370
+ intersection_tolerance = st.select_slider(
371
+ "Intersection tolerance",
372
+ options=[1e-6, 1e-8, 1e-10, 1e-12, 1e-14, 1e-16, 1e-18, 1e-20],
373
+ value=1e-10
 
374
  )
375
+ if st.button("Compute Intersections", key="tab3_button"):
376
+ with col2:
377
+ s_range = (s_min, s_max)
378
+ fig, intersections = generate_curves_plot(z, y_3, beta_3, a, s_range, s_points, intersection_guesses, intersection_tolerance)
379
+ st.plotly_chart(fig, use_container_width=True)
380
+ if len(intersections) > 0:
381
+ st.subheader("Intersection Points")
382
+ for i, s_val in enumerate(intersections):
383
+ y_val = curve1(s_val, z, y_3)
384
+ st.write(f"Point {i+1}: s = {s_val:.6f}, y = {y_val:.6f}")
385
+ else:
386
+ st.write("No intersections found in the given range.")
387
+
388
+ # ----- Tab 4: Differential Analysis -----
389
+ with tab4:
390
+ st.header("Differential Analysis vs. β")
391
+ st.markdown("This page shows the difference between the Upper (blue) and Lower (lightblue) z*(β) curves, along with their first and second derivatives with respect to β.")
392
+ col1, col2 = st.columns([1, 2])
393
+ with col1:
394
+ z_a_diff = st.number_input("z_a", value=1.0, key="z_a_diff")
395
+ y_diff = st.number_input("y", value=1.0, key="y_diff")
396
+ z_min_diff = st.number_input("z_min", value=-10.0, key="z_min_diff")
397
+ z_max_diff = st.number_input("z_max", value=10.0, key="z_max_diff")
398
+ with st.expander("Resolution Settings"):
399
+ beta_steps_diff = st.slider("β steps", min_value=51, max_value=501, value=201, step=50)
400
+ z_steps_diff = st.slider("z grid steps", min_value=1000, max_value=100000, value=50000, step=1000)
401
+ if st.button("Compute Differentials", key="tab4_button"):
402
+ with col2:
403
+ betas_diff, lower_vals, upper_vals = sweep_beta_and_find_z_bounds(z_a_diff, y_diff, z_min_diff, z_max_diff, beta_steps_diff, z_steps_diff)
404
+ diff_curve = upper_vals - lower_vals
405
+ d1 = np.gradient(diff_curve, betas_diff)
406
+ d2 = np.gradient(d1, betas_diff)
407
 
 
408
  fig_diff = go.Figure()
409
+ fig_diff.add_trace(go.Scatter(x=betas_diff, y=diff_curve, mode="lines", name="Difference (Upper - Lower)", line=dict(color="magenta", width=2)))
410
+ fig_diff.add_trace(go.Scatter(x=betas_diff, y=d1, mode="lines", name="First Derivative", line=dict(color="brown", width=2)))
411
+ fig_diff.add_trace(go.Scatter(x=betas_diff, y=d2, mode="lines", name="Second Derivative", line=dict(color="black", width=2)))
412
+ fig_diff.update_layout(title="Differential Analysis vs. β", xaxis_title="β", yaxis_title="Value", hovermode="x unified")
 
 
 
 
 
 
 
 
 
 
 
413
  st.plotly_chart(fig_diff, use_container_width=True)