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

SNOMED Hydrating, Filtering, Special MR Generation #1648

Draft
wants to merge 19 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
feat(snomed): snomed hierachies computed and filtered
Refs: #1442
  • Loading branch information
jonahkaye committed Mar 27, 2024
commit 1565f6737f45eaae7056edaebe462a840996cd14
85 changes: 57 additions & 28 deletions packages/utils/src/terminology-server/snomed-dedup-and-filter.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add documentation explaining what it does and how to use it, please.

Original file line number Diff line number Diff line change
@@ -1,60 +1,80 @@
import fs from "fs";
import path from "path";
import { getCodeDetails } from "./term-server-api";
import { getCodeDetails, getCodeDetailsFull } from "./term-server-api";
import {
populateHashTableFromCodeDetails,
HashTableEntry,
prettyPrintHashTable,
} from "./snomed-heirarchies";

const processDirectory = async (directory: string) => {
// Check if the path is a directory or a file
const stat = fs.statSync(directory);
async function processDirectoryOrFile(
directoryOrFile: string,
processFile: (filePath: string) => Promise<void>
) {
const stat = fs.statSync(directoryOrFile);
if (stat.isFile()) {
// If it's a file, process it directly
if (directory.endsWith(".json")) {
await processFile(directory);
if (directoryOrFile.endsWith(".json")) {
await processFile(directoryOrFile);
}
return; // Exit the function as there's nothing more to do for a file
return;
}

const items = fs.readdirSync(directory, { withFileTypes: true });

const items = fs.readdirSync(directoryOrFile, { withFileTypes: true });
for (const item of items) {
const sourcePath = path.join(directory, item.name);

const sourcePath = path.join(directoryOrFile, item.name);
if (item.isDirectory()) {
// Recursively process the subdirectory
await processDirectory(sourcePath);
await processDirectoryOrFile(sourcePath, processFile);
} else if (item.isFile() && item.name.endsWith(".json")) {
await processFile(sourcePath);
}
}
};
}

const processFile = async (filePath: string) => {
const snomedSystemUrl = "https://snomed.info/sct";
async function computeHashTable(filePath: string, hashTable: Record<string, HashTableEntry>) {
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
const encounteredCodes = new Set();
const entries = data.bundle ? data.bundle.entry : data.entry;

for (const entry of entries) {
const resource = entry.resource;
if (resource && resource.resourceType === "Condition") {
const codings = resource.code?.coding || [];
for (const coding of codings) {
if (coding.system === "https://snomed.info/sct") {
const codeDetails = await getCodeDetailsFull(coding.code, "SNOMEDCT_US");
if (codeDetails) {
await populateHashTableFromCodeDetails(hashTable, codeDetails, coding.code);
}
}
}
}
}
}

async function processFileEntries(filePath: string, hashTable: Record<string, HashTableEntry>) {
const data = JSON.parse(fs.readFileSync(filePath, "utf8"));
const encounteredCodes = new Set();
const entries = data.bundle ? data.bundle.entry : data.entry;

for (let i = entries.length - 1; i >= 0; i--) {
const entry = entries[i];
const resource = entry.resource;
if (resource && resource.resourceType === "Condition") {
const codings = resource.code?.coding || [];

for (const coding of codings) {
if (coding.system === snomedSystemUrl) {
if (coding.system === "https://snomed.info/sct") {
if (encounteredCodes.has(coding.code)) {
console.log(`Removing duplicate code ${coding.code} from ${path.basename(filePath)}`);
entries.splice(i, 1);
} else if (hashTable[coding.code] && !hashTable[coding.code].root) {
console.log(`Removing non-root code ${coding.code} from ${path.basename(filePath)}`);
entries.splice(i, 1);
} else {
encounteredCodes.add(coding.code);
const codeDetails = await getCodeDetails(coding.code, "SNOMEDCT_US");
if (codeDetails && codeDetails.display) {
if (codeDetails.category == "disorder") {
console.log(`Identified disorder for ${coding.code}`);
} else {
console.log(`Filtered out ${codeDetails.category} for ${coding.code}`);
// console.log(`Identified disorder for ${coding.code}`);
}

const updatedText = `${codeDetails.display} (${codeDetails.category})`;
resource.code.text = updatedText;
coding.text = updatedText;
Expand All @@ -67,16 +87,25 @@ const processFile = async (filePath: string) => {

// Write the updated data back to the same file
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf8");
};
}

const main = async () => {
async function main() {
const [directoryPath] = process.argv.slice(2);
if (!directoryPath) {
console.error("Please provide a directory path as an argument.");
process.exit(1);
}
const hashTable: Record<string, HashTableEntry> = {};
// call compute hash table and then after thats done call process file entries
await processDirectoryOrFile(directoryPath, async filePath => {
await computeHashTable(filePath, hashTable);
});

prettyPrintHashTable(hashTable);

await processDirectory(directoryPath).catch(console.error);
};
await processDirectoryOrFile(directoryPath, async filePath => {
await processFileEntries(filePath, hashTable);
});
}

main();
116 changes: 116 additions & 0 deletions packages/utils/src/terminology-server/snomed-heirarchies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
export type HashTableEntry = {
found: boolean;
children: Set<string>;
parents: Set<string>;
root: boolean;
};

export type CodeDetailsResponse = {
parameter: {
name: string;
part: [
{
name: string;
value?: string;
valueCode?: string;
}
];
}[];
};

export async function populateHashTableFromCodeDetails(
hashTable: Record<string, HashTableEntry>,
codeDetails: CodeDetailsResponse,
queriedCode: string
): Promise<Record<string, HashTableEntry>> {
if (hashTable[queriedCode] && !hashTable[queriedCode].found) {
// if we found a parent node, make it the root and its children non-root
if (hashTable[queriedCode].parents.size == 0) {
console.log(
`Updating root node to ${queriedCode}, hashTable[queriedCode].children:`,
hashTable[queriedCode].children
);
hashTable[queriedCode] = { ...hashTable[queriedCode], found: true, root: true };
for (const child of hashTable[queriedCode].children) {
hashTable[child].root = false;
}
}
// if we found a child node, do nothing since it already has a parent which is found and higher
else {
console.log(
`Updating root node for ${queriedCode}, hashTable[queriedCode].parents:`,
hashTable[queriedCode].parents
);
hashTable[queriedCode] = { ...hashTable[queriedCode], found: true, root: false };
}
} else {
hashTable[queriedCode] = { found: true, parents: new Set(), children: new Set(), root: true };
codeDetails.parameter.forEach(param => {
if (param.name === "property") {
const valuePart = param.part.find(part => part.name === "value");
const value = valuePart?.valueCode || valuePart?.value;

const codePart = param.part.find(part => part.name === "code");
const code = codePart?.valueCode || codePart?.value;

// parents
if (value && code === "parent") {
if (!hashTable[value]) {
hashTable[value] = {
found: false,
parents: new Set(),
children: new Set(),
root: false,
};
hashTable[queriedCode].parents.add(value);
// having children indicates your a parent node
hashTable[value].children.add(queriedCode);
}
}
// children
else if (value && !code) {
if (!hashTable[value]) {
hashTable[value] = {
found: false,
parents: new Set(),
children: new Set(),
root: false,
};
hashTable[queriedCode].children.add(value);
// having parents indicates your a chilld node
hashTable[value].parents.add(queriedCode);
}
}
}
});
}
return hashTable;
}

export function prettyPrintHashTable(hashTable: Record<string, HashTableEntry>): void {
const entries = Object.entries(hashTable);

const header = ["Code", "Found", "Root"];
const separator = header.map(() => "---");

const rows = entries.map(([code, entry]) => {
const { found, root } = entry;
const foundStr = found ? "✓" : "✗";
const rootStr = root ? "✓" : "✗";
return [code, foundStr, rootStr];
});

// Calculate the maximum width for each column
const columnWidths = header.map((_, colIndex) =>
Math.max(...rows.map(row => row[colIndex].length), header[colIndex].length)
);

// Print the table
const printRow = (row: string[]) =>
console.log(
"| " + row.map((cell, colIndex) => cell.padEnd(columnWidths[colIndex])).join(" | ") + " |"
);
printRow(header);
printRow(separator);
rows.forEach(row => printRow(row));
}
40 changes: 40 additions & 0 deletions packages/utils/src/terminology-server/term-server-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from "axios";
import { CodeDetailsResponse } from "./snomed-heirarchies";

interface ParameterPart {
name: string;
Expand Down Expand Up @@ -48,6 +49,31 @@ export const codeSystemUrls: Record<string, CodeSystemUrl> = {
},
};

export async function getCodeDetailsFull(
code: string,
codeSystemType: string
): Promise<CodeDetailsResponse | undefined> {
try {
const codeSystem = codeSystemUrls[codeSystemType];
if (!codeSystem) {
console.error("Unsupported code system type:", codeSystemType);
return undefined;
}

const url = `https://localhost:29927/R4/CodeSystem/$lookup?system=${codeSystem.url}&code=${code}`;
const response = await axios.get(url);
return response.data;
} catch (error) {
if (axios.isAxiosError(error) && error.code === "ECONNREFUSED") {
console.error("Connection refused. The server is not reachable at the moment.");
return undefined;
} else {
console.error(`Error fetching code details. Code Not Found ${code}`);
return undefined;
}
}
}

export async function getCodeDetails(
code: string,
codeSystemType: string
Expand Down Expand Up @@ -89,3 +115,17 @@ export async function getCodeDetails(
}
}
}

// async function main() {
// const code = "94222008";
// const codeSystemType = "SNOMEDCT_US";
// const codeDetails = await getCodeDetails(code, codeSystemType);

// if (codeDetails) {
// console.log(`Code Details: Display - ${codeDetails.display}, Category - ${codeDetails.category}`);
// } else {
// console.log("Failed to retrieve code details.");
// }
// }

// main().catch(console.error);
Loading