nds-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +186 -0
- package/bin/nds-mcp.js +33 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +13 -0
- package/dist/constants.js.map +1 -0
- package/dist/db/chargeRadii.d.ts +22 -0
- package/dist/db/chargeRadii.d.ts.map +1 -0
- package/dist/db/chargeRadii.js +75 -0
- package/dist/db/chargeRadii.js.map +1 -0
- package/dist/db/decayFeedings.d.ts +31 -0
- package/dist/db/decayFeedings.d.ts.map +1 -0
- package/dist/db/decayFeedings.js +41 -0
- package/dist/db/decayFeedings.js.map +1 -0
- package/dist/db/ensureDb.d.ts +20 -0
- package/dist/db/ensureDb.d.ts.map +1 -0
- package/dist/db/ensureDb.js +148 -0
- package/dist/db/ensureDb.js.map +1 -0
- package/dist/db/gammas.d.ts +39 -0
- package/dist/db/gammas.d.ts.map +1 -0
- package/dist/db/gammas.js +53 -0
- package/dist/db/gammas.js.map +1 -0
- package/dist/db/levels.d.ts +68 -0
- package/dist/db/levels.d.ts.map +1 -0
- package/dist/db/levels.js +121 -0
- package/dist/db/levels.js.map +1 -0
- package/dist/db/masses.d.ts +16 -0
- package/dist/db/masses.d.ts.map +1 -0
- package/dist/db/masses.js +22 -0
- package/dist/db/masses.js.map +1 -0
- package/dist/db/ndsDb.d.ts +10 -0
- package/dist/db/ndsDb.d.ts.map +1 -0
- package/dist/db/ndsDb.js +61 -0
- package/dist/db/ndsDb.js.map +1 -0
- package/dist/db/nubase.d.ts +21 -0
- package/dist/db/nubase.d.ts.map +1 -0
- package/dist/db/nubase.js +61 -0
- package/dist/db/nubase.js.map +1 -0
- package/dist/db/reactions.d.ts +11 -0
- package/dist/db/reactions.d.ts.map +1 -0
- package/dist/db/reactions.js +54 -0
- package/dist/db/reactions.js.map +1 -0
- package/dist/db/references.d.ts +11 -0
- package/dist/db/references.d.ts.map +1 -0
- package/dist/db/references.js +24 -0
- package/dist/db/references.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest/buildDb.d.ts +36 -0
- package/dist/ingest/buildDb.d.ts.map +1 -0
- package/dist/ingest/buildDb.js +933 -0
- package/dist/ingest/buildDb.js.map +1 -0
- package/dist/ingest/parseAme.d.ts +79 -0
- package/dist/ingest/parseAme.d.ts.map +1 -0
- package/dist/ingest/parseAme.js +186 -0
- package/dist/ingest/parseAme.js.map +1 -0
- package/dist/ingest/parseEnsdf.d.ts +210 -0
- package/dist/ingest/parseEnsdf.d.ts.map +1 -0
- package/dist/ingest/parseEnsdf.js +469 -0
- package/dist/ingest/parseEnsdf.js.map +1 -0
- package/dist/ingest/parseLaserRadii.d.ts +33 -0
- package/dist/ingest/parseLaserRadii.d.ts.map +1 -0
- package/dist/ingest/parseLaserRadii.js +210 -0
- package/dist/ingest/parseLaserRadii.js.map +1 -0
- package/dist/ingest/parseNubase.d.ts +40 -0
- package/dist/ingest/parseNubase.d.ts.map +1 -0
- package/dist/ingest/parseNubase.js +146 -0
- package/dist/ingest/parseNubase.js.map +1 -0
- package/dist/ingest/parseRadii.d.ts +17 -0
- package/dist/ingest/parseRadii.d.ts.map +1 -0
- package/dist/ingest/parseRadii.js +42 -0
- package/dist/ingest/parseRadii.js.map +1 -0
- package/dist/ingest/parseTunl.d.ts +48 -0
- package/dist/ingest/parseTunl.d.ts.map +1 -0
- package/dist/ingest/parseTunl.js +773 -0
- package/dist/ingest/parseTunl.js.map +1 -0
- package/dist/shared/errors.d.ts +20 -0
- package/dist/shared/errors.d.ts.map +1 -0
- package/dist/shared/errors.js +45 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +3 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/sqlite3Cli.d.ts +3 -0
- package/dist/shared/sqlite3Cli.d.ts.map +1 -0
- package/dist/shared/sqlite3Cli.js +106 -0
- package/dist/shared/sqlite3Cli.js.map +1 -0
- package/dist/tooling.d.ts +2 -0
- package/dist/tooling.d.ts.map +1 -0
- package/dist/tooling.js +2 -0
- package/dist/tooling.js.map +1 -0
- package/dist/tools/dispatcher.d.ts +11 -0
- package/dist/tools/dispatcher.d.ts.map +1 -0
- package/dist/tools/dispatcher.js +64 -0
- package/dist/tools/dispatcher.js.map +1 -0
- package/dist/tools/index.d.ts +3 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +3 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/mcpSchema.d.ts +3 -0
- package/dist/tools/mcpSchema.d.ts.map +1 -0
- package/dist/tools/mcpSchema.js +21 -0
- package/dist/tools/mcpSchema.js.map +1 -0
- package/dist/tools/registry.d.ts +22 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +309 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/utils/stdioHygiene.d.ts +7 -0
- package/dist/utils/stdioHygiene.d.ts.map +1 -0
- package/dist/utils/stdioHygiene.js +16 -0
- package/dist/utils/stdioHygiene.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for TUNL (Triangle Universities Nuclear Laboratory) energy level tables.
|
|
3
|
+
*
|
|
4
|
+
* Input: pdftotext -layout output from TUNL EL PDF files.
|
|
5
|
+
* Handles both even-A and odd-A formats, including:
|
|
6
|
+
* - Inline Jπ (8Be 2004 style)
|
|
7
|
+
* - Parity on separate line above OR below energy (12C 2017, 14N 1991)
|
|
8
|
+
* - Fractional Jπ for half-integer spins (5Li, 7Li, 11B, 15N)
|
|
9
|
+
* - Lifetime (τm) and width (Γ) with unit conversion
|
|
10
|
+
*
|
|
11
|
+
* Evaluation years: 1991, 1992, 1993, 1995, 1998, 2002, 2004, 2012, 2017.
|
|
12
|
+
*/
|
|
13
|
+
// ── Element → Z mapping (light nuclei only, A ≤ 20) ──────────────────────────
|
|
14
|
+
const ELEMENT_Z = {
|
|
15
|
+
n: 0, H: 1, He: 2, Li: 3, Be: 4, B: 5, C: 6, N: 7, O: 8, F: 9,
|
|
16
|
+
Ne: 10, Na: 11, Mg: 12,
|
|
17
|
+
};
|
|
18
|
+
function elementToZ(el) {
|
|
19
|
+
const z = ELEMENT_Z[el];
|
|
20
|
+
if (z !== undefined)
|
|
21
|
+
return z;
|
|
22
|
+
const tc = el.charAt(0).toUpperCase() + el.slice(1).toLowerCase();
|
|
23
|
+
const z2 = ELEMENT_Z[tc];
|
|
24
|
+
if (z2 !== undefined)
|
|
25
|
+
return z2;
|
|
26
|
+
throw new Error(`Unknown element for TUNL: ${el}`);
|
|
27
|
+
}
|
|
28
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
29
|
+
const HBAR_KEV_S = 6.582119569e-19; // ℏ in keV·s
|
|
30
|
+
const TIME_UNITS = {
|
|
31
|
+
s: 1, ms: 1e-3, us: 1e-6, 'μs': 1e-6, ns: 1e-9,
|
|
32
|
+
ps: 1e-12, fs: 1e-15, fsec: 1e-15, as: 1e-18,
|
|
33
|
+
};
|
|
34
|
+
const ENERGY_UNITS = {
|
|
35
|
+
keV: 1, MeV: 1000, eV: 0.001, meV: 1e-6,
|
|
36
|
+
};
|
|
37
|
+
// ── Header parsing ────────────────────────────────────────────────────────────
|
|
38
|
+
const TABLE_HEADER_RE = /Table\s+([\d.]+)\s+from\s+\((\d{4}\w+)\):\s*Energy\s+levels?\s+of\s+(\d+)\s+(\w+)/i;
|
|
39
|
+
function detectWidthUnit(text) {
|
|
40
|
+
const headerLines = text.split('\n').slice(0, 15);
|
|
41
|
+
for (let i = 0; i < headerLines.length; i++) {
|
|
42
|
+
const line = headerLines[i];
|
|
43
|
+
// Inline: "Γcm (MeV)" or "Γcm b (MeV)"
|
|
44
|
+
if (/Γcm\s*\w?\s*\(MeV\)/i.test(line))
|
|
45
|
+
return 'MeV';
|
|
46
|
+
// Inline: "Γcm (keV)" → explicit keV
|
|
47
|
+
if (/Γcm\s*\w?\s*\(keV\)/i.test(line))
|
|
48
|
+
return 'keV';
|
|
49
|
+
// Split-line: "Γcm" on this line, "(MeV)" on next line at similar column
|
|
50
|
+
if (/Γcm/.test(line) && i + 1 < headerLines.length) {
|
|
51
|
+
const gammaPos = line.indexOf('Γcm');
|
|
52
|
+
const nextLine = headerLines[i + 1];
|
|
53
|
+
const mevRe = /\(MeV\)/g;
|
|
54
|
+
let m;
|
|
55
|
+
while ((m = mevRe.exec(nextLine)) !== null) {
|
|
56
|
+
if (Math.abs(m.index - gammaPos) < 12)
|
|
57
|
+
return 'MeV';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return 'keV';
|
|
62
|
+
}
|
|
63
|
+
function parseTableHeader(text) {
|
|
64
|
+
const m = text.match(TABLE_HEADER_RE);
|
|
65
|
+
if (!m)
|
|
66
|
+
return null;
|
|
67
|
+
const sourceTable = `Table ${m[1]}`;
|
|
68
|
+
const evaluation = m[2];
|
|
69
|
+
const A = parseInt(m[3], 10);
|
|
70
|
+
const element = m[4];
|
|
71
|
+
const Z = elementToZ(element);
|
|
72
|
+
const defaultWidthUnit = detectWidthUnit(text);
|
|
73
|
+
return { sourceTable, evaluation, A, element, Z, defaultWidthUnit };
|
|
74
|
+
}
|
|
75
|
+
function stripFootnotes(s) {
|
|
76
|
+
// Remove trailing single-letter footnote markers: "1513 ± 15 i" → "1513 ± 15"
|
|
77
|
+
return s.replace(/\s+[a-z]$/g, '').replace(/\s+[a-z],/g, ',').trim();
|
|
78
|
+
}
|
|
79
|
+
export function parseWidth(raw, defaultUnit) {
|
|
80
|
+
const s = stripFootnotes(raw.trim());
|
|
81
|
+
if (!s || s === '-' || s === '–') {
|
|
82
|
+
return { width_keV: null, width_unc_keV: null, width_raw: null, width_relation: null, half_life: null };
|
|
83
|
+
}
|
|
84
|
+
if (/^stable$/i.test(s)) {
|
|
85
|
+
return { width_keV: null, width_unc_keV: null, width_raw: null, width_relation: null, half_life: 'stable' };
|
|
86
|
+
}
|
|
87
|
+
if (/^broad$/i.test(s)) {
|
|
88
|
+
return { width_keV: null, width_unc_keV: null, width_raw: s, width_relation: 'broad', half_life: null };
|
|
89
|
+
}
|
|
90
|
+
let cleaned = s;
|
|
91
|
+
// Strip leading Γ= or τm=
|
|
92
|
+
const isLifetimeExplicit = /^τm?\s*=/i.test(cleaned);
|
|
93
|
+
cleaned = cleaned.replace(/^(?:Γ|τm?)\s*=\s*/i, '').trim();
|
|
94
|
+
// Strip leading |g| = ... lines (magnetic moment, not width)
|
|
95
|
+
if (/^\|g\|/.test(cleaned)) {
|
|
96
|
+
return { width_keV: null, width_unc_keV: null, width_raw: null, width_relation: null, half_life: null };
|
|
97
|
+
}
|
|
98
|
+
// Detect time unit → it's a lifetime
|
|
99
|
+
if (isLifetimeExplicit || /\b(fs|fsec|ps|ns|[μu]s|ms)\b/.test(cleaned)) {
|
|
100
|
+
const lt = parseLifetime(cleaned);
|
|
101
|
+
return { ...lt, width_raw: s, width_relation: lt.width_keV !== null ? '=' : null };
|
|
102
|
+
}
|
|
103
|
+
// Upper limit: < value or ≤ value
|
|
104
|
+
const ltMatch = cleaned.match(/^([<≤])\s*([\d.]+)\s*(?:[×x]\s*10\s*[−\-]\s*(\d+))?\s*(\w+)?/);
|
|
105
|
+
if (ltMatch) {
|
|
106
|
+
const relation = ltMatch[1] === '≤' ? '≤' : '<';
|
|
107
|
+
let val = parseFloat(ltMatch[2]);
|
|
108
|
+
if (ltMatch[3])
|
|
109
|
+
val *= Math.pow(10, -parseInt(ltMatch[3], 10));
|
|
110
|
+
const unit = ltMatch[4] && ENERGY_UNITS[ltMatch[4]] ? ltMatch[4] : defaultUnit;
|
|
111
|
+
const factor = ENERGY_UNITS[unit];
|
|
112
|
+
return { width_keV: val * factor, width_unc_keV: null, width_raw: s, width_relation: relation, half_life: null };
|
|
113
|
+
}
|
|
114
|
+
// Detect leading ≈ or ~
|
|
115
|
+
const isApprox = /^[≈~]/.test(cleaned);
|
|
116
|
+
cleaned = cleaned.replace(/^[≈~]\s*/, '');
|
|
117
|
+
// Scientific notation: (value ± unc) × 10−exp [unit]
|
|
118
|
+
const sciMatch = cleaned.match(/\(?([\d.]+)\s*(?:±\s*([\d.]+))?\)?\s*[×x]\s*10\s*[−\-]\s*(\d+)\s*(\w+)?/);
|
|
119
|
+
if (sciMatch) {
|
|
120
|
+
const val = parseFloat(sciMatch[1]);
|
|
121
|
+
const unc = sciMatch[2] ? parseFloat(sciMatch[2]) : null;
|
|
122
|
+
const exp = parseInt(sciMatch[3], 10);
|
|
123
|
+
const unit = sciMatch[4] && ENERGY_UNITS[sciMatch[4]] ? sciMatch[4] : defaultUnit;
|
|
124
|
+
const factor = ENERGY_UNITS[unit];
|
|
125
|
+
const multiplier = Math.pow(10, -exp) * factor;
|
|
126
|
+
return {
|
|
127
|
+
width_keV: val * multiplier,
|
|
128
|
+
width_unc_keV: unc !== null ? unc * multiplier : null,
|
|
129
|
+
width_raw: s,
|
|
130
|
+
width_relation: isApprox ? '≈' : '=',
|
|
131
|
+
half_life: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
// Γcalc = value (calculated width, still in default unit)
|
|
135
|
+
const calcMatch = cleaned.match(/Γcalc\s*=\s*([\d.]+)/);
|
|
136
|
+
if (calcMatch) {
|
|
137
|
+
const val = parseFloat(calcMatch[1]);
|
|
138
|
+
const factor = ENERGY_UNITS[defaultUnit];
|
|
139
|
+
return { width_keV: val * factor, width_unc_keV: null, width_raw: s, width_relation: 'calc', half_life: null };
|
|
140
|
+
}
|
|
141
|
+
// Plain value ± unc with optional unit
|
|
142
|
+
const valMatch = cleaned.match(/^([\d.]+)\s*(?:±\s*([\d.]+))?\s*(meV|eV|keV|MeV)?/);
|
|
143
|
+
if (valMatch) {
|
|
144
|
+
const val = parseFloat(valMatch[1]);
|
|
145
|
+
const unc = valMatch[2] ? parseFloat(valMatch[2]) : null;
|
|
146
|
+
const unit = valMatch[3] && ENERGY_UNITS[valMatch[3]] ? valMatch[3] : defaultUnit;
|
|
147
|
+
const factor = ENERGY_UNITS[unit];
|
|
148
|
+
return {
|
|
149
|
+
width_keV: val * factor,
|
|
150
|
+
width_unc_keV: unc !== null ? unc * factor : null,
|
|
151
|
+
width_raw: s,
|
|
152
|
+
width_relation: isApprox ? '≈' : '=',
|
|
153
|
+
half_life: null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return { width_keV: null, width_unc_keV: null, width_raw: s, width_relation: null, half_life: null };
|
|
157
|
+
}
|
|
158
|
+
function parseLifetime(s) {
|
|
159
|
+
// Handle asymmetric uncertainties: "12+11\n-6 fs" → just take value
|
|
160
|
+
const m = s.match(/([\d.]+)\s*(?:±\s*([\d.]+))?\s*(fs|fsec|ps|ns|[μu]s|ms|s)\b/);
|
|
161
|
+
if (!m)
|
|
162
|
+
return { width_keV: null, width_unc_keV: null, half_life: s };
|
|
163
|
+
const val = parseFloat(m[1]);
|
|
164
|
+
const unc = m[2] ? parseFloat(m[2]) : null;
|
|
165
|
+
const unitKey = m[3];
|
|
166
|
+
const unitFactor = TIME_UNITS[unitKey] ?? TIME_UNITS[unitKey.replace('μ', 'u')];
|
|
167
|
+
if (!unitFactor)
|
|
168
|
+
return { width_keV: null, width_unc_keV: null, half_life: s };
|
|
169
|
+
const tau_s = val * unitFactor;
|
|
170
|
+
if (tau_s === 0)
|
|
171
|
+
return { width_keV: null, width_unc_keV: null, half_life: s };
|
|
172
|
+
const width_keV = HBAR_KEV_S / tau_s;
|
|
173
|
+
let width_unc_keV = null;
|
|
174
|
+
if (unc !== null) {
|
|
175
|
+
const tau_unc_s = unc * unitFactor;
|
|
176
|
+
width_unc_keV = width_keV * (tau_unc_s / tau_s);
|
|
177
|
+
}
|
|
178
|
+
return { width_keV, width_unc_keV, half_life: null };
|
|
179
|
+
}
|
|
180
|
+
export function parseEnergy(raw) {
|
|
181
|
+
let s = stripFootnotes(raw.trim()).replace(/[()]/g, '').trim();
|
|
182
|
+
if (!s)
|
|
183
|
+
return null;
|
|
184
|
+
if (/^g\.?s\.?$/i.test(s) || s === '0') {
|
|
185
|
+
return { energy_keV: 0, energy_unc_keV: null, energy_raw: raw.trim() };
|
|
186
|
+
}
|
|
187
|
+
// Remove leading ≈
|
|
188
|
+
s = s.replace(/^[≈~]\s*/, '');
|
|
189
|
+
const m = s.match(/^([\d.]+)\s*(?:±\s*([\d.]+))?/);
|
|
190
|
+
if (!m)
|
|
191
|
+
return null;
|
|
192
|
+
const energy_MeV = parseFloat(m[1]);
|
|
193
|
+
const energy_keV = energy_MeV * 1000;
|
|
194
|
+
// TUNL convention: "Ex (MeV ± keV)" → uncertainty is in keV
|
|
195
|
+
const energy_unc_keV = m[2] ? parseFloat(m[2]) : null;
|
|
196
|
+
return { energy_keV, energy_unc_keV, energy_raw: raw.trim() };
|
|
197
|
+
}
|
|
198
|
+
// ── Decay modes parsing ───────────────────────────────────────────────────────
|
|
199
|
+
function parseDecayModes(raw) {
|
|
200
|
+
if (!raw?.trim())
|
|
201
|
+
return null;
|
|
202
|
+
const s = stripFootnotes(raw.trim());
|
|
203
|
+
if (!s || s === '-' || s === '–')
|
|
204
|
+
return null;
|
|
205
|
+
if (/^stable$/i.test(s))
|
|
206
|
+
return 'stable';
|
|
207
|
+
// Extract known decay tokens.
|
|
208
|
+
// Greek letters (γ, α, π) are non-ASCII and \b doesn't work on them in non-Unicode mode.
|
|
209
|
+
// Use a combined regex: no \b around Greek, \b around ASCII tokens.
|
|
210
|
+
const tokens = [];
|
|
211
|
+
const re = /(γ|α|π|\b3\s*He\b|\b6\s*Li\b|\b2α|\b2p\b|\b2n\b|\bp\b|\bn\b|\bd\b|\bt\b)/gi;
|
|
212
|
+
let match;
|
|
213
|
+
while ((match = re.exec(s)) !== null) {
|
|
214
|
+
const tok = match[1].replace(/\s+/g, '');
|
|
215
|
+
if (!tokens.includes(tok))
|
|
216
|
+
tokens.push(tok);
|
|
217
|
+
}
|
|
218
|
+
return tokens.length > 0 ? tokens.join(', ') : null;
|
|
219
|
+
}
|
|
220
|
+
// ── Data line extraction ──────────────────────────────────────────────────────
|
|
221
|
+
function extractDataLines(text) {
|
|
222
|
+
const lines = text.split('\n');
|
|
223
|
+
const dataLines = [];
|
|
224
|
+
let inFootnotes = false;
|
|
225
|
+
for (const line of lines) {
|
|
226
|
+
const trimmed = line.trim();
|
|
227
|
+
if (!trimmed)
|
|
228
|
+
continue;
|
|
229
|
+
// Form-feed characters
|
|
230
|
+
if (trimmed === '\f' || trimmed.startsWith('\f')) {
|
|
231
|
+
inFootnotes = false;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
// New table header resets footnote mode
|
|
235
|
+
if (TABLE_HEADER_RE.test(trimmed)) {
|
|
236
|
+
inFootnotes = false;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (/\(continued\)/i.test(trimmed))
|
|
240
|
+
continue;
|
|
241
|
+
// Column headers
|
|
242
|
+
if (/^Ex[\s(]/i.test(trimmed))
|
|
243
|
+
continue;
|
|
244
|
+
if (/^\(MeV/i.test(trimmed))
|
|
245
|
+
continue;
|
|
246
|
+
if (/^J\s*π/i.test(trimmed))
|
|
247
|
+
continue;
|
|
248
|
+
// Footnotes: single lowercase letter followed by explanation text
|
|
249
|
+
if (/^[a-z]\s{1,4}[A-Z]/.test(trimmed) ||
|
|
250
|
+
/^[a-z]\s{1,4}(?:See|From|For|The|This|Γ|Newly|Situated|These|Primarily|With|Wide|Revisions|Mainly|Support|At|In|Possible|Adopted|Weighted|I\s)/i.test(trimmed)) {
|
|
251
|
+
inFootnotes = true;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (inFootnotes)
|
|
255
|
+
continue;
|
|
256
|
+
// Isolated page numbers
|
|
257
|
+
if (/^\d{1,2}$/.test(trimmed))
|
|
258
|
+
continue;
|
|
259
|
+
// Reaction continuation lines: digits, commas, spaces, parens (must have at least one comma)
|
|
260
|
+
if (/^[\d,\s()]+$/.test(trimmed) && trimmed.includes(','))
|
|
261
|
+
continue;
|
|
262
|
+
// "used in analysis" column subheader
|
|
263
|
+
if (/^\(used in analysis\)/i.test(trimmed))
|
|
264
|
+
continue;
|
|
265
|
+
dataLines.push(line);
|
|
266
|
+
}
|
|
267
|
+
return dataLines;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Split a line into columns by gaps of 3+ spaces.
|
|
271
|
+
* Each "word" (contiguous non-space) is found, then adjacent words
|
|
272
|
+
* separated by < 3 spaces are merged into the same column.
|
|
273
|
+
*/
|
|
274
|
+
function splitColumns(line) {
|
|
275
|
+
const segments = [];
|
|
276
|
+
const re = /(\S+)/g;
|
|
277
|
+
let m;
|
|
278
|
+
let lastEnd = 0;
|
|
279
|
+
while ((m = re.exec(line)) !== null) {
|
|
280
|
+
const gap = m.index - lastEnd;
|
|
281
|
+
if (segments.length === 0 || gap >= 3) {
|
|
282
|
+
segments.push({ text: m[1], startCol: m.index });
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
const prev = segments[segments.length - 1];
|
|
286
|
+
prev.text = line.substring(prev.startCol, m.index + m[1].length).trimEnd();
|
|
287
|
+
}
|
|
288
|
+
lastEnd = m.index + m[0].length;
|
|
289
|
+
}
|
|
290
|
+
return segments;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Check if first column in line looks like an energy value:
|
|
294
|
+
* g.s., 0, numeric values, with optional parens, ≈, uncertainty, footnote letters.
|
|
295
|
+
*/
|
|
296
|
+
function looksLikeEnergy(text) {
|
|
297
|
+
if (/^g\.?s\.?$/i.test(text))
|
|
298
|
+
return true;
|
|
299
|
+
if (text === '0')
|
|
300
|
+
return true;
|
|
301
|
+
// Strip parens and leading ≈
|
|
302
|
+
const cleaned = text.replace(/[()]/g, '').replace(/^[≈~]\s*/, '').trim();
|
|
303
|
+
if (!cleaned)
|
|
304
|
+
return false;
|
|
305
|
+
if (!/^\d/.test(cleaned))
|
|
306
|
+
return false;
|
|
307
|
+
// Numeric energy with optional uncertainty: "3.03 ± 10", "≈ 37", "18.91"
|
|
308
|
+
if (/^\d[\d.]*\s*(?:±\s*[\d.]+)?$/.test(cleaned))
|
|
309
|
+
return true;
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
function isEnergyLine(line) {
|
|
313
|
+
const cols = splitColumns(line);
|
|
314
|
+
if (cols.length === 0)
|
|
315
|
+
return null;
|
|
316
|
+
const first = cols[0];
|
|
317
|
+
// Energy value must start within first 20 characters (rejects continuation lines)
|
|
318
|
+
if (first.startCol > 20)
|
|
319
|
+
return null;
|
|
320
|
+
// Strip footnote markers from energy text
|
|
321
|
+
const text = stripFootnotes(first.text).trim();
|
|
322
|
+
if (!looksLikeEnergy(text))
|
|
323
|
+
return null;
|
|
324
|
+
// Reject if next column starts with energy unit (this is a width value, not Ex)
|
|
325
|
+
if (cols.length > 1 && /^(?:eV|keV|MeV|meV)\b/i.test(cols[1].text.trim()))
|
|
326
|
+
return null;
|
|
327
|
+
// Reject bare page/reaction numbers with no other content
|
|
328
|
+
if (/^\d{1,2}$/.test(text) && cols.length <= 1)
|
|
329
|
+
return null;
|
|
330
|
+
return { energyStr: text, restCols: cols.slice(1) };
|
|
331
|
+
}
|
|
332
|
+
// ── Jπ above/below detection ──────────────────────────────────────────────────
|
|
333
|
+
/**
|
|
334
|
+
* Check if line is a Jπ info line (typically above/below an energy line):
|
|
335
|
+
* - Odd-A: "3−", "3− 1", "(3−)", with optional leading footnote markers
|
|
336
|
+
* - Even-A parity: "+", "−"
|
|
337
|
+
*/
|
|
338
|
+
function isJpiInfoLine(line) {
|
|
339
|
+
const trimmed = line.trim();
|
|
340
|
+
if (!trimmed)
|
|
341
|
+
return null;
|
|
342
|
+
// Must have significant leading whitespace (not a data entry)
|
|
343
|
+
const leadingSpace = line.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
344
|
+
if (leadingSpace < 5)
|
|
345
|
+
return null;
|
|
346
|
+
// Strip leading footnote markers: "b 1+" → "1+"
|
|
347
|
+
let cleaned = trimmed.replace(/^[a-z]+[\s,]*/, '').trim();
|
|
348
|
+
if (!cleaned)
|
|
349
|
+
cleaned = trimmed;
|
|
350
|
+
// Strip trailing footnote markers: "3− 1 a" → "3− 1"
|
|
351
|
+
cleaned = cleaned.replace(/\s+[a-z]$/g, '').trim();
|
|
352
|
+
// Parity-only: just "+" or "−"
|
|
353
|
+
if (/^[+\-−]$/.test(cleaned))
|
|
354
|
+
return cleaned;
|
|
355
|
+
// Jπ numerator for odd-A: "3−", "3− 1", "(3−)", "3+ 1", etc.
|
|
356
|
+
if (/^\(?\s*\d+\s*[+\-−]\s*\)?\s*\d*\s*$/.test(cleaned))
|
|
357
|
+
return cleaned;
|
|
358
|
+
// Multiple Jπ options: "5+ 7+", "5+, 4−, 6−, 7+"
|
|
359
|
+
if (/^\(?\s*\d+\s*[+\-−][\s,\d+\-−]*\)?\s*$/.test(cleaned))
|
|
360
|
+
return cleaned;
|
|
361
|
+
// "T = 3/2" style on its own line
|
|
362
|
+
if (/^T\s*=/.test(cleaned))
|
|
363
|
+
return cleaned;
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Check if a line is a parity-only line (just "+" or "−" with leading whitespace).
|
|
368
|
+
*/
|
|
369
|
+
function isParityOnlyLine(line) {
|
|
370
|
+
const trimmed = line.trim();
|
|
371
|
+
if (/^[+\-−]$/.test(trimmed)) {
|
|
372
|
+
const leadingSpace = line.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
373
|
+
if (leadingSpace >= 5)
|
|
374
|
+
return trimmed;
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Expand pdftotext concatenated fractions for odd-A half-integer values.
|
|
380
|
+
* In PDF, stacked fractions like 3/2 are rendered as "32" or "23" by pdftotext.
|
|
381
|
+
* Only expands 2-digit numbers where one digit is 2 and the other is odd.
|
|
382
|
+
*/
|
|
383
|
+
function expandConcatFraction(s) {
|
|
384
|
+
return s.replace(/(?<![/\d])(\d)(\d)(?![/\d])/g, (match, d1, d2) => {
|
|
385
|
+
const n1 = parseInt(d1, 10);
|
|
386
|
+
const n2 = parseInt(d2, 10);
|
|
387
|
+
// numerator/2 pattern: e.g., "32" → "3/2"
|
|
388
|
+
if (n2 === 2 && n1 % 2 === 1)
|
|
389
|
+
return `${n1}/2`;
|
|
390
|
+
// reversed pattern: e.g., "23" → "3/2"
|
|
391
|
+
if (n1 === 2 && n2 % 2 === 1)
|
|
392
|
+
return `${n2}/2`;
|
|
393
|
+
return match;
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function parseJpiT(raw, jpiAbove, isOddA) {
|
|
397
|
+
const s = stripFootnotes(raw.trim()).replace(/−/g, '-');
|
|
398
|
+
// Check if jpiAbove is parity-only ("+", "−", "-")
|
|
399
|
+
const aboveTrimmed = jpiAbove?.trim().replace(/−/g, '-') ?? null;
|
|
400
|
+
const isParityOnlyAbove = aboveTrimmed !== null && /^[+\-]$/.test(aboveTrimmed);
|
|
401
|
+
// Odd-A with proper Jπ numerator above (not parity-only)
|
|
402
|
+
if (isOddA && jpiAbove && !isParityOnlyAbove) {
|
|
403
|
+
const above = aboveTrimmed;
|
|
404
|
+
// Parse above: "3-", "3- 1", "3+", "(3-)", "( 3- )", "1- 1"
|
|
405
|
+
const aboveMatch = above.match(/\(?\s*(\d+)\s*([+\-])?\s*\)?\s*(?:(\d+))?/);
|
|
406
|
+
if (!aboveMatch)
|
|
407
|
+
return { spin_parity: null, isospin: null };
|
|
408
|
+
const jNum = aboveMatch[1];
|
|
409
|
+
const parity = aboveMatch[2] || '';
|
|
410
|
+
const tNum = aboveMatch[3] ?? null;
|
|
411
|
+
// Parse current line: "2", "2 ; 2", "2 ;2", "; 2"
|
|
412
|
+
const denomMatch = s.match(/^;?\s*(\d+)(?:\s*;?\s*(\d+))?/);
|
|
413
|
+
const jDenom = denomMatch?.[1] ?? '2';
|
|
414
|
+
const jp = jDenom === '1'
|
|
415
|
+
? `${jNum}${parity}`
|
|
416
|
+
: `${jNum}/${jDenom}${parity}`;
|
|
417
|
+
// T: look for T denominator
|
|
418
|
+
let isospin = null;
|
|
419
|
+
if (tNum) {
|
|
420
|
+
const tDenomMatch = s.match(/;\s*(\d+)/);
|
|
421
|
+
if (tDenomMatch) {
|
|
422
|
+
isospin = tDenomMatch[1] === '1' ? tNum : `${tNum}/${tDenomMatch[1]}`;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return { spin_parity: jp, isospin };
|
|
426
|
+
}
|
|
427
|
+
// Even-A, inline format, or odd-A with parity-only/no above line
|
|
428
|
+
// Extract T (after semicolon)
|
|
429
|
+
let isospin = null;
|
|
430
|
+
const tMatch = s.match(/;\s*T?\s*=?\s*([\d(/)+\-\s]+?)(?:\s{2,}|$)/);
|
|
431
|
+
if (tMatch) {
|
|
432
|
+
let tVal = tMatch[1].trim().replace(/\s+/g, '');
|
|
433
|
+
// Remove trailing footnote markers from T
|
|
434
|
+
tVal = tVal.replace(/[a-z]$/, '');
|
|
435
|
+
if (isOddA)
|
|
436
|
+
tVal = expandConcatFraction(tVal);
|
|
437
|
+
isospin = tVal || null;
|
|
438
|
+
}
|
|
439
|
+
// Also check for standalone "T = N" patterns
|
|
440
|
+
const tStandalone = s.match(/T\s*=\s*(\d+(?:\/\d+)?)/);
|
|
441
|
+
if (tStandalone && !isospin) {
|
|
442
|
+
isospin = tStandalone[1];
|
|
443
|
+
}
|
|
444
|
+
// Extract Jπ (before semicolon if present)
|
|
445
|
+
const jpiPart = s.split(';')[0].trim();
|
|
446
|
+
if (!jpiPart)
|
|
447
|
+
return { spin_parity: null, isospin };
|
|
448
|
+
// If jpiPart is just a T assignment (e.g., "T = 1", "(T = 3/2)"), Jπ is unknown
|
|
449
|
+
if (/^\(?T\s*=/i.test(jpiPart))
|
|
450
|
+
return { spin_parity: null, isospin };
|
|
451
|
+
let jp = jpiPart;
|
|
452
|
+
// Remove outer parens for simple cases
|
|
453
|
+
if (jp.startsWith('(') && jp.endsWith(')') && !jp.includes(',')) {
|
|
454
|
+
jp = jp.slice(1, -1).trim();
|
|
455
|
+
}
|
|
456
|
+
// For odd-A inline: expand pdftotext concatenated fractions
|
|
457
|
+
// e.g., "(32)-" → "(3/2)-", "52" → "5/2"
|
|
458
|
+
if (isOddA) {
|
|
459
|
+
jp = expandConcatFraction(jp);
|
|
460
|
+
}
|
|
461
|
+
// Apply parity from above line (even-A parity-separate or odd-A parity-only)
|
|
462
|
+
if (aboveTrimmed && /^[+\-]$/.test(aboveTrimmed) && !/[+\-]/.test(jp)) {
|
|
463
|
+
jp = jp.replace(/\s+/g, '') + aboveTrimmed;
|
|
464
|
+
}
|
|
465
|
+
else if (jpiAbove && !isOddA) {
|
|
466
|
+
const parityAbove = aboveTrimmed;
|
|
467
|
+
if (/^[+\-]$/.test(parityAbove) && !/[+\-]/.test(jp)) {
|
|
468
|
+
jp = jp.replace(/\s+/g, '') + parityAbove;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return { spin_parity: jp.replace(/\s+/g, '') || null, isospin };
|
|
472
|
+
}
|
|
473
|
+
// ── Column content classification ─────────────────────────────────────────────
|
|
474
|
+
function looksLikeJpi(s) {
|
|
475
|
+
const t = s.trim().replace(/−/g, '-');
|
|
476
|
+
if (!t)
|
|
477
|
+
return false;
|
|
478
|
+
// Contains parity symbols (+/-)
|
|
479
|
+
if (/[+\-]/.test(t) && !/±/.test(t))
|
|
480
|
+
return true;
|
|
481
|
+
// Contains semicolon (Jπ;T separator)
|
|
482
|
+
if (t.includes(';'))
|
|
483
|
+
return true;
|
|
484
|
+
// "T = N" style or "(T = N)"
|
|
485
|
+
if (/^\(?T\s*=/i.test(t))
|
|
486
|
+
return true;
|
|
487
|
+
// Small integer 0-9 for spin (like "2" for J denominator)
|
|
488
|
+
if (/^\(?\d{1,2}\)?$/.test(t))
|
|
489
|
+
return true;
|
|
490
|
+
// Multi-option: "(5+, 4-, 6-, 7+)"
|
|
491
|
+
if (/\([^)]*[+\-][^)]*\)/.test(t))
|
|
492
|
+
return true;
|
|
493
|
+
// Contains ≥ (like "≥ 3/2") — Jπ constraint
|
|
494
|
+
if (/≥\s*\d/.test(t))
|
|
495
|
+
return true;
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
function looksLikeDecay(s) {
|
|
499
|
+
const t = s.trim();
|
|
500
|
+
if (/^stable$/i.test(t))
|
|
501
|
+
return true;
|
|
502
|
+
// Contains known decay particle symbols and mostly those + commas/parens/spaces
|
|
503
|
+
if (/[γα]/.test(t))
|
|
504
|
+
return true;
|
|
505
|
+
// "p, n", "n, α", "p", "γ, p, α" etc — short strings of decay tokens
|
|
506
|
+
if (/^[\s,()]*(?:p|n|d|t|π|3\s*He|2[αpn]|6\s*Li)[\s,()pndtγα3He2]*$/i.test(t) && t.length < 40)
|
|
507
|
+
return true;
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
function looksLikeWidth(s) {
|
|
511
|
+
const t = s.trim();
|
|
512
|
+
if (t === '-' || t === '–')
|
|
513
|
+
return true;
|
|
514
|
+
if (/^stable$/i.test(t))
|
|
515
|
+
return true;
|
|
516
|
+
if (/^broad$/i.test(t))
|
|
517
|
+
return true;
|
|
518
|
+
if (/(?:eV|keV|MeV|meV|fs|fsec|ps|ns)\b/i.test(t))
|
|
519
|
+
return true;
|
|
520
|
+
if (/^[<≤≈~]/.test(t))
|
|
521
|
+
return true;
|
|
522
|
+
if (/^(?:Γ|τ)/i.test(t))
|
|
523
|
+
return true;
|
|
524
|
+
if (/^\|g\|/.test(t))
|
|
525
|
+
return true;
|
|
526
|
+
// Numeric value with optional ± (could be width in default units)
|
|
527
|
+
if (/^\d[\d.]*\s*(?:±\s*[\d.]+)?$/.test(stripFootnotes(t)))
|
|
528
|
+
return true;
|
|
529
|
+
// Scientific notation
|
|
530
|
+
if (/×\s*10/.test(t))
|
|
531
|
+
return true;
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
function looksLikeReactions(s) {
|
|
535
|
+
// Comma-separated numbers, possibly with parens
|
|
536
|
+
return /^[\d,\s()]+$/.test(s.trim()) && /\d/.test(s);
|
|
537
|
+
}
|
|
538
|
+
// ── Unified parser ────────────────────────────────────────────────────────────
|
|
539
|
+
function parseEntries(dataLines, meta) {
|
|
540
|
+
const rows = [];
|
|
541
|
+
const isOddA = meta.A % 2 === 1;
|
|
542
|
+
for (let i = 0; i < dataLines.length; i++) {
|
|
543
|
+
const line = dataLines[i];
|
|
544
|
+
const energyResult = isEnergyLine(line);
|
|
545
|
+
if (!energyResult)
|
|
546
|
+
continue;
|
|
547
|
+
const energy = parseEnergy(energyResult.energyStr);
|
|
548
|
+
if (!energy)
|
|
549
|
+
continue;
|
|
550
|
+
// Check line above for Jπ info
|
|
551
|
+
let jpiAbove = null;
|
|
552
|
+
if (i > 0) {
|
|
553
|
+
jpiAbove = isJpiInfoLine(dataLines[i - 1]);
|
|
554
|
+
}
|
|
555
|
+
// Assign columns from the energy line
|
|
556
|
+
const restCols = energyResult.restCols;
|
|
557
|
+
let jpiCol = '';
|
|
558
|
+
let widthCol = '';
|
|
559
|
+
let decayCol = '';
|
|
560
|
+
// Classify remaining columns by content
|
|
561
|
+
assignColumns(restCols, (j, w, d) => { jpiCol = j; widthCol = w; decayCol = d; });
|
|
562
|
+
// For even-A: check for "parity below" pattern
|
|
563
|
+
// If Jπ has no parity character, look at next data line for parity
|
|
564
|
+
if (!isOddA && jpiCol && !/[+\-−]/.test(jpiCol)) {
|
|
565
|
+
if (i + 1 < dataLines.length) {
|
|
566
|
+
const parity = isParityOnlyLine(dataLines[i + 1]);
|
|
567
|
+
if (parity) {
|
|
568
|
+
jpiAbove = parity; // pass as "above" — parseJpiT treats it the same
|
|
569
|
+
// If we had no width/decay on the energy line, look 2 lines ahead
|
|
570
|
+
if (!widthCol && !decayCol && i + 2 < dataLines.length) {
|
|
571
|
+
const belowLine = dataLines[i + 2];
|
|
572
|
+
if (!isEnergyLine(belowLine) && !isJpiInfoLine(belowLine)) {
|
|
573
|
+
const belowCols = splitColumns(belowLine);
|
|
574
|
+
// These are width + decay columns
|
|
575
|
+
for (const col of belowCols) {
|
|
576
|
+
const t = col.text.trim();
|
|
577
|
+
if (looksLikeReactions(t))
|
|
578
|
+
continue;
|
|
579
|
+
if (!widthCol && looksLikeWidth(t)) {
|
|
580
|
+
widthCol = t;
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (!decayCol && looksLikeDecay(t)) {
|
|
584
|
+
decayCol = t;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (!widthCol) {
|
|
588
|
+
widthCol = t;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (!decayCol) {
|
|
592
|
+
decayCol = t;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// For odd-A: check next line for ";T_denom + width + decay"
|
|
601
|
+
if (isOddA && i + 1 < dataLines.length) {
|
|
602
|
+
const nextLine = dataLines[i + 1];
|
|
603
|
+
if (!isJpiInfoLine(nextLine) && !isEnergyLine(nextLine)) {
|
|
604
|
+
const nextCols = splitColumns(nextLine);
|
|
605
|
+
let startIdx = 0;
|
|
606
|
+
// Check if first column is ";T_denom"
|
|
607
|
+
if (nextCols.length > 0 && /^;\s*\d+/.test(nextCols[0].text)) {
|
|
608
|
+
jpiCol = jpiCol + ' ' + nextCols[0].text;
|
|
609
|
+
startIdx = 1;
|
|
610
|
+
}
|
|
611
|
+
// Fill in missing width/decay from next line
|
|
612
|
+
for (let ci = startIdx; ci < nextCols.length; ci++) {
|
|
613
|
+
const t = nextCols[ci].text.trim();
|
|
614
|
+
if (looksLikeReactions(t))
|
|
615
|
+
continue;
|
|
616
|
+
if (!widthCol && looksLikeWidth(t)) {
|
|
617
|
+
widthCol = t;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (!decayCol && looksLikeDecay(t)) {
|
|
621
|
+
decayCol = t;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (!widthCol) {
|
|
625
|
+
widthCol = t;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (!decayCol) {
|
|
629
|
+
decayCol = t;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
// For even-A with no width/decay on energy line and no parity-below,
|
|
635
|
+
// still check next line for width/decay
|
|
636
|
+
if (!isOddA && !widthCol && !decayCol && i + 1 < dataLines.length) {
|
|
637
|
+
const nextLine = dataLines[i + 1];
|
|
638
|
+
if (!isEnergyLine(nextLine) && !isJpiInfoLine(nextLine) && !isParityOnlyLine(nextLine)) {
|
|
639
|
+
// Check if it could be a width/decay continuation
|
|
640
|
+
const leadSpace = nextLine.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
641
|
+
if (leadSpace >= 25) {
|
|
642
|
+
const nextCols = splitColumns(nextLine);
|
|
643
|
+
for (const col of nextCols) {
|
|
644
|
+
const t = col.text.trim();
|
|
645
|
+
if (looksLikeReactions(t))
|
|
646
|
+
continue;
|
|
647
|
+
if (!widthCol && looksLikeWidth(t)) {
|
|
648
|
+
widthCol = t;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (!decayCol && looksLikeDecay(t)) {
|
|
652
|
+
decayCol = t;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (!widthCol) {
|
|
656
|
+
widthCol = t;
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (!decayCol) {
|
|
660
|
+
decayCol = t;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
// Parse Jπ;T
|
|
667
|
+
const jpiT = parseJpiT(jpiCol, jpiAbove, isOddA);
|
|
668
|
+
// Parse width
|
|
669
|
+
const width = parseWidth(widthCol, meta.defaultWidthUnit);
|
|
670
|
+
// Parse decay modes
|
|
671
|
+
let halfLife = width.half_life;
|
|
672
|
+
let decayModes = parseDecayModes(decayCol);
|
|
673
|
+
if (/stable/i.test(decayCol))
|
|
674
|
+
halfLife = 'stable';
|
|
675
|
+
if (width.half_life === 'stable' && !decayModes)
|
|
676
|
+
decayModes = 'stable';
|
|
677
|
+
// Check if width column had "stable" text (as in "stable" being in the Γ column)
|
|
678
|
+
if (/stable/i.test(widthCol) && !decayModes)
|
|
679
|
+
decayModes = 'stable';
|
|
680
|
+
rows.push({
|
|
681
|
+
Z: meta.Z,
|
|
682
|
+
A: meta.A,
|
|
683
|
+
element: meta.element,
|
|
684
|
+
energy_keV: energy.energy_keV,
|
|
685
|
+
energy_unc_keV: energy.energy_unc_keV,
|
|
686
|
+
energy_raw: energy.energy_raw,
|
|
687
|
+
spin_parity: jpiT.spin_parity,
|
|
688
|
+
isospin: jpiT.isospin,
|
|
689
|
+
width_keV: width.width_keV,
|
|
690
|
+
width_unc_keV: width.width_unc_keV,
|
|
691
|
+
width_raw: width.width_raw,
|
|
692
|
+
width_relation: width.width_relation,
|
|
693
|
+
half_life: halfLife,
|
|
694
|
+
decay_modes: decayModes,
|
|
695
|
+
evaluation: meta.evaluation,
|
|
696
|
+
table_label: meta.sourceTable,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
return rows;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Assign restCols to jpi, width, decay slots using content classification.
|
|
703
|
+
* Columns appear in order: Jπ;T, Γ, Decay, Reactions.
|
|
704
|
+
* Some may be absent, causing later columns to shift left.
|
|
705
|
+
* The first column is Jπ;T ONLY if it looks like a Jπ value.
|
|
706
|
+
*/
|
|
707
|
+
function assignColumns(cols, assign) {
|
|
708
|
+
let jpi = '';
|
|
709
|
+
let width = '';
|
|
710
|
+
let decay = '';
|
|
711
|
+
if (cols.length === 0) {
|
|
712
|
+
assign('', '', '');
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
let startIdx = 0;
|
|
716
|
+
const firstText = cols[0].text.trim();
|
|
717
|
+
// First column is Jπ;T only if it actually looks like one
|
|
718
|
+
if (looksLikeJpi(firstText)) {
|
|
719
|
+
jpi = cols[0].text;
|
|
720
|
+
startIdx = 1;
|
|
721
|
+
}
|
|
722
|
+
// Process remaining columns
|
|
723
|
+
for (let i = startIdx; i < cols.length; i++) {
|
|
724
|
+
const text = cols[i].text.trim();
|
|
725
|
+
if (looksLikeReactions(text))
|
|
726
|
+
break; // everything after is reactions
|
|
727
|
+
if (!width && (looksLikeWidth(text) || (!jpi && !looksLikeDecay(text)))) {
|
|
728
|
+
width = text;
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (!decay && looksLikeDecay(text)) {
|
|
732
|
+
decay = text;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
// Ambiguous — assign to first empty slot
|
|
736
|
+
if (!width) {
|
|
737
|
+
width = text;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (!decay) {
|
|
741
|
+
decay = text;
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
assign(jpi, width, decay);
|
|
746
|
+
}
|
|
747
|
+
// ── Main entry point ──────────────────────────────────────────────────────────
|
|
748
|
+
export function parseTunlLevels(text) {
|
|
749
|
+
const meta = parseTableHeader(text);
|
|
750
|
+
if (!meta) {
|
|
751
|
+
console.error('TUNL: could not parse table header');
|
|
752
|
+
return [];
|
|
753
|
+
}
|
|
754
|
+
const dataLines = extractDataLines(text);
|
|
755
|
+
if (dataLines.length === 0)
|
|
756
|
+
return [];
|
|
757
|
+
return parseEntries(dataLines, meta);
|
|
758
|
+
}
|
|
759
|
+
export function parseTunlLevelsWithMeta(text, Z, A, element, evaluation, sourceTable) {
|
|
760
|
+
const meta = {
|
|
761
|
+
sourceTable,
|
|
762
|
+
evaluation,
|
|
763
|
+
A,
|
|
764
|
+
element,
|
|
765
|
+
Z,
|
|
766
|
+
defaultWidthUnit: detectWidthUnit(text),
|
|
767
|
+
};
|
|
768
|
+
const dataLines = extractDataLines(text);
|
|
769
|
+
if (dataLines.length === 0)
|
|
770
|
+
return [];
|
|
771
|
+
return parseEntries(dataLines, meta);
|
|
772
|
+
}
|
|
773
|
+
//# sourceMappingURL=parseTunl.js.map
|