From 13b1167ec2c68dedb5378ce69cf5436ed675e005 Mon Sep 17 00:00:00 2001 From: "Robert L. Read" <read.robert@gmail.com> Date: Sun, 28 Jun 2020 16:58:16 -0500 Subject: [PATCH] adding work-of-breathing --- breath_plot.html | 281 ++++++++++++++++++++++++++++------------------- 1 file changed, 171 insertions(+), 110 deletions(-) diff --git a/breath_plot.html b/breath_plot.html index b89e506..42b8fcb 100644 --- a/breath_plot.html +++ b/breath_plot.html @@ -325,7 +325,7 @@ an interactive or static analysis of a respiration. It's primary purpose is </div> </div> <div> - <label for="taip">TAIP (ms): </label> + <label for="taip">Rise Time (ms): </label> <div class="value_and_fences"> <div class="calcnum"> <label id="taip"> </label> @@ -343,7 +343,7 @@ an interactive or static analysis of a respiration. It's primary purpose is </div> </div> <div> - <label for="taip">TRIP (ms): </label> + <label for="taip">Fall Time (ms): </label> <div class="value_and_fences"> <div class="calcnum"> <label id="trip"> </label> @@ -359,6 +359,23 @@ an interactive or static analysis of a respiration. It's primary purpose is </div> </div> </div> + <div> + <label for="wob">W. of B. (J/L): </label> + <div class="value_and_fences"> + <div class="calcnum"> + <label id="wob"> </label> + </div> + <div class="vertical_alarms"> + <div class="limit max"> + <label for="wob_h">H:</label> + <input class="fence" id="wob_h" type='text'> </input> + </div> + <div class="limit min"> + <label for="wob_l">L:</label> + <input class="fence" id="wob_l" type='text'> </input> + </div> + </div> + </div> </div> </div> </div> @@ -370,11 +387,11 @@ an interactive or static analysis of a respiration. It's primary purpose is <div> Parameters: <div> - <label for="tarip_h">TAIP/TRIP High Pressure (cm H2O):</label> + <label for="tarip_h">Rise/Fall High Pressure (cm H2O):</label> <input class="fence" id="tarip_h" type='text'> </input> </div> <div> - <label for="tarip_l">TAIP/TRIP Low Pressure (cm H2O):</label> + <label for="tarip_l">Rise/Fall Low Pressure (cm H2O):</label> <input class="fence" id="tarip_l" type='text'> </input> </div> </div> @@ -667,10 +684,30 @@ function plot(samples, trans, breaths) { ,0); var max_v = Math.max(max_inh,max_exh); + // not sure why I have null work, have to understand. + var max_work_per_liter = 0; + var work_per_liter = []; + for(var i = 0; i < breaths.length; i++) { + var b = breaths[i]; + if (b.work != null) { + var wpl = b.work / b.vol_i; + if (wpl > max_work_per_liter) + max_work_per_liter = wpl; + var center = ((trans[b.trans_begin_inhale].ms + trans[b.trans_cross_zero].ms) - 2*min) / (2.0 * 1000.0); + work_per_liter.push({ wpl: wpl, center: center }); + } + } + var exh_v = unpack(breaths, 'vol_e').map(e => 100 * e / max_v); var t_exh_v = unpack(breaths, 'vol_e').map(e => Math.round(e*1000.0)+"ml exh"); var inh_v = unpack(breaths, 'vol_i').map(i => 100 * i / max_v); var t_inh_v = unpack(breaths, 'vol_i').map(e => Math.round(e*1000.0)+"ml inh"); + + + var work_v = work_per_liter.map(w => (1/3) * (100 * w.wpl / max_work_per_liter)); + var t_work = work_per_liter.map(w => w.wpl.toFixed(2)+"J/L"); + var work_centers = unpack(work_per_liter, 'center'); + // now to graph properly, I must find the center of an inhalation. // I have packed these into the breaths... var inhale_centers = breaths.map(b => ((trans[b.trans_begin_inhale].ms + trans[b.trans_cross_zero].ms) - 2*min) / (2.0 * 1000.0)); @@ -698,6 +735,17 @@ function plot(samples, trans, breaths) { text: t_exh_v, }; + var workPlot = {type: "scatter", mode: "markers+text", + name: "WoB J", + textposition: 'bottom center', + x: work_centers, + y: work_v, + text: t_work, + marker: { size: 8, + color: 'blue', + symbol: 'triangle-right'} + }; + event_graph.push(workPlot); event_graph.push(inhPlot); event_graph.push(exhPlot); } @@ -733,7 +781,7 @@ function plot(samples, trans, breaths) { // Basically, we will show only the first of these in a "run" // This is rather difficult; we will instead just use 200 ms // as a window. - const time_windwow_ms = 200; + const TIME_WINDOW_MS = 400; var lows = []; var highs = []; @@ -741,10 +789,10 @@ function plot(samples, trans, breaths) { for(var i = 0; i < messages.length; i++) { var m = messages[i]; if (m.buff == FLOW_TOO_LOW) { - if ((lows.length == 0) || ((lows.length > 0) && (lows[lows.length-1].ms < (m.ms - 200)))) + if ((lows.length == 0) || ((lows.length > 0) && (lows[lows.length-1].ms < (m.ms - TIME_WINDOW_MS)))) lows.push(m); } else if (m.buff == FLOW_TOO_HIGH) { - if ((highs.length == 0) || ((highs.length > 0) && (highs[highs.length-1].ms < (m.ms - 200)))) + if ((highs.length == 0) || ((highs.length > 0) && (highs[highs.length-1].ms < (m.ms - TIME_WINDOW_MS)))) highs.push(m); } else { others.push(m); @@ -933,7 +981,13 @@ function compute_fio2_mean(secs,samples) { } } - +// returns: +// [ respiration rate, +// tidal volume (avg), +// minute_volume +// EIRatio, +// Work-of-Breathing (avg) +// ] function compute_respiration_rate(secs,samples,transitions,breaths) { // In order to compute the number of breaths // in the last s seconds, I compute those breaths @@ -944,7 +998,7 @@ function compute_respiration_rate(secs,samples,transitions,breaths) { var first_inhale_ms = -1; var last_inhale_ms = -1; if (breaths.length == 0) { - return [0,0,0,"NA"]; + return [0,0,0,"NA","NA"]; } else { const recent_ms = samples[samples.length - 1].ms; var cur_ms = recent_ms; @@ -959,6 +1013,9 @@ function compute_respiration_rate(secs,samples,transitions,breaths) { // divide by the inhlation_duration. var vol_ci = 0.0; + var wob = 0.0; + var wob_cnt = 0; + while((i >=0) && (breaths[i].ms > (cur_ms - secs*1000))) { cnt++; vol_i += breaths[i].vol_i; @@ -975,6 +1032,11 @@ function compute_respiration_rate(secs,samples,transitions,breaths) { const zero_ms = transitions[breaths[i].trans_cross_zero].ms; time_inh += (zero_ms - inh_ms); time_exh += (exh_ms - zero_ms); + + if (breaths[i].work != null) { + wob += breaths[i].work / breaths[i].vol_i; + wob_cnt++; + } i--; } if ((cnt > 1) && (first_inhale_ms != last_inhale_ms)) { @@ -990,16 +1052,18 @@ function compute_respiration_rate(secs,samples,transitions,breaths) { var tidal_volume = 1000.0 * vol_i / cnt; var EIratio = (time_inh == 0) ? null : time_exh / time_inh; + var WorkOfBreathing_J_per_L = wob / wob_cnt; return [ rr, tidal_volume, minute_volume, - EIratio]; + EIratio, + WorkOfBreathing_J_per_L]; } else { - return [0,0,0,"NA"]; + return [0,0,0,"NA","NA"]; } } - } +} var LIMITS = { max: { h: 40, @@ -1021,7 +1085,9 @@ var LIMITS = { taip: { h: 100, // These are in ms l: 0}, trip: { h: 100, // These are in ms - l: 0}, + l: 0}, + wob: { h: 3, + l: 0.2}, } function load_ui_with_defaults(limits) { @@ -1067,13 +1133,19 @@ function process(samples) { var [transitions,breaths] = computeMovingWindowTrace(samples,t,v); plot(samples,transitions,breaths); // How many seconds backwards should we look? Perhaps 20? - var [bpm,tv,mv,EIratio,fio2] = compute_respiration_rate(RESPIRATION_RATE_WINDOW_SECONDS,samples,transitions,breaths); + var [bpm,tv,mv,EIratio,wob] = compute_respiration_rate(RESPIRATION_RATE_WINDOW_SECONDS,samples,transitions,breaths); var alarms = []; $("#bpm").text(bpm.toFixed(1)); $("#tv").text(tv.toFixed(0)); - $("#mv").text((mv).toFixed(2)); + $("#mv").text(mv.toFixed(2)); + + if (wob == "NA") { + $("#wob").text("NA"); + } else { + $("#wob").text(wob.toFixed(2)); + } if (EIratio == "NA") { $("#ier").text("NA"); } else { @@ -1096,8 +1168,10 @@ function process(samples) { alarms = alarms.concat(check_high_and_low(LIMITS,"bpm",bpm.toFixed(1),b_ms)); alarms = alarms.concat(check_high_and_low(LIMITS,"tv",tv.toFixed(0),b_ms)); - alarms = alarms.concat(check_high_and_low(LIMITS,"mv",(mv).toFixed(2),b_ms)); + alarms = alarms.concat(check_high_and_low(LIMITS,"mv",mv.toFixed(2),b_ms)); alarms = alarms.concat(check_high_and_low(LIMITS,"ier",(1.0 / EIratio).toFixed(1),b_ms)); + if (wob != "NA") + alarms = alarms.concat(check_high_and_low(LIMITS,"wob",wob.toFixed(2),b_ms)); // These must be moved out; in fact, // they must be made configurable! @@ -1182,26 +1256,34 @@ function retrieveAndPlot(){ // WARNING: This is a hack...if the timestamp is negative, // we treat it as a limited (beyond range of sensor) measurement. // Our goal is to warn about this, but for now we will just - // ignore and correct. + // ignore and correct. + if (cur_sam == null) { + console.log("No return"); + return; + } if (cur_sam && cur_sam.length == 0) { console.log("no samples; potential misconfiguration!"); } else { - cur_sam = sanitize_samples(cur_sam); - if (INITS_ONLY) { - samples = cur_sam; - INITS_ONLY = false; - } else { - var discard = Math.max(0, - samples.length + cur_sam.length - MAX_SAMPLES_TO_STORE_S); - samples = samples.slice(discard); - } - - - samples = samples.concat(cur_sam); - // We are not guaranteeed to get samples in order - // we sort them.... - samples = samples.sort((a,b) => a.ms - b.ms); - process(samples); + if (typeof(cur_sam) == "string") { + console.log(cur_sam); + } else { + cur_sam = sanitize_samples(cur_sam); + if (INITS_ONLY) { + samples = cur_sam; + INITS_ONLY = false; + } else { + var discard = Math.max(0, + samples.length + cur_sam.length - MAX_SAMPLES_TO_STORE_S); + samples = samples.slice(discard); + } + + + samples = samples.concat(cur_sam); + // We are not guaranteeed to get samples in order + // we sort them.... + samples = samples.sort((a,b) => a.ms - b.ms); + process(samples); + } } }, error: function(xhr, ajaxOptions, thrownError) { @@ -1416,11 +1498,10 @@ function PressureVolumeWork(breath, transitions, samples) { var beginTime_ms = beginTransition.ms; var endTransition = transitions[breath.trans_cross_zero]; var endTime_ms = endTransition.ms; - var flows = samples.filter(s => s.event == 'M' && s.type == 'F' && s.ms >= beginTime_ms && s.ms <= endTime_ms); - var pressures = samples.filter(s => s.event == 'M' && s.type == 'D' && s.loc == 'A'&& s.ms >= beginTime_ms && s.ms <= endTime_ms); - - console.log(flows); - console.log(pressures); + var flows = samples.filter(s => s.event == 'M' && s.type == 'F' && + s.ms >= beginTime_ms && s.ms <= endTime_ms); + var pressures = samples.filter(s => s.event == 'M' && s.type == 'D' && + s.loc == 'A'&& s.ms >= beginTime_ms && s.ms <= endTime_ms); // Note: The algorithm below relies on the fact that there is // only one flow or pressure with a single ms value; and that @@ -1433,8 +1514,8 @@ function PressureVolumeWork(breath, transitions, samples) { var ct = Math.min(flows[0].ms,pressures[0].ms); var lfp = { val : flows[0].val, ms: flows[0].ms } ; // last flow point var lpp = { val : pressures[0].val, ms: pressures[0].ms }; // last pressure_point - var fi = increment_past(flows,ct,0); // Index of next flow sample - var pi = increment_past(pressures,ct,0); // Index of next pressure sample + var fi = increment_past(flows,flows[0].ms,0); // Index of next flow sample + var pi = increment_past(pressures,pressures[0].ms,0); // Index of next pressure sample var w = 0; // current work // compute flow at time ms give index and last point @@ -1445,17 +1526,24 @@ function PressureVolumeWork(breath, transitions, samples) { return last.val + (cur.val - last.val)*(ms - ms0)/(ms1 - ms0); } function increment_past(array,ms,index) { + var begin = index; while(index < array.length && array[index].ms <= ms) index++; + if (index == begin) debugger; if (index >= array.length) return null; else return index; } + // A fundamental invariant: + // pressures[pi].ms > lpp.ms + // flows[pi].ms > lfp.ms while ((fi + pi) < (flows.length + pressures.length)) { // Invariant always increment fi or pi // fi and pi point to unprocessed value + console.assert(pressures[pi].ms > lpp.ms); + console.assert(flows[fi].ms > lfp.ms); var ms; if (pressures[pi].ms <= flows[fi].ms) { // process pressure ms = pressures[pi].ms; @@ -1473,7 +1561,8 @@ function PressureVolumeWork(breath, transitions, samples) { if ((fi === null) || (pi === null)) break; var dur_s = (ms - ct) / 1000; - console.log("dur_s",dur_s); + console.assert(pressures[pi].ms > lpp.ms); + console.assert(flows[fi].ms > lfp.ms); var nf = f(ms,flows[fi],lfp); var np = f(ms,pressures[pi],lpp); var f1 = (lfp.val + nf)/2; @@ -1490,48 +1579,6 @@ function PressureVolumeWork(breath, transitions, samples) { lpp = { val : p1, ms: ms }; ct = ms; } - console.log("begin transition time, end transition time:",beginTime_ms,endTime_ms); - - console.log("ms,w",ms,w); - //var pressureVolume_prod= 0; - //for(var j = a; j < z-1; j++) { - // // I'll use qadrilateral approximation. - // // We'll form each quadrilateral between two samples. - // var ms = flows[j+1].ms - flows[j].ms; - // var ht = (((flows[j+1].val*pressures[j+1].val) + (flows[j].val*pressures[j+1].val ))/2) * CONVERT_PIRDS_TO_SLM; - // // Flow in standard liters per minute, - // // divide by 60000 to get liters/s - // pressureVolume_prod += ms * ht/60000; - // if (isNaN(pressureVolume_prod)) { - // debugger; - // } - // } - // pressure cm H2O --> atm (divide by 1033) - // return pressureVolume_prod/1033 - - // average pressure * average flow ~ approximation of work - var pAv_cm = 0; - for (var i = 0; i<pressures.length; i++) { - pAv_cm += pressures[i].val/10; - } - pAv_cm /= pressures.length; - var pressures_pascales = pAv_cm * 98.0665; //cm to pasc. - var fAv_lpm = 0; - for (var i = 0; i<flows.length; i++) { - fAv_lpm += flows[i].val/1000; - } - fAv_lpm /= flows.length; - console.log("flows:",flows,"\npressures:",pressures) - console.log("pAv, vAv = ",pAv_cm,fAv_lpm); - var flow_cubicMetersPerSecond = fAv_lpm/1000/60; //lpm to cmps - var avPower = pressures_pascales*flow_cubicMetersPerSecond; // Watts - var avWork = avPower * (endTime_ms - beginTime_ms)/1000; // Joules - - - console.log("Power (Watts):",avPower); - console.log("Work (Joules):",avWork); - - console.log("ms, Work main (Joules)",ms,w); return w; } } @@ -1542,42 +1589,52 @@ function generate_synthetic_trace() { const SAMPLES_MS_PER_SAMPLE = 1; var trace = []; var cur = 0; - function push_samples(start,num,d,f) { + // p is a probability of occuring. + function push_samples(start,num,d,f,pd,pf) { for(var i = start; i < start+num; i++) { - trace.push( - { - event: "M", - loc: "A", - ms: i, - num: 0, - type: "D", - val: d - }); - trace.push( - { - event: "M", - loc: "A", - ms: i, - num: 0, - type: "F", - val: f - }); + if (Math.random() < pd) { + trace.push( + { + event: "M", + loc: "A", + ms: i, + num: 0, + type: "D", + val: d + }); + } + if (Math.random() < pf) { + trace.push( + { + event: "M", + loc: "A", + ms: i, + num: 0, + type: "F", + val: f + }); + } } } - push_samples(0,SAMPLES_PER_PHASE,-200,-50000); - push_samples(SAMPLES_PER_PHASE,SAMPLES_PER_PHASE,200,50000); - push_samples(SAMPLES_PER_PHASE*2,SAMPLES_PER_PHASE,-200,-50000); - push_samples(SAMPLES_PER_PHASE*3,SAMPLES_PER_PHASE,200,50000); + const P1 = 1/2; + const P2 = 1/2; + push_samples(SAMPLES_PER_PHASE,SAMPLES_PER_PHASE,200,50000,P1,P2); + push_samples(0,SAMPLES_PER_PHASE,-200,-50000,P1,P2); + push_samples(SAMPLES_PER_PHASE,SAMPLES_PER_PHASE,200,50000,P1,P2); + push_samples(SAMPLES_PER_PHASE*2,SAMPLES_PER_PHASE,-200,-50000,P1,P2); + push_samples(SAMPLES_PER_PHASE*3,SAMPLES_PER_PHASE,200,50000,P1,P2); return trace; } +function nearp(x,y,d) { + return Math.abs(x - y) <= d; +} function testWorkSynthetic(){ // breaths give us inspiration transition points var samples = generate_synthetic_trace(); const JOULES_IN_BREATH = 1 * 1961.33 * 50000 / (60e+6); - var flows = samples.filter(s => s.event == 'M' && s.type == 'F'); var first_time = flows[0].ms; var last_time = flows[flows.length - 1].ms; @@ -1592,10 +1649,10 @@ function testWorkSynthetic(){ // breaths give us inspiration transition points console.log(breaths); for(i = 0; i<breaths.length; i++) { var w = PressureVolumeWork(breaths[i], transitions, samples); - console.assert((w == null) || (w == JOULES_IN_BREATH)); - console.log("final = ",w); + console.assert((w == null) || (nearp(w,JOULES_IN_BREATH),0.1)); + console.log("final (Joules) = ",w); } - + return true; } @@ -1680,6 +1737,8 @@ function testWorkSynthetic(){ // breaths give us inspiration transition points trans_end_exhale: i, } ); + var w = PressureVolumeWork(breaths[breaths.length-1], transitions, samples); + breaths[breaths.length-1].work = w; beg = i; expiring = false; vole = integrateSamples(last,transitions[i].sample,flows); @@ -1758,6 +1817,8 @@ function computeMovingWindowTrace(samples,t,v) { trans_end_exhale: i, } ); + var w = PressureVolumeWork(breaths[0], transitions, samples); + breaths[0].work = w; beg = i; expiring = false; vole = integrateSamples(last,transitions[i].sample,flows); -- GitLab