From 26ec335f26a4ac74c5596b5194a4e1c31a476059 Mon Sep 17 00:00:00 2001
From: "Robert L. Read" <read.robert@gmail.com>
Date: Sat, 4 Jul 2020 10:36:30 -0500
Subject: [PATCH] moving math out into a separate file for reuse

---
 breath_plot.html       | 1011 ++++++++++++++--------------------------
 js/respiration_math.js |  708 ++++++++++++++++++++++++++++
 2 files changed, 1050 insertions(+), 669 deletions(-)
 create mode 100644 js/respiration_math.js

diff --git a/breath_plot.html b/breath_plot.html
index 2c05228..4d86403 100644
--- a/breath_plot.html
+++ b/breath_plot.html
@@ -28,6 +28,9 @@ Breath Plot: COVID-19 Respiration Analysis Software
 	<!-- Load plotly.js into the DOM -->
   <script src='https://cdn.plot.ly/plotly-latest.min.js'></script>
 
+  <script src='js/respiration_math.js'></script>
+
+
     <title>Public Invention Respiration Analysis</title>
 
 </head>
@@ -539,7 +542,7 @@ function unpack(rows, key) {
     return rows.map(function(row) { return row[key]; });
 }
 
-const CONVERT_PIRDS_TO_SLM = 1/1000;
+// const CONVERT_PIRDS_TO_SLM = 1/1000;
 
 // we have now changed this, there will be flow and
 // pressure in the same samples, and we should filter.
@@ -938,151 +941,6 @@ function check_alarms(limits,key,limit,val,f,ms) {
 }
 
 
