1) Data assets (ready now)
- Master (Known vs Predicted + Gap) — CSV
isotope_master_table_known_vs_predicted_with_gap.csv→ download - Master (Known vs Predicted + Gap) — Markdown
isotope_master_table_known_vs_predicted_with_gap.md→ download - Predicted-Only Ledger — CSV
isotope_predicted_only_Z1-118.csv→ download - Predicted-Only Ledger — Markdown
isotope_predicted_only_Z1-118.md→ download - Strict Known Ledger — CSV
isotope_master_summary_Z1-118_strict.csv→ download
2) Data contract (single table)
Table name: isotopes_master
| column | type | notes |
|---|---|---|
element_z | INT | atomic number (1–118) |
element_symbol | TEXT(3) | “H”, “He”, … |
element_label | TEXT | “H (1)” exactly as in files |
known_isotopes | INT | experimentally observed, strict stable definition applied elsewhere |
stable_isotopes_strict | INT | strict IUPAC stable count |
unstable_isotopes | INT | computed = known − stable |
predicted_isotopes_est | INT | Neufcourt-scaled total (sums to 7,759) |
gap_pred_minus_known | INT | predicted − known |
If you prefer a normalized model, split “known”, “predicted” and “metadata” into separate tables keyed by
element_z.
SQL DDL (Postgres / MySQL compatible):
CREATE TABLE isotopes_master (
element_z INT PRIMARY KEY,
element_symbol VARCHAR(3) NOT NULL,
element_label VARCHAR(16) NOT NULL,
known_isotopes INT NOT NULL,
stable_isotopes_strict INT NOT NULL,
unstable_isotopes INT NOT NULL,
predicted_isotopes_est INT NOT NULL,
gap_pred_minus_known INT NOT NULL
);
Upsert loader (Postgres):
-- assuming a temp staging table or CSV loader
-- columns in CSV: Element (Z), Isotopes Known, Stable, Unstable, Predicted Isotopes (est.), Gap (Predicted - Known)
-- element_symbol is parsed from Element (Z) token before the space
-- element_z is parsed from inside parentheses
-- Example: programmatic load recommended (see ETL note below)
ETL note: parse element_symbol and element_z from element_label (“Fe (26)”) in your loader. If you want, I can generate a clean JSON with explicit element_z and element_symbol columns.
3) Lightweight REST/GraphQL façade (optional)
REST shape (GET /isotopes):
[
{
"element_z": 26,
"element_symbol": "Fe",
"known_isotopes": 28,
"stable_isotopes_strict": 4,
"unstable_isotopes": 24,
"predicted_isotopes_est": 66,
"gap_pred_minus_known": 38
}
]
GraphQL sketch:
type IsotopeSummary {
element_z: Int!
element_symbol: String!
known: Int!
stable_strict: Int!
unstable: Int!
predicted: Int!
gap: Int!
}
type Query {
isotopes(z: Int, symbol: String): [IsotopeSummary!]!
topGaps(limit: Int = 20): [IsotopeSummary!]!
}
4) SolveForce UI drop-in (React/Next.js)
Paste this component into your frontend. It expects a JSON endpoint at /api/isotopes that returns the shape above; or, if you want to load the CSV directly, I added a CSV branch (commented).
import React, { useEffect, useMemo, useState } from "react";
// Minimal styling — replace with your design system
const cell = "px-3 py-2 border-b";
const th = "px-3 py-2 border-b font-semibold text-left";
const inputClass = "border rounded px-2 py-1";
type Row = {
element_z: number;
element_symbol: string;
known_isotopes: number;
stable_isotopes_strict: number;
unstable_isotopes: number;
predicted_isotopes_est: number;
gap_pred_minus_known: number;
};
export default function IsotopeMasterTable() {
const [rows, setRows] = useState<Row[]>([]);
const [q, setQ] = useState("");
const [sortKey, setSortKey] = useState<keyof Row>("gap_pred_minus_known");
const [asc, setAsc] = useState(false);
useEffect(() => {
// Option A: API
fetch("/api/isotopes")
.then(r => r.json())
.then(setRows)
.catch(console.error);
// Option B: load CSV directly (uncomment, remove Option A).
// fetch("/files/isotope_master_table_known_vs_predicted_with_gap.csv")
// .then(r => r.text())
// .then(text => csvToRows(text))
// .then(setRows)
// .catch(console.error);
}, []);
const filtered = useMemo(() => {
const term = q.trim().toLowerCase();
const base = term
? rows.filter(r =>
r.element_symbol.toLowerCase().includes(term) ||
String(r.element_z).includes(term)
)
: rows.slice();
const sorted = base.sort((a, b) => {
const A = a[sortKey], B = b[sortKey];
if (A < B) return asc ? -1 : 1;
if (A > B) return asc ? 1 : -1;
return 0;
});
return sorted;
}, [rows, q, sortKey, asc]);
const totals = useMemo(() => {
const reduce = (k: keyof Row) => filtered.reduce((s, r) => s + Number(r[k] || 0), 0);
return {
elements: filtered.length,
known: reduce("known_isotopes"),
stable: reduce("stable_isotopes_strict"),
unstable: reduce("unstable_isotopes"),
predicted: reduce("predicted_isotopes_est"),
gap: reduce("gap_pred_minus_known"),
};
}, [filtered]);
const headers: { key: keyof Row; label: string }[] = [
{ key: "element_z", label: "Z" },
{ key: "element_symbol", label: "Element" },
{ key: "known_isotopes", label: "Known" },
{ key: "stable_isotopes_strict", label: "Stable (strict)" },
{ key: "unstable_isotopes", label: "Unstable" },
{ key: "predicted_isotopes_est", label: "Predicted" },
{ key: "gap_pred_minus_known", label: "Gap" },
];
return (
<div className="max-w-full">
<div className="flex gap-3 items-center mb-3">
<input
className={inputClass}
placeholder="Filter by Z or symbol (e.g. 26 or Fe)"
value={q}
onChange={e => setQ(e.target.value)}
/>
<select
className={inputClass}
value={String(sortKey)}
onChange={e => setSortKey(e.target.value as keyof Row)}
>
{headers.map(h => (
<option key={String(h.key)} value={String(h.key)}>{h.label}</option>
))}
</select>
<button className={inputClass} onClick={() => setAsc(a => !a)}>
Sort: {asc ? "Asc" : "Desc"}
</button>
<a className={inputClass} href="/files/isotope_master_table_known_vs_predicted_with_gap.csv" download>
Download CSV
</a>
<a className={inputClass} href="/files/isotope_master_table_known_vs_predicted_with_gap.md" download>
Download MD
</a>
</div>
<div className="mb-2 text-sm">
<b>Totals (filtered):</b> Elements {totals.elements} • Known {totals.known} • Stable {totals.stable} • Unstable {totals.unstable} • Predicted {totals.predicted} • Gap {totals.gap}
</div>
<div className="overflow-auto border rounded">
<table className="min-w-[900px] w-full text-sm">
<thead>
<tr>
{headers.map(h => (
<th key={String(h.key)} className={th}>{h.label}</th>
))}
</tr>
</thead>
<tbody>
{filtered.map(r => (
<tr key={r.element_z}>
<td className={cell}>{r.element_z}</td>
<td className={cell}>{r.element_symbol}</td>
<td className={cell}>{r.known_isotopes}</td>
<td className={cell}>{r.stable_isotopes_strict}</td>
<td className={cell}>{r.unstable_isotopes}</td>
<td className={cell}>{r.predicted_isotopes_est}</td>
<td className={cell}>{r.gap_pred_minus_known}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
// Optional CSV loader helper if you serve files instead of an API
function csvToRows(text: string): Row[] {
const [head, ...lines] = text.split(/\r?\n/).filter(Boolean);
const cols = head.split(",");
const idx = (name: string) => cols.indexOf(name);
return lines.map(line => {
const t = line.split(",");
const label = t[idx("Element (Z)")];
const symbol = label.split(" ")[0];
const z = Number(label.match(/\((\d+)\)/)?.[1] || 0);
return {
element_z: z,
element_symbol: symbol,
known_isotopes: Number(t[idx("Isotopes Known")]),
stable_isotopes_strict: Number(t[idx("Stable")]),
unstable_isotopes: Number(t[idx("Unstable")]),
predicted_isotopes_est: Number(t[idx("Predicted Isotopes (est.)")]),
gap_pred_minus_known: Number(t[idx("Gap (Predicted - Known)")]),
};
});
}
Routing tips (Next.js):
- Put the CSV/MD in
/public/files/so the download links work. - Implement
/api/isotopesto return the JSON array (or just point the component to the CSV loader).
5) “Top gaps” endpoint (optional)
You’ll likely want to surface where the missing isotopes are largest:
SQL (Postgres):
SELECT element_z, element_symbol, known_isotopes, predicted_isotopes_est,
(predicted_isotopes_est - known_isotopes) AS gap
FROM isotopes_master
ORDER BY gap DESC
LIMIT 20;
6) Deployment checklist
- Host files (CSV/MD) under a SolveForce-served path (e.g.,
/files/...). - ETL CSV → DB with the DDL above (or serve CSV directly if DB isn’t needed).
- Drop in the React component; wire it to
/api/isotopesor to the CSV path. - QA the totals on the glass:
- Known = 3,269 • Stable (strict) = 273 • Predicted = 7,759 • Gap = 4,490
- (Optional) add filters for blocks (s/p/d/f) or ranges (e.g., Z=20–50).
If you want, I can also export a clean JSON payload (/mnt/data/isotopes_master.json) with explicit element_z and element_symbol keys so your backend doesn’t have to parse labels.