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'''
{el["number"]}{el["symbol"]}{el["atomic_mass"]:.4f}
{el["name"]}
{el["category"]}| Group / Period | {el.get("group","N/A")} / {el["period"]} | Block | {el["block"]} |
| Phase | {el["phase"]} | Atomic Mass | {el["atomic_mass"]:.4f} u |
| Electron Config | {el.get("electron_config","N/A")} | Electronegativity | {el.get("electronegativity","N/A")} |
| Melting Point | {el.get("melting_point","N/A")} K | Boiling Point | {el.get("boiling_point","N/A")} K |
| Density | {el.get("density","N/A")} | Ionization Energy | {el.get("ionization_energy","N/A")} kJ/mol |
| Oxidation States | {el.get("oxidation_states","N/A")} |
| Discovery | {el.get("discovered_by","Unknown")} ({el.get("discovery_year","?")}) |
'''
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'| {s} | {n} | {c} | {m:.4f} | {su:.4f} | {su/total*100:.1f}% |
' for s,n,c,m,su in rows)
return f'Molar Mass of {formula}
| Symbol | Element | Count | Mass | Subtotal | % |
{trows}
'
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'| {e} | {sum(found[i]*comps[i].get(e,0) for i in range(nr))} | {sum(found[nr+j]*comps[nr+j].get(e,0) for j in range(len(pstr)))} |
' for e in elems)
return f''
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 = ''
for idx, (cat, ions) in enumerate(IONS.items()):
c = colors[idx % len(colors)]
html += f'
{cat}
'
for sym, name in ions:
html += f'
'
html += '
'
html += '
'
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()