-// Return, [min,avg,max] pressures (no smoothing)!
-function compute_pressures(secs,samples) {
-  var pressures = samples.filter(s => s.event == 'M' && s.type == 'D' && s.loc == 'A');
-
-  if (pressures.length == 0) {
-    return [0,0,0,[]];
-  } else {
-    const recent_ms = pressures[pressures.length - 1].ms;
-    var cur_ms = recent_ms;
-    var cnt = 0.0;
-    var i = pressures.length - 1;
-    var cur_sample = pressures[i];
-
-    var min = Number.MAX_VALUE;
-    var max = Number.MIN_VALUE;
-    var sum = 0;
-    var alarms = [];
-    while((i >=0) && (pressures[i].ms > (cur_ms - secs*1000))) {
-      var p = pressures[i].val / 10.0;  // this is now cm H2O
-      if (p < min ) {
-        min = p;
-      }
-      if (p > max) {
-        max = p;
-      }
-      sum += p;
-      cnt++;
-      alarms = alarms.concat(check_alarms(LIMITS,"max","h",p,(a,b) =>(a > b),pressures[i].ms));
-      alarms = alarms.concat(check_alarms(LIMITS,"max","l",p,(a,b) =>(a < b),pressures[i].ms));
-      i--;
-    }
-    return [min,sum/cnt,max,alarms];
-  }
-}
-
-function compute_fio2_mean(secs,samples) {
-  var oxygens = samples.filter(s => s.event == 'M' && s.type == 'O' && s.loc == 'A');
-
-  if (oxygens.length == 0) {
-    return null;
-  } else {
-    const recent_ms = oxygens[oxygens.length - 1].ms;
-    var cur_ms = recent_ms;
-    var cnt = 0.0;
-    var i = oxygens.length - 1;
-
-    var sum = 0;
-    var alarms = [];
-    while((i >=0) && (oxygens[i].ms > (cur_ms - secs*1000))) {
-      var oxy = oxygens[i].val; // oxygen concentration as a percentage
-      sum += oxy;
-      cnt++;
-      i--;
-    }
-
-    var fio2_avg = sum / cnt;
-
-    return fio2_avg;
-  }
-}
-
-// 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
-  // whose time stamp is s seconds from the most recent sample
-
-  // We will compute respiration rate by counting breaths
-  // and dividing (cnt - 1) by time the first and last inhalation
-  var first_inhale_ms = -1;
-  var last_inhale_ms = -1;
-  if (breaths.length == 0) {
-    return [0,0,0,"NA","NA"];
-  } else {
-    const recent_ms = samples[samples.length - 1].ms;
-    var cur_ms = recent_ms;
-    var cnt = 0.0;
-    var vol_i = 0.0;
-    var vol_e = 0.0;
-    var i = breaths.length - 1;
-    var time_inh = 0;
-    var time_exh = 0;
-    // fully completed inhalation volume does not include the
-    // most recent breath; we need it to be able to accurately
-    // 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;
-      if (i < (breaths.length -1)) {
-        vol_ci += breaths[i].vol_i;
-      }
-      vol_e += breaths[i].vol_e;
-
-      const inh_ms = transitions[breaths[i].trans_begin_inhale].ms;
-      // note i is counting down in this loop...
-      if (last_inhale_ms < 0) last_inhale_ms = inh_ms;
-      first_inhale_ms = inh_ms;
-      const exh_ms = transitions[breaths[i].trans_end_exhale].ms;
-      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)) {
-      var inhalation_duration = last_inhale_ms - first_inhale_ms;
-      var inhalation_duration_min = inhalation_duration / (60.0 * 1000.0);
-      var rr = (cnt - 1) / inhalation_duration_min;
-      var duration_minutes = secs / 60.0;
-
-      // This is liters per minute. vol_ci is in liters.
-      // inhalation_duration is in ms.
-      var minute_volume =  vol_ci / inhalation_duration_min;
-
-      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,
-        WorkOfBreathing_J_per_L];
-    } else {
-      return [0,0,0,"NA","NA"];
-    }
-  }
-}
-
 var LIMITS = {
   max: { h: 40,
          l: 0},
@@ -1220,7 +1078,7 @@ function process(samples) {
     $("#trip").text("NA");
   }
 
-  var [min,avg,max,palarms] = compute_pressures(RESPIRATION_RATE_WINDOW_SECONDS,samples);
+  var [min,avg,max,palarms] = compute_pressures(RESPIRATION_RATE_WINDOW_SECONDS,samples,alarms,LIMITS);
   alarms = alarms.concat(palarms);
 
   $("#min").text(min.toFixed(1));
@@ -1283,6 +1141,9 @@ function retrieveAndPlot(){
               console.log("no samples; potential misconfiguration!");
             } else {
               if (typeof(cur_sam) == "string") {
+                console.log("Error!");
+                stop_interval_timer();
+                $("#livetoggle").prop("checked",false);
                 console.log(cur_sam);
               } else {
                 cur_sam = sanitize_samples(cur_sam);
@@ -1295,7 +1156,6 @@ function retrieveAndPlot(){
 	          samples = samples.slice(discard);
 	        }
 
-
 	        samples = samples.concat(cur_sam);
 	        // We are not guaranteeed to get samples in order
 	        // we sort them....
@@ -1315,405 +1175,217 @@ function retrieveAndPlot(){
 }
 
 
-function compute_transitions(vm,flows) {
-  var transitions = [];
-  var state = 0; // Let 1 mean inspiration, -1 mean expiration, 0 neither
-  for(var i = 0; i < flows.length; i++) {
-    var f = flows[i].val * CONVERT_PIRDS_TO_SLM;
-    //    var ms = flows[i].ms-first_time;
-    var ms = flows[i].ms;
-    if (state == 0) {
-      if (f > vm) {
-	state = 1;
-	transitions.push({ state: 1, sample: i, ms: ms})
-      } else if (f < -vm) {
-	state = -1;
-	transitions.push({ state: -1, sample: i, ms: ms})
-      }
-    } else if (state == 1) {
-      if (f < -vm) {
-	state = -1;
-	transitions.push({ state: -1, sample: i, ms: ms})
-      } else if (f < vm) {
-	state = 0;
-	transitions.push({ state: 0, sample: i, ms: ms})
-      }
-    } else if (state == -1) {
-      if (f > vm) {
-	state = 1;
-	transitions.push({ state: 1, sample: i, ms: ms})
-      } else if (f > 0) {
-	state = 0;
-	transitions.push({ state: 0, sample: i, ms: ms})
-      }
-    }
-  }
-  return transitions;
-}
-
-// produces a set of rising signals, time in ms of the leading edge of the rise and the trailing edge of the rise
-// an array of 2-tuple
-// taip == true implies compute TAIP, else compute TRIP
-// Possibly this routine should be generalized to a general rise-time routine.
-function compute_TAIP_or_TRIP_signals(min,max,pressures,taip) {
-  var pressures = pressures.filter(s => s.event == 'M' && s.type == 'D' && s.loc == 'A');
-  const responseBegin = 0.1;
-  const responseEnd = 0.9;
-
-  var signals = [];
-  var foundMinSignal = false;
-
-  const highFence = (min + (responseEnd * (max - min)))*10;
-  const lowFence = (min + (responseBegin * (max - min)))*10;
-
-  var cur_signal_start;
-  var state = -1; // Let 1 mean rising, -1 mean fallen, 0 risen, but not fallen
-  var first_sample_index = taip ? 0 : pressures.length-1 ;
-  var last_sample_index = taip ? pressures.length-1 : 0;
-  var increment = taip ? 1 : -1;
-  for(var i = first_sample_index; i != last_sample_index; i+=increment) {
-    var p = pressures[i].val;
-    var ms = pressures[i].ms;
-    if (state == -1) {
-      if (p >= lowFence) {
-	      state = 1;
-        cur_signal_start = pressures[i];
-      }
-      if (p >= highFence){
-        signals.push([ms,ms]);
-      }
-    } else if (state == 1) {
-      //console.log("state = 1",cur_signal_start); for debugging
-      if (p >= highFence) {
-        signals.push(taip ? [cur_signal_start.ms,ms] : [ms,cur_signal_start.ms])
-        state = 0;
-      } else if (p <= lowFence) {
-        state = -1;
-        cur_signal_start = null;
-      }
-    } else if (state == 0) {
-      if (p <= lowFence) {
-	state = -1;
-      }
-    }
-  }
-  return signals;
-}
-
-function compute_mean_TRIP_or_TAIP_sigs(sigs,min,max,pressures,taip){
-  if (sigs.length == 0){
-    return "NA";
-  } else {
-    var sum = 0;
-    for(var i = 0; i < sigs.length; i++) {
-      sum += sigs[i][1] - sigs[i][0];  // time in ms
-    }
-    return sum / sigs.length;
-  }
-}
-
-function testdata(){
-  //0 (not good enough), 10 (rising), 20 (above threshold) all 10 ms apart
-  //saw tooth function
-  var data = []; // pushing 50 things into it
-  for(var i = 0; i < 10; i++) {
-    var ms = i*5*10;
-    data[i*5+0] = {event:'M',loc:'A',ms:ms + 0,type:'D',val: 0};
-    data[i*5+1] = {event:'M',loc:'A',ms:ms + 10,type:'D',val: 100};
-    data[i*5+2] = {event:'M',loc:'A',ms:ms + 20,type:'D',val: 200};
-    data[i*5+3] = {event:'M',loc:'A',ms:ms + 30,type:'D',val: 100};
-    data[i*5+4] = {event:'M',loc:'A',ms:ms + 40,type:'D',val: 0};
-  }
-  return data;
-}
-
-function testdataSine(period_sm){ // period expressed in # of samples, each sample 10 ms
-  //0 (not good enough), 10 (rising), 20 (above threshold) all 10 ms apart
-  //sine tooth function
-  var data = []; // pushing 50 things into it
-  for(var i = 0; i < 1000; i++) {
-    var ms = i*10;
-    data[i] = {event:'M',loc:'A',ms:ms + 20,type:'D',val: 200*Math.sin(2*Math.PI*i/period_sm)};
-  }
-  return data;
-}
-
-function compute_mean_TRIP_or_TAIP(min,max,samples,taip) {
-  return compute_mean_TRIP_or_TAIP_sigs(
-    compute_TAIP_or_TRIP_signals(min,max,samples,taip),
-    min,max,samples,taip);
-}
-
-function test_compute_TAIP() {
-  var samples = testdata();
-  const TAIP_min = 0; // cm of H2O
-  const TAIP_max = 20; // cm of H2O
-  var TAIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,true);
-  console.assert(TAIP_m == 10);
-  for (i = 50; i<150; i+=10) {
-    var sinewave = testdataSine(i);
-    var TAIP_m = compute_mean_TRIP_or_TAIP(min,max,sinewave,true);
-  }
-}
-
-function compute_current_TAIP(TAIP_min,TAIP_max){ //uses samples from a global var
-  var TAIP_m = compute_mean_TRIP_or_TAIP(TAIP_min,TAIP_max,samples,true);
-  return TAIP_m;
-}
-
-// Because TAIP and TRIP are symmetric when viewed from
-// from the direction of the samples; this tests that
-// as a prelude to computing a single way.
-
-function reverseArray(arr) {
-  var newArray = [];
-  for (var i = arr.length - 1; i >= 0; i--) {
-    newArray.push(arr[i]);
-  }
-  return newArray;
-}
-function test_TRIP_and_TAIP_are_symmetric() {
-  var samples = testdata();
-  var rsamples = reverseArray(samples);
-  const min = 0;
-  const max = 20;
-  var TRIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,false);
-  var TRIP_m_r = -compute_mean_TRIP_or_TAIP(min,max,rsamples,true);
-  console.assert(TRIP_m == TRIP_m_r);
-  console.assert(TRIP_m == 10);
-  for (i = 50; i<150; i+=10) {
-    var sinewave = testdataSine(i);
-    var rsinewave = reverseArray(sinewave);
-    var TRIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,false);
-    var TRIP_m_r = -compute_mean_TRIP_or_TAIP(min,max,rsamples,true);
-    console.assert(TRIP_m == TRIP_m_r);
-    var TAIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,true);
-    var TAIP_m_r = -compute_mean_TRIP_or_TAIP(min,max,rsamples,false);
-    console.assert(TAIP_m == TAIP_m_r);
-  }
-}
-
-function compute_current_TRIP(TRIP_min,TRIP_max, samples)
-{ //uses samples from a global var
-  if (samples.length == 0){
-    return "NA";
-  }
-  else {
-    var TRIP_m = compute_mean_TAIP_or_TRIP(TRIP_min,TRIP_max,false);
-    return TRIP_m;
-  }
-}
-
-
-
-  // A routine to calculate work per breath
-function PressureVolumeWork(breath, transitions, samples) {
-  // -1 for quadilateral approximation
-  if (breath.vol_i == 0) {
-    return null;
-  } else {
-    var beginTransition = transitions[breath.trans_begin_inhale];
-    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);
-
-    // Note: The algorithm below relies on the fact that there is
-    // only one flow or pressure with a single ms value; and that
-    // increvementing an index necessarily incremenst the .ms value.
-
-    // Without two samples, we have no duration and can't define
-    // work.
-    if (pressures.length < 2 || flows.length < 2) return null;
-
-    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,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
-    // This is just a simple linear interpolation
-    function f(ms,cur,last) {
-      var ms0 = last.ms;
-      var ms1 = cur.ms;
-      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;
-        pi = increment_past(pressures,ms,pi);
-        if (flows[fi].ms <= ms) {
-          fi = increment_past(flows,ms,fi);
-        }
-      } else {
-        ms = flows[fi].ms;
-        fi = increment_past(flows,ms,fi);
-        if (pressures[pi].ms <= ms) {
-          pi = increment_past(pressures,ms,pi);
-        }
-      }
-      if ((fi === null) || (pi === null))
-        break;
-      var dur_s = (ms - ct) / 1000;
-      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;
-      var p1 = (lpp.val + np)/2;
-      // convert 10ths of cm H2O to pascals..
-      var p1_pa = (p1 * 98.0665) / 10;
-
-      // convert flows in lpm to cubic meters per seconds
-      var f1_m_cubed_per_s = f1 / (1000 * 1000 * 60);
-
-      // work is now in Joules!
-      w += dur_s * p1_pa * f1_m_cubed_per_s;
-      lfp = { val : f1, ms: ms };
-      lpp = { val : p1, ms: ms };
-      ct = ms;
-    }
-    return w;
-  }
-}
-
-// Let's first set up a perfectly square 1-second
-function generate_synthetic_trace() {
-  const SAMPLES_PER_PHASE = 1000;
-  const SAMPLES_MS_PER_SAMPLE = 1;
-  var trace = [];
-  var cur = 0;
-  // p is a probability of occuring.
-  function push_samples(start,num,d,f,pd,pf) {
-    for(var i = start; i < start+num; i++) {
-      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
-          });
-      }
-    }
-  }
-  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;
-    var duration = last_time - first_time;
-    console.log(flows);
 
 
-  const vm = 10;
-  // There is a problem here that this does not create a transition at the beginning.
-  var transitions = compute_transitions(vm,flows);
-    var breaths = compute_breaths_based_without_negative_flow(transitions,flows);
-    console.log(breaths);
-    for(i = 0; i<breaths.length; i++) {
-      var w = PressureVolumeWork(breaths[i], transitions, samples);
-      console.assert((w == null) || (nearp(w,JOULES_IN_BREATH),0.1));
-      console.log("final (Joules) = ",w);
-    }
-  return true;
-}
 
