Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve accuracy calculation #134

Merged
merged 8 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,4 @@
"engines": {
"node": ">=20.0.0"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Rename the existing column to a temporary name
ALTER TABLE "VitalsStat" RENAME COLUMN "accuracy" TO "accuracy_temp";
ALTER TABLE "VitalsStat" RENAME COLUMN "cumulativeAccuracy" TO "cumulativeAccuracy_temp";

-- Add the new column with the Json type
ALTER TABLE "VitalsStat" ADD COLUMN "accuracy" Json;
ALTER TABLE "VitalsStat" ADD COLUMN "cumulativeAccuracy" Json;

-- Copy the data from the old column to the new column, converting floats to JSON
UPDATE "VitalsStat" SET "accuracy" = json_build_object('overall', "accuracy_temp", 'metrics', '[]'::json);
UPDATE "VitalsStat" SET "cumulativeAccuracy" = json_build_object('overall', "cumulativeAccuracy_temp", 'metrics', '[]'::json);

-- Drop the temporary column
ALTER TABLE "VitalsStat" DROP COLUMN "accuracy_temp";
ALTER TABLE "VitalsStat" DROP COLUMN "cumulativeAccuracy_temp";

4 changes: 2 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ model VitalsStat {
vitalsFromObservation Json
vitalsFromImage Json
gptDetails Json
accuracy Float
cumulativeAccuracy Float
accuracy Json
cumulativeAccuracy Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
39 changes: 10 additions & 29 deletions src/controller/ObservationController.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import type { Request, Response } from "express";



import { BadRequestException } from "@/Exception/BadRequestException";
import { NotFoundException } from "@/Exception/NotFoundException";
import type { DailyRoundObservation, LastObservationData, Observation, ObservationStatus, ObservationType, ObservationTypeWithWaveformTypes, StaticObservation } from "@/types/observation";
import type {
LastObservationData,
Observation,
ObservationStatus,
ObservationType,
ObservationTypeWithWaveformTypes,
StaticObservation,
} from "@/types/observation";
import { WebSocket } from "@/types/ws";
import { ObservationsMap } from "@/utils/ObservationsMap";
import { catchAsync } from "@/utils/catchAsync";
import { hostname } from "@/utils/configs";
import { makeDataDumpToJson } from "@/utils/makeDataDump";
import { filterClients } from "@/utils/wsUtils";


export var staticObservations: StaticObservation[] = [];
var activeDevices: string[] = [];
var lastRequestData = {};
Expand All @@ -22,31 +24,10 @@ var logData: {
}[] = [];
var statusData: ObservationStatus[] = [];
var lastObservationData: LastObservationData = {};
let observationData: { time: Date; data: Observation[][] }[] = [];
export let observationData: { time: Date; data: Observation[][] }[] = [];


const S3_DATA_DUMP_INTERVAL = 1000 * 60 * 60;
const DEFAULT_LISTING_LIMIT = 10;

setInterval(() => {
makeDataDumpToJson(
observationData,
`${hostname}/${new Date().getTime()}.json`,
{
slug: "s3_observations_dump",
options: {
schedule: {
type: "interval",
unit: "minutes",
value: S3_DATA_DUMP_INTERVAL / (1000 * 60),
},
},
},
);

observationData = [];
}, S3_DATA_DUMP_INTERVAL);

const getTime = (date: string) =>
new Date(date.replace(" ", "T").concat("+0530"));

Expand Down Expand Up @@ -296,4 +277,4 @@ export class ObservationController {
filterStatusData();
return res.json(statusData);
});
}
}
149 changes: 124 additions & 25 deletions src/cron/automatedDailyRounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import path from "path";



