/* This script was used to automate the MM-execution and MM-results' export for exploring machine learning to derive DQ-assessment knoweldge. The results from 19.02.2021 base on this script. */ (function(exports){ self = this; exports.is = true; /* init function for server side part of app - intended to be invoked when client is loaded */ exports.initServerApp = function(app, dev, req, utils) { /* returns the connection_id of the newest connection from global.connections */ exports.select_newest_connection = function() { var connection_ids = Object.keys(global.connections); var newest = connection_ids[0]; connection_ids.forEach(conn_id => { if (global.connections[conn_id].start_time > global.connections[newest].start_time) { newest = conn_id; } }); return newest; } /* possibility to initiate storing contexts to file or loading them from file via REST-API */ exports.save_or_load_contexts = function (req, res) { var d = new Date(); console.log(d.toLocaleString() + "." + d.getMilliseconds() + ": save_or_load_contexts."); if (req.originalUrl.replace("/contexts/","")=="load") { //load contexts from file var filename = "/testcases/contexts.json"; utils.readFileAsText(filename, function(iData) { global.contexts = JSON.parse(iData); if (!global.contexts["Basic RFFfdGVzdDp0"].vector_strings) {global.contexts["Basic RFFfdGVzdDp0"].vector_strings = {};}; d = new Date(); console.log(d.toLocaleString() + "." + d.getMilliseconds() + ": Contexts loading done (from file)."); res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({load_from_file: true})); //derive paths //(only use small amount of dataset, since i know its enough) var smaller_data_array = []; for (var i=0; i<10; i++) { smaller_data_array.push(global.contexts["Basic RFFfdGVzdDp0"].data[i]); }; var new_paths = app.extractPathsFromData(smaller_data_array, {}, {}); d = new Date(); console.log(d.toLocaleString() + "." + d.getMilliseconds() + ": Deriving paths done."); //send as server send event to client var connection_id = dev.select_newest_connection(); global.connections[connection_id].write('id: ' + (new Date()).toLocaleTimeString() + '\n'); global.connections[connection_id].write("data: " + JSON.stringify({paths: new_paths}) + '\n\n'); }); } else { try { var site_extension = "_Site"+req.originalUrl.replace("/contexts/",""); global.contexts.vectors = {}; //we don't need them in stored context global.contexts.vector_strings = {}; //we don't need them in stored context utils.writeToFile("/testcases/contexts"+site_extension+".json", JSON.stringify(global.contexts), true); res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({saved_to_file: true})); } catch (error) { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({saved_to_file: false, error: error})); } } }; app.get('/contexts/*', dev.save_or_load_contexts); /* possibility to send results from stored context instead of executing MM */ if (false) { exports.send_raw_results = function (MM_hash) { var res = global.contexts["Basic RFFfdGVzdDp0"].raw_results[MM_hash]; //assemble message var tmp_obj = {MM_hash: MM_hash, results: ""}; //modify if there was an error in executing MM to be returned - no dimension_level in raw_results if (res.indexOf('dimension_level')==-1) { tmp_obj.error = true; tmp_obj.results = [{dimension_level:"", value:-1, size:0}]; } var tmp_envelope = JSON.stringify(tmp_obj); tmp_envelope = tmp_envelope.replace('""', res); //send message as server send event to client var connection_id = dev.select_newest_connection(); global.connections[connection_id].write('id: ' + (new Date()).toLocaleTimeString() + '\n'); global.connections[connection_id].write("data: " + tmp_envelope + '\n\n'); }; } /* possibility to initiate outcome generation via REST-API */ exports.generate_outcome = function (req, res) { //set up var tmp_CDSS_success, tmp_label; var d = global.contexts["Basic RFFfdGVzdDp0"].data; var number_of_cases = 10500; var last_site = "all"; o = { case_id_missing: {CDSS_success:0, label:"completeness_issue", counter:0, reference: number_of_cases}, cases_per_site_very_low: {CDSS_success:0.4, counter:0, reference: number_of_cases}, cases_per_site_low: {CDSS_success:0.7, counter:0, reference: number_of_cases}, start_time_21: {CDSS_success:0, label:"timeliness_issue", counter:0, reference: number_of_cases}, start_time_7: {CDSS_success:0.80, counter:0, reference: number_of_cases}, gender_missing: {CDSS_success:0.95, counter:0, reference: number_of_cases}, height_or_weight_missing: {CDSS_success:0.8, counter:0, reference: number_of_cases}, contradicting_entries_risk_factor: {CDSS_success:0.9, label:"plausibility_issue", counter:0, reference: 3*number_of_cases}, missing_info_risk_factor: {CDSS_success:0.9, counter:0, reference: 3*number_of_cases}, blood_pressure_none: {CDSS_success:0, label:"completeness_issue", counter:0, reference: number_of_cases}, blood_pressure_2: {CDSS_success:0.8, label:"completeness_issue", counter:0, reference: number_of_cases}, blood_pressure_outlier: {CDSS_success:0.9, label:"plausibility_issue", counter:0, reference: 20332}, blood_pressure_density_0: {CDSS_success:0.9, label:"plausibility_issue", counter:0, reference: number_of_cases}, HF_0: {CDSS_success:0, label:"completeness_issue", counter:0, reference: number_of_cases}, HF_5: {CDSS_success:0.8, label:"completeness_issue", counter:0, reference: number_of_cases}, HF_10: {CDSS_success:0.95, label:"completeness_issue", counter:0, reference: number_of_cases}, HF_density_2_min: {CDSS_success:0.95, label:"density_issue", counter:0, reference: 102154}, HF_density_0: {CDSS_success:0.90, label:"plausibility_issue", counter:0, reference: number_of_cases}, HF_outlier: {CDSS_success:0.8, label:"plausibility_issue", counter:0, reference: 102154} } console.log("Deriving outcomes per case."); //loop through dataset d.forEach((row, j) => { //set default values for begin tmp_CDSS_success = 0.95; //cdss-success 95% tmp_label = { completeness_issue:0, timeliness_issue:0, density_issue:0, plausibility_issue:0}; //now check which rules to trigger for case and apply them where appropriate //cases per site < 300 - CDSS_success (probablility of correct imaginary prediction) var site_count = (parseInt(row["[openEHR-EHR-COMPOSITION.report.v1]/context/health_care_facility"].name.match(/\d+/)[0])+1)*50; last_site = row["[openEHR-EHR-COMPOSITION.report.v1]/context/health_care_facility"].name.match(/\d+/)[0]; if (site_count < 200) { apply("cases_per_site_very_low"); } //cases < 700 else if (site_count < 400) { apply("cases_per_site_low"); } //case id missing if (!row["[openEHR-EHR-COMPOSITION.report.v1]/context/other_context[at0001]/items[openEHR-EHR-CLUSTER.case_identification.v0]/items[at0001]"]) { apply("case_id_missing"); } //composition start_time/origin if (row["[openEHR-EHR-COMPOSITION.report.v1]/context/start_time"]) { tmp_d = new Date(row["[openEHR-EHR-COMPOSITION.report.v1]/context/start_time"].value); //older than 21 days - CDSS_success = 0, label: timeliness_issue var days_ago_7 = new Date(); days_ago_7.setDate(days_ago_7.getDate() - 7); var days_ago_21 = new Date(); days_ago_21.setDate(days_ago_21.getDate() - 21); if (tmp_d { //contradicting entries if (item.items.length > 1) { apply("contradicting_entries_risk_factor"); } //missing info else if (item.items[0].archetype_node_id == "openEHR-EHR-EVALUATION.absence.v2"){ apply("missing_info_risk_factor"); } }); //blood pressure //no BP entries at all if ((!row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Blutdruck nach 5 Minuten Ruhe']"]) || (row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Blutdruck nach 5 Minuten Ruhe']"].items.length == 0)) { apply("blood_pressure_none"); } //less than 2 entries else if (row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Blutdruck nach 5 Minuten Ruhe']"].items.length < 2){ apply("blood_pressure_2"); } else { //density = 0, i.e. both measurments have the same timestamp if ( (new Date(row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Blutdruck nach 5 Minuten Ruhe']"].items[0].data.origin.value))-(new Date(row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Blutdruck nach 5 Minuten Ruhe']"].items[1].data.origin.value)) == 0 ) { apply("blood_pressure_density_0"); } } if (row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Blutdruck nach 5 Minuten Ruhe']"]) { row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Blutdruck nach 5 Minuten Ruhe']"].items.forEach((item, i) => { //systolic > 300 or diastolic > 200 if ((item.data.events[0].data.items[0].value.magnitude > 300) || (item.data.events[0].data.items[0].value.magnitude > 200)) { apply("blood_pressure_outlier"); } }); } //heartrate //check for time between events > 2min if (row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Herzfrequenz (Pulsmessung)']/items[openEHR-EHR-OBSERVATION.pulse.v2]"]) { var last_e; row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Herzfrequenz (Pulsmessung)']/items[openEHR-EHR-OBSERVATION.pulse.v2]"].data.events.forEach((e, i) => { if (i>0) { if ( (new Date(e.time.value)) - (new Date(last_e.time.value)) > 120000 ) { apply("HF_density_2_min"); } if ((i==1) && ((new Date(e.time.value)) - (new Date(last_e.time.value)) == 0 )) { apply("HF_density_0"); } } //implausible value if (e.data.items[0].value.magnitude >240) { apply("HF_outlier"); } last_e = e; }); //no values if (row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Herzfrequenz (Pulsmessung)']/items[openEHR-EHR-OBSERVATION.pulse.v2]"].data.events.length<1) { apply("HF_0"); } //less than 5 values else if (row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Herzfrequenz (Pulsmessung)']/items[openEHR-EHR-OBSERVATION.pulse.v2]"].data.events.length<5) { apply("HF_5"); } //less than 10 values else if (row["[openEHR-EHR-COMPOSITION.report.v1]/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Herzfrequenz (Pulsmessung)']/items[openEHR-EHR-OBSERVATION.pulse.v2]"].data.events.length<10) { apply("HF_10"); } } else { apply("HF_0"); } //add whole outcome information to dataset row setting CDSS_success for case 3 times, each time randomly considering probablility of correct/wrong prediction (if a good ML result occurs 3 times we can be pretty sure that we were not just lucky with how our CDSS_success outcome turned out) row["completeness_issues"] = tmp_label["completeness_issue"]; row["timeliness_issues"] = tmp_label["timeliness_issue"]; row["density_issues"] = tmp_label["density_issue"]; row["plausibility_issues"] = tmp_label["plausibility_issue"]; row["CDSS_success_1"] = 0 + bl(Math.round(tmp_CDSS_success*100)); row["CDSS_success_2"] = 0 + bl(Math.round(tmp_CDSS_success*100)); row["CDSS_success_3"] = 0 + bl(Math.round(tmp_CDSS_success*100)); if (j<10) {console.log(row);} // bl returns true or false with given probability of true function bl(probability_for_bad_luck) { return (r(100) <= probability_for_bad_luck); } function r(max) { return Math.floor((Math.random() * max) + 1); } }); //method to apply a triggered rule on a case function apply(key) { //count triggering of rule o[key].counter++; //adjust CDSS_success probability for case tmp_CDSS_success = tmp_CDSS_success * o[key].CDSS_success; //if DQ-issue label is present for rule append issue label for case it if (o[key].label) { tmp_label[o[key].label]++; } } //for all rules add info about proportion of cases to which this rule was applied for (var property in o) { //NOTE percent values refer to all sites together o[property].percent = (o[property].counter/o[property].reference) * 100; o[property].CDSS_success_effect = o[property].counter * o[property].CDSS_success; } console.log(o); //write to file system so i have info about number of triggered rules for used outcome available utils.writeToFile("/testcases/triggered_rules_"+last_site+".json", JSON.stringify(o), true); console.log("Generating outcomes done."); //don't forget to send response to client res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({generated_outcome:true})); }; app.get('/outcome', dev.generate_outcome); }; /* init function for client side part of app*/ exports.initClientApp = async function() { toggle_nav(); //hide knowledge bases dev.add_dimensions_for_ML();//add dimensions per_case, per_case_10 and per_case_100 for ML research for (var i = 0; i<20; i++) { //run for -1 only to get all sites console.log(">>> Starting iteration for Site"+i); dev.set_AQL_for_ML(i); var res = await dev.get_data_and_paths_for_ML(); if (vm.MMs().length>0) { vm.MMs.remove(aMM => {return true}); } //load MMs from KB if (true) { dev.load_MMs_from_KBs_for_ML(); } //or just derive MMs and stop after that else { dev.derive_MMs_for_ML(); return; //don't do any other stuff. Don't run them all now! } if ((i>-1) && (res)) { //only run MMs and export results when iteration for multiple sites res = await dev.run_MMs_for_ML(); res = await dev.save_context_for_ML(i,res); var header_row = (i<1) ? true : false; //only create header_row if it is Site0 or all Sites (i=-1) vm.export_MMs_results_csv(header_row, res); } } }; exports.save_context_for_ML = async function(site_nr) { try { var my_result = await $.ajax({ async: true, type: "GET", contentType:"application/json", url: config.server_app.protocol+config.server_app.address+":"+config.server_app.port + "/contexts/"+site_nr, beforeSend: function (xhr) { xhr.setRequestHeader ("Authorization", "Basic " + config.authString); } }); } catch (error) { console.log(error); } return my_result; } exports.run_MMs_for_ML = async function() { console.log("Executing MMs..."); vm.executeMMs();//run all MMs function check_completion() { return new Promise(resolve => { setTimeout(() => { if (vm.running_MMs()==0) { setTimeout(() => { resolve(true); },5000); } else { resolve(check_completion()); } },5000); }); } var result = await check_completion(); console.log("Running MMs done."); return result; } exports.get_data_and_paths_for_ML = async function(load_from_file = false) { console.log("Retrieving data and paths ..."); vm.paths = []; //trigger server to laod stored contexts from file and derive paths (reproducible since file can be made publicly available (containing data including outcome and MM-results) and also much faster) if (load_from_file) { try { var my_result = await $.ajax({ async: true, type: "GET", contentType:"application/json", url: config.server_app.protocol+config.server_app.address+":"+config.server_app.port + "/contexts/load", beforeSend: function (xhr) { xhr.setRequestHeader ("Authorization", "Basic " + config.authString); } }); } catch (error) { console.log(error); } return my_result; } //retrieve data from think //to store contexts simply open localhost:3000/contexts/save in Browser (GET request to storing endpoint) else { //retrieve paths setTimeout(function() { vm.get_paths(); },1000); //timeout because connection has to be established first } function check_completion() { return new Promise(resolve => { if ((vm.paths.length>0) && (vm.retrieving_data_completed()==true)) { resolve(true); } else { setTimeout(() => { resolve(check_completion()); },1000); } }); } var result = await check_completion(); console.log("Retrieving data and paths done."); return result; } exports.add_dimensions_for_ML = function() { //add dimensions for cases vm.add_dimension_to_dimension_list("per_case", 'sprintf("%s",item1)', JSON.stringify([{domainPath: "dataset-row/composition/context/other_context[at0001]/items[openehr-ehr-cluster.case_identification.v0]/items[at0001][at0001]/value/value", datatype:"string", pathExtension: null, item: "item1"}])); } //set AQL to retrieve data for site with given number (-1 for all sites) exports.set_AQL_for_ML = function(site_nr = -1) { //set initial aql query var site_constraint = (site_nr>-1) ? `AND a/context/health_care_facility/name = "Site${site_nr}" ` : `ORDER BY a/context/health_care_facility/name, a/context/start_time/value `; vm.aql(` select a/composer as composer, a/context/start_time as composition_start_time, a/context/health_care_facility as site, a/context/other_context[at0001]/items[openEHR-EHR-CLUSTER.case_identification.v0]/items[at0001] as case_identification, a/content[openEHR-EHR-EVALUATION.gender.v1]/data[at0002]/items[at0022]/value as gender, a/content[openEHR-EHR-OBSERVATION.height.v2]/data[at0001]/events[at0002]/data[at0003]/items[at0004]/value as heigth, a/content[openEHR-EHR-OBSERVATION.body_weight.v2]/data[at0002]/events[at0003]/data[at0001]/items[at0004]/value as weight, a/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Kardiovaskuläre Risikofaktoren'] as risc_factors, a/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Blutdruck nach 5 Minuten Ruhe'] as blood_pressure, a/content[openEHR-EHR-SECTION.adhoc.v1 and name/value='Herzfrequenz (Pulsmessung)']/items[openEHR-EHR-OBSERVATION.pulse.v2] as heart_rate from EHR e contains COMPOSITION a[openEHR-EHR-COMPOSITION.report.v1] where a/archetype_details/template_id/value = "Anamnese" ${site_constraint} offset 0 limit 11000`.trim()); } exports.generate_outcome_data_for_ML = async function() { console.log("Generating outcome..."); try { res = await $.ajax({ async: true, type: "GET", contentType:"application/json", url: config.server_app.protocol+config.server_app.address+":"+config.server_app.port + "/outcome", beforeSend: function (xhr) { xhr.setRequestHeader ("Authorization", "Basic " + config.authString); } }); } catch (error) { console.log(error); } //add paths for added outcome data vm.paths.push("dataset-row/cdss_success_1"); vm.paths.push("dataset-row/cdss_success_2"); vm.paths.push("dataset-row/cdss_success_3"); vm.paths.push("dataset-row/completeness_issues"); vm.paths.push("dataset-row/timeliness_issues"); vm.paths.push("dataset-row/density_issues"); vm.paths.push("dataset-row/plausibility_issues"); console.log("Generating outcome done."); return res.generated_outcome; } exports.load_MMs_from_KBs_for_ML = function() { console.log("Load MMs from KBs..."); //load kb_Outcome_Measures.json //count + mean_CDSS_success: overall,(per_site,) per_day, per_day_of_week, per_case vm.kbMMs().forEach((kb, i) => { if (kb.kb_tag == "Outcome_Measures") { vm.clicked_kb(vm.kbMMs()[i], {target:{parentNode:{className:""}}}); } }); //load other MMs vm.kbMMs().forEach((kb, i) => { if (kb.kb_tag == "ML_Knowledge") { vm.clicked_kb(vm.kbMMs()[i], {target:{parentNode:{className:""}}}); } }); console.log("Loading MMs from KBs done."); return true; } exports.derive_MMs_for_ML = function() { vm.derive_by_rm_type(); var filter_string = ` (!tags.includes("plot")) && (!tags.includes("deciles")) && (!tags.includes("percentiles")) && (!tags.includes("mode")) && (!tags.includes("days_from")) && (iMM.tags.indexOf("duplicates")==-1) && (iMM.tags.indexOf("level_counts")==-1) && (iMM.tags.indexOf("relative_count")==-1) && (iMM.tags.indexOf("level_relative_counts")==-1) && (iMM.tags.indexOf(",per_")==-1) && ( (iMM.tags.indexOf("row_count")>-1) || (iMM.domainPaths[0].domainPath.indexOf("start_time")>-1) || ((iMM.domainPaths[0].domainPath.indexOf("case_identification")>-1) && (iMM.domainPaths[0].domainPath.indexOf("value/value")>-1)) || (iMM.domainPaths[0].domainPath.indexOf("gender")>-1) || ((iMM.domainPaths[0].domainPath.indexOf("height")>-1) && (iMM.domainPaths[0].domainPath.indexOf("magnitude")>-1)) || ((iMM.domainPaths[0].domainPath.indexOf("body_weight")>-1) && (iMM.domainPaths[0].domainPath.indexOf("magnitude")>-1)) || ((iMM.domainPaths[0].domainPath.indexOf("risikofaktoren")>-1) && (iMM.domainPaths[0].domainPath.indexOf("value/value")>-1)) || ( (iMM.domainPaths[0].domainPath.indexOf("blood")>-1) && ( (iMM.domainPaths[0].domainPath.indexOf("magnitude")>-1) || (iMM.domainPaths[0].domainPath.indexOf("origin/value")>-1)) ) || ( (iMM.domainPaths[0].domainPath.indexOf("pulse")>-1) && ( (iMM.domainPaths[0].domainPath.indexOf("magnitude")>-1) || (iMM.domainPaths[0].domainPath.indexOf("time/value")>-1) ) ) )`; //MMs filtered for domainPaths item0 used in fictive CDSS vm.mmFilter(filter_string); vm.filterMMs(); vm.add_dimension_to_MMs(vm.dimensions()[0]);//per_site vm.filterMMs();//always filter after applying one dimension to not apply dimension to another MM on a dimension vm.add_dimension_to_MMs(vm.dimensions()[6]);//per_day vm.filterMMs(); vm.add_dimension_to_MMs(vm.dimensions()[7]);//per_day_of_week vm.filterMMs(); vm.add_dimension_to_MMs(vm.dimensions()[8]);//per_case vm.filterMMs(); vm.add_dimension_to_MMs(vm.dimensions()[9]);//per_case_10 vm.filterMMs(); vm.add_dimension_to_MMs(vm.dimensions()[10]);//per_case_100 //now we need the dimension MMs vm.mmFilter(filter_string.replace('(iMM.tags.indexOf("per_")==-1) &&', '')); vm.filterMMs(); vm.add_aggregations(); //now we can show MMs which were just filtered because adding dimensions doesn't make sense on them vm.mmFilter(filter_string .replace('(iMM.tags.indexOf(",per_")==-1) &&', '')); vm.filterMMs(); //remove all not visible MMs vm.MMs.remove(aMM => {return (aMM.visible()==false)}); //remove all MMs refering to the absolute values of timepoints //filter by !density,!currency and domain path asUnixTS and remove all visible MMs vm.mmFilter(` (!tags.includes("density")) && (!tags.includes("currency")) && (iMM.single_domain_path_to_text(iMM.domainPaths[0]).indexOf("asUnixTS")>-1)`); vm.filterMMs(); vm.MMs.remove(aMM => {return aMM.visible()}); vm.mmFilter(""); vm.filterMMs(); //export MMs vm.export_MMs_as_KB(); } })(typeof exports === 'undefined'? this['dev']={}: exports);