-
-  function testWork(samples){ // breaths give us inspiration transition points
-    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;
-    var duration = last_time - first_time;
-    console.log(flows);
-
-    const vm = 10;
-    var transitions = compute_transitions(vm,flows);
-    var breaths = compute_breaths_based_without_negative_flow(transitions,flows);
-    console.log(breaths);
-    for(i = 0; i<breaths.length; i++) {
-      var w = PressureVolumeWork(breaths[i], transitions, samples);
-      console.log(w);
-    }
-   }
-
-
-
-  // This should be in liters...
-  function integrateSamples(a,z,flows) {
-    // -1 for quadilateral approximation
-    var vol = 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 + flows[j].val )/2) * CONVERT_PIRDS_TO_SLM;
-      // Flow is actually in standard liters per minute,
-      // so to get liters we divide by 60 to it l/s,
-      // and and divde by 1000 to convert ms to seconds.
-      // We could do that here, but will move constants
-      // to end...
-      vol += ms * ht;
-      if (isNaN(vol)) {
-        debugger;
-      }
-    }
-    return vol/(60*1000);
-  }
+//   // A routine to calculate work per breath
+// function PressureVolumeWork(breath, transitions, samples) {
+//   // -1 for quadilateral approximation
+//   if (breath.vol_i == 0) {
+//     return null;
+//   } else {
+//     var beginTransition = transitions[breath.trans_begin_inhale];
+//     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);
+
+//     // Note: The algorithm below relies on the fact that there is
+//     // only one flow or pressure with a single ms value; and that
+//     // increvementing an index necessarily incremenst the .ms value.
+
+//     // Without two samples, we have no duration and can't define
+//     // work.
+//     if (pressures.length < 2 || flows.length < 2) return null;
+
+//     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,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
+//     // This is just a simple linear interpolation
+//     function f(ms,cur,last) {
+//       var ms0 = last.ms;
+//       var ms1 = cur.ms;
+//       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;
+//         pi = increment_past(pressures,ms,pi);
+//         if (flows[fi].ms <= ms) {
+//           fi = increment_past(flows,ms,fi);
+//         }
+//       } else {
+//         ms = flows[fi].ms;
+//         fi = increment_past(flows,ms,fi);
+//         if (pressures[pi].ms <= ms) {
+//           pi = increment_past(pressures,ms,pi);
+//         }
+//       }
+//       if ((fi === null) || (pi === null))
+//         break;
+//       var dur_s = (ms - ct) / 1000;
+//       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;
+//       var p1 = (lpp.val + np)/2;
+//       // convert 10ths of cm H2O to pascals..
+//       var p1_pa = (p1 * 98.0665) / 10;
+
+//       // convert flows in lpm to cubic meters per seconds
+//       var f1_m_cubed_per_s = f1 / (1000 * 1000 * 60);
+
+//       // work is now in Joules!
+//       w += dur_s * p1_pa * f1_m_cubed_per_s;
+//       lfp = { val : f1, ms: ms };
+//       lpp = { val : p1, ms: ms };
+//       ct = ms;
+//     }
+//     return w;
+//   }
+// }
+
+// // Let's first set up a perfectly square 1-second
+// function generate_synthetic_trace() {
+//   const SAMPLES_PER_PHASE = 1000;
+//   const SAMPLES_MS_PER_SAMPLE = 1;
+//   var trace = [];
+//   var cur = 0;
+//   // p is a probability of occuring.
+//   function push_samples(start,num,d,f,pd,pf) {
+//     for(var i = start; i < start+num; i++) {
+//       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
+//           });
+//       }
+//     }
+//   }
+//   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;
+//     var duration = last_time - first_time;
+//     console.log(flows);
+
+
+//   const vm = 10;
+//   // There is a problem here that this does not create a transition at the beginning.
+//   var transitions = compute_transitions(vm,flows);
+//     var breaths = compute_breaths_based_without_negative_flow(transitions,flows);
+//     console.log(breaths);
+//     for(i = 0; i<breaths.length; i++) {
+//       var w = PressureVolumeWork(breaths[i], transitions, samples);
+//       console.assert((w == null) || (nearp(w,JOULES_IN_BREATH),0.1));
+//       console.log("final (Joules) = ",w);
+//     }
+//   return true;
+// }
+
+
+//   function testWork(samples){ // breaths give us inspiration transition points
+//     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;
+//     var duration = last_time - first_time;
+//     console.log(flows);
+
+//     const vm = 10;
+//     var transitions = compute_transitions(vm,flows);
+//     var breaths = compute_breaths_based_without_negative_flow(transitions,flows);
+//     console.log(breaths);
+//     for(i = 0; i<breaths.length; i++) {
+//       var w = PressureVolumeWork(breaths[i], transitions, samples);
+//       console.log(w);
+//     }
+//    }
+
+
+
+  // // This should be in liters...
+  // function integrateSamples(a,z,flows) {
+  //   // -1 for quadilateral approximation
+  //   var vol = 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 + flows[j].val )/2) * CONVERT_PIRDS_TO_SLM;
+  //     // Flow is actually in standard liters per minute,
+  //     // so to get liters we divide by 60 to it l/s,
+  //     // and and divde by 1000 to convert ms to seconds.
+  //     // We could do that here, but will move constants
+  //     // to end...
+  //     vol += ms * ht;
+  //     if (isNaN(vol)) {
+  //       debugger;
+  //     }
+  //   }
+  //   return vol/(60*1000);
+  // }
 
   // This is based only on inhalations, and
   // is therefore functional when there is a check valve
