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")} KBoiling 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}
{total:.4f}
g/mol
{trows}
SymbolElementCountMassSubtotal%
' 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'
Balanced Equation
{bal}

Atom Count Verification

{vrows}
ElementLeftRight
' 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'
{sym}
{name}
' 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()