Vishwas1 commited on
Commit
76277cf
·
verified ·
1 Parent(s): bcdf3c7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +170 -71
app.py CHANGED
@@ -2,14 +2,17 @@ import gradio as gr
2
  import pandas as pd
3
  import numpy as np
4
  import matplotlib.pyplot as plt
5
- from typing import Dict, List, Optional
6
  from periodictable import elements
7
 
8
- # ---------- helpers ----------
 
 
9
  def to_float(x):
 
10
  if x is None:
11
  return np.nan
12
- v = getattr(x, "nominal_value", x) # handles uncertainties.UFloat
13
  try:
14
  return float(v)
15
  except Exception:
@@ -26,39 +29,39 @@ NUMERIC_PROPS = [
26
  ]
27
 
28
  CURATED_FACTS: Dict[str, List[str]] = {
29
- "H": ["Lightest element; ~74% of visible matter is H in stars."],
30
- "He": ["Inert and super light; cryogenics & balloons."],
31
- "Li": ["Lithium-ion batteries power phones & EVs."],
32
- "C": ["Diamond vs graphite = same element, different structure."],
33
- "N": ["~78% of Earth's atmosphere is N₂."],
34
- "O": ["~21% of air; essential for respiration."],
35
- "Na": ["Reacts violently with water."],
36
- "Mg": ["Bright white flame in flares."],
37
  "Si": ["Semiconductor backbone."],
38
  "Cl": ["Disinfectant; elemental Cl₂ is toxic."],
39
- "Fe": ["Steel core; oxygen transport in blood (heme)."],
40
- "Cu": ["Great conductor; forms green patina."],
41
  "Ag": ["Highest electrical conductivity."],
42
  "Au": ["Very unreactive; great for electronics/jewelry."],
43
  "Hg": ["Liquid metal at room temp; toxic."],
44
- "Pb": ["Dense, malleable; toxic—phase-out in fuels/paints."],
45
- "U": ["Reactor fuel (U-235)."],
46
  "Pu": ["Man-made in quantity; nuclear uses."],
47
  "F": ["Most electronegative; extremely reactive."],
48
- "Ne": ["Classic red-orange neon glow."],
49
- "Xe": ["Used in bright flashes/HID lamps."],
50
  }
51
 
52
  GROUP_FACTS = {
53
- "alkali": "Alkali metal: very reactive; forms +1 cations; reacts with water.",
54
- "alkaline-earth": "Alkaline earth metal: reactive; forms +2 cations.",
55
- "transition": "Transition metal: catalysts, colorful compounds, multiple oxidation states.",
56
- "post-transition": "Post-transition metal: softer, lower melting than transition metals.",
57
  "metalloid": "Metalloid: between metals and nonmetals; often semiconductors.",
58
- "nonmetal": "Nonmetal: forms covalent compounds; huge biological roles.",
59
- "halogen": "Halogen: very reactive nonmetals; −1 state; forms salts.",
60
- "noble-gas": "Noble gas: inert, monatomic gases.",
61
- "lanthanide": "Lanthanide: rare earths; magnets, lasers, phosphors.",
62
  "actinide": "Actinide: radioactive; nuclear materials.",
63
  }
64
 
@@ -113,30 +116,83 @@ def build_elements_df() -> pd.DataFrame:
113
 
114
  DF = build_elements_df()
115
 
116
- # ---------- hardcoded main-grid layout (periods 1–7, groups 1–18) ----------
117
- # None = empty cell; numbers = atomic numbers
 
 
118
  GRID = [
119
- # P1
120
  [1, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, 2],
121
- # P2
122
  [3, 4, None, None, None, None, None, None, None, None, None, None, 5, 6, 7, 8, 9, 10],
123
- # P3
124
  [11, 12, None, None, None, None, None, None, None, None, None, None, 13, 14, 15, 16, 17, 18],
125
- # P4
126
  [19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36],
127
- # P5
128
  [37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54],
129
- # P6 (La shown at group 3)
130
  [55, 56, 57, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86],
131
- # P7 (Ac shown at group 3)
132
  [87, 88, 89, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118],
133
  ]
