Skip to main content
Glama
datasampler.test.ts24.4 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { Bundle, Observation } from '@medplum/fhirtypes'; import { LOINC, SNOMED, UCUM } from './constants'; import { DataSampler, expandSampledObservation, summarizeObservations } from './datasampler'; function sum(x: number, y: number): number { return x + y; } describe('DataSampler', () => { test('Empty summary', () => { const sample = new DataSampler({ code: { text: 'Data' } }); const result = sample.summarize({ text: 'Test' }, (data) => data.reduce(sum, 0)); expect(result).toStrictEqual<Observation>( expect.objectContaining({ resourceType: 'Observation', status: 'final', code: { text: 'Test' }, valueQuantity: { value: 0 }, component: [ { code: { text: 'Data' }, valueSampledData: { origin: { value: 0 }, dimensions: 1, period: 0, }, }, ], }) ); }); test('Single data point', () => { const sample = new DataSampler({ code: { text: 'Data' } }); sample.addData(2); const result = sample.summarize({ text: 'Test' }, (data) => data.reduce(sum, 0)); expect(result).toStrictEqual<Observation>( expect.objectContaining({ resourceType: 'Observation', status: 'final', code: { text: 'Test' }, valueQuantity: { value: 2 }, component: [ { code: { text: 'Data' }, valueSampledData: { origin: { value: 0 }, dimensions: 1, period: 0, data: '2', }, }, ], }) ); }); test('Sum of two data points', () => { const sample = new DataSampler({ code: { text: 'Data' } }); sample.addData(1, 1); const result = sample.summarize({ text: 'Test' }, (data) => data.reduce(sum, 0)); expect(result).toStrictEqual<Observation>( expect.objectContaining({ resourceType: 'Observation', status: 'final', code: { text: 'Test' }, valueQuantity: { value: 2 }, component: [ { code: { text: 'Data' }, valueSampledData: { origin: { value: 0 }, dimensions: 1, period: 0, data: '1 1', }, }, ], }) ); }); test('Average of Observations', () => { const sample = new DataSampler(); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 72, unit: 'bpm' }, }); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 55, unit: 'bpm' }, }); const result = sample.summarize( { coding: [{ system: 'http://loinc.org', code: '41920-0' }] }, (data) => data.reduce(sum, 0) / data.length ); expect(result).toStrictEqual( expect.objectContaining<Observation>({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '41920-0' }] }, valueQuantity: { value: 63.5, unit: 'bpm' }, component: [ { code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueSampledData: { origin: { value: 0, unit: 'bpm' }, dimensions: 1, period: 0, data: '72 55', }, }, ], }) ); }); test('Code and unit matching ignores display text', () => { const sample = new DataSampler(); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4', display: 'Heart rate' }] }, valueQuantity: { value: 72, unit: 'bpm', system: UCUM, code: '{beats}/min' }, }); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4', display: 'Pulse' }] }, valueQuantity: { value: 55, unit: 'beats per min.', system: UCUM, code: '{beats}/min' }, }); const result = sample.summarize( { coding: [{ system: 'http://loinc.org', code: '41920-0', display: 'Heart rate 1h Mean' }] }, (data) => data.reduce(sum, 0) / data.length ); expect(result).toStrictEqual( expect.objectContaining<Observation>({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '41920-0', display: 'Heart rate 1h Mean' }] }, valueQuantity: { value: 63.5, unit: 'bpm', system: UCUM, code: '{beats}/min' }, component: [ { code: { coding: [{ system: 'http://loinc.org', code: '8867-4', display: 'Heart rate' }] }, valueSampledData: { origin: { value: 0, unit: 'bpm', system: UCUM, code: '{beats}/min' }, dimensions: 1, period: 0, data: '72 55', }, }, ], }) ); }); test('Throws on data point unit mismatch', () => { const sample = new DataSampler(); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '29463-7', display: 'Body weight' }] }, valueQuantity: { value: 72, system: UCUM, code: 'kg' }, }); expect(() => sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '29463-7', display: 'Body weight' }] }, valueQuantity: { value: 55, system: UCUM, code: '[lb_av]', unit: 'lb' }, }) ).toThrow(/^Incorrect unit/); expect(() => sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '29463-7', display: 'Body weight' }] }, valueQuantity: { value: 55, unit: 'lb' }, }) ).toThrow(/^Incorrect unit/); }); test('Throws on data point code mismatch', () => { const sample = new DataSampler(); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '29463-7', display: 'Body weight' }] }, valueQuantity: { value: 72, system: UCUM, code: 'kg' }, }); expect(() => sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://snomed.info/sct', code: '27113001', display: 'Body weight' }] }, valueQuantity: { value: 55, system: UCUM, code: 'kg' }, }) ).toThrow(/does not match code/); }); test('Requires code for data points', () => { const sample = new DataSampler(); sample.addData(1, 2, 3); expect(() => sample.summarize({ text: 'Summary' }, (data) => data.reduce(sum, 0))).toThrow(/code .* required/i); }); test('Computed value with unit different from data points', () => { const sample = new DataSampler(); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 72, unit: 'bpm' }, }); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 55, unit: 'bpm' }, }); const result = sample.summarize({ text: 'Skewness' }, (_data) => ({ value: 0, unit: 'skew' })); expect(result).toStrictEqual( expect.objectContaining<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'Skewness' }, valueQuantity: { value: 0, unit: 'skew' }, component: [ { code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueSampledData: { origin: { value: 0, unit: 'bpm' }, dimensions: 1, period: 0, data: '72 55', }, }, ], }) ); }); test('Allow recording plain integer values from Observation', () => { const sample = new DataSampler(); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 72, unit: 'bpm' }, }); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueInteger: 55, }); const result = sample.summarize( { coding: [{ system: 'http://loinc.org', code: '41920-0' }] }, (data) => data.reduce(sum, 0) / data.length ); expect(result).toStrictEqual( expect.objectContaining<Observation>({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '41920-0' }] }, valueQuantity: { value: 63.5, unit: 'bpm' }, component: [ { code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueSampledData: { origin: { value: 0, unit: 'bpm' }, dimensions: 1, period: 0, data: '72 55', }, }, ], }) ); }); test('Allow recording full data set from SampledData', () => { const sample = new DataSampler(); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 72, unit: 'bpm' }, }); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueSampledData: { origin: { value: 0, unit: 'bpm' }, period: 0, dimensions: 1, data: '55 88 11', }, }); const result = sample.summarize( { coding: [{ system: 'http://loinc.org', code: '41920-0' }] }, (data) => data.reduce(sum, 0) / data.length ); expect(result).toStrictEqual( expect.objectContaining<Observation>({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '41920-0' }] }, valueQuantity: { value: 56.5, unit: 'bpm' }, component: [ { code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueSampledData: { origin: { value: 0, unit: 'bpm' }, dimensions: 1, period: 0, data: '72 55 88 11', }, }, ], }) ); }); test('Adjusts for SampledData scaling', () => { const sample = new DataSampler({ sampling: { origin: { value: 50, unit: 'bpm' }, period: 0, dimensions: 1 } }); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 72, unit: 'bpm' }, }); sample.addObservation({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueSampledData: { origin: { value: 60, unit: 'bpm' }, period: 0, dimensions: 1, factor: 0.5, data: '6 14 -20', }, }); const result = sample.summarize( { coding: [{ system: 'http://loinc.org', code: '41920-0' }] }, (data) => data.reduce(sum, 0) / data.length ); expect(result).toStrictEqual( expect.objectContaining<Observation>({ resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '41920-0' }] }, valueQuantity: { value: 63, unit: 'bpm' }, component: [ { code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueSampledData: { origin: { value: 50, unit: 'bpm' }, dimensions: 1, period: 0, data: '22 13 17 0', }, }, ], }) ); }); test('summarizeObservations', () => { const obs: Observation[] = [ { resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 72, unit: 'bpm' }, }, { resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 55, unit: 'bpm' }, }, ]; const result = summarizeObservations(obs, { text: 'Skewness' }, (_data) => ({ value: 0, unit: 'skew' })); expect(result).toStrictEqual( expect.objectContaining<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'Skewness' }, valueQuantity: { value: 0, unit: 'skew' }, component: [ { code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueSampledData: { origin: { value: 0, unit: 'bpm' }, dimensions: 1, period: 0, data: '72 55', }, }, ], }) ); }); test('summarizeObservations with Bundle', () => { const obs: Bundle<Observation> = { resourceType: 'Bundle', type: 'collection', entry: [ { resource: { resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 72, unit: 'bpm' }, }, }, { resource: { resourceType: 'Observation', status: 'final', code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueQuantity: { value: 55, unit: 'bpm' }, }, }, ], }; const result = summarizeObservations(obs, { text: 'Skewness' }, (_data) => ({ value: 0, unit: 'skew' })); expect(result).toStrictEqual( expect.objectContaining<Observation>({ resourceType: 'Observation', status: 'final', code: { text: 'Skewness' }, valueQuantity: { value: 0, unit: 'skew' }, component: [ { code: { coding: [{ system: 'http://loinc.org', code: '8867-4' }] }, valueSampledData: { origin: { value: 0, unit: 'bpm' }, dimensions: 1, period: 0, data: '72 55', }, }, ], }) ); }); test('expandSampledObservation with single data stream', () => { const obs: Observation = { resourceType: 'Observation', status: 'amended', code: { text: 'Eye position' }, effectiveDateTime: '2025-05-29T13:44:31-07:00', encounter: { reference: 'Encounter/abc' }, valueSampledData: { origin: { value: 0, unit: 'mm' }, dimensions: 2, period: 100, factor: 0.25, data: '0 0 5 -1 8 1', }, }; const results = expandSampledObservation(obs); expect(results).toHaveLength(6); expect(results).toStrictEqual<Observation[]>( expect.arrayContaining([ { resourceType: 'Observation', status: 'amended', code: { text: 'Eye position' }, encounter: { reference: 'Encounter/abc' }, effectiveDateTime: '2025-05-29T20:44:31.000Z', valueQuantity: { value: 0, unit: 'mm' }, }, { resourceType: 'Observation', status: 'amended', code: { text: 'Eye position' }, encounter: { reference: 'Encounter/abc' }, effectiveDateTime: '2025-05-29T20:44:31.000Z', valueQuantity: { value: 0, unit: 'mm' }, }, { resourceType: 'Observation', status: 'amended', code: { text: 'Eye position' }, encounter: { reference: 'Encounter/abc' }, effectiveDateTime: '2025-05-29T20:44:31.100Z', valueQuantity: { value: 1.25, unit: 'mm' }, }, { resourceType: 'Observation', status: 'amended', code: { text: 'Eye position' }, encounter: { reference: 'Encounter/abc' }, effectiveDateTime: '2025-05-29T20:44:31.100Z', valueQuantity: { value: -0.25, unit: 'mm' }, }, { resourceType: 'Observation', status: 'amended', code: { text: 'Eye position' }, encounter: { reference: 'Encounter/abc' }, effectiveDateTime: '2025-05-29T20:44:31.200Z', valueQuantity: { value: 2, unit: 'mm' }, }, { resourceType: 'Observation', status: 'amended', code: { text: 'Eye position' }, encounter: { reference: 'Encounter/abc' }, effectiveDateTime: '2025-05-29T20:44:31.200Z', valueQuantity: { value: 0.25, unit: 'mm' }, }, ]) ); }); test('expand Observation with components', () => { const obs: Observation = { resourceType: 'Observation', id: 'bp-pct', status: 'final', effectivePeriod: { start: '2025-05-29T14:00:00Z', end: '2025-05-29T14:30:00Z', }, code: { coding: [{ system: LOINC, code: '71896-5', display: 'Pediatric blood pressure percentile' }] }, valueQuantity: { value: 54, unit: '%', system: UCUM, code: '%' }, derivedFrom: [{ reference: 'Media/rawData' }], subject: { reference: 'Patient/baby' }, component: [ { code: { coding: [ { system: SNOMED, code: '72313002', display: 'Systolic arterial pressure' }, { system: LOINC, code: '8480-6', display: 'Systolic blood pressure' }, ], }, valueSampledData: { origin: { value: 0, unit: 'mmHg', system: UCUM, code: 'mm[Hg]' }, dimensions: 1, period: 10 * 60 * 1000, // 10 minutes data: '120 132 141 139', }, }, { code: { coding: [ { system: SNOMED, code: '1091811000000102', display: 'Diastolic arterial pressure' }, { system: LOINC, code: '8462-4', display: 'Diastolic blood pressure' }, ], }, valueSampledData: { origin: { value: 0, unit: 'mmHg', system: UCUM, code: 'mm[Hg]' }, dimensions: 1, period: 10 * 60 * 1000, // 10 minutes data: '78 76 87 83', }, }, ], }; const results = expandSampledObservation(obs); expect(results).toHaveLength(8); expect(results).toStrictEqual( expect.arrayContaining([ { resourceType: 'Observation', status: 'final', effectiveDateTime: '2025-05-29T14:00:00.000Z', code: { coding: [ { system: SNOMED, code: '72313002', display: 'Systolic arterial pressure' }, { system: LOINC, code: '8480-6', display: 'Systolic blood pressure' }, ], }, valueQuantity: { value: 120, unit: 'mmHg', system: UCUM, code: 'mm[Hg]' }, derivedFrom: [{ reference: 'Media/rawData' }, { reference: 'Observation/bp-pct' }], subject: { reference: 'Patient/baby' }, }, { resourceType: 'Observation', status: 'final', effectiveDateTime: '2025-05-29T14:10:00.000Z', code: { coding: [ { system: SNOMED, code: '72313002', display: 'Systolic arterial pressure' }, { system: LOINC, code: '8480-6', display: 'Systolic blood pressure' }, ], }, valueQuantity: { value: 132, unit: 'mmHg', system: UCUM, code: 'mm[Hg]' }, derivedFrom: [{ reference: 'Media/rawData' }, { reference: 'Observation/bp-pct' }], subject: { reference: 'Patient/baby' }, }, { resourceType: 'Observation', status: 'final', effectiveDateTime: '2025-05-29T14:20:00.000Z', code: { coding: [ { system: SNOMED, code: '72313002', display: 'Systolic arterial pressure' }, { system: LOINC, code: '8480-6', display: 'Systolic blood pressure' }, ], }, valueQuantity: { value: 141, unit: 'mmHg', system: UCUM, code: 'mm[Hg]' }, derivedFrom: [{ reference: 'Media/rawData' }, { reference: 'Observation/bp-pct' }], subject: { reference: 'Patient/baby' }, }, { resourceType: 'Observation', status: 'final', effectiveDateTime: '2025-05-29T14:30:00.000Z', code: { coding: [ { system: SNOMED, code: '72313002', display: 'Systolic arterial pressure' }, { system: LOINC, code: '8480-6', display: 'Systolic blood pressure' }, ], }, valueQuantity: { value: 139, unit: 'mmHg', system: UCUM, code: 'mm[Hg]' }, derivedFrom: [{ reference: 'Media/rawData' }, { reference: 'Observation/bp-pct' }], subject: { reference: 'Patient/baby' }, }, { resourceType: 'Observation', status: 'final', effectiveDateTime: '2025-05-29T14:00:00.000Z', code: { coding: [ { system: SNOMED, code: '1091811000000102', display: 'Diastolic arterial pressure' }, { system: LOINC, code: '8462-4', display: 'Diastolic blood pressure' }, ], }, valueQuantity: { value: 78, unit: 'mmHg', system: UCUM, code: 'mm[Hg]' }, derivedFrom: [{ reference: 'Media/rawData' }, { reference: 'Observation/bp-pct' }], subject: { reference: 'Patient/baby' }, }, { resourceType: 'Observation', status: 'final', effectiveDateTime: '2025-05-29T14:10:00.000Z', code: { coding: [ { system: SNOMED, code: '1091811000000102', display: 'Diastolic arterial pressure' }, { system: LOINC, code: '8462-4', display: 'Diastolic blood pressure' }, ], }, valueQuantity: { value: 76, unit: 'mmHg', system: UCUM, code: 'mm[Hg]' }, derivedFrom: [{ reference: 'Media/rawData' }, { reference: 'Observation/bp-pct' }], subject: { reference: 'Patient/baby' }, }, { resourceType: 'Observation', status: 'final', effectiveDateTime: '2025-05-29T14:20:00.000Z', code: { coding: [ { system: SNOMED, code: '1091811000000102', display: 'Diastolic arterial pressure' }, { system: LOINC, code: '8462-4', display: 'Diastolic blood pressure' }, ], }, valueQuantity: { value: 87, unit: 'mmHg', system: UCUM, code: 'mm[Hg]' }, derivedFrom: [{ reference: 'Media/rawData' }, { reference: 'Observation/bp-pct' }], subject: { reference: 'Patient/baby' }, }, { resourceType: 'Observation', status: 'final', effectiveDateTime: '2025-05-29T14:30:00.000Z', code: { coding: [ { system: SNOMED, code: '1091811000000102', display: 'Diastolic arterial pressure' }, { system: LOINC, code: '8462-4', display: 'Diastolic blood pressure' }, ], }, valueQuantity: { value: 83, unit: 'mmHg', system: UCUM, code: 'mm[Hg]' }, derivedFrom: [{ reference: 'Media/rawData' }, { reference: 'Observation/bp-pct' }], subject: { reference: 'Patient/baby' }, }, ]) ); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server