Spaces:
Running
Running
Update app.py
Browse files
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 |
-
|
10 |
-
|
11 |
-
|
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
|
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
|
46 |
Delta_expr = (
|
47 |
-
|
48 |
-
|
49 |
)
|
50 |
|
51 |
-
#
|
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 |
-
|
57 |
-
|
58 |
-
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
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 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
|
104 |
-
|
105 |
-
|
106 |
-
|
|
|
|
|
|
|
107 |
|
108 |
@st.cache_data
|
109 |
def compute_low_y_curve(betas, z_a, y):
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
|
|
|
|
|
|
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 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
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 |
-
|
145 |
-
|
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 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
188 |
-
|
189 |
-
|
190 |
-
|
191 |
-
|
192 |
-
|
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 |
-
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
|
|
|
|
321 |
|
322 |
def generate_root_plots(beta, y, z_a, z_min, z_max, n_points):
|
323 |
-
|
324 |
-
|
325 |
-
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
341 |
-
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
346 |
-
|
347 |
-
|
348 |
-
|
349 |
-
|
350 |
-
|
351 |
-
|
352 |
-
|
353 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
360 |
-
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
365 |
-
|
366 |
-
|
367 |
-
|
368 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
379 |
|
|
|
380 |
st.title("Cubic Root Analysis")
|
381 |
|
382 |
-
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
400 |
with col2:
|
401 |
-
|
402 |
-
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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("
|
432 |
-
|
433 |
col1, col2 = st.columns([1, 2])
|
434 |
-
|
435 |
with col1:
|
436 |
-
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
|
|
|
|
441 |
with st.expander("Resolution Settings"):
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
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 |
-
|
455 |
-
|
456 |
-
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|