saad-sust commited on
Commit
8c663bc
Β·
verified Β·
1 Parent(s): 0010b7e

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +160 -1
app.py CHANGED
@@ -20,6 +20,10 @@ from sympy.parsing.sympy_parser import (
20
  implicit_multiplication_application
21
  )
22
  import re # MUST be last β€” 'from sympy import *' would overwrite re otherwise
 
 
 
 
23
 
24
  # ── Page config ──────────────────────────────────────────────────────
25
  st.set_page_config(
@@ -1475,6 +1479,158 @@ def run_sympy(problem: str) -> dict:
1475
 
1476
  return {"type": "general", "result": None, "latex": ""}
1477
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1478
  # ════════════════════════════════════════════════════════════════════
1479
  # GROQ API β€” free, fast, Llama 3.3 70B
1480
  # ════════════════════════════════════════════════════════════════════
@@ -1667,7 +1823,7 @@ def ask_ai(problem: str, sympy_info: dict, history: list) -> str:
1667
  "C. State exact trig values directly: $\\sin(\\pi)=0$, $\\cos(\\pi)=-1$ β€” never recompute.\n"
1668
  "D. For Lagrange/Newton interpolation: DO NOT re-derive the polynomial.\n"
1669
  " Show basis polynomials then state final polynomial from the verified result.\n"
1670
- "E. For Euler/RK4: show k-values at each step then give y_{n+1}.\n\n"
1671
 
1672
  "=== THEORY OF NUMBERS RULES ===\n"
1673
  "A. For congruences $ax \\equiv b \\pmod{n}$: always show full Euclidean algorithm steps.\n"
@@ -2015,6 +2171,9 @@ if problem and problem != st.session_state.last_submitted:
2015
  # Step 2: Stream AI response word by word
2016
  answer = ask_ai_streaming(problem, sympy_result, st.session_state.messages)
2017
 
 
 
 
2018
  # Save to history
2019
  st.session_state.messages.append({"role": "user", "content": problem})
2020
  st.session_state.messages.append({"role": "assistant", "content": answer})
 
20
  implicit_multiplication_application
21
  )
22
  import re # MUST be last β€” 'from sympy import *' would overwrite re otherwise
23
+ import matplotlib
24
+ matplotlib.use('Agg') # non-interactive backend β€” required on HuggingFace Spaces
25
+ import matplotlib.pyplot as plt
26
+ import numpy as np
27
 
28
  # ── Page config ──────────────────────────────────────────────────────
