Spaces:
Build error
Build error
import gradio as gr | |
import pandas as pd | |
import numpy as np | |
import matplotlib.pyplot as plt | |
from typing import Dict, List, Optional | |
from periodictable import elements | |
# ---------- helpers ---------- | |
def to_float(x): | |
if x is None: | |
return np.nan | |
v = getattr(x, "nominal_value", x) # handles uncertainties.UFloat | |
try: | |
return float(v) | |
except Exception: | |
return np.nan | |
NUMERIC_PROPS = [ | |
("mass", "Atomic mass (u)"), | |
("density", "Density (g/cm^3)"), | |
("electronegativity", "Pauling electronegativity"), | |
("boiling_point", "Boiling point (K)"), | |
("melting_point", "Melting point (K)"), | |
("vdw_radius", "van der Waals radius (pm)"), | |
("covalent_radius", "Covalent radius (pm)"), | |
] | |
CURATED_FACTS: Dict[str, List[str]] = { | |
"H": ["Lightest element; ~74% of visible matter is H in stars."], | |
"He": ["Inert and super light; cryogenics & balloons."], | |
"Li": ["Lithium-ion batteries power phones & EVs."], | |
"C": ["Diamond vs graphite = same element, different structure."], | |
"N": ["~78% of Earth's atmosphere is N₂."], | |
"O": ["~21% of air; essential for respiration."], | |
"Na": ["Reacts violently with water."], | |
"Mg": ["Bright white flame in flares."], | |
"Si": ["Semiconductor backbone."], | |
"Cl": ["Disinfectant; elemental Cl₂ is toxic."], | |
"Fe": ["Steel core; oxygen transport in blood (heme)."], | |
"Cu": ["Great conductor; forms green patina."], | |
"Ag": ["Highest electrical conductivity."], | |
"Au": ["Very unreactive; great for electronics/jewelry."], | |
"Hg": ["Liquid metal at room temp; toxic."], | |
"Pb": ["Dense, malleable; toxic—phase-out in fuels/paints."], | |
"U": ["Reactor fuel (U-235)."], | |
"Pu": ["Man-made in quantity; nuclear uses."], | |
"F": ["Most electronegative; extremely reactive."], | |
"Ne": ["Classic red-orange neon glow."], | |
"Xe": ["Used in bright flashes/HID lamps."], | |
} | |
GROUP_FACTS = { | |
"alkali": "Alkali metal: very reactive; forms +1 cations; reacts with water.", | |
"alkaline-earth": "Alkaline earth metal: reactive; forms +2 cations.", | |
"transition": "Transition metal: catalysts, colorful compounds, multiple oxidation states.", | |
"post-transition": "Post-transition metal: softer, lower melting than transition metals.", | |
"metalloid": "Metalloid: between metals and nonmetals; often semiconductors.", | |
"nonmetal": "Nonmetal: forms covalent compounds; huge biological roles.", | |
"halogen": "Halogen: very reactive nonmetals; −1 state; forms salts.", | |
"noble-gas": "Noble gas: inert, monatomic gases.", | |
"lanthanide": "Lanthanide: rare earths; magnets, lasers, phosphors.", | |
"actinide": "Actinide: radioactive; nuclear materials.", | |
} | |
def classify_category(el) -> str: | |
try: | |
if el.block == "s" and el.group == 1 and el.number != 1: | |
return "alkali" | |
if el.block == "s" and el.group == 2: | |
return "alkaline-earth" | |
if el.block == "d": | |
return "transition" | |
if el.block == "p" and el.group == 17: | |
return "halogen" | |
if el.block == "p" and el.group == 18: | |
return "noble-gas" | |
if el.block == "f" and 57 <= el.number <= 71: | |
return "lanthanide" | |
if el.block == "f" and 89 <= el.number <= 103: | |
return "actinide" | |
if el.block == "p" and not el.metallic: | |
return "nonmetal" | |
if el.block == "p" and el.metallic: | |
return "post-transition" | |
except Exception: | |
pass | |
return "post-transition" if getattr(el, "metallic", False) else "nonmetal" | |
def build_elements_df() -> pd.DataFrame: | |
rows = [] | |
for Z in range(1, 119): | |
el = elements[Z] | |
if el is None: | |
continue | |
rows.append({ | |
"Z": el.number, | |
"symbol": el.symbol, | |
"name": el.name.title(), | |
"period": getattr(el, "period", None), | |
"group": getattr(el, "group", None), | |
"block": getattr(el, "block", None), | |
"mass": to_float(getattr(el, "mass", None)), | |
"density": to_float(getattr(el, "density", None)), | |
"electronegativity": to_float(getattr(el, "electronegativity", None)), | |
"boiling_point": to_float(getattr(el, "boiling_point", None)), | |
"melting_point": to_float(getattr(el, "melting_point", None)), | |
"vdw_radius": to_float(getattr(el, "vdw_radius", None)), | |
"covalent_radius": to_float(getattr(el, "covalent_radius", None)), | |
"category": classify_category(el), | |
"is_radioactive": bool(getattr(el, "radioactive", False)), | |
}) | |
return pd.DataFrame(rows).sort_values("Z").reset_index(drop=True) | |
DF = build_elements_df() | |
# ---------- hardcoded main-grid layout (periods 1–7, groups 1–18) ---------- | |
# None = empty cell; numbers = atomic numbers | |
GRID = [ | |
# P1 | |
[1, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, 2], | |
# P2 | |
[3, 4, None, None, None, None, None, None, None, None, None, None, 5, 6, 7, 8, 9, 10], | |
# P3 | |
[11, 12, None, None, None, None, None, None, None, None, None, None, 13, 14, 15, 16, 17, 18], | |
# P4 | |
[19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36], | |
# P5 | |
[37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54], | |
# P6 (La shown at group 3) | |
[55, 56, 57, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86], | |
# P7 (Ac shown at group 3) | |
[87, 88, 89, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118], | |
] | |
# f-block lists we display separately (omit La & Ac because they’re in the main grid) | |
LAN = list(range(58, 72)) # Ce..Lu | |
ACT = list(range(90, 104)) # Th..Lr | |
# ---------- plotting ---------- | |
def plot_trend(trend_df: pd.DataFrame, prop_key: str, Z: int, symbol: str): | |
fig, ax = plt.subplots() | |
ax.scatter(trend_df["Z"], trend_df[prop_key]) | |
sel = trend_df.loc[trend_df["Z"] == Z, prop_key] | |
if not sel.empty and not pd.isna(sel.values[0]): | |
ax.scatter([Z], [sel.values[0]], s=80) | |
ax.text(Z, sel.values[0], symbol, ha="center", va="bottom") | |
ax.set_xlabel("Atomic number (Z)") | |
ax.set_ylabel(dict(NUMERIC_PROPS)[prop_key]) | |
ax.set_title(f"{dict(NUMERIC_PROPS)[prop_key]} across the periodic table") | |
fig.tight_layout() | |
return fig | |
def plot_heatmap(property_key: str): | |
prop_label = dict(NUMERIC_PROPS)[property_key] | |
max_period, max_group = len(GRID), len(GRID[0]) | |
grid_vals = np.full((max_period, max_group), np.nan, dtype=float) | |
for r in range(max_period): | |
for c in range(max_group): | |
z = GRID[r][c] | |
if z is None: | |
continue | |
val = DF.loc[DF["Z"] == z, property_key].values[0] | |
if not pd.isna(val): | |
grid_vals[r, c] = float(val) | |
fig, ax = plt.subplots() | |
im = ax.imshow(grid_vals, origin="upper", aspect="auto") | |
ax.set_xticks(range(max_group)) | |
ax.set_xticklabels([str(i) for i in range(1, max_group + 1)]) | |
ax.set_yticks(range(max_period)) | |
ax.set_yticklabels([str(i) for i in range(1, max_period + 1)]) | |
ax.set_xlabel("Group") | |
ax.set_ylabel("Period") | |
ax.set_title(f"Periodic heatmap: {prop_label}") | |
fig.colorbar(im, ax=ax, label=prop_label) | |
fig.tight_layout() | |
return fig | |
# ---------- callbacks ---------- | |
def element_info(z_or_symbol: str): | |
try: | |
if z_or_symbol.isdigit(): | |
Z = int(z_or_symbol) | |
_ = elements[Z] | |
else: | |
el = elements.symbol(z_or_symbol) | |
Z = el.number | |
except Exception: | |
return f"Unknown element: {z_or_symbol}", None, None | |
row = DF.loc[DF["Z"] == Z].iloc[0].to_dict() | |
symbol = row["symbol"] | |
facts = [] | |
facts.extend(CURATED_FACTS.get(symbol, [])) | |
facts.append(GROUP_FACTS.get(row["category"], None)) | |
facts = [f for f in facts if f] | |
def show(v): # nicer NaN -> — | |
return v if (v is not None and not pd.isna(v)) else "—" | |
props_lines = [ | |
f"{row['name']} ({symbol}), Z = {Z}", | |
f"Period {int(row['period']) if not pd.isna(row['period']) else '—'}, " | |
f"Group {row['group'] if row['group'] is not None else '—'}, " | |
f"Block {row['block']} | Category: {row['category'].replace('-', ' ').title()}", | |
f"Atomic mass: {show(row['mass'])} u", | |
f"Density: {show(row['density'])} g/cm³", | |
f"Electronegativity: {show(row['electronegativity'])} (Pauling)", | |
f"Melting point: {show(row['melting_point'])} K | Boiling point: {show(row['boiling_point'])} K", | |
f"vdW radius: {show(row['vdw_radius'])} pm | Covalent radius: {show(row['covalent_radius'])} pm", | |
f"Radioactive: {'Yes' if row['is_radioactive'] else 'No'}", | |
] | |
info_text = "\n".join(props_lines) | |
facts_text = "\n• ".join(["Interesting facts:"] + facts) if facts else "No fact on file—still cool though!" | |
prop_key = "electronegativity" if not pd.isna(row["electronegativity"]) else "mass" | |
trend_df = DF[["Z", "symbol", prop_key]].dropna() | |
fig = plot_trend(trend_df, prop_key, Z, symbol) | |
return info_text, facts_text, fig | |
def handle_button_click(z: int): | |
return element_info(str(z)) | |
def search_element(query: str): | |
query = (query or "").strip() | |
if not query: | |
return gr.update(), gr.update(), gr.update() | |
return element_info(query) | |
# ---------- UI ---------- | |
with gr.Blocks(title="Interactive Periodic Table") as demo: | |
gr.Markdown("Click an element or search by symbol/name/atomic number.") | |
with gr.Row(): | |
# Inspector | |
with gr.Column(scale=1): | |
gr.Markdown("### Inspector") | |
search = gr.Textbox(label="Search (symbol/name/Z)", placeholder="e.g., C, Iron, 79") | |
info = gr.Textbox(label="Properties", lines=10, interactive=False) | |
facts = gr.Markdown("Select an element to see fun facts.") | |
trend = gr.Plot() | |
search.submit(search_element, inputs=[search], outputs=[info, facts, trend]) | |
gr.Markdown("### Trend heatmap") | |
prop = gr.Dropdown(choices=[k for k, _ in NUMERIC_PROPS], value="electronegativity", label="Property") | |
heat = gr.Plot() | |
prop.change(lambda k: plot_heatmap(k), inputs=[prop], outputs=[heat]) | |
demo.load(lambda: plot_heatmap("electronegativity"), outputs=[heat]) | |
# Main table | |
with gr.Column(scale=2): | |
gr.Markdown("### Main Table") | |
with gr.Row(): | |
for g in range(1, 19): | |
gr.Markdown(f"**{g}**") | |
for r in range(len(GRID)): | |
with gr.Row(): | |
for c in range(len(GRID[0])): | |
z = GRID[r][c] | |
if z is None: | |
gr.Button("", interactive=False) | |
else: | |
sym = DF.loc[DF["Z"] == z, "symbol"].values[0] | |
btn = gr.Button(sym) | |
btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)], | |
outputs=[info, facts, trend]) | |
gr.Markdown("### f-block (lanthanides & actinides)") | |
with gr.Row(): | |
for z in LAN: | |
sym = DF.loc[DF["Z"] == z, "symbol"].values[0] | |
gr.Button(sym).click(handle_button_click, inputs=[gr.Number(z, visible=False)], | |
outputs=[info, facts, trend]) | |
with gr.Row(): | |
for z in ACT: | |
sym = DF.loc[DF["Z"] == z, "symbol"].values[0] | |
gr.Button(sym).click(handle_button_click, inputs=[gr.Number(z, visible=False)], | |
outputs=[info, facts, trend]) | |
if __name__ == "__main__": | |
demo.launch() | |