export interface IElement { name: string; radius: number; } export type TPeriodicTable = Record; /** * Layout and visual grouping for the periodic-table s, p, d, and f blocks. * Here `f` covers the complete detached lanthanide and actinide series. */ export type ElementBlock = "s" | "p" | "d" | "f"; /** * @description Chemical-element data plus conventional periodic-table * placement. `period` and `group` describe the chemistry; `row` and `column` * are one-based UI grid coordinates. The detached lanthanide and actinide * series use rows 8 and 9, columns 3–17, with `group: null`. * * @example * ```ts * const carbon = PeriodicTableElements.find(({ symbol }) => symbol === "C"); * // carbon?.period === 2; carbon?.group === 14 * // carbon?.row === 2; carbon?.column === 14 * ``` */ export interface PeriodicTableElement extends IElement { /** Standard chemical symbol, for example `"C"` or `"Fe"`. */ readonly symbol: string; /** Atomic number, from 1 for hydrogen through 118 for oganesson. */ readonly atomicNumber: number; /** Horizontal period in the chemical periodic table, from 1 through 7. */ readonly period: number; /** * International Union of Pure and Applied Chemistry (IUPAC) group from * 1 through 18, or `null` for the detached f-block. */ readonly group: number | null; /** Periodic-table block classification used for layout and visual grouping. */ readonly block: ElementBlock; /** One-based visual row; rows 8 and 9 hold the detached f-block. */ readonly row: number; /** One-based visual column in the conventional 18-column layout. */ readonly column: number; } /** * Neutral-atom van der Waals radii in Å. The values are the surface radii * published by Charry & Tkatchenko (JCTC 2024, DOI 10.1021/acs.jctc.4c00784), * converted from bohr. Keeping these separate from the covalent radii below is * important: Spacefill is a physical size model, not a scaled bond model. */ export const VanDerWaalsRadii: Readonly> = { H: 1.675, // biome-ignore lint/suspicious/noApproximativeNumericConstant: published radius is exactly 1.414 Å. He: 1.414, Li: 2.799, Be: 2.269, B: 2.08, C: 1.91, N: 1.798, O: 1.715, F: 1.631, Ne: 1.554, Na: 2.797, Mg: 2.485, Al: 2.412, Si: 2.265, P: 2.139, S: 2.063, Cl: 1.981, Ar: 1.905, K: 3.037, Ca: 2.792, Sc: 2.597, Ti: 2.608, V: 2.557, Cr: 2.54, Mn: 2.468, Fe: 2.436, Co: 2.395, Ni: 2.355, Cu: 2.342, Zn: 2.277, Ga: 2.362, Ge: 2.288, As: 2.196, Se: 2.186, Br: 2.087, Kr: 2.021, Rb: 3.08, Sr: 2.873, Y: 2.794, Zr: 2.651, Nb: 2.6, Mo: 2.557, Tc: 2.522, Ru: 2.489, Rh: 2.455, Pd: 2.153, Ag: 2.395, Cd: 2.334, In: 2.453, Sn: 2.382, Sb: 2.312, Te: 2.271, I: 2.225, Xe: 2.167, Cs: 3.181, Ba: 3.009, La: 2.909, Ce: 2.89, Pr: 2.912, Nd: 2.896, Pm: 2.88, Sm: 2.863, Eu: 2.845, Gd: 2.785, Tb: 2.814, Dy: 2.801, Ho: 2.779, Er: 2.764, Tm: 2.747, Yb: 2.734, Lu: 2.728, Hf: 2.619, Ta: 2.498, W: 2.466, Re: 2.436, Os: 2.407, Ir: 2.388, Pt: 2.348, Au: 2.254, Hg: 2.235, Tl: 2.362, Pb: 2.342, Bi: 2.348, Po: 2.319, At: 2.304, Rn: 2.245, Fr: 3.077, Ra: 2.966, Ac: 2.886, Th: 2.916, Pa: 2.774, U: 2.705, Np: 2.767, Pu: 2.715, Am: 2.709, Cm: 2.746, Bk: 2.694, Cf: 2.683, Es: 2.672, Fm: 2.656, Md: 2.641, No: 2.644, Lr: 3.08, Rf: 2.651, Db: 2.304, Sg: 2.288, Bh: 2.271, Hs: 2.254, Mt: 2.236, Ds: 2.216, Rg: 2.217, Cn: 2.174, Nh: 2.186, Fl: 2.206, Mc: 2.482, Ts: 2.508, Og: 2.413, }; const METAL_ELEMENTS = new Set([ "Li", "Be", "Na", "Mg", "Al", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", ]); /** Normalize aromatic/lowercase element symbols to standard notation. */ export function normalizeElement(element: string): string { if (element.length === 0) return element; return element[0].toUpperCase() + element.slice(1).toLowerCase(); } export function getVanDerWaalsRadius(element: string): number { const normalized = normalizeElement(element); return ( VanDerWaalsRadii[normalized] ?? Math.max((PeriodicTable[normalized]?.radius ?? 0.8) + 0.8, 1.5) ); } export function isMetalElement(element: string): boolean { return METAL_ELEMENTS.has(normalizeElement(element)); } export const PeriodicTable: TPeriodicTable = { H: { radius: 0.38, name: "Hydrogen" }, He: { radius: 0.32, name: "Helium" }, Li: { radius: 1.34, name: "Lithium" }, Be: { radius: 0.9, name: "Beryllium" }, B: { radius: 0.82, name: "Boron" }, C: { radius: 0.77, name: "Carbon" }, N: { radius: 0.75, name: "Nitrogen" }, O: { radius: 0.73, name: "Oxygen" }, F: { radius: 0.71, name: "Fluorine" }, Ne: { radius: 0.69, name: "Neon" }, Na: { radius: 1.54, name: "Sodium" }, Mg: { radius: 1.3, name: "Magnesium" }, Al: { radius: 1.18, name: "Aluminum" }, Si: { radius: 1.11, name: "Silicon" }, P: { radius: 1.06, name: "Phosphorus" }, S: { radius: 1.02, name: "Sulfur" }, Cl: { radius: 0.99, name: "Chlorine" }, Ar: { radius: 0.97, name: "Argon" }, K: { radius: 1.96, name: "Potassium" }, Ca: { radius: 1.74, name: "Calcium" }, Sc: { radius: 1.44, name: "Scandium" }, Ti: { radius: 1.32, name: "Titanium" }, V: { radius: 1.22, name: "Vanadium" }, Cr: { radius: 1.18, name: "Chromium" }, Mn: { radius: 1.17, name: "Manganese" }, Fe: { radius: 1.17, name: "Iron" }, Co: { radius: 1.16, name: "Cobalt" }, Ni: { radius: 1.15, name: "Nickel" }, Cu: { radius: 1.17, name: "Copper" }, Zn: { radius: 1.25, name: "Zinc" }, Ga: { radius: 1.26, name: "Gallium" }, Ge: { radius: 1.22, name: "Germanium" }, As: { radius: 1.19, name: "Arsenic" }, Se: { radius: 1.2, name: "Selenium" }, Br: { radius: 1.2, name: "Bromine" }, Kr: { radius: 1.16, name: "Krypton" }, Rb: { radius: 2.1, name: "Rubidium" }, Sr: { radius: 1.85, name: "Strontium" }, Y: { radius: 1.63, name: "Yttrium" }, Zr: { radius: 1.54, name: "Zirconium" }, Nb: { radius: 1.47, name: "Niobium" }, Mo: { radius: 1.38, name: "Molybdenum" }, Tc: { radius: 1.28, name: "Technetium" }, Ru: { radius: 1.25, name: "Ruthenium" }, Rh: { radius: 1.25, name: "Rhodium" }, Pd: { radius: 1.2, name: "Palladium" }, Ag: { radius: 1.28, name: "Silver" }, Cd: { radius: 1.36, name: "Cadmium" }, In: { radius: 1.42, name: "Indium" }, Sn: { radius: 1.4, name: "Tin" }, Sb: { radius: 1.4, name: "Antimony" }, Te: { radius: 1.36, name: "Tellurium" }, I: { radius: 1.33, name: "Iodine" }, Xe: { radius: 1.31, name: "Xenon" }, Cs: { radius: 2.25, name: "Cesium" }, Ba: { radius: 1.98, name: "Barium" }, La: { radius: 1.69, name: "Lanthanum" }, Ce: { radius: 1.65, name: "Cerium" }, Pr: { radius: 1.65, name: "Praseodymium" }, Nd: { radius: 1.64, name: "Neodymium" }, Pm: { radius: 1.63, name: "Promethium" }, Sm: { radius: 1.62, name: "Samarium" }, Eu: { radius: 1.61, name: "Europium" }, Gd: { radius: 1.6, name: "Gadolinium" }, Tb: { radius: 1.59, name: "Terbium" }, Dy: { radius: 1.58, name: "Dysprosium" }, Ho: { radius: 1.57, name: "Holmium" }, Er: { radius: 1.56, name: "Erbium" }, Tm: { radius: 1.55, name: "Thulium" }, Yb: { radius: 1.54, name: "Ytterbium" }, Lu: { radius: 1.53, name: "Lutetium" }, Hf: { radius: 1.52, name: "Hafnium" }, Ta: { radius: 1.51, name: "Tantalum" }, W: { radius: 1.5, name: "Tungsten" }, Re: { radius: 1.49, name: "Rhenium" }, Os: { radius: 1.48, name: "Osmium" }, Ir: { radius: 1.47, name: "Iridium" }, Pt: { radius: 1.46, name: "Platinum" }, Au: { radius: 1.45, name: "Gold" }, Hg: { radius: 1.44, name: "Mercury" }, Tl: { radius: 1.43, name: "Thallium" }, Pb: { radius: 1.42, name: "Lead" }, Bi: { radius: 1.41, name: "Bismuth" }, Po: { radius: 1.4, name: "Polonium" }, At: { radius: 1.39, name: "Astatine" }, Rn: { radius: 1.38, name: "Radon" }, Fr: { radius: 1.37, name: "Francium" }, Ra: { radius: 1.36, name: "Radium" }, Ac: { radius: 1.35, name: "Actinium" }, Th: { radius: 1.34, name: "Thorium" }, Pa: { radius: 1.33, name: "Protactinium" }, U: { radius: 1.32, name: "Uranium" }, Np: { radius: 1.31, name: "Neptunium" }, Pu: { radius: 1.3, name: "Plutonium" }, Am: { radius: 1.29, name: "Americium" }, Cm: { radius: 1.28, name: "Curium" }, Bk: { radius: 1.27, name: "Berkelium" }, Cf: { radius: 1.26, name: "Californium" }, Es: { radius: 1.25, name: "Einsteinium" }, Fm: { radius: 1.24, name: "Fermium" }, Md: { radius: 1.23, name: "Mendelevium" }, No: { radius: 1.22, name: "Nobelium" }, Lr: { radius: 1.21, name: "Lawrencium" }, Rf: { radius: 1.2, name: "Rutherfordium" }, Db: { radius: 1.19, name: "Dubnium" }, Sg: { radius: 1.18, name: "Seaborgium" }, Bh: { radius: 1.17, name: "Bohrium" }, Hs: { radius: 1.16, name: "Hassium" }, Mt: { radius: 1.15, name: "Meitnerium" }, Ds: { radius: 1.14, name: "Darmstadtium" }, Rg: { radius: 1.13, name: "Roentgenium" }, Cn: { radius: 1.12, name: "Copernicium" }, Nh: { radius: 1.11, name: "Nihonium" }, Fl: { radius: 1.1, name: "Flerovium" }, Mc: { radius: 1.09, name: "Moscovium" }, Lv: { radius: 1.08, name: "Livermorium" }, Ts: { radius: 1.07, name: "Tennessine" }, Og: { radius: 1.06, name: "Oganesson" }, } as const; interface ElementPlacement { period: number; group: number | null; block: ElementBlock; row: number; column: number; } interface PeriodSegment { startColumn: number; symbols: readonly string[]; } interface PeriodRow { period: number; segments: readonly PeriodSegment[]; } const MainPeriodRows: readonly PeriodRow[] = [ { period: 1, segments: [ { startColumn: 1, symbols: ["H"] }, { startColumn: 18, symbols: ["He"] }, ], }, { period: 2, segments: [ { startColumn: 1, symbols: ["Li", "Be"] }, { startColumn: 13, symbols: ["B", "C", "N", "O", "F", "Ne"], }, ], }, { period: 3, segments: [ { startColumn: 1, symbols: ["Na", "Mg"] }, { startColumn: 13, symbols: ["Al", "Si", "P", "S", "Cl", "Ar"], }, ], }, { period: 4, segments: [ { startColumn: 1, symbols: [ "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", ], }, ], }, { period: 5, segments: [ { startColumn: 1, symbols: [ "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", ], }, ], }, { period: 6, segments: [ { startColumn: 1, symbols: ["Cs", "Ba"] }, { startColumn: 4, symbols: [ "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", ], }, ], }, { period: 7, segments: [ { startColumn: 1, symbols: ["Fr", "Ra"] }, { startColumn: 4, symbols: [ "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og", ], }, ], }, ]; const Lanthanides = [ "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", ] as const; const Actinides = [ "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", ] as const; function blockForMainTable(symbol: string, group: number): ElementBlock { if (group <= 2 || symbol === "He") { return "s"; } if (group <= 12) { return "d"; } return "p"; } function createElementPlacements(): ReadonlyMap { const placements = new Map(); for (const row of MainPeriodRows) { for (const segment of row.segments) { segment.symbols.forEach((symbol, index) => { const column = segment.startColumn + index; placements.set(symbol, { period: row.period, group: column, block: blockForMainTable(symbol, column), row: row.period, column, }); }); } } const addFBlock = ( symbols: readonly string[], period: number, row: number, ) => { symbols.forEach((symbol, index) => { placements.set(symbol, { period, group: null, block: "f", row, column: index + 3, }); }); }; addFBlock(Lanthanides, 6, 8); addFBlock(Actinides, 7, 9); return placements; } const ElementPlacements = createElementPlacements(); /** * @description Complete element catalog in atomic-number order. Each frozen * entry carries one-based coordinates for the conventional 18-column layout. * The f-block—the lanthanide and actinide series commonly drawn below the main * table—occupies rows 8 and 9. The array and every entry are frozen at runtime. * * @example * ```ts * const iron = PeriodicTableElements.find(({ symbol }) => symbol === "Fe"); * console.log(iron?.atomicNumber, iron?.row, iron?.column); // 26, 4, 8 * ``` */ export const PeriodicTableElements: readonly PeriodicTableElement[] = Object.freeze( Object.entries(PeriodicTable).map(([symbol, element], index) => { const placement = ElementPlacements.get(symbol); if (!placement) { throw new Error(`Missing periodic-table placement for ${symbol}`); } return Object.freeze({ symbol, ...element, atomicNumber: index + 1, ...placement, }); }), );