29
  st.set_page_config(
 
1479
 
1480
  return {"type": "general", "result": None, "latex": ""}
1481
 
1482
+ # ════════════════════════════════════════════════════════════════════
1483
+ # GRAPH PLOTTING β€” only when user asks to plot/graph/draw/visualize
1484
+ # ════════════════════════════════════════════════════════════════════
1485
+ def plot_graph(problem: str, sympy_info: dict):
1486
+ """
1487
+ Plot graph only when user explicitly requests it.
1488
+ Keywords: plot, graph, draw, sketch, visualize, show graph.
1489
+ Supports: single function, derivative overlay, ODE solution, equation roots.
1490
+ Safe fallback β€” never crashes the app.
1491
+ """
1492
+ p = problem.lower().strip()
1493
+
1494
+ # ── Only trigger on explicit plot keywords ──────────────────────
1495
+ if not any(k in p for k in ["plot", "graph", "draw",
1496
+ "sketch", "visualize", "show graph"]):
1497
+ return # user didn't ask for graph β€” do nothing
1498
+
1499
+ try:
1500
+ x_sym = sp.Symbol('x')
1501
+ tfms = standard_transformations + (implicit_multiplication_application,)
1502
+ ld = {
1503
+ "x": x_sym, "e": sp.E, "E": sp.E,
1504
+ "pi": sp.pi, "PI": sp.pi,
1505
+ "sin": sp.sin, "cos": sp.cos, "tan": sp.tan,
1506
+ "exp": sp.exp, "log": sp.log, "ln": sp.log,
1507
+ "sqrt": sp.sqrt
1508
+ }
1509
+
1510
+ def clean_expr(s):
1511
+ s = re.sub(r"\s+", "", s)
1512
+ s = re.sub(r"\^", "**", s)
1513
+ return s
1514
+
1515
+ # ── Extract expression from problem ─────────────────────────
1516
+ expr_sym = None
1517
+ label = ""
1518
+
1519
+ # Try to extract from sympy_info first (already parsed)
1520
+ if sympy_info.get("type") in ["Derivative", "Integral", "Equation",
1521
+ "ODE", "Limit", "Curvature"]:
1522
+ # Extract f(x) from problem text
1523
+ for kw in ["plot", "graph", "draw", "sketch", "visualize",
1524
+ "of", "for", "function"]:
1525
+ parts = p.split(kw, 1)
1526
+ if len(parts) > 1:
1527
+ raw = parts[1].strip()
1528
+ raw = re.sub(r"\s*(dx|with\s*respect.*|at\s+x.*)$", "", raw).strip()
1529
+ raw = clean_expr(raw)
1530
+ if raw:
1531
+ try:
1532
+ expr_sym = parse_expr(raw, transformations=tfms,
1533
+ local_dict=ld)
1534
+ label = raw
1535
+ break
1536
+ except Exception:
1537
+ continue
1538
+
1539
+ # Fallback β€” extract any f(x) = ... or just expression
1540
+ if expr_sym is None:
1541
+ m = re.search(r"(?:f\s*\(x\)\s*=\s*|y\s*=\s*|of\s+|plot\s+|graph\s+|draw\s+)"
1542
+ r"([^\s,]+(?:\s*[\+\-\*/\^]\s*[^\s,]+)*)", p)
1543
+ if m:
1544
+ raw = clean_expr(m.group(1).strip())
1545
+ try:
1546
+ expr_sym = parse_expr(raw, transformations=tfms, local_dict=ld)
1547
+ label = raw
1548
+ except Exception:
1549
+ pass
1550
+
1551
+ if expr_sym is None:
1552
+ st.info("πŸ“Š Could not extract a plottable expression. "
1553
+ "Try: *plot f(x) = x^2 + 3x - 4*")
1554
+ return
1555
+
1556
+ # ── Determine x range ────────────────────────────────────────
1557
+ x_range_m = re.search(
1558
+ r"(?:from|for|on|between)\s*([-\d\.]+)\s*(?:to|and)\s*([-\d\.]+)", p)
1559
+ if x_range_m:
1560
+ x_min = float(x_range_m.group(1))
1561
+ x_max = float(x_range_m.group(2))
1562
+ else:
1563
+ x_min, x_max = -10, 10 # default range
1564
+
1565
+ # ── Lambdify for fast numpy evaluation ───────────────────────
1566
+ f_num = sp.lambdify(x_sym, expr_sym, modules=["numpy"])
1567
+ df_sym = sp.diff(expr_sym, x_sym)
1568
+ df_num = sp.lambdify(x_sym, df_sym, modules=["numpy"])
1569
+
1570
+ x_vals = np.linspace(x_min, x_max, 800)
1571
+
1572
+ # Safe evaluation β€” replace infinities/errors with NaN
1573
+ with np.errstate(all='ignore'):
1574
+ y_vals = np.array(f_num(x_vals), dtype=float)
1575
+ dy_vals = np.array(df_num(x_vals), dtype=float)
1576
+
1577
+ y_vals[~np.isfinite(y_vals)] = np.nan
1578
+ dy_vals[~np.isfinite(dy_vals)] = np.nan
1579
+
1580
+ # ── Build plot ───────────────────────────────────────────────
1581
+ fig, ax = plt.subplots(figsize=(8, 4))
1582
+ fig.patch.set_facecolor('#0f0f0f')
1583
+ ax.set_facecolor('#1a1a1a')
1584
+
1585
+ # Plot f(x)
1586
+ ax.plot(x_vals, y_vals, color='#3b82f6', linewidth=2,
1587
+ label=f'$f(x) = {sp.latex(expr_sym)}$')
1588
+
1589
+ # If derivative type β€” also plot f'(x)
1590
+ if sympy_info.get("type") == "Derivative":
1591
+ ax.plot(x_vals, dy_vals, color='#f59e0b', linewidth=1.8,
1592
+ linestyle='--', label=f"$f'(x) = {sp.latex(df_sym)}$")
1593
+
1594
+ # x and y axes
1595
+ ax.axhline(0, color='#444', linewidth=0.8)
1596
+ ax.axvline(0, color='#444', linewidth=0.8)
1597
+
1598
+ # Grid
1599
+ ax.grid(True, color='#2a2a2a', linewidth=0.6, linestyle='--')
1600
+
1601
+ # Labels and title
1602
+ ax.set_xlabel('x', color='#ececec', fontsize=11)
1603
+ ax.set_ylabel('y', color='#ececec', fontsize=11)
1604
+ ax.set_title(f'Graph of $f(x) = {sp.latex(expr_sym)}$',
1605
+ color='#ffffff', fontsize=12, pad=10)
1606
+
1607
+ # Tick colors
1608
+ ax.tick_params(colors='#888', labelsize=9)
1609
+ for spine in ax.spines.values():
1610
+ spine.set_edgecolor('#2a2a2a')
1611
+
1612
+ # Legend
1613
+ ax.legend(facecolor='#1a1a1a', edgecolor='#2a2a2a',
1614
+ labelcolor='#ececec', fontsize=9)
1615
+
1616
+ # Smart y-axis limits β€” ignore outliers
1617
+ valid_y = y_vals[np.isfinite(y_vals)]
1618
+ if len(valid_y) > 0:
1619
+ y_med = np.median(valid_y)
1620
+ y_std = np.std(valid_y)
1621
+ y_lo = max(valid_y.min(), y_med - 5*y_std)
1622
+ y_hi = min(valid_y.max(), y_med + 5*y_std)
1623
+ padding = (y_hi - y_lo) * 0.1 if y_hi != y_lo else 1
1624
+ ax.set_ylim(y_lo - padding, y_hi + padding)
1625
+
1626
+ plt.tight_layout()
1627
+ st.pyplot(fig)
1628
+ plt.close(fig) # free memory
1629
+
1630
+ except Exception:
1631
+ pass # silent fallback β€” never crash the app
1632
+
1633
+
1634
  # ════════════════════════════════════════════════════════════════════
1635
  # GROQ API β€” free, fast, Llama 3.3 70B
1636
  # ════════════════════════════════════════════════════════════════════
 
1823
  "C. State exact trig values directly: $\\sin(\\pi)=0$, $\\cos(\\pi)=-1$ β€” never recompute.\n"
1824
  "D. For Lagrange/Newton interpolation: DO NOT re-derive the polynomial.\n"
1825
  " Show basis polynomials then state final polynomial from the verified result.\n"
1826
+ "E. For Euler/RK4: show k-values at each step then give $y_{n+1}$.\n\n"
1827
 
1828
  "=== THEORY OF NUMBERS RULES ===\n"
1829
  "A. For congruences $ax \\equiv b \\pmod{n}$: always show full Euclidean algorithm steps.\n"
 
2171
  # Step 2: Stream AI response word by word
2172
  answer = ask_ai_streaming(problem, sympy_result, st.session_state.messages)
2173
 
2174
+ # Step 3: Plot graph if user asked for it
2175
+ plot_graph(problem, sympy_result)
2176
+
2177
  # Save to history
2178
  st.session_state.messages.append({"role": "user", "content": problem})
2179
  st.session_state.messages.append({"role": "assistant", "content": answer})