134
-
135
- # f-block lists we display separately (omit La & Ac because they’re in the main grid)
136
  LAN = list(range(58, 72)) # Ce..Lu
137
  ACT = list(range(90, 104)) # Th..Lr
138
 
139
- # ---------- plotting ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  def plot_trend(trend_df: pd.DataFrame, prop_key: str, Z: int, symbol: str):
141
  fig, ax = plt.subplots()
142
  ax.scatter(trend_df["Z"], trend_df[prop_key])
@@ -162,21 +218,48 @@ def plot_heatmap(property_key: str):
162
  val = DF.loc[DF["Z"] == z, property_key].values[0]
163
  if not pd.isna(val):
164
  grid_vals[r, c] = float(val)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
165
  fig, ax = plt.subplots()
166
- im = ax.imshow(grid_vals, origin="upper", aspect="auto")
167
- ax.set_xticks(range(max_group))
168
- ax.set_xticklabels([str(i) for i in range(1, max_group + 1)])
169
- ax.set_yticks(range(max_period))
170
- ax.set_yticklabels([str(i) for i in range(1, max_period + 1)])
171
- ax.set_xlabel("Group")
172
- ax.set_ylabel("Period")
173
  ax.set_title(f"Periodic heatmap: {prop_label}")
174
  fig.colorbar(im, ax=ax, label=prop_label)
175
  fig.tight_layout()
176
  return fig
177
 
178
- # ---------- callbacks ----------
179
- def element_info(z_or_symbol: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  try:
181
  if z_or_symbol.isdigit():
182
  Z = int(z_or_symbol)
@@ -185,17 +268,12 @@ def element_info(z_or_symbol: str):
185
  el = elements.symbol(z_or_symbol)
186
  Z = el.number
187
  except Exception:
188
- return f"Unknown element: {z_or_symbol}", None, None
189
 
190
  row = DF.loc[DF["Z"] == Z].iloc[0].to_dict()
191
  symbol = row["symbol"]
192
 
193
- facts = []
194
- facts.extend(CURATED_FACTS.get(symbol, []))
195
- facts.append(GROUP_FACTS.get(row["category"], None))
196
- facts = [f for f in facts if f]
197
-
198
- def show(v): # nicer NaN -> —
199
  return v if (v is not None and not pd.isna(v)) else "—"
200
 
201
  props_lines = [
@@ -211,35 +289,48 @@ def element_info(z_or_symbol: str):
211
  f"Radioactive: {'Yes' if row['is_radioactive'] else 'No'}",
212
  ]
213
  info_text = "\n".join(props_lines)
214
- facts_text = "\n• ".join(["Interesting facts:"] + facts) if facts else "No fact on file—still cool though!"
215
 
216
  prop_key = "electronegativity" if not pd.isna(row["electronegativity"]) else "mass"
217
  trend_df = DF[["Z", "symbol", prop_key]].dropna()
218
  fig = plot_trend(trend_df, prop_key, Z, symbol)
219
- return info_text, facts_text, fig
220
 
221
- def handle_button_click(z: int):
222
- return element_info(str(z))
 
 
 
223
 
224
- def search_element(query: str):
225
  query = (query or "").strip()
226
  if not query:
227
- return gr.update(), gr.update(), gr.update()
228
- return element_info(query)
 
 
 
 
 
 
229
 
230
- # ---------- UI ----------
 
 
231
  with gr.Blocks(title="Interactive Periodic Table") as demo:
232
  gr.Markdown("Click an element or search by symbol/name/atomic number.")
233
 
234
  with gr.Row():
235
- # Inspector
236
  with gr.Column(scale=1):
237
  gr.Markdown("### Inspector")
 
238
  search = gr.Textbox(label="Search (symbol/name/Z)", placeholder="e.g., C, Iron, 79")
239
  info = gr.Textbox(label="Properties", lines=10, interactive=False)
240
- facts = gr.Markdown("Select an element to see fun facts.")
241
  trend = gr.Plot()
242
- search.submit(search_element, inputs=[search], outputs=[info, facts, trend])
 
 
 
243
 
244
  gr.Markdown("### Trend heatmap")
245
  prop = gr.Dropdown(choices=[k for k, _ in NUMERIC_PROPS], value="electronegativity", label="Property")
@@ -262,21 +353,29 @@ with gr.Blocks(title="Interactive Periodic Table") as demo:
262
  else:
263
  sym = DF.loc[DF["Z"] == z, "symbol"].values[0]
264
  btn = gr.Button(sym)
265
- btn.click(handle_button_click, inputs=[gr.Number(z, visible=False)],
266
- outputs=[info, facts, trend])
 
 
 
267
 
268
  gr.Markdown("### f-block (lanthanides & actinides)")
269
  with gr.Row():
270
  for z in LAN:
271
  sym = DF.loc[DF["Z"] == z, "symbol"].values[0]
272
- gr.Button(sym).click(handle_button_click, inputs=[gr.Number(z, visible=False)],
273
- outputs=[info, facts, trend])
 
 
 
274
  with gr.Row():
275
  for z in ACT:
276
  sym = DF.loc[DF["Z"] == z, "symbol"].values[0]
277
- gr.Button(sym).click(handle_button_click, inputs=[gr.Number(z, visible=False)],
278
- outputs=[info, facts, trend])
 
 
 
279
 
280
  if __name__ == "__main__":
281
  demo.launch()
282
-
 
2
  import pandas as pd
3
  import numpy as np
4
  import matplotlib.pyplot as plt
5
+ from typing import Dict, List, Optional, Tuple
6
  from periodictable import elements
7
 
8
+ # =========================
9
+ # Helpers & data utilities
10
+ # =========================
11
  def to_float(x):
12
+ """Coerce periodictable values (incl. uncertainties) to float; else NaN."""
13
  if x is None:
14
  return np.nan
15
+ v = getattr(x, "nominal_value", x)
16
  try:
17
  return float(v)
18
  except Exception:
 
29
  ]
