File size: 9,474 Bytes
ee56013
 
5c5cb4d
2f94119
 
5c5cb4d
 
 
 
ee56013
 
2f94119
3e7998b
2f94119
ee56013
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5c5cb4d
ee56013
 
0531f23
ee56013
5c5cb4d
 
ee56013
 
 
 
 
 
 
 
 
 
 
 
5c5cb4d
 
ee56013
 
 
 
5c5cb4d
ee56013
 
 
5c5cb4d
 
 
 
 
 
ee56013
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7998b
 
 
ee56013
 
 
 
 
 
 
 
e87a45f
ee56013
 
 
e87a45f
ee56013
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7998b
 
2f94119
ee56013
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3e7998b
5c5cb4d
ee56013
 
 
 
 
 
 
 
5c5cb4d
ee56013
 
f09d15a
2f94119
 
ee56013
e87a45f
ee56013
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# app.py
# LanguageBridge — Math Fast Agent (SymPy)  (2025-11-04 修正版)
import re
import gradio as gr
import sympy as sp
from sympy.parsing.sympy_parser import (
    parse_expr, standard_transformations,
    implicit_multiplication_application, convert_xor
)
from sympy import Interval, Union, S, And, Or
from sympy.solvers.inequalities import solve_univariate_inequality

TITLE = "LanguageBridge — Math Fast Agent (SymPy)"

# ----------------------------
# 1) 安全正規化:全形→半形、特殊符號、常見中文習慣
# ----------------------------
_ASCII_TABLE = str.maketrans({
    # 括號與標點
    "(": "(", ")": ")", ",": ",", "、": ",", ";": ";", ":": ":",
    ".": ".", "。": ".", ",": ",", "!": "!", "?": "?",
    # 運算與關係
    "+": "+", "-": "-", "~": "~", "=": "=", "*": "*", "/": "/",
    "<": "<", ">": ">", "≤": "<=", "≥": ">=",
    "∧": "&", "∨": "|",
    # 乘號、根號
    "×": "*", "√": "sqrt",
    # 希臘字母/常數
    "π": "pi", "Π": "pi",
    "θ": "theta", "Θ": "theta",
})

# 允許保留的英文單字(其餘中文字會被視為非數學描述)
_ALLOWED_WORDS = {
    "sin","cos","tan","cot","sec","csc",
    "asin","acos","atan","sinh","cosh","tanh","log","ln","exp","sqrt","abs",
    "diff","integrate","factor","simplify","expand"
}

def normalize_ascii(s: str) -> str:
    s = (s or "").replace("\u00A0", " ").replace("\u3000", " ")  # 不換行空白/全形空白
    s = s.translate(_ASCII_TABLE)
    s = s.replace("^", "**")  # 2^3 -> 2**3
    # 把中文逗號/頓號已轉成 ,;允許拿來分割「等式 + 條件」
    return s

# d/dx(...) → diff(..., x)
_DDX_RE = re.compile(r"d\s*/\s*d([A-Za-z_]\w*)\s*\(\s*(.+)\s*\)\s*$")

def desugar_ddx(s: str) -> str:
    m = _DDX_RE.match(s.strip())
    if not m:
        return s
    var = m.group(1)
    inside = m.group(2)
    return f"diff(({inside}), {var})"

# 基本轉換:隱式乘法、^ 號
TRANS = standard_transformations + (implicit_multiplication_application, convert_xor)

def to_sympy_expr(raw: str):
    s = normalize_ascii(raw)
    s = desugar_ddx(s)
    return parse_expr(s, transformations=TRANS)

def to_sympy_eq(raw: str):
    s = normalize_ascii(raw)
    # 只取第一個 "=" 左右,避免 a=b=c 之類字串誤用
    if "=" not in s:
        raise ValueError("等式缺少 '='")
    L, R = s.split("=", 1)
    return sp.Eq(parse_expr(L, transformations=TRANS),
                 parse_expr(R, transformations=TRANS))

# ----------------------------
# 2) 解析「逗號分隔的不等式條件」→ 定義域 (Interval/Set)
#    例: "0 <= theta < 2*pi" 或 "x>=0" 或 "x in [0,1]"
# ----------------------------
def parse_domain_chunks(chunks, var):
    """
    將類似 "0 <= theta < 2*pi"、"theta<pi/3"、"x in [0,1]" 轉成 Set
    以交集的方式逐一收斂 domain。
    """
    domain = S.Reals
    for c in chunks:
        c = normalize_ascii(c.strip())
        if not c:
            continue

        # 支援 x in [a,b] / (a,b) / [a,b) / (a,b]
        m = re.match(rf"^{var}\s*in\s*([\[\(])\s*(.+)\s*,\s*(.+)\s*([\]\)])$", c)
        if m:
            left_open = m.group(1) == "("
            right_open = m.group(4) == ")"
            a = parse_expr(m.group(2), transformations=TRANS)
            b = parse_expr(m.group(3), transformations=TRANS)
            domain = domain.intersect(Interval(a, b, left_open, right_open))
            continue

        # 一般不等式/連鎖不等式:交給 solve_univariate_inequality
        try:
            ineq = parse_expr(c, transformations=TRANS, evaluate=False)
            set_part = solve_univariate_inequality(ineq, var, relational=False)
            domain = domain.intersect(set_part)
        except Exception:
            # 不可解析就保留原 domain,不報錯
            pass
    return domain

# ----------------------------
# 3) 輔助:檢查是否混雜太多非數學描述
# ----------------------------
_NON_MATH_RE = re.compile(r"[^\dA-Za-z_+\-*/^().,;=<>!&| \[\]{}:]+")

