Spaces:
Paused
Paused
File size: 13,157 Bytes
bb05879 | 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 | 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()
|