30
 
31
  CURATED_FACTS: Dict[str, List[str]] = {
32
+ "H": ["Lightest element; dominant in stars."],
33
+ "He": ["Inert; used in cryogenics and balloons."],
34
+ "Li": ["Key in Li-ion batteries."],
35
+ "C": ["Same element diamond vs graphite (allotropy)."],
36
+ "N": ["~78% of Earths atmosphere (N₂)."],
37
+ "O": ["~21% of air; crucial for respiration."],
38
+ "Na": ["Violently reacts with water."],
39
+ "Mg": ["Burns with bright white flame."],
40
  "Si": ["Semiconductor backbone."],
41
  "Cl": ["Disinfectant; elemental Cl₂ is toxic."],
42
+ "Fe": ["Steel & blood (heme) MVP."],
43
+ "Cu": ["Great conductor; green patina."],
44
  "Ag": ["Highest electrical conductivity."],
45
  "Au": ["Very unreactive; great for electronics/jewelry."],
46
  "Hg": ["Liquid metal at room temp; toxic."],
47
+ "Pb": ["Dense; toxicity drove phase-outs."],
48
+ "U": ["Nuclear fuel (U-235)."],
49
  "Pu": ["Man-made in quantity; nuclear uses."],
50
  "F": ["Most electronegative; extremely reactive."],
51
+ "Ne": ["Classic red-orange glow tubes."],
52
+ "Xe": ["HID lamps & flashes."],
53
  }
54
 