@@ -1729,131 +1401,132 @@ function testWorkSynthetic(){ // breaths give us inspiration transition points
   // We still want to track zeros, but now must strack them
   // as a falling signal.
 
-  function compute_breaths_based_without_negative_flow(transitions,flows) {
-    var beg = 0;
-    var zero = 0;
-    var last = 0;
-    var voli = 0;
-    var vole = 0;
-
-    var breaths = [];
-    var expiring = true;
-
-    for(var i = 0; i < transitions.length; i++) {
-      // We're looking for the end of the inhalation here!!
-      if (((i -1) >= 0) && transitions[i-1].state == 1 &&
-          (transitions[i].state == 0 || transitions[i].state == -1 )) {
-        zero = i;
-      }
-      if (expiring && transitions[i].state == 1) {
-        breaths.push({ ms: transitions[i].ms,
-		       sample: transitions[i].sample,
-		       vol_e: vole,
-		       vol_i: voli,
-                       trans_begin_inhale: beg,
-                       trans_cross_zero: zero,
-                       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);
-
-        last = transitions[i].sample;
-      }
-      if (!expiring && ((transitions[i].state == -1) || (transitions[i].state == 0)))  {
-        expiring = true;
-        voli = integrateSamples(last,transitions[i].sample,flows);
-        last = transitions[i].sample;
-      }
-    }
-    return breaths;
-  }
-
-// A simple computation of a moving window trace
-// computing [A + -B], where A is volume to left
-// of sample int time window t, and B is volume to right
-// t is in milliseconds
-function computeMovingWindowTrace(samples,t,v) {
-
-  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;
-  var duration = last_time - first_time;
-
-  // Here is an idea...
-  // We define you to be in one of three states:
-  // Inspiring, expiring, or neither.
-  // Every transition between these states is logged.
-  // Having two inspirations between an expiration is
-  // weird but could happen.
-  // We record transitions.
-  // When the time series crossed a fixed threshold
-  // or zero, it causes a transition. If you are inspiring,
-  // you have to cross zero to transition to neither,
-  // and you start expiring when you cross the treshold.
-
-  // This is measured in standard liters per minute.
-  const vm = 10; // previously used 4
-
-  // We will model this as a list of transitions.
-  // A breath is any number of inspirations followed by
-  // any number of expirations. (I+)(E+)
-
-  var transitions = compute_transitions(vm,flows);
-
-  // Now that we have transitions, we can apply a
-  // diferrent algorithm to try to define "breaths".
-  // Because a breath is defined as an inspiration
-  // and then an expiration, we will define a breath
-  // as from the first inspiration, until there has
-  // been one expiration, until the next inspiration.
-  var breaths = [];
-  var expiring = false;
-
-  function compute_breaths_based_on_exhalations(transitions) {
-    var beg = 0;
-    var zero = 0;
-    var last = 0;
-    var voli = 0;
-    var vole = 0;
-
-    for(var i = 0; i < transitions.length; i++) {
-      // We're looking for the end of the inhalation here!!
-      if (((i -1) >= 0) && transitions[i-1].state == 1 && (transitions[i].state == 0 || transitions[i].state == -1 )) {
-        zero = i;
-      }
-      if (expiring && transitions[i].state == 1) {
-        breaths.push({ ms: transitions[i].ms,
-		       sample: transitions[i].sample,
-		       vol_e: vole,
-		       vol_i: voli,
-                       trans_begin_inhale: beg,
-                       trans_cross_zero: zero,
-                       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);
-        last = transitions[i].sample;
-      }
-      if (!expiring && transitions[i].state == -1) {
-        expiring = true;
-        voli = integrateSamples(last,transitions[i].sample,flows);
-        last = transitions[i].sample;
-      }
-    }
-  }
-
-  breaths = compute_breaths_based_without_negative_flow(transitions,flows);
-
-  return [transitions,breaths];
-}
+  // function compute_breaths_based_without_negative_flow(transitions,flows) {
+  //   var beg = 0;
+  //   var zero = 0;
+  //   var last = 0;
+  //   var voli = 0;
+  //   var vole = 0;
+
+  //   var breaths = [];
+  //   var expiring = true;
+
+  //   for(var i = 0; i < transitions.length; i++) {
+  //     // We're looking for the end of the inhalation here!!
+  //     if (((i -1) >= 0) && transitions[i-1].state == 1 &&
+  //         (transitions[i].state == 0 || transitions[i].state == -1 )) {
+  //       zero = i;
+  //     }
+  //     if (expiring && transitions[i].state == 1) {
+  //       breaths.push({ ms: transitions[i].ms,
+  //       	       sample: transitions[i].sample,
+  //       	       vol_e: vole,
+  //       	       vol_i: voli,
+  //                      trans_begin_inhale: beg,
+  //                      trans_cross_zero: zero,
+  //                      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);
+
+  //       last = transitions[i].sample;
+  //     }
+  //     if (!expiring && ((transitions[i].state == -1) || (transitions[i].state == 0)))  {
+  //       expiring = true;
+  //       voli = integrateSamples(last,transitions[i].sample,flows);
+  //       last = transitions[i].sample;
+  //     }
+  //   }
+  //   return breaths;
+  // }
+
+
+// // A simple computation of a moving window trace
+// // computing [A + -B], where A is volume to left
+// // of sample int time window t, and B is volume to right
+// // t is in milliseconds
+// function computeMovingWindowTrace(samples,t,v) {
+
+//   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;
+//   var duration = last_time - first_time;
+
+//   // Here is an idea...
+//   // We define you to be in one of three states:
+//   // Inspiring, expiring, or neither.
+//   // Every transition between these states is logged.
+//   // Having two inspirations between an expiration is
+//   // weird but could happen.
+//   // We record transitions.
+//   // When the time series crossed a fixed threshold
+//   // or zero, it causes a transition. If you are inspiring,
+//   // you have to cross zero to transition to neither,
+//   // and you start expiring when you cross the treshold.
+
+//   // This is measured in standard liters per minute.
+//   const vm = 10; // previously used 4
+
+//   // We will model this as a list of transitions.
+//   // A breath is any number of inspirations followed by
+//   // any number of expirations. (I+)(E+)
+
+//   var transitions = compute_transitions(vm,flows);
+
+//   // Now that we have transitions, we can apply a
+//   // diferrent algorithm to try to define "breaths".
+//   // Because a breath is defined as an inspiration
+//   // and then an expiration, we will define a breath
+//   // as from the first inspiration, until there has
+//   // been one expiration, until the next inspiration.
+//   var breaths = [];
+//   var expiring = false;
+
+//   function compute_breaths_based_on_exhalations(transitions) {
+//     var beg = 0;
+//     var zero = 0;
+//     var last = 0;
+//     var voli = 0;
+//     var vole = 0;
+
+//     for(var i = 0; i < transitions.length; i++) {
+//       // We're looking for the end of the inhalation here!!
+//       if (((i -1) >= 0) && transitions[i-1].state == 1 && (transitions[i].state == 0 || transitions[i].state == -1 )) {
+//         zero = i;
+//       }
+//       if (expiring && transitions[i].state == 1) {
+//         breaths.push({ ms: transitions[i].ms,
+// 		       sample: transitions[i].sample,
+// 		       vol_e: vole,
+// 		       vol_i: voli,
+//                        trans_begin_inhale: beg,
+//                        trans_cross_zero: zero,
+//                        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);
+//         last = transitions[i].sample;
+//       }
+//       if (!expiring && transitions[i].state == -1) {
+//         expiring = true;
+//         voli = integrateSamples(last,transitions[i].sample,flows);
+//         last = transitions[i].sample;
+//       }
+//     }
+//   }
+
+//   breaths = compute_breaths_based_without_negative_flow(transitions,flows);
+
+//   return [transitions,breaths];
+// }
 
 
 $("#useofficial").click(function() {
diff --git a/js/respiration_math.js b/js/respiration_math.js
new file mode 100644
index 0000000..5565869
--- /dev/null
+++ b/js/respiration_math.js
@@ -0,0 +1,708 @@
+/*
+respiration_math.js : COVID-19 Respiration Analysis Software
+Copyright (C) 2020  Robert L. Read
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+
+const CONVERT_PIRDS_TO_SLM = 1/1000;
+
+function unpack(rows, key) {
+    return rows.map(function(row) { return row[key]; });
+}
+
+
+function compute_transitions(vm,flows) {
+  var transitions = [];
+  var state = 0; // Let 1 mean inspiration, -1 mean expiration, 0 neither
+  for(var i = 0; i < flows.length; i++) {
+    var f = flows[i].val * CONVERT_PIRDS_TO_SLM;
+    //    var ms = flows[i].ms-first_time;
+    var ms = flows[i].ms;
+    if (state == 0) {
+      if (f > vm) {
+	state = 1;
+	transitions.push({ state: 1, sample: i, ms: ms})
+      } else if (f < -vm) {
+	state = -1;
+	transitions.push({ state: -1, sample: i, ms: ms})
+      }
+    } else if (state == 1) {
+      if (f < -vm) {
+	state = -1;
+	transitions.push({ state: -1, sample: i, ms: ms})
+      } else if (f < vm) {
+	state = 0;
+	transitions.push({ state: 0, sample: i, ms: ms})
+      }
+    } else if (state == -1) {
+      if (f > vm) {
+	state = 1;
+	transitions.push({ state: 1, sample: i, ms: ms})
+      } else if (f > 0) {
+	state = 0;
+	transitions.push({ state: 0, sample: i, ms: ms})
+      }
+    }
+  }
+  return transitions;
+}
+
+// Return, [min,avg,max] pressures (no smoothing)!
+function compute_pressures(secs,samples,alarms,limits) {
+  var pressures = samples.filter(s => s.event == 'M' && s.type == 'D' && s.loc == 'A');
+
+  if (pressures.length == 0) {
+    return [0,0,0,[]];
+  } else {
+    const recent_ms = pressures[pressures.length - 1].ms;
+    var cur_ms = recent_ms;
+    var cnt = 0.0;
+    var i = pressures.length - 1;
+    var cur_sample = pressures[i];
+
+    var min = Number.MAX_VALUE;
+    var max = Number.MIN_VALUE;
+    var sum = 0;
+    var alarms = [];
+    while((i >=0) && (pressures[i].ms > (cur_ms - secs*1000))) {
+      var p = pressures[i].val / 10.0;  // this is now cm H2O
+      if (p < min ) {
+        min = p;
+      }
+      if (p > max) {
+        max = p;
+      }
+      sum += p;
+      cnt++;
+      alarms = alarms.concat(check_alarms(limits,"max","h",p,(a,b) =>(a > b),pressures[i].ms));
+      alarms = alarms.concat(check_alarms(limits,"max","l",p,(a,b) =>(a < b),pressures[i].ms));
+      i--;
+    }
+    return [min,sum/cnt,max,alarms];
+  }
+}
+
+
+function compute_fio2_mean(secs,samples) {
+  var oxygens = samples.filter(s => s.event == 'M' && s.type == 'O' && s.loc == 'A');
+
+  if (oxygens.length == 0) {
+    return null;
+  } else {
+    const recent_ms = oxygens[oxygens.length - 1].ms;
+    var cur_ms = recent_ms;
+    var cnt = 0.0;
+    var i = oxygens.length - 1;
+
+    var sum = 0;
+    var alarms = [];
+    while((i >=0) && (oxygens[i].ms > (cur_ms - secs*1000))) {
+      var oxy = oxygens[i].val; // oxygen concentration as a percentage
+      sum += oxy;
+      cnt++;
+      i--;
+    }
+
+    var fio2_avg = sum / cnt;
+
+    return fio2_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
+  // whose time stamp is s seconds from the most recent sample
+
+  // We will compute respiration rate by counting breaths
+  // and dividing (cnt - 1) by time the first and last inhalation
+  var first_inhale_ms = -1;
+  var last_inhale_ms = -1;
+  if (breaths.length == 0) {
+    return [0,0,0,"NA","NA"];
+  } else {
+    const recent_ms = samples[samples.length - 1].ms;
+    var cur_ms = recent_ms;
+    var cnt = 0.0;
+    var vol_i = 0.0;
+    var vol_e = 0.0;
+    var i = breaths.length - 1;
+    var time_inh = 0;
+    var time_exh = 0;
+    // fully completed inhalation volume does not include the
+    // most recent breath; we need it to be able to accurately
+    // 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;
+      if (i < (breaths.length -1)) {
+        vol_ci += breaths[i].vol_i;
+      }
+      vol_e += breaths[i].vol_e;
+
+      const inh_ms = transitions[breaths[i].trans_begin_inhale].ms;
+      // note i is counting down in this loop...
+      if (last_inhale_ms < 0) last_inhale_ms = inh_ms;
+      first_inhale_ms = inh_ms;
+      const exh_ms = transitions[breaths[i].trans_end_exhale].ms;
+      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)) {
+      var inhalation_duration = last_inhale_ms - first_inhale_ms;
+      var inhalation_duration_min = inhalation_duration / (60.0 * 1000.0);
+      var rr = (cnt - 1) / inhalation_duration_min;
+      var duration_minutes = secs / 60.0;
+
+      // This is liters per minute. vol_ci is in liters.
+      // inhalation_duration is in ms.
+      var minute_volume =  vol_ci / inhalation_duration_min;
+
+      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,
+        WorkOfBreathing_J_per_L];
+    } else {
+      return [0,0,0,"NA","NA"];
+    }
+  }
+}
+
+
+// produces a set of rising signals, time in ms of the leading edge of the rise and the trailing edge of the rise
+// an array of 2-tuple
+// taip == true implies compute TAIP, else compute TRIP
+// Possibly this routine should be generalized to a general rise-time routine.
+function compute_TAIP_or_TRIP_signals(min,max,pressures,taip) {
+  var pressures = pressures.filter(s => s.event == 'M' && s.type == 'D' && s.loc == 'A');
+  const responseBegin = 0.1;
+  const responseEnd = 0.9;
+
+  var signals = [];
+  var foundMinSignal = false;
+
+  const highFence = (min + (responseEnd * (max - min)))*10;
+  const lowFence = (min + (responseBegin * (max - min)))*10;
+
+  var cur_signal_start;
+  var state = -1; // Let 1 mean rising, -1 mean fallen, 0 risen, but not fallen
+  var first_sample_index = taip ? 0 : pressures.length-1 ;
+  var last_sample_index = taip ? pressures.length-1 : 0;
+  var increment = taip ? 1 : -1;
+  for(var i = first_sample_index; i != last_sample_index; i+=increment) {
+    var p = pressures[i].val;
+    var ms = pressures[i].ms;
+    if (state == -1) {
+      if (p >= lowFence) {
+	      state = 1;
+        cur_signal_start = pressures[i];
+      }
+      if (p >= highFence){
+        signals.push([ms,ms]);
+      }
+    } else if (state == 1) {
+      //console.log("state = 1",cur_signal_start); for debugging
+      if (p >= highFence) {
+        signals.push(taip ? [cur_signal_start.ms,ms] : [ms,cur_signal_start.ms])
+        state = 0;
+      } else if (p <= lowFence) {
+        state = -1;
+        cur_signal_start = null;
+      }
+    } else if (state == 0) {
+      if (p <= lowFence) {
+	state = -1;
+      }
+    }
+  }
+  return signals;
+}
+
+function compute_mean_TRIP_or_TAIP_sigs(sigs,min,max,pressures,taip){
+  if (sigs.length == 0){
+    return "NA";
+  } else {
+    var sum = 0;
+    for(var i = 0; i < sigs.length; i++) {
+      sum += sigs[i][1] - sigs[i][0];  // time in ms
+    }
+    return sum / sigs.length;
+  }
+}
+
+function testdata(){
+  //0 (not good enough), 10 (rising), 20 (above threshold) all 10 ms apart
+  //saw tooth function
+  var data = []; // pushing 50 things into it
+  for(var i = 0; i < 10; i++) {
+    var ms = i*5*10;
+    data[i*5+0] = {event:'M',loc:'A',ms:ms + 0,type:'D',val: 0};
+    data[i*5+1] = {event:'M',loc:'A',ms:ms + 10,type:'D',val: 100};
+    data[i*5+2] = {event:'M',loc:'A',ms:ms + 20,type:'D',val: 200};
+    data[i*5+3] = {event:'M',loc:'A',ms:ms + 30,type:'D',val: 100};
+    data[i*5+4] = {event:'M',loc:'A',ms:ms + 40,type:'D',val: 0};
+  }
+  return data;
+}
+
+function testdataSine(period_sm){ // period expressed in # of samples, each sample 10 ms
+  //0 (not good enough), 10 (rising), 20 (above threshold) all 10 ms apart
+  //sine tooth function
+  var data = []; // pushing 50 things into it
+  for(var i = 0; i < 1000; i++) {
+    var ms = i*10;
+    data[i] = {event:'M',loc:'A',ms:ms + 20,type:'D',val: 200*Math.sin(2*Math.PI*i/period_sm)};
+  }
+  return data;
+}
+
+function compute_mean_TRIP_or_TAIP(min,max,samples,taip) {
+  return compute_mean_TRIP_or_TAIP_sigs(
+    compute_TAIP_or_TRIP_signals(min,max,samples,taip),
+    min,max,samples,taip);
+}
+
+function test_compute_TAIP() {
+  var samples = testdata();
+  const TAIP_min = 0; // cm of H2O
+  const TAIP_max = 20; // cm of H2O
+  var TAIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,true);
+  console.assert(TAIP_m == 10);
+  for (i = 50; i<150; i+=10) {
+    var sinewave = testdataSine(i);
+    var TAIP_m = compute_mean_TRIP_or_TAIP(min,max,sinewave,true);
+  }
+}
+
+function compute_current_TAIP(TAIP_min,TAIP_max){ //uses samples from a global var
+  var TAIP_m = compute_mean_TRIP_or_TAIP(TAIP_min,TAIP_max,samples,true);
+  return TAIP_m;
+}
+
+// Because TAIP and TRIP are symmetric when viewed from
+// from the direction of the samples; this tests that
+// as a prelude to computing a single way.
+
+function reverseArray(arr) {
+  var newArray = [];
+  for (var i = arr.length - 1; i >= 0; i--) {
+    newArray.push(arr[i]);
+  }
+  return newArray;
+}
+function test_TRIP_and_TAIP_are_symmetric() {
+  var samples = testdata();
+  var rsamples = reverseArray(samples);
+  const min = 0;
+  const max = 20;
+  var TRIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,false);
+  var TRIP_m_r = -compute_mean_TRIP_or_TAIP(min,max,rsamples,true);
+  console.assert(TRIP_m == TRIP_m_r);
+  console.assert(TRIP_m == 10);
+  for (i = 50; i<150; i+=10) {
+    var sinewave = testdataSine(i);
+    var rsinewave = reverseArray(sinewave);
+    var TRIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,false);
+    var TRIP_m_r = -compute_mean_TRIP_or_TAIP(min,max,rsamples,true);
+    console.assert(TRIP_m == TRIP_m_r);
+    var TAIP_m = compute_mean_TRIP_or_TAIP(min,max,samples,true);
+    var TAIP_m_r = -compute_mean_TRIP_or_TAIP(min,max,rsamples,false);
+    console.assert(TAIP_m == TAIP_m_r);
+  }
+}
+
+function compute_current_TRIP(TRIP_min,TRIP_max, samples)
+{ //uses samples from a global var
+  if (samples.length == 0){
+    return "NA";
+  }
+  else {
+    var TRIP_m = compute_mean_TAIP_or_TRIP(TRIP_min,TRIP_max,false);
+    return TRIP_m;
+  }
+}
+
+
+
+
+  // A routine to calculate work per breath
+function PressureVolumeWork(breath, transitions, samples) {
+  // -1 for quadilateral approximation
+  if (breath.vol_i == 0) {
+    return null;
+  } else {
+    var beginTransition = transitions[breath.trans_begin_inhale];
+    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);
+
+    // Note: The algorithm below relies on the fact that there is
+    // only one flow or pressure with a single ms value; and that
+    // increvementing an index necessarily incremenst the .ms value.
+
+    // Without two samples, we have no duration and can't define
+    // work.
+    if (pressures.length < 2 || flows.length < 2) return null;
+
+    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,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
+    // This is just a simple linear interpolation
+    function f(ms,cur,last) {
+      var ms0 = last.ms;
+      var ms1 = cur.ms;
+      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;
+        pi = increment_past(pressures,ms,pi);
+        if (flows[fi].ms <= ms) {
+          fi = increment_past(flows,ms,fi);
+        }
+      } else {
+        ms = flows[fi].ms;
+        fi = increment_past(flows,ms,fi);
+        if (pressures[pi].ms <= ms) {
+          pi = increment_past(pressures,ms,pi);
+        }
+      }
+      if ((fi === null) || (pi === null))
+        break;
+      var dur_s = (ms - ct) / 1000;
+      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;
+      var p1 = (lpp.val + np)/2;
+      // convert 10ths of cm H2O to pascals..
+      var p1_pa = (p1 * 98.0665) / 10;
+
+      // convert flows in lpm to cubic meters per seconds
+      var f1_m_cubed_per_s = f1 / (1000 * 1000 * 60);
+
+      // work is now in Joules!
+      w += dur_s * p1_pa * f1_m_cubed_per_s;
+      lfp = { val : f1, ms: ms };
+      lpp = { val : p1, ms: ms };
+      ct = ms;
+    }
+    return w;
+  }
+}
+
+// Let's first set up a perfectly square 1-second
+function generate_synthetic_trace() {
+  const SAMPLES_PER_PHASE = 1000;
+  const SAMPLES_MS_PER_SAMPLE = 1;
+  var trace = [];
+  var cur = 0;
+  // p is a probability of occuring.
+  function push_samples(start,num,d,f,pd,pf) {
+    for(var i = start; i < start+num; i++) {
+      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
+          });
+      }
+    }
+  }
+  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;
+    var duration = last_time - first_time;
+    console.log(flows);
+
+
+  const vm = 10;
+  // There is a problem here that this does not create a transition at the beginning.
+  var transitions = compute_transitions(vm,flows);
+    var breaths = compute_breaths_based_without_negative_flow(transitions,flows);
+    console.log(breaths);
+    for(i = 0; i<breaths.length; i++) {
+      var w = PressureVolumeWork(breaths[i], transitions, samples);
+      console.assert((w == null) || (nearp(w,JOULES_IN_BREATH),0.1));
+      console.log("final (Joules) = ",w);
+    }
+  return true;
+}
+
+
+  function testWork(samples){ // breaths give us inspiration transition points
+    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;
+    var duration = last_time - first_time;
+    console.log(flows);
+
+    const vm = 10;
+    var transitions = compute_transitions(vm,flows);
+    var breaths = compute_breaths_based_without_negative_flow(transitions,flows);
+    console.log(breaths);
+    for(i = 0; i<breaths.length; i++) {
+      var w = PressureVolumeWork(breaths[i], transitions, samples);
+      console.log(w);
+    }
+   }
+
+
+
+  // This should be in liters...
+  function integrateSamples(a,z,flows) {
+    // -1 for quadilateral approximation
+    var vol = 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 + flows[j].val )/2) * CONVERT_PIRDS_TO_SLM;
+      // Flow is actually in standard liters per minute,
+      // so to get liters we divide by 60 to it l/s,
+      // and and divde by 1000 to convert ms to seconds.
+      // We could do that here, but will move constants
+      // to end...
+      vol += ms * ht;
+      if (isNaN(vol)) {
+        debugger;
+      }
+    }
+    return vol/(60*1000);
+  }
+
+  // This is based only on inhalations, and
+  // is therefore functional when there is a check valve
+  // in place. Such a system will rarely
+  // have negative flows, and we must mark
+  // the beginning of a breath from a transition to a "1"
+  // state from any other state.
+  // This algorithm is simple: A breath begins on a trasition
+  // to 1 from a not 1 state. This algorithm is susceptible
+  // to "stutter" near the boundary point, but if necessary
+  // a digital filter would sove that; we have not yet found
+  // that level of sophistication needed.
+  // We still want to track zeros, but now must strack them
+  // as a falling signal.
+
+  function compute_breaths_based_without_negative_flow(transitions,flows) {
+    var beg = 0;
+    var zero = 0;
+    var last = 0;
+    var voli = 0;
+    var vole = 0;
+
+    var breaths = [];
+    var expiring = true;
+
+    for(var i = 0; i < transitions.length; i++) {
+      // We're looking for the end of the inhalation here!!
+      if (((i -1) >= 0) && transitions[i-1].state == 1 &&
+          (transitions[i].state == 0 || transitions[i].state == -1 )) {
+        zero = i;
+      }
+      if (expiring && transitions[i].state == 1) {
+        breaths.push({ ms: transitions[i].ms,
+		       sample: transitions[i].sample,
+		       vol_e: vole,
+		       vol_i: voli,
+                       trans_begin_inhale: beg,
+                       trans_cross_zero: zero,
+                       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);
+
+        last = transitions[i].sample;
+      }
+      if (!expiring && ((transitions[i].state == -1) || (transitions[i].state == 0)))  {
+        expiring = true;
+        voli = integrateSamples(last,transitions[i].sample,flows);
+        last = transitions[i].sample;
+      }
+    }
+    return breaths;
+  }
+
+
+// A simple computation of a moving window trace
+// computing [A + -B], where A is volume to left
+// of sample int time window t, and B is volume to right
+// t is in milliseconds
+function computeMovingWindowTrace(samples,t,v) {
+
+  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;
+  var duration = last_time - first_time;
+
+  // Here is an idea...
+  // We define you to be in one of three states:
+  // Inspiring, expiring, or neither.
+  // Every transition between these states is logged.
+  // Having two inspirations between an expiration is
+  // weird but could happen.
+  // We record transitions.
+  // When the time series crossed a fixed threshold
+  // or zero, it causes a transition. If you are inspiring,
+  // you have to cross zero to transition to neither,
+  // and you start expiring when you cross the treshold.
+
+  // This is measured in standard liters per minute.
+  const vm = 10; // previously used 4
+
+  // We will model this as a list of transitions.
+  // A breath is any number of inspirations followed by
+  // any number of expirations. (I+)(E+)
+
+  var transitions = compute_transitions(vm,flows);
+
+  // Now that we have transitions, we can apply a
+  // diferrent algorithm to try to define "breaths".
+  // Because a breath is defined as an inspiration
+  // and then an expiration, we will define a breath
+  // as from the first inspiration, until there has
+  // been one expiration, until the next inspiration.
+  var breaths = [];
+  var expiring = false;
+
+  function compute_breaths_based_on_exhalations(transitions) {
+    var beg = 0;
+    var zero = 0;
+    var last = 0;
+    var voli = 0;
+    var vole = 0;
+
+    for(var i = 0; i < transitions.length; i++) {
+      // We're looking for the end of the inhalation here!!
+      if (((i -1) >= 0) && transitions[i-1].state == 1 && (transitions[i].state == 0 || transitions[i].state == -1 )) {
+        zero = i;
+      }
+      if (expiring && transitions[i].state == 1) {
+        breaths.push({ ms: transitions[i].ms,
+		       sample: transitions[i].sample,
+		       vol_e: vole,
+		       vol_i: voli,
+                       trans_begin_inhale: beg,
+                       trans_cross_zero: zero,
+                       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);
+        last = transitions[i].sample;
+      }
+      if (!expiring && transitions[i].state == -1) {
+        expiring = true;
+        voli = integrateSamples(last,transitions[i].sample,flows);
+        last = transitions[i].sample;
+      }
+    }
+  }
+
+  breaths = compute_breaths_based_without_negative_flow(transitions,flows);
+
+  return [transitions,breaths];
+}
-- 
GitLab