Skip to content

Commit

Permalink
Merge pull request #134 from coronasafe/improve_accuracy_calculation
Browse files Browse the repository at this point in the history
Improve accuracy calculation
  • Loading branch information
khavinshankar authored Jun 13, 2024
2 parents cc95994 + 0d8a128 commit cb67248
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 75 deletions.
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

0 comments on commit cb67248

Please sign in to comment.