55
  GROUP_FACTS = {
56
+ "alkali": "Alkali metal: very reactive; forms +1; reacts with water.",
57
+ "alkaline-earth": "Alkaline earth metal: reactive; forms +2.",
58
+ "transition": "Transition metal: variable oxidation states; often colored compounds.",
59
+ "post-transition": "Post-transition metal: softer; lower melting than transition metals.",
60
  "metalloid": "Metalloid: between metals and nonmetals; often semiconductors.",
61
+ "nonmetal": "Nonmetal: covalent chemistry; key biological roles.",
62
+ "halogen": "Halogen: ns²np⁵; gains 1e⁻; forms salts.",
63
+ "noble-gas": "Noble gas: ns²np⁶; inert, monatomic.",
64
+ "lanthanide": "Lanthanide: rare earths; magnets/lasers/phosphors.",
65
  "actinide": "Actinide: radioactive; nuclear materials.",
66
  }
67
 
 
116
 
117
  DF = build_elements_df()
118
 
119
+ # =========================
120
+ # Hard-coded periodic layout
121
+ # =========================
122
+ # Periods 1–7, groups 1–18; La/Ac shown in group 3; f-block split below.
123
  GRID = [
 
124
  [1, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, 2],
 
125
  [3, 4, None, None, None, None, None, None, None, None, None, None, 5, 6, 7, 8, 9, 10],
 
126
  [11, 12, None, None, None, None, None, None, None, None, None, None, 13, 14, 15, 16, 17, 18],
 
127
  [19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36],
 
128
  [37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54],
 
129
  [55, 56, 57, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86],
 
130
  [87, 88, 89, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118],
131
  ]
 
 
132
  LAN = list(range(58, 72)) # Ce..Lu
133
  ACT = list(range(90, 104)) # Th..Lr
134
 
135
+ def find_pos_in_grid(Z:int) -> Tuple[Optional[int], Optional[int]]:
136
+ for r in range(len(GRID)):
137
+ for c in range(len(GRID[0])):
138
+ if GRID[r][c] == Z:
139
+ return (r+1, c+1) # human-friendly (period, group)
140
+ return (None, None)
141
+
142
+ # =========================
143
+ # Explanations
144
+ # =========================
145
+ def valence_pattern(period:int, group:int, block:str) -> str:
146
+ if period is None or group is None or block is None:
147
+ return "Valence pattern unavailable."
148
+ n = period
149
+ if block == "s":
150
+ return f"{n}s¹" if group == 1 else f"{n}s²"
151
+ if block == "p" and 13 <= group <= 18:
152
+ p_e = group - 12 # 1..6
153
+ return f"{n}s²{n}p^{p_e}"
154
+ if block == "d":
155
+ return f"{n-1}d^(1–10){n}s^(0–2) (incomplete d-subshell)"
156
+ if block == "f":
157
+ return f"{n-2}f^(1–14){n-1}d^(0–1){n}s² (f-block)"
158
+ return "Valence pattern unavailable."
159
+
160
+ def explain_element(row:dict, Z:int) -> str:
161
+ period, group = find_pos_in_grid(Z)
162
+ block = row["block"]
163
+ cat = row["category"]
164
+ en = row["electronegativity"]
165
+ dens = row["density"]
166
+
167
+ lines = []
168
+ # Valence / block logic
169
+ lines.append(f"**Valence & block:** {valence_pattern(period, group, block)}; {cat.replace('-', ' ')}.")
170
+ # Reactivity / tendencies
171
+ if group == 1:
172
+ lines.append("**Reactivity:** Group 1 (ns¹) → easily loses 1 e⁻ (forms +1), reacts strongly with water.")
173
+ elif group == 2:
174
+ lines.append("**Reactivity:** Group 2 (ns²) → tends to lose 2 e⁻ (forms +2).")
175
+ elif group == 17:
176
+ lines.append("**Reactivity:** Halogen (ns²np⁵) → tends to gain 1 e⁻; oxidizing; reactivity decreases down the group.")
177
+ elif group == 18:
178
+ lines.append("**Reactivity:** Noble gas (ns²np⁶) → filled shell, minimal reactivity.")
179
+ elif block == "d":
180
+ lines.append("**d-block behavior:** Partially filled d-orbitals → multiple oxidation states; often colored complexes.")
181
+ # Property tie-ins
182
+ if not pd.isna(en) and not pd.isna(row["period"]):
183
+ same_period = DF[(DF["period"] == row["period"]) & (~DF["electronegativity"].isna())]
184
+ if len(same_period):
185
+ med = same_period["electronegativity"].median()
186
+ qual = "higher-than-average" if en > med else "lower-than-average"
187
+ lines.append(f"**Electronegativity:** {en:.2f} ({qual} within period {int(row['period'])}).")
188
+ if not pd.isna(dens):
189
+ lines.append(f"**Density:** {dens:g} g/cm³ — linked to atomic mass and packing typical for its category.")
190
+
191
+ return "### Why it behaves this way\n" + "\n".join(f"- {t}" for t in lines)
192
+
193
+ # =========================
194
+ # Plotting (Matplotlib -> gr.Plot)
195
+ # =========================
196
  def plot_trend(trend_df: pd.DataFrame, prop_key: str, Z: int, symbol: str):
