From b90e002e398c4533318af8d219ec51ae8c45b5b3 Mon Sep 17 00:00:00 2001 From: Enrique Miralles-Dolz Date: Thu, 21 May 2026 22:01:50 -0500 Subject: [PATCH] adds ENDF-manual-backed reference lookups --- CHANGELOG.md | 9 + README.md | 28 +- endfReference.js | 570 +++++++++++++++++++++++++++++++++++++ extension.js | 276 ++++++------------ package.json | 3 + test/endfReference.test.js | 61 ++++ 6 files changed, 751 insertions(+), 196 deletions(-) create mode 100644 endfReference.js create mode 100644 test/endfReference.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index a91713a..bd0f26c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this extension will be documented in this file. +## Unreleased + +- Added manual-backed ENDF reference hovers for `NSUB`, `NLIB`, `INT`, `LR`, and + common record types such as `TAB1`, `CONT`, and `SEND`. +- Expanded `MF` and `MT` descriptions through a shared curated reference module. +- Added lightweight completions for common ENDF lookup codes. +- Added Node-based unit tests for reference lookup behavior and ENDF number + parsing. + ## 0.0.2 - Updated the extension icon package asset to preserve transparent rounded corners. diff --git a/README.md b/README.md index 6a388bd..4648055 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,22 @@ the status bar; hover shows the fuller explanation. - common `MF` and `MT` numbers are expanded to their ENDF-6 descriptions - textual references such as `MF=3`, `MF3`, `MF 12`, `MT=102`, `MT1`, and `MT=52-82` are also explained +- manual-backed references for `NSUB`, `NLIB`, `INT`, `LR`, and ENDF record + types such as `TEXT`, `CONT`, `HEAD`, `LIST`, `TAB1`, `TAB2`, `INTG`, + `SEND`, `FEND`, `MEND`, and `TEND` are explained in hovers + +## Reference Completions + +The extension provides lightweight completions for common ENDF manual lookup +codes. In ENDF documents, type a supported key followed by `=` or a space to see +curated suggestions: + +- `MF=` for file-type numbers +- `MT=` for common reaction type numbers and important ranges +- `NSUB=` for sublibrary numbers +- `NLIB=` for library identifiers +- `INT=` for interpolation laws +- `LR=` for residual breakup flags Payload field meanings are often record-specific, so the hover reports the reliable fixed-field role unless a standard `MF`/`MT` code description applies. @@ -91,9 +107,15 @@ not included in the packaged extension. ## Development -The extension entry point is `extension.js`, the language contribution is -declared in `package.json`, and the TextMate grammar is in -`syntaxes/endf.tmLanguage.json`. +The extension entry point is `extension.js`, the manual-backed reference data is +curated in `endfReference.js`, the language contribution is declared in +`package.json`, and the TextMate grammar is in `syntaxes/endf.tmLanguage.json`. + +Run the lightweight unit tests with: + +```shell +npm test +``` To test changes, open the repository in VS Code and use the provided `.vscode/launch.json` configuration. Reload the Extension Development Host diff --git a/endfReference.js b/endfReference.js new file mode 100644 index 0000000..0d6337d --- /dev/null +++ b/endfReference.js @@ -0,0 +1,570 @@ +const MF_DESCRIPTIONS = new Map([ + [0, 'End-record marker used with SEND, FEND, MEND, or TEND records.'], + [1, 'General information.'], + [2, 'Resonance parameter data.'], + [3, 'Reaction cross sections.'], + [4, 'Angular distributions of emitted particles.'], + [5, 'Energy distributions of emitted particles.'], + [6, 'Product energy-angle distributions.'], + [7, 'Thermal neutron scattering law data.'], + [8, 'Radioactivity and fission-product yield data.'], + [9, 'Multiplicities for radioactive nuclide production.'], + [10, 'Cross sections for radioactive nuclide production.'], + [12, 'Photon production multiplicities and transition probability arrays.'], + [13, 'Photon production cross sections.'], + [14, 'Photon angular distributions.'], + [15, 'Continuous photon energy spectra.'], + [23, 'Photo- or electro-atomic interaction cross sections.'], + [26, 'Electro-atomic angle and energy distributions.'], + [27, 'Atomic form factors or scattering functions.'], + [28, 'Atomic relaxation data.'], + [30, 'Data covariances from parameter covariances and sensitivities.'], + [31, 'Covariances of average neutron multiplicities.'], + [32, 'Covariances of resonance parameters.'], + [33, 'Covariances of reaction cross sections.'], + [34, 'Covariances of angular distributions.'], + [35, 'Covariances of energy distributions.'], + [39, 'Covariances of radionuclide production yields.'], + [40, 'Covariances of radionuclide production cross sections.'] +]); + +const MT_DESCRIPTIONS = new Map([ + [0, 'End-record marker used with SEND, FEND, MEND, or TEND records.'], + [1, 'Neutron total cross section.'], + [2, 'Elastic scattering cross section.'], + [3, 'Nonelastic cross section.'], + [4, 'Production of one neutron; sum of discrete and continuum neutron-production sections.'], + [5, 'Sum of reactions not otherwise represented by another MT number.'], + [10, 'Total continuum reaction, used for derived files.'], + [11, 'Production of two neutrons and a deuteron, plus a residual.'], + [16, 'Production of two neutrons, usually written (n,2n).'], + [17, 'Production of three neutrons, usually written (n,3n).'], + [18, 'Particle-induced fission.'], + [19, 'First-chance neutron-induced fission.'], + [20, 'Second-chance neutron-induced fission.'], + [21, 'Third-chance neutron-induced fission.'], + [22, 'Production of a neutron and an alpha particle, plus a residual.'], + [23, 'Production of a neutron and three alpha particles, plus a residual.'], + [24, 'Production of two neutrons and an alpha particle, plus a residual.'], + [25, 'Production of three neutrons and an alpha particle, plus a residual.'], + [27, 'Absorption reaction sum; rarely used.'], + [28, 'Production of a neutron and a proton, plus a residual.'], + [29, 'Production of a neutron and two alpha particles, plus a residual.'], + [30, 'Production of two neutrons and two alpha particles, plus a residual.'], + [32, 'Production of a neutron and a deuteron, plus a residual.'], + [33, 'Production of a neutron and a triton, plus a residual.'], + [34, 'Production of a neutron and a He-3 particle, plus a residual.'], + [35, 'Production of a neutron, a deuteron, and two alpha particles, plus a residual.'], + [36, 'Production of a neutron, a triton, and two alpha particles, plus a residual.'], + [37, 'Production of four neutrons, plus a residual.'], + [38, 'Fourth-chance neutron-induced fission.'], + [41, 'Production of two neutrons and a proton, plus a residual.'], + [42, 'Production of three neutrons and a proton, plus a residual.'], + [44, 'Production of a neutron and two protons, plus a residual.'], + [45, 'Production of a neutron, a proton, and an alpha particle, plus a residual.'], + [50, 'Production of a neutron leaving the residual nucleus in the ground state.'], + [91, 'Continuum neutron production not included in the discrete representation.'], + [101, 'Neutron disappearance reaction sum; rarely used.'], + [102, 'Radiative capture.'], + [103, 'Production of a proton, plus a residual.'], + [104, 'Production of a deuteron, plus a residual.'], + [105, 'Production of a triton, plus a residual.'], + [106, 'Production of a He-3 particle, plus a residual.'], + [107, 'Production of an alpha particle, plus a residual.'], + [108, 'Production of two alpha particles, plus a residual.'], + [109, 'Production of three alpha particles, plus a residual.'], + [111, 'Production of two protons, plus a residual.'], + [112, 'Production of a proton and an alpha particle, plus a residual.'], + [113, 'Production of a triton and two alpha particles, plus a residual.'], + [114, 'Production of a deuteron and two alpha particles, plus a residual.'], + [115, 'Production of a proton and a deuteron, plus a residual.'], + [116, 'Production of a proton and a triton, plus a residual.'], + [117, 'Production of a deuteron and an alpha particle, plus a residual.'], + [151, 'Resonance parameters used to calculate cross sections in resolved and unresolved energy regions.'], + [451, 'Heading, title information, descriptive data, and directory information.'], + [452, 'Average total number of neutrons released per fission event.'], + [454, 'Independent fission product yield data.'], + [455, 'Average number of delayed neutrons released per fission event.'], + [456, 'Average number of prompt neutrons released per fission event.'], + [457, 'Radioactive decay data.'], + [458, 'Energy release in fission for incident neutrons.'], + [459, 'Cumulative fission product yield data.'], + [460, 'Delayed fission photons.'], + [500, 'Total charged-particle stopping power.'], + [501, 'Total photo- or electro-atomic interaction.'], + [502, 'Photon coherent scattering.'], + [504, 'Photon incoherent scattering.'], + [505, 'Imaginary scattering factor.'], + [506, 'Real scattering factor.'], + [515, 'Pair production in the electron field.'], + [516, 'Pair production sum of MT=515 and MT=517.'], + [517, 'Pair production in the nuclear field.'], + [522, 'Ionization, summed over all subshells.'], + [523, 'Photo-excitation cross section.'], + [525, 'Large angle scattering.'], + [526, 'Total electro-atomic scattering.'], + [527, 'Electro-atomic bremsstrahlung.'], + [528, 'Electro-atomic excitation cross section.'], + [533, 'Atomic relaxation data.'] +]); + +const MT_RANGES = [ + { + start: 51, + end: 90, + label: '51-90', + description: (value) => `Production of a neutron with the residual nucleus in excited level ${value - 50}.` + }, + { + start: 152, + end: 181, + label: '152-181', + description: 'Additional open reaction channels for higher incident energies.' + }, + { + start: 191, + end: 193, + label: '191-193', + description: 'Additional charged-particle production reaction channels.' + }, + { + start: 203, + end: 207, + label: '203-207', + description: 'Total gas-production reactions in derived files.' + }, + { + start: 209, + end: 218, + label: '209-218', + description: 'High-energy total particle-production reactions.' + }, + { + start: 251, + end: 253, + label: '251-253', + description: 'Derived elastic-scattering quantities for neutron data.' + }, + { + start: 301, + end: 450, + label: '301-450', + description: (value) => `Energy-release parameter for reaction MT=${value - 300}; used in derived files.` + }, + { + start: 534, + end: 572, + label: '534-572', + description: 'Photoelectric or electro-atomic subshell cross section.' + }, + { + start: 600, + end: 648, + label: '600-648', + description: (value) => `Production of a proton leaving the residual nucleus in level ${value - 600}.` + }, + { + start: 649, + end: 649, + label: '649', + description: 'Production of a proton in the continuum.' + }, + { + start: 650, + end: 698, + label: '650-698', + description: (value) => `Production of a deuteron leaving the residual nucleus in level ${value - 650}.` + }, + { + start: 699, + end: 699, + label: '699', + description: 'Production of a deuteron in the continuum.' + }, + { + start: 700, + end: 748, + label: '700-748', + description: (value) => `Production of a triton leaving the residual nucleus in level ${value - 700}.` + }, + { + start: 749, + end: 749, + label: '749', + description: 'Production of a triton in the continuum.' + }, + { + start: 750, + end: 798, + label: '750-798', + description: (value) => `Production of a He-3 particle leaving the residual nucleus in level ${value - 750}.` + }, + { + start: 799, + end: 799, + label: '799', + description: 'Production of a He-3 particle in the continuum.' + }, + { + start: 800, + end: 848, + label: '800-848', + description: (value) => `Production of an alpha particle leaving the residual nucleus in level ${value - 800}.` + }, + { + start: 849, + end: 849, + label: '849', + description: 'Production of an alpha particle in the continuum.' + }, + { + start: 851, + end: 870, + label: '851-870', + description: 'Lumped reaction covariance or derived reaction grouping.' + }, + { + start: 875, + end: 890, + label: '875-890', + description: (value) => `Production of two neutrons with the residual nucleus in level ${value - 875}.` + }, + { + start: 891, + end: 891, + label: '891', + description: 'Production of two neutrons in the continuum.' + } +]; + +const NSUB_DESCRIPTIONS = new Map([ + [0, 'Photo-nuclear data.'], + [1, 'Photo-induced fission product yields.'], + [3, 'Photo-atomic interaction data.'], + [4, 'Radioactive decay data.'], + [5, 'Spontaneous fission product yields.'], + [6, 'Atomic relaxation data.'], + [10, 'Incident-neutron data.'], + [11, 'Neutron-induced fission product yields.'], + [12, 'Thermal neutron scattering data.'], + [19, 'Neutron standards data.'], + [113, 'Electro-atomic interaction data.'], + [10010, 'Incident-proton data.'], + [10011, 'Proton-induced fission product yields.'], + [10020, 'Incident-deuteron data.'], + [10030, 'Incident-triton data.'], + [20030, 'Incident-helion data.'], + [20040, 'Incident-alpha data.'] +]); + +const NLIB_DESCRIPTIONS = new Map([ + [0, 'ENDF/B, United States Evaluated Nuclear Data File.'], + [1, 'ENDF/A, United States Evaluated Nuclear Data File.'], + [2, 'JEFF, NEA Joint Evaluated Fission and Fusion File.'], + [3, 'EFF, European Fusion File.'], + [4, 'ENDF/B High Energy File.'], + [5, 'CENDL, China Evaluated Nuclear Data Library.'], + [6, 'JENDL, Japan Evaluated Nuclear Data Library.'], + [17, 'TENDL, TALYS Evaluated Nuclear Data Library.'], + [18, 'ROSFOND, Russian evaluated neutron data library.'], + [21, 'WPEC-SG23 fission product library.'], + [31, 'INDL/V, IAEA Evaluated Neutron Data Library.'], + [32, 'INDL/A, IAEA Nuclear Data Activation Library.'], + [33, 'FENDL, IAEA Fusion Evaluated Nuclear Data Library.'], + [34, 'IRDF, IAEA International Reactor Dosimetry File.'], + [35, 'BROND, Russian Evaluated Nuclear Data File, IAEA version.'], + [36, 'INGDB-90 geophysics data.'], + [37, 'FENDL/A, FENDL activation evaluations.'], + [41, 'BROND, original Russian Evaluated Nuclear Data File.'] +]); + +const INT_DESCRIPTIONS = new Map([ + [1, 'Constant interpolation in x, also called histogram interpolation.'], + [2, 'Linear interpolation in x and y, also called linear-linear interpolation.'], + [3, 'Linear interpolation in ln(x), also called linear-log interpolation.'], + [4, 'Linear interpolation in ln(y), also called log-linear interpolation.'], + [5, 'Linear interpolation in ln(x) and ln(y), also called log-log interpolation.'], + [6, 'Special one-dimensional charged-particle cross-section interpolation law.'], + [11, 'Method of corresponding points using constant interpolation.'], + [12, 'Method of corresponding points using linear-linear interpolation.'], + [13, 'Method of corresponding points using linear-log interpolation.'], + [14, 'Method of corresponding points using log-linear interpolation.'], + [15, 'Method of corresponding points using log-log interpolation.'], + [21, 'Unit-base interpolation using constant interpolation.'], + [22, 'Unit-base interpolation using linear-linear interpolation.'], + [23, 'Unit-base interpolation using linear-log interpolation.'], + [24, 'Unit-base interpolation using log-linear interpolation.'], + [25, 'Unit-base interpolation using log-log interpolation.'] +]); + +const LR_DESCRIPTIONS = new Map([ + [0, 'Simple reaction; product identity is implicit in MT and only gamma rays may be emitted additionally.'], + [1, 'Complex or breakup reaction; product identities and multiplicities are given explicitly in File 6.'], + [22, 'Residual breakup flag: alpha emitted, plus residual if any.'], + [23, 'Residual breakup flag: three alpha particles emitted, plus residual if any.'], + [24, 'Residual breakup flag: neutron and alpha emitted, plus residual if any.'], + [25, 'Residual breakup flag: two neutrons and alpha emitted, plus residual if any.'], + [28, 'Residual breakup flag: proton emitted, plus residual if any.'], + [29, 'Residual breakup flag: two alpha particles emitted, plus residual if any.'] +]); + +const RECORD_TYPE_DESCRIPTIONS = new Map([ + ['TPID', { + description: 'Tape identifier record at the beginning of an ENDF tape.', + format: 'A 66-column text field followed by MAT, MF, MT, and optional NS.' + }], + ['TEXT', { + description: '66-column Hollerith text record, usually for File 1 comments.', + format: '[MAT,MF,MT/ HL] TEXT' + }], + ['CONT', { + description: 'Control record with two floating fields and four integer/control fields.', + format: '[MAT,MF,MT/ C1,C2,L1,L2,N1,N2] CONT' + }], + ['HEAD', { + description: 'Section-opening CONT record whose C1 and C2 fields contain ZA and AWR.', + format: '[MAT,MF,MT/ ZA,AWR,L1,L2,N1,N2] HEAD' + }], + ['DIR', { + description: 'Directory record; a CONT-like record with the first two fields blank in character mode.', + format: 'CONT-shaped directory entry in MF=1, MT=451.' + }], + ['END', { + description: 'Generic term for SEND, FEND, MEND, and TEND structural end records.', + format: 'Zeroed payload fields with tail fields identifying the end level.' + }], + ['SEND', { + description: 'End of section.', + format: '[MAT,MF,0/ 0.0,0.0,0,0,0,0] SEND' + }], + ['FEND', { + description: 'End of file within a material.', + format: '[MAT,0,0/ 0.0,0.0,0,0,0,0] FEND' + }], + ['MEND', { + description: 'End of material.', + format: '[0,0,0/ 0.0,0.0,0,0,0,0] MEND' + }], + ['TEND', { + description: 'End of tape.', + format: '[-1,0,0/ 0.0,0.0,0,0,0,0] TEND' + }], + ['LIST', { + description: 'Record for a list of NPL numbers following a CONT-shaped header.', + format: '[MAT,MF,MT/ C1,C2,L1,L2,NPL,N2/ B(n)] LIST' + }], + ['TAB1', { + description: 'One-dimensional tabulated function record with interpolation regions and x,y pairs.', + format: '[MAT,MF,MT/ C1,C2,L1,L2,NR,NP/ x-int / y(x)] TAB1' + }], + ['TAB2', { + description: 'Control record for tabulating a two-dimensional function over successive z values.', + format: '[MAT,MF,MT/ C1,C2,L1,L2,NR,NZ/ z-int] TAB2' + }], + ['INTG', { + description: 'Integer-format record used to store correlation matrix data.', + format: '[MAT,MF,MT/ II,JJ,KIJ] INTG' + }] +]); + +const COMPLETION_TABLES = new Map([ + ['MF', mapToCompletionEntries('MF', MF_DESCRIPTIONS)], + ['MT', [ + ...mapToCompletionEntries('MT', MT_DESCRIPTIONS), + ...MT_RANGES.map((range) => ({ + label: range.label, + insertText: String(range.start), + detail: `MT ${range.label}`, + documentation: rangeDocumentation(range), + sortText: paddedSort(range.start) + })) + ].sort(compareCompletionEntries)], + ['NSUB', mapToCompletionEntries('NSUB', NSUB_DESCRIPTIONS)], + ['NLIB', mapToCompletionEntries('NLIB', NLIB_DESCRIPTIONS)], + ['INT', mapToCompletionEntries('INT', INT_DESCRIPTIONS)], + ['LR', mapToCompletionEntries('LR', LR_DESCRIPTIONS)] +]); + +function describeRecordKey(key, value, tail) { + const normalizedKey = normalizeKey(key); + if (normalizedKey === 'MAT') { + return describeMat(value, tail); + } + + if (normalizedKey === 'MF') { + return describeFromMap(MF_DESCRIPTIONS, value, 'ENDF file number. This identifies the broad data category for the record.'); + } + + if (normalizedKey === 'MT') { + return describeMt(value); + } + + if (normalizedKey === 'NS') { + return 'Sequence number for the physical record. It is optional in some ENDF-style files.'; + } + + if (normalizedKey === 'NSUB') { + return describeFromMap(NSUB_DESCRIPTIONS, value, 'ENDF sublibrary number, defined as 10 * IPART + ITYPE.'); + } + + if (normalizedKey === 'NLIB') { + return describeFromMap(NLIB_DESCRIPTIONS, value, 'ENDF library identifier.'); + } + + if (normalizedKey === 'INT') { + return describeFromMap(INT_DESCRIPTIONS, value, 'ENDF interpolation law code.'); + } + + if (normalizedKey === 'LR') { + return describeFromMap(LR_DESCRIPTIONS, value, 'Residual breakup flag for sequential or complex reactions.'); + } + + return 'ENDF fixed-column control value.'; +} + +function describeReferenceRange(key, firstValue, secondValue) { + const normalizedKey = normalizeKey(key); + const first = Number(firstValue); + const second = Number(secondValue); + const firstDescription = describeRecordKey(normalizedKey, first); + const secondDescription = describeRecordKey(normalizedKey, second); + + if (normalizedKey === 'MT') { + const coveringRange = MT_RANGES.find((range) => first >= range.start && second <= range.end); + if (coveringRange) { + return `${normalizedKey} range ${firstValue}-${secondValue}: ${describeRange(coveringRange, first)} through ${describeRange(coveringRange, second)}`; + } + } + + return `${normalizedKey} range ${firstValue}-${secondValue}. First value: ${firstDescription} Last value: ${secondDescription}`; +} + +function getRecordTypeInfo(raw) { + const key = normalizeKey(raw); + const info = RECORD_TYPE_DESCRIPTIONS.get(key); + if (!info) { + return undefined; + } + + return { key, ...info }; +} + +function getCompletionEntries(key) { + const entries = COMPLETION_TABLES.get(normalizeKey(key)); + return entries ? entries.map((entry) => ({ ...entry })) : []; +} + +function getNumericCompletionContext(linePrefix) { + const match = linePrefix.match(/(?:^|[^A-Za-z0-9_])(?MF|MT|NSUB|NLIB|INT|LR)\s*(?:=|-)?\s*(?\d*)$/i); + if (!match?.groups) { + return undefined; + } + + return { + key: normalizeKey(match.groups.key), + valuePrefix: match.groups.valuePrefix || '' + }; +} + +function parseEndfNumber(raw) { + const trimmed = raw.trim(); + if (!trimmed) { + return undefined; + } + + let normalized = trimmed.replace(/[Dd]/g, 'E'); + if (!/[Ee]/.test(normalized)) { + normalized = normalized.replace(/^([+-]?(?:\d+\.\d*|\.\d+|\d+))([+-]\d+)$/, '$1E$2'); + } + + const value = Number(normalized); + if (!Number.isFinite(value)) { + return undefined; + } + + return formatNumber(value); +} + +function describeMat(value, tail) { + if (tail?.mat === -1 && tail?.mf === 0 && tail?.mt === 0) { + return 'TEND marker: end of tape.'; + } + + if (tail?.mat === 0 && tail?.mf === 0 && tail?.mt === 0) { + return 'MEND marker: end of material.'; + } + + if (value === 0) { + return 'End-of-material marker when MF and MT are also zero.'; + } + + return 'Material identifier assigned by the ENDF library.'; +} + +function describeMt(value) { + if (MT_DESCRIPTIONS.has(value)) { + return MT_DESCRIPTIONS.get(value); + } + + const range = MT_RANGES.find((candidate) => value >= candidate.start && value <= candidate.end); + if (range) { + return describeRange(range, value); + } + + return 'ENDF reaction or section number. Check the ENDF-6 manual for this specific MT code.'; +} + +function describeFromMap(map, value, fallback) { + return map.get(value) || fallback; +} + +function describeRange(range, value) { + return typeof range.description === 'function' ? range.description(value) : range.description; +} + +function rangeDocumentation(range) { + if (typeof range.description === 'function') { + return `${range.label}: ${range.description(range.start)}`; + } + + return `${range.label}: ${range.description}`; +} + +function mapToCompletionEntries(key, map) { + return [...map.entries()].map(([value, description]) => ({ + label: String(value), + insertText: String(value), + detail: `${key} ${value}`, + documentation: description, + sortText: paddedSort(value) + })).sort(compareCompletionEntries); +} + +function compareCompletionEntries(left, right) { + return left.sortText.localeCompare(right.sortText) || left.label.localeCompare(right.label); +} + +function paddedSort(value) { + return String(value).padStart(6, '0'); +} + +function normalizeKey(key) { + return String(key).toUpperCase(); +} + +function formatNumber(value) { + if (Number.isInteger(value)) { + return String(value); + } + + return Number(value.toPrecision(12)).toString(); +} + +module.exports = { + describeRecordKey, + describeReferenceRange, + getCompletionEntries, + getNumericCompletionContext, + getRecordTypeInfo, + parseEndfNumber +}; diff --git a/extension.js b/extension.js index c3ae16d..185f62a 100644 --- a/extension.js +++ b/extension.js @@ -1,4 +1,12 @@ const vscode = require('vscode'); +const { + describeRecordKey, + describeReferenceRange, + getCompletionEntries, + getNumericCompletionContext, + getRecordTypeInfo, + parseEndfNumber +} = require('./endfReference'); const PAYLOAD_FIELDS = [ { @@ -46,85 +54,8 @@ const TAIL_FIELDS = [ { key: 'NS', start: 75, end: 80 } ]; -const MF_DESCRIPTIONS = new Map([ - [0, 'End-record marker used with SEND, FEND, MEND, or TEND records.'], - [1, 'General information.'], - [2, 'Resonance parameter data.'], - [3, 'Reaction cross sections.'], - [4, 'Angular distributions of emitted particles.'], - [5, 'Energy distributions of emitted particles.'], - [6, 'Product energy-angle distributions.'], - [7, 'Thermal neutron scattering law data.'], - [8, 'Radioactive decay and fission product yield data.'], - [9, 'Multiplicities for radioactive nuclide production.'], - [10, 'Cross sections for radioactive nuclide production.'], - [12, 'Photon production multiplicities and transition probabilities.'], - [13, 'Photon production cross sections.'], - [14, 'Photon angular distributions.'], - [15, 'Continuous photon energy spectra.'], - [23, 'Photon or electro-atomic interaction cross sections.'], - [26, 'Electro-atomic angle and energy distributions.'], - [27, 'Atomic form factors or scattering functions.'], - [28, 'Atomic relaxation data.'], - [30, 'Data covariances from parameter covariances and uncertainties.'], - [31, 'Covariances of average neutron multiplicities.'], - [32, 'Covariances of resonance parameters.'], - [33, 'Covariances of neutron cross sections.'], - [34, 'Covariances of angular distributions.'], - [35, 'Covariances of energy distributions.'], - [40, 'Covariances of radionuclide production cross sections.'] -]); - -const MT_DESCRIPTIONS = new Map([ - [0, 'End-record marker used with SEND, FEND, MEND, or TEND records.'], - [1, 'Total cross section.'], - [2, 'Elastic scattering.'], - [3, 'Nonelastic cross section.'], - [4, 'Total inelastic scattering.'], - [5, 'Sum of reactions not otherwise represented by another MT number.'], - [11, '2nd neutron production.'], - [16, '(n,2n).'], - [17, '(n,3n).'], - [18, 'Fission.'], - [19, 'First-chance fission.'], - [20, 'Second-chance fission.'], - [21, 'Third-chance fission.'], - [22, '(n,n alpha).'], - [24, '(n,2n alpha).'], - [25, '(n,3n alpha).'], - [28, '(n,np).'], - [32, '(n,nd).'], - [33, '(n,nt).'], - [34, '(n,n He-3).'], - [37, '(n,4n).'], - [38, 'Fourth-chance fission.'], - [41, '(n,2np).'], - [42, '(n,3np).'], - [44, '(n,n2p).'], - [45, '(n,np alpha).'], - [91, 'Continuum inelastic scattering.'], - [101, 'Neutron disappearance.'], - [102, 'Radiative capture, usually (n,gamma).'], - [103, '(n,p).'], - [104, '(n,d).'], - [105, '(n,t).'], - [106, '(n,He-3).'], - [107, '(n,alpha).'], - [108, '(n,2 alpha).'], - [109, '(n,3 alpha).'], - [111, '(n,2p).'], - [112, '(n,p alpha).'], - [151, 'Resolved and unresolved resonance parameters.'], - [452, 'Total neutron multiplicity, nu-bar.'], - [455, 'Delayed neutron multiplicity.'], - [456, 'Prompt neutron multiplicity.'], - [458, 'Energy release from fission.'], - [459, 'Cumulative fission product yield.'], - [460, 'Delayed photon data.'], - [451, 'Descriptive data and directory.'] -]); - -const REFERENCE_PATTERN = /\b(MAT|MF|MT)\s*(=|-)?\s*(\d+)(?:\s*(-)\s*(\d+))?\b/g; +const REFERENCE_PATTERN = /\b(MAT|MF|MT|NS|NSUB|NLIB|INT|LR)\s*(=|-)?\s*(\d+)(?:\s*(-)\s*(\d+))?\b/gi; +const RECORD_TYPE_PATTERN = /\b(TPID|TEXT|CONT|DIR|HEAD|END|SEND|FEND|MEND|TEND|LIST|TAB1|TAB2|INTG)\b/gi; const NUMBER_PATTERN = /[+-]?(?:\d+\.\d*|\.\d+|\d+)(?:[EeDd][+-]?\d+|[+-]\d+)?/g; function activate(context) { @@ -145,6 +76,12 @@ function activate(context) { } })); + context.subscriptions.push(vscode.languages.registerCompletionItemProvider({ language: 'endf' }, { + provideCompletionItems(document, position) { + return provideReferenceCompletions(document, position); + } + }, '=', ' ')); + const updateStatusBar = () => { const editor = vscode.window.activeTextEditor; if (!editor || editor.document.languageId !== 'endf') { @@ -171,6 +108,37 @@ function activate(context) { function deactivate() {} +function provideReferenceCompletions(document, position) { + const linePrefix = document.lineAt(position.line).text.slice(0, position.character); + const context = getNumericCompletionContext(linePrefix); + if (!context) { + return undefined; + } + + const entries = getCompletionEntries(context.key) + .filter((entry) => !context.valuePrefix || entry.label.startsWith(context.valuePrefix) || entry.insertText.startsWith(context.valuePrefix)); + if (!entries.length) { + return undefined; + } + + const replacementRange = new vscode.Range( + position.line, + position.character - context.valuePrefix.length, + position.line, + position.character + ); + + return entries.map((entry) => { + const item = new vscode.CompletionItem(entry.label, vscode.CompletionItemKind.Value); + item.insertText = entry.insertText; + item.detail = entry.detail; + item.documentation = new vscode.MarkdownString(entry.documentation); + item.range = replacementRange; + item.sortText = entry.sortText; + return item; + }); +} + function getStatusPosition(editor) { const selection = editor.selection; if (!selection.isEmpty && selection.start.line === selection.end.line) { @@ -193,6 +161,7 @@ function getEndfInfoAt(document, position) { const line = document.lineAt(position.line).text; const column = position.character; return describeTextReference(line, position.line, column) + || describeRecordTypeReference(line, position.line, column) || describeTailField(line, position.line, column) || describePayloadField(line, position.line, column); } @@ -206,11 +175,9 @@ function describeTextReference(line, lineNumber, column) { continue; } - const key = match[1]; + const key = match[1].toUpperCase(); const firstValue = match[3]; const secondValue = match[5]; - const firstStart = start + match[0].indexOf(firstValue); - const firstEnd = firstStart + firstValue.length; const secondStart = secondValue ? start + match[0].lastIndexOf(secondValue) : undefined; const secondEnd = secondStart !== undefined ? secondStart + secondValue.length : undefined; const selectedRaw = secondStart !== undefined && column >= secondStart && column < secondEnd @@ -240,6 +207,38 @@ function describeTextReference(line, lineNumber, column) { return undefined; } +function describeRecordTypeReference(line, lineNumber, column) { + RECORD_TYPE_PATTERN.lastIndex = 0; + for (const match of line.matchAll(RECORD_TYPE_PATTERN)) { + const start = match.index; + const end = start + match[0].length; + if (column < start || column >= end) { + continue; + } + + const info = getRecordTypeInfo(match[1]); + if (!info) { + continue; + } + + const markdown = [ + `**ENDF ${info.key} record**`, + '', + `Raw: \`${match[0]}\``, + `Meaning: ${info.description}`, + info.format ? `Format: \`${info.format}\`` : undefined + ].filter(Boolean).join('\n\n'); + + return { + range: new vscode.Range(lineNumber, start, lineNumber, end), + markdown, + statusText: `${info.key}: ${info.description}` + }; + } + + return undefined; +} + function describeTailField(line, lineNumber, column) { const tailField = TAIL_FIELDS.find((field) => column >= field.start && column < field.end); if (!tailField) { @@ -332,33 +331,6 @@ function findNumberInField(raw, fieldStart, column) { return undefined; } -function parseEndfNumber(raw) { - const trimmed = raw.trim(); - if (!trimmed) { - return undefined; - } - - let normalized = trimmed.replace(/[Dd]/g, 'E'); - if (!/[Ee]/.test(normalized)) { - normalized = normalized.replace(/^([+-]?(?:\d+\.\d*|\.\d+|\d+))([+-]\d+)$/, '$1E$2'); - } - - const value = Number(normalized); - if (!Number.isFinite(value)) { - return undefined; - } - - return formatNumber(value); -} - -function formatNumber(value) { - if (Number.isInteger(value)) { - return String(value); - } - - return Number(value.toPrecision(12)).toString(); -} - function parseTail(line) { return { mat: toNumber(line.slice(66, 70)), @@ -378,88 +350,6 @@ function toNumber(raw) { return Number.isFinite(value) ? value : undefined; } -function describeRecordKey(key, value, tail) { - if (key === 'MAT') { - return describeMat(value, tail); - } - - if (key === 'MF') { - return MF_DESCRIPTIONS.get(value) || 'ENDF file number. This identifies the broad data category for the record.'; - } - - if (key === 'MT') { - return describeMt(value); - } - - if (key === 'NS') { - return 'Sequence number for the physical record. It is optional in some ENDF-style files.'; - } - - return 'ENDF fixed-column control value.'; -} - -function describeMat(value, tail) { - if (tail?.mat === -1 && tail?.mf === 0 && tail?.mt === 0) { - return 'TEND marker: end of tape.'; - } - - if (tail?.mat === 0 && tail?.mf === 0 && tail?.mt === 0) { - return 'MEND marker: end of material.'; - } - - if (value === 0) { - return 'End-of-material marker when MF and MT are also zero.'; - } - - return 'Material identifier assigned by the ENDF library.'; -} - -function describeMt(value) { - if (MT_DESCRIPTIONS.has(value)) { - return MT_DESCRIPTIONS.get(value); - } - - if (value >= 51 && value <= 90) { - return `Discrete inelastic scattering level ${value - 50}.`; - } - - if (value >= 600 && value <= 649) { - return 'Proton production reaction, level, or continuum subsection.'; - } - - if (value >= 650 && value <= 699) { - return 'Deuteron production reaction, level, or continuum subsection.'; - } - - if (value >= 700 && value <= 749) { - return 'Triton production reaction, level, or continuum subsection.'; - } - - if (value >= 750 && value <= 799) { - return 'Helium-3 production reaction, level, or continuum subsection.'; - } - - if (value >= 800 && value <= 849) { - return 'Alpha production reaction, level, or continuum subsection.'; - } - - if (value >= 851 && value <= 870) { - return 'Lumped reaction covariance or derived reaction grouping.'; - } - - return 'ENDF reaction or section number. Check the ENDF-6 manual for this specific MT code.'; -} - -function describeReferenceRange(key, firstValue, secondValue) { - const first = Number(firstValue); - const second = Number(secondValue); - if (key === 'MT' && first >= 51 && second <= 90) { - return `MT range ${firstValue}-${secondValue}: discrete inelastic scattering levels ${first - 50}-${second - 50}.`; - } - - return `${key} range ${firstValue}-${secondValue}. First value: ${describeRecordKey(key, first)} Last value: ${describeRecordKey(key, second)}`; -} - function describeRecordContext(tail) { if (tail.mat === undefined && tail.mf === undefined && tail.mt === undefined) { return undefined; @@ -471,11 +361,11 @@ function describeRecordContext(tail) { } if (tail.mf !== undefined) { - parts.push(`MF ${tail.mf}${MF_DESCRIPTIONS.has(tail.mf) ? ` (${MF_DESCRIPTIONS.get(tail.mf)})` : ''}`); + parts.push(`MF ${tail.mf} (${describeRecordKey('MF', tail.mf)})`); } if (tail.mt !== undefined) { - parts.push(`MT ${tail.mt}${describeMt(tail.mt) ? ` (${describeMt(tail.mt)})` : ''}`); + parts.push(`MT ${tail.mt} (${describeRecordKey('MT', tail.mt)})`); } if (tail.ns !== undefined) { diff --git a/package.json b/package.json index c967970..7ab549b 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "engines": { "vscode": "^1.80.0" }, + "scripts": { + "test": "node --test" + }, "activationEvents": [ "onLanguage:endf" ], diff --git a/test/endfReference.test.js b/test/endfReference.test.js new file mode 100644 index 0000000..3ef8b14 --- /dev/null +++ b/test/endfReference.test.js @@ -0,0 +1,61 @@ +const assert = require('node:assert/strict'); +const test = require('node:test'); +const { + describeRecordKey, + describeReferenceRange, + getCompletionEntries, + getNumericCompletionContext, + getRecordTypeInfo, + parseEndfNumber +} = require('../endfReference'); + +test('describes core MF codes from the manual table', () => { + assert.equal(describeRecordKey('MF', 3), 'Reaction cross sections.'); +}); + +test('describes common and ranged MT codes', () => { + assert.equal(describeRecordKey('MT', 51), 'Production of a neutron with the residual nucleus in excited level 1.'); + assert.equal(describeRecordKey('MT', 102), 'Radiative capture.'); + assert.equal(describeRecordKey('MT', 600), 'Production of a proton leaving the residual nucleus in level 0.'); + assert.equal(describeRecordKey('MT', 875), 'Production of two neutrons with the residual nucleus in level 0.'); +}); + +test('describes NSUB, INT, and LR codes', () => { + assert.equal(describeRecordKey('NSUB', 10), 'Incident-neutron data.'); + assert.equal(describeRecordKey('INT', 6), 'Special one-dimensional charged-particle cross-section interpolation law.'); + assert.equal(describeRecordKey('LR', 24), 'Residual breakup flag: neutron and alpha emitted, plus residual if any.'); +}); + +test('describes record types', () => { + assert.equal(getRecordTypeInfo('TAB1').description, 'One-dimensional tabulated function record with interpolation regions and x,y pairs.'); +}); + +test('falls back for unknown numeric references', () => { + assert.equal(describeRecordKey('MT', 999), 'ENDF reaction or section number. Check the ENDF-6 manual for this specific MT code.'); + assert.equal(describeRecordKey('NSUB', 999), 'ENDF sublibrary number, defined as 10 * IPART + ITYPE.'); +}); + +test('describes ranges with selected endpoint meanings', () => { + assert.match(describeReferenceRange('MT', '52', '82'), /MT range 52-82/); + assert.match(describeReferenceRange('MT', '52', '82'), /excited level 2/); +}); + +test('parses ENDF implicit-exponent numbers', () => { + assert.equal(parseEndfNumber('3.007000+3'), '3007'); + assert.equal(parseEndfNumber('6.955732+0'), '6.955732'); + assert.equal(parseEndfNumber('1.250000-2'), '0.0125'); +}); + +test('finds numeric completion contexts', () => { + assert.deepEqual(getNumericCompletionContext('MT='), { key: 'MT', valuePrefix: '' }); + assert.deepEqual(getNumericCompletionContext('some text NSUB=1'), { key: 'NSUB', valuePrefix: '1' }); +}); + +test('provides completion entries for lookup keys', () => { + const mfEntries = getCompletionEntries('MF'); + assert.ok(mfEntries.some((entry) => entry.label === '3' && entry.documentation === 'Reaction cross sections.')); + + const mtEntries = getCompletionEntries('MT'); + assert.ok(mtEntries.some((entry) => entry.label === '102' && entry.documentation === 'Radiative capture.')); + assert.ok(mtEntries.some((entry) => entry.label === '875-890')); +});