Awesome. I can’t directly plug into your SolveForce stack from here, but I packaged everything so it drops cleanly into your interface. Below is a tight “integration kit”—data contract, API/DB shape, and a ready-to-use React component you can paste into your app (sortable, filterable, with CSV/Markdown download buttons).


1) Data assets (ready now)

  • Master (Known vs Predicted + Gap) — CSV
    isotope_master_table_known_vs_predicted_with_gap.csvdownload
  • Master (Known vs Predicted + Gap) — Markdown
    isotope_master_table_known_vs_predicted_with_gap.mddownload
  • Predicted-Only Ledger — CSV
    isotope_predicted_only_Z1-118.csvdownload
  • Predicted-Only Ledger — Markdown
    isotope_predicted_only_Z1-118.mddownload
  • Strict Known Ledger — CSV
    isotope_master_summary_Z1-118_strict.csvdownload

2) Data contract (single table)

Table name: isotopes_master

columntypenotes
element_zINTatomic number (1–118)
element_symbolTEXT(3)“H”, “He”, …
element_labelTEXT“H (1)” exactly as in files
known_isotopesINTexperimentally observed, strict stable definition applied elsewhere
stable_isotopes_strictINTstrict IUPAC stable count
unstable_isotopesINTcomputed = known − stable
predicted_isotopes_estINTNeufcourt-scaled total (sums to 7,759)
gap_pred_minus_knownINTpredicted − 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/isotopes to 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

  1. Host files (CSV/MD) under a SolveForce-served path (e.g., /files/...).
  2. ETL CSV → DB with the DDL above (or serve CSV directly if DB isn’t needed).
  3. Drop in the React component; wire it to /api/isotopes or to the CSV path.
  4. QA the totals on the glass:
    • Known = 3,269 • Stable (strict) = 273 • Predicted = 7,759 • Gap = 4,490
  5. (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.