197
  fig, ax = plt.subplots()
198
  ax.scatter(trend_df["Z"], trend_df[prop_key])
 
218
  val = DF.loc[DF["Z"] == z, property_key].values[0]
219
  if not pd.isna(val):
220
  grid_vals[r, c] = float(val)
221
+
222
+ if np.isnan(grid_vals).all():
223
+ fig, ax = plt.subplots()
224
+ ax.axis("off")
225
+ ax.text(0.5, 0.5, f"No data for {prop_label}", ha="center", va="center", fontsize=12)
226
+ fig.tight_layout()
227
+ return fig
228
+
229
+ masked = np.ma.masked_invalid(grid_vals)
230
+ finite_vals = grid_vals[~np.isnan(grid_vals)]
231
+ if finite_vals.size >= 2:
232
+ vmin, vmax = np.nanpercentile(finite_vals, [5, 95])
233
+ else:
234
+ vmin, vmax = np.nanmin(finite_vals), np.nanmax(finite_vals)
235
+
236
  fig, ax = plt.subplots()
237
+ im = ax.imshow(masked, origin="upper", aspect="auto", vmin=vmin, vmax=vmax)
238
+ ax.set_xticks(range(max_group)); ax.set_xticklabels([str(i) for i in range(1, max_group + 1)])
239
+ ax.set_yticks(range(max_period)); ax.set_yticklabels([str(i) for i in range(1, max_period + 1)])
240
+ ax.set_xlabel("Group"); ax.set_ylabel("Period")
 
 
 
241
  ax.set_title(f"Periodic heatmap: {prop_label}")
242
  fig.colorbar(im, ax=ax, label=prop_label)
243
  fig.tight_layout()
244
  return fig
245
 
246
+ # =========================
247
+ # Core callbacks
248
+ # =========================
249
+ def compose_facts(row:dict, Z:int, show_expl:bool) -> str:
250
+ symbol = row["symbol"]
251
+ facts = []
252
+ facts.extend(CURATED_FACTS.get(symbol, []))
253
+ gf = GROUP_FACTS.get(row["category"], None)
254
+ if gf:
255
+ facts.append(gf)
256
+ facts_text = "\n• ".join(["**Interesting facts:**"] + facts) if facts else ""
257
+ if show_expl:
258
+ expl = explain_element(row, Z)
259
+ facts_text = (facts_text + "\n\n" if facts_text else "") + expl
260
+ return facts_text if facts_text else "No fact on file—still cool though!"
261
+
262
+ def element_info(z_or_symbol: str, show_expl: bool):
263
  try:
264
  if z_or_symbol.isdigit():
265
  Z = int(z_or_symbol)
 
268
  el = elements.symbol(z_or_symbol)
269
  Z = el.number
270
  except Exception:
271
+ return f"Unknown element: {z_or_symbol}", "No data", None, None # info, facts, fig, current_Z
272
 
