profplate's picture
Create app.py
bb05879 verified
import gradio as gr
import json, re, os
DATA_PATH = os.path.join(os.path.dirname(__file__), "elements.json")
with open(DATA_PATH) as f:
ELEMENTS = json.load(f)
SYM_MAP = {e["symbol"]: e for e in ELEMENTS}
NAME_MAP = {e["name"].lower(): e for e in ELEMENTS}
CATEGORY_COLORS = {"Alkali Metal": "#FF6B6B", "Alkaline Earth": "#FFA07A", "Transition Metal": "#7EC8E3", "Post-Transition": "#98D8C8", "Metalloid": "#C3B1E1", "Noble Gas": "#FFFACD", "Halogen": "#F0E68C", "Nonmetal": "#90EE90", "Lanthanide": "#FFB6C1", "Actinide": "#DDA0DD"}
def lookup_element(query):
query = query.strip()
if not query:
return "Please enter an element name, symbol, or atomic number."
el = None
if query.isdigit():
num = int(query)
for e in ELEMENTS:
if e["number"] == num:
el = e
break
if el is None:
el = SYM_MAP.get(query) or SYM_MAP.get(query.capitalize())
if el is None:
el = NAME_MAP.get(query.lower())
if el is None:
return f"Element '{query}' not found. Try a name, symbol, or number."
cc = CATEGORY_COLORS.get(el["category"], "#DDD")
return f'''<div style="font-family:system-ui,sans-serif;max-width:680px;margin:0 auto"><div style="display:flex;align-items:center;gap:24px;margin-bottom:20px"><div style="background:{cc};border-radius:16px;width:120px;height:120px;display:flex;flex-direction:column;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(0,0,0,.15)"><span style="font-size:12px;opacity:.7">{el["number"]}</span><span style="font-size:42px;font-weight:700">{el["symbol"]}</span><span style="font-size:11px">{el["atomic_mass"]:.4f}</span></div><div><h2 style="margin:0 0 4px 0;font-size:28px">{el["name"]}</h2><span style="background:{cc};padding:3px 10px;border-radius:12px;font-size:13px">{el["category"]}</span></div></div><table style="width:100%;border-collapse:collapse;font-size:14px"><tr style="background:#f8f9fa"><td style="padding:8px 12px;font-weight:600">Group / Period</td><td style="padding:8px 12px">{el.get("group","N/A")} / {el["period"]}</td><td style="padding:8px 12px;font-weight:600">Block</td><td style="padding:8px 12px">{el["block"]}</td></tr><tr><td style="padding:8px 12px;font-weight:600">Phase</td><td style="padding:8px 12px">{el["phase"]}</td><td style="padding:8px 12px;font-weight:600">Atomic Mass</td><td style="padding:8px 12px">{el["atomic_mass"]:.4f} u</td></tr><tr style="background:#f8f9fa"><td style="padding:8px 12px;font-weight:600">Electron Config</td><td style="padding:8px 12px;font-family:monospace">{el.get("electron_config","N/A")}</td><td style="padding:8px 12px;font-weight:600">Electronegativity</td><td style="padding:8px 12px">{el.get("electronegativity","N/A")}</td></tr><tr><td style="padding:8px 12px;font-weight:600">Melting Point</td><td style="padding:8px 12px">{el.get("melting_point","N/A")} K</td><td style="padding:8px 12px;font-weight:600">Boiling Point</td><td style="padding:8px 12px">{el.get("boiling_point","N/A")} K</td></tr><tr style="background:#f8f9fa"><td style="padding:8px 12px;font-weight:600">Density</td><td style="padding:8px 12px">{el.get("density","N/A")}</td><td style="padding:8px 12px;font-weight:600">Ionization Energy</td><td style="padding:8px 12px">{el.get("ionization_energy","N/A")} kJ/mol</td></tr><tr><td style="padding:8px 12px;font-weight:600">Oxidation States</td><td style="padding:8px 12px" colspan="3">{el.get("oxidation_states","N/A")}</td></tr><tr style="background:#f8f9fa"><td style="padding:8px 12px;font-weight:600">Discovery</td><td style="padding:8px 12px" colspan="3">{el.get("discovered_by","Unknown")} ({el.get("discovery_year","?")})</td></tr></table></div>'''
def parse_formula(formula):
tokens = re.findall(r'([A-Z][a-z]?)(\\d*)', formula)
comp = {}
for sym, count in tokens:
if sym:
comp[sym] = comp.get(sym, 0) + (int(count) if count else 1)
return comp
def expand_formula(formula):
try:
return _parse_group(formula, 0, len(formula))
except Exception:
return parse_formula(formula) or None
def _parse_group(formula, start, end):
comp = {}
i = start
while i < end:
if formula[i] == '(':
depth, j = 1, i + 1
while j < end and depth > 0:
if formula[j] == '(': depth += 1
elif formula[j] == ')': depth -= 1
j += 1
inner = _parse_group(formula, i + 1, j - 1)
k, num_str = j, ""
while k < end and formula[k].isdigit(): num_str += formula[k]; k += 1
mult = int(num_str) if num_str else 1
for s, c in inner.items(): comp[s] = comp.get(s, 0) + c * mult
i = k
elif formula[i].isupper():
sym = formula[i]
i += 1
while i < end and formula[i].islower(): sym += formula[i]; i += 1
num_str = ""
while i < end and formula[i].isdigit(): num_str += formula[i]; i += 1
comp[sym] = comp.get(sym, 0) + (int(num_str) if num_str else 1)
else:
i += 1
return comp
def calc_molar_mass(formula):
formula = formula.strip()
if not formula: return "Enter a formula like H2O, NaCl, or C6H12O6."
expanded = expand_formula(formula)
if expanded is None: return f"Could not parse: {formula}"
rows, total = [], 0.0
for sym, count in expanded.items():
el = SYM_MAP.get(sym)
if el is None: return f"Unknown element: {sym}"
mass = el["atomic_mass"]
sub = mass * count
total += sub
rows.append((sym, el["name"], count, mass, sub))
trows = ''.join(f'<tr><td style="padding:6px 12px;font-weight:600">{s}</td><td style="padding:6px 12px">{n}</td><td style="padding:6px 12px;text-align:center">{c}</td><td style="padding:6px 12px;text-align:right">{m:.4f}</td><td style="padding:6px 12px;text-align:right">{su:.4f}</td><td style="padding:6px 12px;text-align:right">{su/total*100:.1f}%</td></tr>' for s,n,c,m,su in rows)
return f'<div style="font-family:system-ui,sans-serif;max-width:600px;margin:0 auto"><div style="text-align:center;margin-bottom:16px"><span style="font-size:16px;color:#666">Molar Mass of</span> <span style="font-size:24px;font-weight:700">{formula}</span></div><div style="background:linear-gradient(135deg,#667eea,#764ba2);color:white;text-align:center;padding:20px;border-radius:12px;margin-bottom:16px"><div style="font-size:36px;font-weight:700">{total:.4f}</div><div style="font-size:14px;opacity:.9">g/mol</div></div><table style="width:100%;border-collapse:collapse;font-size:14px"><tr style="background:#f0f0f0;font-weight:600"><td style="padding:6px 12px">Symbol</td><td style="padding:6px 12px">Element</td><td style="padding:6px 12px;text-align:center">Count</td><td style="padding:6px 12px;text-align:right">Mass</td><td style="padding:6px 12px;text-align:right">Subtotal</td><td style="padding:6px 12px;text-align:right">%</td></tr>{trows}</table></div>'
def balance_equation(equation):
from itertools import product as iprod
equation = equation.strip()
if not equation: return "Enter an equation like: Fe + O2 -> Fe2O3"
sides = re.split(r'\s*(?:->|=)\s*', equation)
if len(sides) != 2: return "Use -> or = to separate reactants and products."
rstr = [s.strip() for s in sides[0].split('+')]
pstr = [s.strip() for s in sides[1].split('+')]
compounds = rstr + pstr
nr = len(rstr)
comps, elems = [], set()
for c in compounds:
p = expand_formula(c)
if p is None: return f"Could not parse: {c}"
comps.append(p)
elems.update(p.keys())
elems = sorted(elems)
def check(coeffs):
for e in elems:
if sum(coeffs[i]*comps[i].get(e,0) for i in range(nr)) != sum(coeffs[nr+j]*comps[nr+j].get(e,0) for j in range(len(pstr))):
return False
return True
found = None
for coeffs in iprod(range(1, 21), repeat=len(compounds)):
if check(coeffs):
found = coeffs
break
if found is None: return "Could not balance this equation."
lp = [f"{found[i]}{c}" if found[i]>1 else c for i,c in enumerate(rstr)]
rp = [f"{found[nr+j]}{c}" if found[nr+j]>1 else c for j,c in enumerate(pstr)]
bal = " + ".join(lp) + " -> " + " + ".join(rp)
vrows = ''.join(f'<tr><td style="padding:4px 12px;font-weight:600">{e}</td><td style="padding:4px 12px;text-align:center">{sum(found[i]*comps[i].get(e,0) for i in range(nr))}</td><td style="padding:4px 12px;text-align:center">{sum(found[nr+j]*comps[nr+j].get(e,0) for j in range(len(pstr)))}</td></tr>' for e in elems)
return f'<div style="font-family:system-ui,sans-serif;max-width:600px;margin:0 auto"><div style="background:linear-gradient(135deg,#11998e,#38ef7d);color:white;text-align:center;padding:20px;border-radius:12px;margin-bottom:16px"><div style="font-size:13px;opacity:.85;margin-bottom:4px">Balanced Equation</div><div style="font-size:22px;font-weight:700;font-family:monospace">{bal}</div></div><h4 style="margin:12px 0 8px">Atom Count Verification</h4><table style="width:100%;border-collapse:collapse;font-size:14px"><tr style="background:#f0f0f0;font-weight:600"><td style="padding:4px 12px">Element</td><td style="padding:4px 12px;text-align:center">Left</td><td style="padding:4px 12px;text-align:center">Right</td></tr>{vrows}</table></div>'
IONS = {
"Monatomic Cations": [("H+","Hydrogen"),("Li+","Lithium"),("Na+","Sodium"),("K+","Potassium"),("Ag+","Silver"),("Mg2+","Magnesium"),("Ca2+","Calcium"),("Ba2+","Barium"),("Zn2+","Zinc"),("Al3+","Aluminum")],
"Monatomic Anions": [("F-","Fluoride"),("Cl-","Chloride"),("Br-","Bromide"),("I-","Iodide"),("O2-","Oxide"),("S2-","Sulfide"),("N3-","Nitride")],
"Polyatomic Ions": [("CO3 2-","Carbonate"),("NO3-","Nitrate"),("SO4 2-","Sulfate"),("PO4 3-","Phosphate"),("ClO3-","Chlorate"),("ClO4-","Perchlorate")],
"Transition Metal Ions": [("Cu+","Copper(I)"),("Cu2+","Copper(II)"),("Fe2+","Iron(II)"),("Fe3+","Iron(III)"),("Pb2+","Lead(II)"),("MnO4-","Permanganate")],
"Special Ions": [("NH4+","Ammonium"),("OH-","Hydroxide"),("HCO3-","Bicarbonate"),("CH3COO-","Acetate"),("CN-","Cyanide")],
}
def build_ions_html():
colors = ["#FF6B6B","#FFA07A","#7EC8E3","#98D8C8","#C3B1E1"]
html = '<div style="font-family:system-ui,sans-serif;max-width:700px;margin:0 auto">'
for idx, (cat, ions) in enumerate(IONS.items()):
c = colors[idx % len(colors)]
html += f'<h3 style="margin:16px 0 8px;color:#333">{cat}</h3><div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px">'
for sym, name in ions:
html += f'<div style="background:{c}22;border:2px solid {c};border-radius:10px;padding:8px 14px;text-align:center;min-width:80px"><div style="font-size:18px;font-weight:700">{sym}</div><div style="font-size:11px;color:#555">{name}</div></div>'
html += '</div>'
html += '</div>'
return html
with gr.Blocks(title="Chemistry Toolkit", theme=gr.themes.Soft()) as demo:
gr.Markdown("# Chemistry Toolkit\n*Interactive chemistry reference inspired by [Zperiod](https://zperiod.app)*")
with gr.Tab("Element Lookup"):
gr.Markdown("Search by **name**, **symbol**, or **atomic number**.")
with gr.Row():
elem_input = gr.Textbox(label="Search", placeholder="e.g. Gold, Au, or 79", scale=3)
elem_btn = gr.Button("Look Up", variant="primary", scale=1)
elem_output = gr.HTML()
elem_btn.click(lookup_element, inputs=elem_input, outputs=elem_output)
elem_input.submit(lookup_element, inputs=elem_input, outputs=elem_output)
gr.Examples(["Oxygen", "Fe", "79", "Carbon", "Cl"], inputs=elem_input)
with gr.Tab("Molar Mass"):
gr.Markdown("Enter a chemical formula to calculate its **molar mass**.")
with gr.Row():
mm_input = gr.Textbox(label="Chemical Formula", placeholder="e.g. H2O, CaCO3", scale=3)
mm_btn = gr.Button("Calculate", variant="primary", scale=1)
mm_output = gr.HTML()
mm_btn.click(calc_molar_mass, inputs=mm_input, outputs=mm_output)
mm_input.submit(calc_molar_mass, inputs=mm_input, outputs=mm_output)
gr.Examples(["H2O", "NaCl", "C6H12O6", "Ca(OH)2", "H2SO4"], inputs=mm_input)
with gr.Tab("Equation Balancer"):
gr.Markdown("Enter an unbalanced equation using `->` or `=` to separate sides.")
with gr.Row():
eq_input = gr.Textbox(label="Unbalanced Equation", placeholder="e.g. Fe + O2 -> Fe2O3", scale=3)
eq_btn = gr.Button("Balance", variant="primary", scale=1)
eq_output = gr.HTML()
eq_btn.click(balance_equation, inputs=eq_input, outputs=eq_output)
eq_input.submit(balance_equation, inputs=eq_input, outputs=eq_output)
gr.Examples(["Fe + O2 -> Fe2O3", "H2 + O2 -> H2O", "CH4 + O2 -> CO2 + H2O", "Na + Cl2 -> NaCl"], inputs=eq_input)
with gr.Tab("Common Ions"):
gr.Markdown("Quick reference for common ions.")
gr.HTML(build_ions_html())
if __name__ == "__main__":
demo.launch()