Spaces:
Build error
Build error
import math | |
from dataclasses import asdict, dataclass | |
from typing import Dict, List, Optional, Tuple | |
import gradio as gr | |
import pandas as pd | |
import plotly.express as px | |
# External dependency: | |
# pip install periodictable | |
from periodictable import elements | |
# ----------------------------- | |
# Data extraction helpers | |
# ----------------------------- | |
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)"), | |
] | |
# Some curated quick facts. We'll augment with group-based facts so every element gets at least one. | |
CURATED_FACTS: Dict[str, List[str]] = { | |
"H": ["Lightest element; ~74% of the visible universe by mass is hydrogen in stars."], | |
"He": ["Inert, used in cryogenics and balloons; second lightest element."], | |
"Li": ["Batteries MVP: lithium-ion cells power phones and EVs."], | |
"C": ["Backbone of life; diamond and graphite are pure carbon with wildly different properties."], | |
"N": ["~78% of Earth's atmosphere is nitrogen (mostly N₂)."], | |
"O": ["Essential for respiration; ~21% of Earth's atmosphere."], | |
"Na": ["Sodium metal reacts violently with water—handle only under oil or inert gas."], | |
"Mg": ["Burns with a bright white flame; used in flares and fireworks."], | |
"Al": ["Light and strong; forms a protective oxide layer that resists corrosion."], | |
"Si": ["Silicon is the basis of modern electronics—hello, semiconductors."], | |
"Cl": ["Powerful disinfectant; elemental chlorine is toxic, compounds are widely useful."], | |
"Ar": ["Argon is used to provide inert atmospheres for welding and 3D printing."], | |
"Fe": ["Core of steel; iron is essential in hemoglobin for oxygen transport."], | |
"Cu": ["Excellent electrical conductor; iconic blue-green patina (verdigris)."], | |
"Ag": ["Highest electrical conductivity of all metals; historically used as currency."], | |
"Au": ["Very unreactive ('noble'); prized for electronics and jewelry."], | |
"Hg": ["Only metal that's liquid at room temperature; toxic—use with care."], | |
"Pb": ["Dense and malleable; toxicity led to phase-out from gasoline and paints."], | |
"U": ["Radioactive; used as nuclear reactor fuel (U-235)."], | |
"Pu": ["Man-made in quantity; key in certain nuclear technologies."], | |
"F": ["Most electronegative element; extremely reactive."], | |
"Ne": ["Neon glows striking red-orange in discharge tubes—classic signs."], | |
"Xe": ["Xenon makes bright camera flashes and high-intensity lamps."], | |
} | |
GROUP_FACTS = { | |
"alkali": "Alkali metal: very reactive soft metal; forms +1 cations and reacts with water.", | |
"alkaline-earth": "Alkaline earth metal: reactive (less than Group 1); forms +2 cations.", | |
"transition": "Transition metal: often good catalysts, colorful compounds, multiple oxidation states.", | |
"post-transition": "Post-transition metal: softer metals with lower melting points than transition metals.", | |
"metalloid": "Metalloid: properties between metals and nonmetals; often semiconductors.", | |
"nonmetal": "Nonmetal: tends to form covalent compounds; wide range of roles in biology and materials.", | |
"halogen": "Halogen: very reactive nonmetals; form salts with metals and −1 oxidation state.", | |
"noble-gas": "Noble gas: chemically inert under most conditions; monatomic gases.", | |
"lanthanide": "Lanthanide: f-block rare earths; notable for magnets, lasers, and phosphors.", | |
"actinide": "Actinide: radioactive f-block; includes nuclear fuel materials.", | |
} | |
# Map periodictable categories into the above buckets | |
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 == "p" and el.group in (13, 14, 15, 16) and el.metallic: | |
return "post-transition" | |
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 == "p" and not el.metallic: | |
return "nonmetal" | |
if el.block == "f" and 57 <= el.number <= 71: | |
return "lanthanide" | |
if el.block == "f" and 89 <= el.number <= 103: | |
return "actinide" | |
except Exception: | |
pass | |
return "nonmetal" if not getattr(el, "metallic", False) else "post-transition" | |
# Build a dataframe of elements | |
def build_elements_df() -> pd.DataFrame: | |
rows = [] | |
for Z in range(1, 119): | |
el = elements[Z] | |
if el is None: | |
continue | |
data = { | |
"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": getattr(el, "mass", None), | |
"density": getattr(el, "density", None), | |
"electronegativity": getattr(el, "electronegativity", None), | |
"boiling_point": getattr(el, "boiling_point", None), | |
"melting_point": getattr(el, "melting_point", None), | |
"vdw_radius": getattr(el, "vdw_radius", None), | |
"covalent_radius": getattr(el, "covalent_radius", None), | |
"category": classify_category(el), | |
"is_radioactive": bool(getattr(el, "radioactive", False)), | |
} | |
rows.append(data) | |
df = pd.DataFrame(rows).sort_values("Z").reset_index(drop=True) | |
return df | |
DF = build_elements_df() | |
# Layout positions: group (1-18) x period (1-7); f-block as two rows | |
MAX_GROUP = 18 | |
MAX_PERIOD = 7 | |
GRID: List[List[Optional[int]]] = [[None for _ in range(MAX_GROUP)] for _ in range(MAX_PERIOD)] | |
for _, row in DF.iterrows(): | |
period, group, Z = int(row["period"]), row["group"], int(row["Z"]) | |
if group is None: | |
continue | |
GRID[period-1][group-1] = Z | |
# f-block positions (lanthanides/actinides) - show in separate rows | |
LAN = [z for z in DF["Z"] if 57 <= z <= 71] | |
ACT = [z for z in DF["Z"] if 89 <= z <= 103] | |
# ----------------------------- | |
# UI callbacks | |
# ----------------------------- | |
def element_info(z_or_symbol: str): | |
# Accept atomic number or symbol | |
try: | |
if z_or_symbol.isdigit(): | |
Z = int(z_or_symbol) | |
el = 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'] | |
# Build facts | |
facts = [] | |
facts.extend(CURATED_FACTS.get(symbol, [])) | |
facts.append(GROUP_FACTS.get(row['category'], None)) | |
facts = [f for f in facts if f] | |
# Properties text | |
props_lines = [ | |
f\"{row['name']} ({symbol}), Z = {Z}\", | |
f\"Period {int(row['period'])}, Group {row['group']}, Block {row['block']} | Category: {row['category'].replace('-', ' ').title()}\", | |
f\"Atomic mass: {row['mass'] if row['mass'] else '—'} u\", | |
f\"Density: {row['density'] if row['density'] else '—'} g/cm³\", | |
f\"Electronegativity: {row['electronegativity'] if row['electronegativity'] else '—'} (Pauling)\", | |
f\"Melting point: {row['melting_point'] if row['melting_point'] else '—'} K | Boiling point: {row['boiling_point'] if row['boiling_point'] else '—'} K\", | |
f\"vdW radius: {row['vdw_radius'] if row['vdw_radius'] else '—'} pm | Covalent radius: {row['covalent_radius'] if row['covalent_radius'] else '—'} 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!\" | |
# Trend plot (Atomic number vs selected property) | |
# We'll default to electronegativity if available, else mass. | |
prop_key = 'electronegativity' if not pd.isna(row['electronegativity']) else 'mass' | |
label = dict(NUMERIC_PROPS)[prop_key] | |
trend_df = DF[['Z', 'symbol', prop_key]].dropna() | |
fig = px.scatter( | |
trend_df, x='Z', y=prop_key, hover_name='symbol', title=f'{label} across the periodic table', | |
) | |
# Highlight selected element | |
fig.add_scatter(x=[Z], y=[row[prop_key]] if row[prop_key] else [None], | |
mode='markers+text', text=[symbol], textposition='top center') | |
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) | |
def heatmap(property_key: str): | |
prop_label = dict(NUMERIC_PROPS)[property_key] | |
# Create a pseudo-2D matrix for the s/p/d blocks (7x18) with property values | |
import numpy as np | |
grid_vals = np.full((MAX_PERIOD, MAX_GROUP), None, dtype=object) | |
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] | |
grid_vals[r, c] = val if not pd.isna(val) else None | |
fig = px.imshow( | |
grid_vals.astype(float), | |
origin='upper', | |
labels=dict(color=prop_label, x='Group', y='Period'), | |
x=list(range(1, MAX_GROUP+1)), | |
y=list(range(1, MAX_PERIOD+1)), | |
title=f'Periodic heatmap: {prop_label}', | |
aspect='auto', | |
color_continuous_scale='Viridis' | |
) | |
return fig | |
# ----------------------------- | |
# Build UI | |
# ----------------------------- | |
with gr.Blocks(title="Interactive Periodic Table", css=\"\"\" | |
.button-cell {min-width: 40px; height: 40px; padding: 0.25rem; font-weight: 600;} | |
.symbol {font-size: 0.95rem;} | |
.small {font-size: 0.7rem; opacity: 0.8;} | |
.grid {display: grid; grid-template-columns: repeat(18, 1fr); gap: 4px;} | |
.fgrid {display: grid; grid-template-columns: repeat(15, 1fr); gap: 4px;} | |
.header {text-align:center; font-weight:700; margin: 0.5rem 0;} | |
\"\"\") as demo: | |
gr.Markdown(\"# 🧪 Interactive Periodic Table\\nClick an element or search by symbol/name/atomic number.\") | |
with gr.Row(): | |
with gr.Column(scale=2): | |
gr.Markdown(\"### Main Table\") | |
main_buttons = [] | |
with gr.Group(): | |
with gr.Row(): | |
gr.HTML('<div class=\"grid\">' + ''.join([f'<div class=\"header\">{g}</div>' for g in range(1, 19)]) + '</div>') | |
# Build button grid | |
rows = [] | |
for r in range(MAX_PERIOD): | |
with gr.Row(): | |
cells = [] | |
for c in range(MAX_GROUP): | |
z = GRID[r][c] | |
if z is None: | |
btn = gr.Button(\"\", elem_classes=[\"button-cell\"]) | |
btn.click(lambda: (gr.update(), gr.update(), gr.update())) | |
else: | |
sym = DF.loc[DF['Z'] == z, 'symbol'].values[0] | |
btn = gr.Button(sym, elem_classes=[\"button-cell\"]) | |
btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)], outputs=[ | |
gr.Textbox(interactive=False), gr.Markdown(), gr.Plot()]) | |
cells.append(btn) | |
rows.append(cells) | |
gr.Markdown(\"### f-block (lanthanides & actinides)\") | |
with gr.Row(): | |
# Lanthanides row | |
lan_buttons = [] | |
for z in LAN: | |
sym = DF.loc[DF['Z'] == z, 'symbol'].values[0] | |
btn = gr.Button(sym, elem_classes=[\"button-cell\"]) | |
btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)], outputs=[ | |
gr.Textbox(interactive=False), gr.Markdown(), gr.Plot()]) | |
lan_buttons.append(btn) | |
with gr.Row(): | |
# Actinides row | |
act_buttons = [] | |
for z in ACT: | |
sym = DF.loc[DF['Z'] == z, 'symbol'].values[0] | |
btn = gr.Button(sym, elem_classes=[\"button-cell\"]) | |
btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)], outputs=[ | |
gr.Textbox(interactive=False), gr.Markdown(), gr.Plot()]) | |
act_buttons.append(btn) | |
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(heatmap, inputs=[prop], outputs=[heat]) | |
# Initialize | |
heat.update(heatmap(\"electronegativity\")) | |
gr.Markdown(\"---\\nBuilt with **Gradio** + **periodictable**. Data completeness varies by element; some values may be missing.\") | |
if __name__ == \"__main__\": | |
demo.launch() | |