273
  row = DF.loc[DF["Z"] == Z].iloc[0].to_dict()
274
  symbol = row["symbol"]
275
 
276
+ def show(v):
 
 
 
 
 
277
  return v if (v is not None and not pd.isna(v)) else "—"
278
 
279
  props_lines = [
 
289
  f"Radioactive: {'Yes' if row['is_radioactive'] else 'No'}",
290
  ]
291
  info_text = "\n".join(props_lines)
 
292
 
293
  prop_key = "electronegativity" if not pd.isna(row["electronegativity"]) else "mass"
294
  trend_df = DF[["Z", "symbol", prop_key]].dropna()
295
  fig = plot_trend(trend_df, prop_key, Z, symbol)
 
296
 
297
+ facts_text = compose_facts(row, Z, show_expl)
298
+ return info_text, facts_text, fig, Z
299
+
300
+ def handle_button_click(z: int, show_expl: bool):
301
+ return element_info(str(z), show_expl)
302
 
303
+ def search_element(query: str, show_expl: bool):
304
  query = (query or "").strip()
305
  if not query:
306
+ return gr.update(), gr.update(), gr.update(), gr.update()
307
+ return element_info(query, show_expl)
308
+
309
+ def refresh_facts(current_Z: Optional[int], show_expl: bool):
310
+ if current_Z is None:
311
+ return gr.update()
312
+ row = DF.loc[DF["Z"] == current_Z].iloc[0].to_dict()
313
+ return compose_facts(row, int(current_Z), show_expl)
314
 
315
+ # =========================
316
+ # UI (Gradio 4.29.0)
317
+ # =========================
318
  with gr.Blocks(title="Interactive Periodic Table") as demo:
319
  gr.Markdown("Click an element or search by symbol/name/atomic number.")
320
 
321
  with gr.Row():
322
+ # Inspector & controls
323
  with gr.Column(scale=1):
324
  gr.Markdown("### Inspector")
325
+ show_expl = gr.Checkbox(label="Show advanced explanation", value=False)
326
  search = gr.Textbox(label="Search (symbol/name/Z)", placeholder="e.g., C, Iron, 79")
327
  info = gr.Textbox(label="Properties", lines=10, interactive=False)
328
+ facts = gr.Markdown("Select an element to see facts and explanations.")
329
  trend = gr.Plot()
330
+ current_Z = gr.State(value=None)
331
+
332
+ search.submit(search_element, inputs=[search, show_expl], outputs=[info, facts, trend, current_Z])
333
+ show_expl.change(refresh_facts, inputs=[current_Z, show_expl], outputs=[facts])
334
 
335
  gr.Markdown("### Trend heatmap")
336
  prop = gr.Dropdown(choices=[k for k, _ in NUMERIC_PROPS], value="electronegativity", label="Property")
 
353
  else:
354
  sym = DF.loc[DF["Z"] == z, "symbol"].values[0]
355
  btn = gr.Button(sym)
356
+ btn.click(
357
+ handle_button_click,
358
+ inputs=[gr.Number(z, visible=False), show_expl],
359
+ outputs=[info, facts, trend, current_Z],
360
+ )
361
 
362
  gr.Markdown("### f-block (lanthanides & actinides)")
363
  with gr.Row():
364
  for z in LAN:
365
  sym = DF.loc[DF["Z"] == z, "symbol"].values[0]
366
+ gr.Button(sym).click(
367
+ handle_button_click,
368
+ inputs=[gr.Number(z, visible=False), show_expl],
369
+ outputs=[info, facts, trend, current_Z],
370
+ )
371
  with gr.Row():
372
  for z in ACT:
373
  sym = DF.loc[DF["Z"] == z, "symbol"].values[0]
374
+ gr.Button(sym).click(
375
+ handle_button_click,
376
+ inputs=[gr.Number(z, visible=False), show_expl],
377
+ outputs=[info, facts, trend, current_Z],
378
+ )
379
 
380
  if __name__ == "__main__":
381
  demo.launch()