diff --git a/src/__tests__/units/actions/lcms.test.tsx b/src/__tests__/units/actions/lcms.test.tsx index d1e68c68..35e52b33 100644 --- a/src/__tests__/units/actions/lcms.test.tsx +++ b/src/__tests__/units/actions/lcms.test.tsx @@ -15,6 +15,7 @@ import hplcMsTicPosJcamp from "../../fixtures/lc_ms_jcamp_tic_pos"; import hplcMsTicNegJcamp from "../../fixtures/lc_ms_jcamp_tic_neg"; import hplcMsUvvisJcamp from "../../fixtures/lc_ms_jcamp_uvvis"; import lcMsMzChemstationJcamp from "../../fixtures/lc_ms_jcamp_mz_chemstation"; +import msJcamp from "../../fixtures/ms_jcamp"; const hasSpectrumData = (entity: any) => { const data = entity?.spectra?.[0]?.data?.[0]; @@ -170,6 +171,20 @@ describe('LCMS ExtractJcamp', () => { const pages = extractPages(entity); expect(new Set(pages).size).toBeGreaterThan(1); }); + + it('Extract a plain MS jcamp keeps the MS layout instead of LC/MS', () => { + const entity: any = ExtractJcamp(msJcamp); + expect(entity.layout).toEqual(LIST_LAYOUT.MS); + }); + + it('Extract Chemstation MZ still resolves LC/MS when it also carries generic chem-spectra markers', () => { + const injected = lcMsMzChemstationJcamp.replace( + '##DATA TYPE=MASS SPECTRUM', + '##DATA TYPE=MASS SPECTRUM\n##$CSCATEGORY=SPECTRUM\n##$CSSCANAUTOTARGET=1', + ); + const entity: any = ExtractJcamp(injected); + expect(entity.layout).toEqual(LIST_LAYOUT.LC_MS); + }); }); describe('LCMS grouping', () => { diff --git a/src/features/lc-ms/parsing/chemstation.js b/src/features/lc-ms/parsing/chemstation.js index 806d272c..12d6bb8a 100644 --- a/src/features/lc-ms/parsing/chemstation.js +++ b/src/features/lc-ms/parsing/chemstation.js @@ -67,10 +67,9 @@ export const isChemstationLcms = (source, jcamp) => { const scanMode = String(info.SCAN_MODE || info.SCANMODE || '').toUpperCase(); const type = String(info.TYPE || '').toUpperCase(); const software = String(info.SOFTWARE || '').toUpperCase(); - const csCategory = jcamp?.info?.$CSCATEGORY; - const categories = Array.isArray(csCategory) - ? csCategory.map((c) => String(c).toUpperCase()) - : []; + const categories = [] + .concat(jcamp?.info?.$CSCATEGORY || []) + .map((c) => String(c).toUpperCase()); const hasPolarityCategory = categories.some( (c) => c.includes('POSITIVE') || c.includes('NEGATIVE') || c.includes('NEUTRAL'), @@ -123,7 +122,7 @@ export const isChemstationLcms = (source, jcamp) => { if ( hasMassSpectrumRootDataType - && (hasMassSpectrumDataType || hasScanModeHint || hasTypeHint || hasSoftwareHint) + && (hasScanModeHint || hasTypeHint || hasSoftwareHint || hasTicOrUvvisCategory) ) { return true; } @@ -134,5 +133,15 @@ export const isChemstationLcms = (source, jcamp) => { return true; } + // A multi-page MASS SPECTRUM root with no other chromatographic signal is ambiguous: + // both a genuine Chemstation m/z export and a plain multi-page MS NTUPLES export look + // like this. The only field that reliably tells them apart is this units triplet, which + // is unique to the plain export - so it only vetoes this weak fallback, never the + // stronger positive evidence handled above. + if (hasMassSpectrumRootDataType && hasMultipleSpectra && hasPageMetadata) { + const hasMsUnitsTriplet = /##UNITS\s*=\s*M\/Z,\s*RELATIVE ABUNDANCE,\s*SECONDS/i.test(source); + return !hasMsUnitsTriplet; + } + return false; };