import { staticObservations } from "@/controller/ObservationController";
import { observationData, staticObservations } from "@/controller/ObservationController";
import prisma from "@/lib/prisma";
import { AssetBed } from "@/types/asset";
import { CameraParams } from "@/types/camera";
Expand All @@ -23,7 +23,7 @@ import { careApi, openaiApiKey, openaiApiVersion, openaiVisionModel, saveDailyRo
import { getPatientId } from "@/utils/dailyRoundUtils";
import { downloadImage } from "@/utils/downloadImageWithDigestRouter";
import { parseVitalsFromImage } from "@/utils/ocr";
import { caclculateVitalsAccuracy } from "@/utils/vitalsAccuracy";
import { Accuracy, calculateVitalsAccuracy } from "@/utils/vitalsAccuracy";


const UPDATE_INTERVAL = 60 * 60 * 1000;
Expand Down Expand Up @@ -92,12 +92,11 @@ export async function getVitalsFromImage(imageUrl: string) {
return null;
}

// const date = data.time_stamp ? new Date(data.time_stamp) : new Date();
// const isoDate =
// date.toString() !== "Invalid Date"
// ? date.toISOString()
// : new Date().toISOString();
const isoDate = new Date().toISOString();
const date = data.time_stamp ? new Date(data.time_stamp) : new Date();
const isoDate =
date.toString() !== "Invalid Date"
? date.toISOString()
: new Date().toISOString();

const payload = {
taken_at: isoDate,
Expand Down Expand Up @@ -127,7 +126,7 @@ export async function getVitalsFromImage(imageUrl: string) {
payload.bp = {};
}

return payload;
return payloadHasData(payload) ? payload : null;
}

export async function fileAutomatedDailyRound(
Expand Down Expand Up @@ -231,7 +230,7 @@ export async function getVitalsFromObservations(assetHostname: string) {
}

const data = observation.observations;
return {
const vitals = {
taken_at: observation.last_updated,
spo2: getValueFromData("SpO2", data),
ventilator_spo2: getValueFromData("SpO2", data),
Expand All @@ -248,6 +247,8 @@ export async function getVitalsFromObservations(assetHostname: string) {
rounds_type: "AUTOMATED",
is_parsed_by_ocr: false,
} as DailyRoundObservation;

return payloadHasData(vitals) ? vitals : null;
}

export function payloadHasData(payload: Record<string, any>): boolean {
Expand All @@ -264,6 +265,72 @@ export function payloadHasData(payload: Record<string, any>): boolean {
});
}

export function getVitalsFromObservationsForAccuracy(
deviceId: string,
time: string,
) {
// TODO: consider optimizing this
const observations = observationData
.reduce((acc, curr) => {
return [...acc, ...curr.data];
}, [] as Observation[][])
.find(
(observation) =>
observation[0].device_id === deviceId &&
new Date(observation[0]["date-time"]).toISOString() ===
new Date(time).toISOString(),
);

if (!observations) {
return null;
}

const vitals = observations.reduce(
(acc, curr) => {
switch (curr.observation_id) {
case "SpO2":
return { ...acc, spo2: curr.value, ventilator_spo2: curr.value };
case "respiratory-rate":
return { ...acc, resp: curr.value };
case "heart-rate":
return { ...acc, pulse: curr.value ?? acc.pulse };
case "pulse-rate":
return { ...acc, pulse: acc.pulse ?? curr.value };
case "body-temperature1":
return {
...acc,
temperature: curr.value ?? acc.temperature,
temperature_measured_at: curr["date-time"],
};
case "body-temperature2":
return {
...acc,
temperature: acc.temperature ?? curr.value,
temperature_measured_at: curr["date-time"],
};
case "blood-pressure":
return {
...acc,
bp: {
systolic: curr.systolic.value,
diastolic: curr.diastolic.value,
map: curr.map?.value,
},
};
default:
return acc;
}
},
{
taken_at: time,
rounds_type: "AUTOMATED",
is_parsed_by_ocr: false,
} as DailyRoundObservation,
);

return payloadHasData(vitals) ? vitals : null;
}

export async function automatedDailyRounds() {
console.log("Automated daily rounds");
const monitors = await prisma.asset.findMany({
Expand Down Expand Up @@ -293,12 +360,14 @@ export async function automatedDailyRounds() {
: await getVitalsFromObservations(monitor.ipAddress);

console.log(
saveDailyRound
? "Skipping vitals from observations as saving daily round is enabled"
saveVitalsStat
? "Skipping vitals from observations as saving vitals stat is enabled"
: `Vitals from observations: ${JSON.stringify(vitals)}`,
);

if (!vitals && openaiApiKey) {
console.log(`Getting vitals from camera for the patient ${patient_id}`);

if (!asset_beds || asset_beds.length === 0) {
console.error(
`No asset beds found for the asset ${monitor.externalId}`,
Expand Down Expand Up @@ -344,29 +413,53 @@ export async function automatedDailyRounds() {
console.log(`Vitals from image: ${JSON.stringify(vitals)}`);
}

if (saveVitalsStat) {
const vitalsFromObservation = await getVitalsFromObservations(
if (vitals && saveVitalsStat) {
const vitalsFromObservation = await getVitalsFromObservationsForAccuracy(
monitor.ipAddress,
new Date(vitals.taken_at!).toISOString(),
);
console.log(
`Vitals from observations: ${JSON.stringify(vitalsFromObservation)}`,
`Vitals from observations for accuracy: ${JSON.stringify(vitalsFromObservation)}`,
);

const accuracy = caclculateVitalsAccuracy(vitals, vitalsFromObservation);
const accuracy = calculateVitalsAccuracy(vitals, vitalsFromObservation);

if (accuracy !== null) {
console.log(`Accuracy: ${accuracy}%`);
console.log(`Accuracy: ${accuracy.overall}%`);

const lastVitalRecord = await prisma.vitalsStat.findFirst({
orderBy: { createdAt: "desc" },
});
const weight = lastVitalRecord?.id; // number of records
const cumulativeAccuracy = lastVitalRecord
? (weight! * lastVitalRecord.cumulativeAccuracy + accuracy) /
(weight! + 1)
: accuracy;
const cumulativeAccuracy = (
lastVitalRecord?.cumulativeAccuracy as Accuracy
).metrics.map((metric) => {
const latestMetric = accuracy.metrics.find(
(m) => m.field === metric.field,
);

await prisma.vitalsStat.create({
return {
...metric,
accuracy: lastVitalRecord
? (metric.accuracy * weight! + latestMetric?.accuracy!) /
(weight! + 1)
: latestMetric?.accuracy!,
falsePositive:
lastVitalRecord && latestMetric?.falsePositive
? (metric.falsePositive! * weight! +
latestMetric?.falsePositive!) /
(weight! + 1)
: metric.falsePositive,
falseNegative:
lastVitalRecord && latestMetric?.falseNegative
? (metric.falseNegative! * weight! +
latestMetric?.falseNegative!) /
(weight! + 1)
: metric.falseNegative,
};
});

prisma.vitalsStat.create({
data: {
imageId: _id,
vitalsFromImage: JSON.parse(JSON.stringify(vitals)),
Expand All @@ -382,11 +475,17 @@ export async function automatedDailyRounds() {
},
});
}

vitals = vitalsFromObservation ?? vitals;
}

if (!vitals || !payloadHasData(vitals)) {
const vitalsFromObservation = await getVitalsFromObservations(
monitor.ipAddress,
);
console.log(
`Vitals from observations: ${JSON.stringify(vitalsFromObservation)}`,
);
vitals = vitalsFromObservation ?? vitals;

if (!vitals) {
console.error(`No vitals found for the patient ${patient_id}`);
return;
}
Expand Down
18 changes: 18 additions & 0 deletions src/cron/observationsS3Dump.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { observationData } from "@/controller/ObservationController";
import { hostname } from "@/utils/configs";
import { makeDataDumpToJson } from "@/utils/makeDataDump";

export async function observationsS3Dump() {
const data = [...observationData];
makeDataDumpToJson(data, `${hostname}/${new Date().getTime()}.json`, {
slug: "s3_observations_dump",
options: {
schedule: {
type: "crontab",
value: "30 * * * *",
},
},
});

observationData.splice(0, data.length);
}
4 changes: 4 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { observationsS3Dump } from "./cron/observationsS3Dump";
import { vitalsStatS3Dump } from "./cron/vitalsStatS3Dump";
import * as cron from "node-cron";

Expand All @@ -21,6 +22,9 @@ process.env.CHECKPOINT_DISABLE = "1";

cron.schedule("0 */1 * * *", automatedDailyRounds); // every hour

// scheduled to run at 30th minute of every hour so that the automatedDailyRounds can use the data without any issues
cron.schedule("30 * * * *", observationsS3Dump); // every hour (30th minute)

if (s3DumpVitalsStat) {
cron.schedule("0 0 * * *", vitalsStatS3Dump); // every day at midnight
}
Expand Down
1 change: 0 additions & 1 deletion src/utils/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const sentryEnv = process.env.SENTRY_ENV ?? "unknown";
export const sentryTracesSampleRate = parseFloat(
process.env.SENTRY_SAMPLE_RATE ?? "0.01",
);

export const saveDailyRound =
(process.env.SAVE_DAILY_ROUND || "true") === "true";
export const saveVitalsStat =
Expand Down
Loading
Loading