def looks_like_math(s: str) -> bool:
    # 去除已允許的單字與函數名
    tmp = s
    for w in _ALLOWED_WORDS:
        tmp = re.sub(rf"\b{w}\b", "", tmp)
    # 若還剩大量非數學符號(尤其中文敘述),視為不適合直接解析
    return not _NON_MATH_RE.search(tmp)

# ----------------------------
# 4) 主邏輯
# ----------------------------
def solve_math(q: str):
    q = (q or "").strip()
    if not q:
        return (
            "請輸入算式(可多行或用分號 ; 分隔)。\n"
            "例:\n"
            "  1) 2x + 5 = 11\n"
            "  2) sin(theta) = sqrt(3)/2 , 0 <= theta < 2*pi\n"
            "  3) factor(x^2 - 9x + 18)\n"
            "  4) d/dx ((x^2+1)*(x-3))\n"
        )

    # 允許多行 or 分號
    segs = [s for line in q.split("\n") for s in line.split(";")]
    segs = [s.strip() for s in segs if s.strip()]

    # 只要有 '=' 視為求解方程(可聯立)
    if any("=" in s for s in segs):
        # 先把「每行的 equation 與 同行後面的不等式/條件」分開
        equations = []
        all_symbols = set()
        per_eq_domains = []

        for line in segs:
            # 用逗號分出「主等式」與「條件」
            parts = [p.strip() for p in line.split(",") if p.strip()]
            if not parts:
                continue
            if "=" in parts[0]:
                eq = to_sympy_eq(parts[0])
                equations.append(eq)
                all_symbols |= (eq.lhs.free_symbols | eq.rhs.free_symbols)
                # 解析該行其餘不等式作為此等式變數的 domain
                # 僅對「一元」等式給 domain(多元聯立先忽略 domain)
                per_eq_domains.append(parts[1:] if len(parts) > 1 else [])
            else:
                # 這行不是等式,可能是額外條件,先忽略(或日後擴充)
                pass

        if not equations:
            return "解析失敗:未偵測到等式。"

        # 多元聯立 → 用 solve(無 domain)
        # 一元等式(只有一個主要變數)→ 用 solveset + domain
        # 嘗試偵測「主變數」
        main_syms = set()
        for eq in equations:
            main_syms |= (eq.lhs.free_symbols | eq.rhs.free_symbols)
        if len(main_syms) == 1:
            var = next(iter(main_syms))
            # 匯總所有行的 domain 條件(若有)
            domain_chunks = []
            for ch in per_eq_domains:
                domain_chunks.extend(ch)
            domain = parse_domain_chunks(domain_chunks, str(var))
            solset = sp.solveset(sp.Eq(equations[0].lhs - equations[0].rhs, 0), var, domain=domain)
            if solset is sp.EmptySet:
                return "無解(考慮定義域後)。"
            return f"解:{sp.simplify(solset)}"
        else:
            # 多元聯立:舊路徑(不支援 domain)
            try:
                sol = sp.solve(equations, list(main_syms), dict=True)
                if not sol:
                    return "無解或需要更多條件。"
                lines = []
                for i, d in enumerate(sol, 1):
                    items = ", ".join([f"{k} = {sp.simplify(v)}" for k, v in d.items()])
                    lines.append(f"解 {i}: {items}")
                return "\n".join(lines)
            except Exception as e:
                return f"解析失敗:{e}"

    # 否則當作一般表達式:簡化 / 因式 / 微分 / 積分
    expr_text = normalize_ascii(q)
    if not looks_like_math(expr_text):
        return "偵測到大量非數學描述,請改以純算式輸入(可用逗號加入條件)。\n例:sin(theta)=sqrt(3)/2 , 0<=theta<2*pi"

    try:
        expr = to_sympy_expr(expr_text)
    except Exception as e:
        return f"解析失敗:{e}"

    out_lines = []
    try:
        out_lines.append(f"簡化:{sp.simplify(expr)}")
    except Exception:
        out_lines.append(f"簡化:{expr}")

    try:
        fact = sp.factor(expr)
        if fact != expr:
            out_lines.append(f"因式分解:{fact}")
    except Exception:
        pass

    try:
        x = next(iter(expr.free_symbols)) if expr.free_symbols else sp.symbols("x")
        out_lines.append(f"對 {x} 微分:{sp.diff(expr, x)}")
        out_lines.append(f"對 {x} 積分:{sp.integrate(expr, x)}")
    except Exception:
        pass

    return "\n".join(out_lines) if out_lines else f"結果:{expr}"

# ----------------------------
# 5) UI
# ----------------------------
with gr.Blocks(title=TITLE) as demo:
    gr.Markdown(
        "## " + TITLE + "\n"
        "貼上算式(可多行 / 分號 `;` 分隔)。支援隱式乘法(例:`3x`, `2(x+1)`, `(x+1)(x-1)`, `2sqrt(x)`)與 `^`。\n\n"
        "**範例**:\n"
        "- `2x + 5 = 11`\n"
        "- `sin(theta) = sqrt(3)/2 , 0 <= theta < 2*pi`\n"
        "- `factor(x^2 - 9x + 18)`\n"
        "- `d/dx ((x^2+1)*(x-3))`\n"
        "※ 若有定義域,請以逗號分隔放在等式後面;可用 `in [a,b]`、`0<=x<1` 等寫法。\n"
    )
    q = gr.Textbox(lines=6, label="題目 / 算式(可含聯立方程與條件)")
    out = gr.Textbox(lines=14, label="輸出")
    gr.Button("送出 🚀").click(fn=solve_math, inputs=q, outputs=out)

if __name__ == "__main__":
    # Space 內會由 runner 啟動;本地測試可開啟
    demo.launch()