Franny Dean commited on
Commit
5b91528
1 Parent(s): 2a0c362

attempt to add lvad

Browse files
Files changed (2) hide show
  1. .ipynb_checkpoints/app-checkpoint.py +321 -5
  2. app.py +321 -4
.ipynb_checkpoints/app-checkpoint.py CHANGED
@@ -495,7 +495,265 @@ def pvloop_simulator_plot_only(Rm, Ra, Emax, Emin, Vd, Tc, start_v):
495
  plt.title('Simulated PI-SSL LV Pressure Volume Loop', fontsize=16)
496
  return plot
497
 
498
- ## Demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
  def generate_example():
501
  # get random input
@@ -518,6 +776,53 @@ def generate_example():
518
  animated = "prediction.mp4"
519
  return video, animated, Rm, Ra, Emax, Emin, Vd, Tc, start_v
520
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  title = "<h1 style='text-align: center; margin-bottom: 1rem'> Non-Invasive Medical Digital Twins using Physics-Informed Self-Supervised Learning </h1>"
522
 
523
  description = """
@@ -529,10 +834,16 @@ We demonstrate the ability of our model to predict left ventricular pressure-vol
529
 
530
  title2 = "<h3 style='text-align: center'> Physics-based model simulation</h3>"
531
 
532
- description2 = """\n \n
 
533
  Our model uses a hydraulic analogy model of cardiac function from <a href='https://ieeexplore.ieee.org/document/4729737/keywords#keywords'>Simaan et al 2008</a>. Below you can input values of predicted parameters and output a simulated pressure-volume loop predicted from the <a href='https://ieeexplore.ieee.org/document/4729737/keywords#keywords'>Simaan et al 2008</a> model, which is an ordinary differential equation. Tune parameters and press 'Run simulation.'
534
  """
535
 
 
 
 
 
 
536
  gr.Markdown(title)
537
  gr.Markdown(description)
538
 
@@ -573,13 +884,18 @@ with gr.Blocks() as demo:
573
  sl5 = gr.Slider(4.0, 25.0, value= 4.0, label="Vd (ml)")
574
  sl6 = gr.Slider(0.4, 1.7, value= 0.4, label="Tc (s)")
575
  sl7 = gr.Slider(0.0, 280.0, value= 140., label="start_v (ml)")
576
-
 
 
 
 
 
 
577
 
578
  generate_button.click(fn=generate_example, outputs = [video,plot,Rm,Ra,Emax,Emin,Vd,Tc,start_v])
579
 
580
-
581
  simulation_button.click(fn=pvloop_simulator_plot_only, inputs = [sl1,sl2,sl3,sl4,sl5,sl6,sl7], outputs = [gr.Plot()])
582
 
583
-
584
 
585
  demo.launch()
 
495
  plt.title('Simulated PI-SSL LV Pressure Volume Loop', fontsize=16)
496
  return plot
497
 
498
+ #########################################
499
+ # LVAD functions
500
+ # RELU for diodes
501
+ def r(u):
502
+ return max(u, 0.)
503
+
504
+ def heart_ode0(y, t, Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emax, Emin, Tc, Vd):
505
+ x1, x2, x3, x4, x5 = y #here y is a vector of 5 values (not functions), at time t, used for getting (dy/dt)(t)
506
+ P_lv = Plv(x1+Vd,Emax,Emin,t,Tc,Vd)
507
+ dydt = [r(x2-P_lv)/Rm-r(P_lv-x4)/Ra, (x3-x2)/(Rs*Cr)-r(x2-P_lv)/(Cr*Rm), (x2-x3)/(Rs*Cs)+x5/Cs, -x5/Ca+r(P_lv-x4)/(Ca*Ra), (x4-x3-Rc*x5)/Ls]
508
+ return dydt
509
+
510
+ def getslope(y1, y2, y3, x1, x2, x3):
511
+ sum_x = x1 + x2 + x3
512
+ sum_y = y1 + y2 + y3
513
+ sum_xy = x1*y1 + x2*y2 + x3*y3
514
+ sum_xx = x1*x1 + x2*x2 + x3*x3
515
+ # calculate the coefficients of the least-squares line
516
+ n = 3
517
+ slope = (n*sum_xy - sum_x*sum_y) / (n*sum_xx - sum_x*sum_x)
518
+ return slope
519
+
520
+ ### ODE: for each t (here fixed), gives dy/dt as a function of y(t) at that t, so can be used for integrating the vector y over time
521
+ #it is run for each t going from 0 to tmax
522
+ def lvad_ode(y, t, Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emax, Emin, Tc, Vd, ratew):
523
+
524
+ #from simaan2008dynamical:
525
+ Ri = 0.0677
526
+ R0 = 0.0677
527
+ Rk = 0
528
+ x1bar = 1.
529
+ alpha = -3.5
530
+ Li = 0.0127
531
+ L0 = 0.0127
532
+ b0 = -0.296
533
+ b1 = -0.027
534
+ b2 = 9.9025e-7
535
+
536
+ x1, x2, x3, x4, x5, x6, x7 = y #here y is a vector of 5 values (not functions), at time t, used for getting (dy/dt)(t)
537
+
538
+ P_lv = Plv(x1+Vd,Emax,Emin,t,Tc,Vd)
539
+ if (P_lv <= x1bar): Rk = alpha * (P_lv - x1bar)
540
+ Lstar = Li + L0 + b1
541
+ Lstar2 = -Li -L0 +b1
542
+ Rstar = Ri + + R0 + Rk + b0
543
+
544
+ dydt = [-x6 + r(x2-P_lv)/Rm-r(P_lv-x4)/Ra, (x3-x2)/(Rs*Cr)-r(x2-P_lv)/(Cr*Rm), (x2-x3)/(Rs*Cs)+x5/Cs, -x5/Ca+r(P_lv-x4)/(Ca*Ra) + x6/Ca, (x4-x3)/Ls-Rc*x5/Ls, -P_lv / Lstar2 + x4/Lstar2 + (Ri+R0+Rk-b0) / Lstar2 * x6 - b2 / Lstar2 * x7**2, ratew]
545
+
546
+ return dydt
547
+
548
+ #returns pv loop and ef when there is no lvad:
549
+ def f_nolvad(Tc, start_v, Emax, showpvloop):
550
+
551
+ N = 20
552
+ global Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emin, Vd
553
+
554
+ start_pla = float(start_v*Elastance(Emax, Emin, 0, Tc))
555
+ start_pao = 75.
556
+ start_pa = start_pao
557
+ start_qt = 0 #aortic flow is Q_T and is 0 at ED, also see Fig5 in simaan2008dynamical
558
+
559
+ y0 = [start_v, start_pla, start_pa, start_pao, start_qt]
560
+
561
+ t = np.linspace(0, Tc*N, int(60000*N)) #spaced numbers over interval (start, stop, number_of_steps), 60000 time instances for each heart cycle
562
+ #changed to 60000 for having integer positions for Tmax
563
+ #obtain 5D vector solution:
564
+ sol = odeint(heart_ode0, y0, t, args = (Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emax, Emin, Tc,Vd)) #t: list of values
565
+
566
+ result_Vlv = np.array(sol[:, 0]) + Vd
567
+ result_Plv = np.array([Plv(v, Emax, Emin, xi, Tc, Vd) for xi,v in zip(t,sol[:, 0])])
568
+
569
+ #if showpvloop: plt.plot(result_Vlv[18*60000:20*60000], result_Plv[18*60000:20*60000], color='black', label='Without LVAD')
570
+
571
+ ved = sol[19*60000, 0] + Vd
572
+ ves = sol[200*int(60/Tc)+9000+19*60000, 0] + Vd
573
+ ef = (ved-ves)/ved * 100.
574
+ minv = min(result_Vlv[19*60000:20*60000-1])
575
+ minp = min(result_Plv[19*60000:20*60000-1])
576
+
577
+ result_pao = np.array(sol[:, 3])
578
+ pao_ed = min(result_pao[(N-1)*60000:N*60000-1])
579
+ pao_es = max(result_pao[(N-1)*60000:N*60000-1])
580
+
581
+ return ef, pao_ed, pao_es, ((ved - ves) * 60/Tc ) / 1000, sol[19*60000, 0], sol[19*60000, 1], sol[19*60000, 2], sol[19*60000, 3], sol[19*60000, 4], result_Vlv[18*60000:20*60000], result_Plv[18*60000:20*60000]
582
+
583
+ #returns the w at which suction occurs: (i.e. for which the slope of the envelopes of x6 becomes negative)
584
+ def get_suctionw(Tc, start_v, Emax, y00, y01, y02, y03, y04, w0, x60, ratew): #slope is slope0 for w
585
+
586
+ N = 70
587
+ global Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emin, Vd
588
+
589
+ start_pla = float(start_v*Elastance(Emax, Emin, 0, Tc))
590
+ start_pao = 75.
591
+ start_pa = start_pao
592
+ start_qt = 0 #aortic flow is Q_T and is 0 at ED, also see Fig5 in simaan2008dynamical
593
+
594
+ y0 = [start_v, start_pla, start_pa, start_pao, start_qt, x60, w0]
595
+ y0 = [y00, y01, y02, y03, y04, x60, w0]
596
+
597
+ ncycle = 20000
598
+ n = N * ncycle
599
+ sol = np.zeros((n, 7))
600
+ t = np.linspace(0., Tc * N, n)
601
+ for j in range(7):
602
+ sol[0][j] = y0[j]
603
+
604
+ result_Vlv = []
605
+ result_Plv = []
606
+ result_x6 = []
607
+ result_x7 = []
608
+ envx6 = []
609
+ timesenvx6 = []
610
+ slopes = []
611
+ ws = []
612
+
613
+ minx6 = 99999
614
+ tmin = 0
615
+ tlastupdate = 0
616
+ lastw = w0
617
+ update = 1
618
+
619
+ #solve the ODE step by step by adding dydt*dt:
620
+ for j in range(0, n-1):
621
+ #update y with dydt * dt
622
+ y = sol[j]
623
+ dydt = lvad_ode(y, t[j], Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emax, Emin, Tc, Vd, ratew)
624
+ for k in range(7):
625
+ dydt[k] = dydt[k] * (t[j+1] - t[j])
626
+ sol[j+1] = sol[j] + dydt
627
+
628
+ #update the min of x6 in the current cylce. also keep the time at which the min is obtained (for getting the slope later)
629
+ if (minx6 > sol[j][5]):
630
+ minx6 = sol[j][5]
631
+ tmin = t[j]
632
+
633
+ #add minimum of x6 once each cycle ends: (works). then reset minx6 to 99999 for calculating again the minimum
634
+ if (j%ncycle==0 and j>1):
635
+ envx6.append(minx6)
636
+ timesenvx6.append(tmin)
637
+ minx6 = 99999
638
+
639
+ if (len(envx6)>=3):
640
+ slope = getslope(envx6[-1], envx6[-2], envx6[-3], timesenvx6[-1], timesenvx6[-2], timesenvx6[-3])
641
+ slopes.append(slope)
642
+ ws.append(y[6])
643
+
644
+ for i in range(n):
645
+ result_x6.append(sol[i, 5])
646
+ result_x7.append(sol[i, 6])
647
+
648
+ suction_w = 0
649
+ for i in range(2, len(slopes)):
650
+ if (slopes[i] < 0):
651
+ suction_w = ws[i-1]
652
+ break
653
+
654
+ return suction_w
655
+
656
+ def f_lvad(Tc, start_v, Emax, c, slope, w0, x60, y00, y01, y02, y03, y04): #slope is slope0 for w
657
+
658
+ N = 70
659
+ global Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emin, Vd
660
+
661
+ y0 = [y00, y01, y02, y03, y04, x60, w0]
662
+
663
+ ncycle = 10000
664
+ n = N * ncycle
665
+ sol = np.zeros((n, 7))
666
+ t = np.linspace(0., Tc * N, n)
667
+ for j in range(7):
668
+ sol[0][j] = y0[j]
669
+
670
+ result_Vlv = []
671
+ result_Plv = []
672
+ result_x6 = []
673
+ result_x7 = []
674
+ envx6 = []
675
+ timesenvx6 = []
676
+
677
+ minx6 = 99999
678
+ tmin = 0
679
+ tlastupdate = 0
680
+ lastw = w0
681
+ update = 1
682
+ ratew = 0 #6000/60
683
+
684
+ #solve the ODE step by step by adding dydt*dt:
685
+ for j in range(0, n-1):
686
+ #update y with dydt * dt
687
+ y = sol[j]
688
+ dydt = lvad_ode(y, t[j], Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emax, Emin, Tc, Vd, ratew)
689
+ for k in range(7):
690
+ dydt[k] = dydt[k] * (t[j+1] - t[j])
691
+ sol[j+1] = sol[j] + dydt
692
+
693
+ #update the min of x6 in the current cylce. also keep the time at which the min is obtained (for getting the slope later)
694
+ if (minx6 > sol[j][5]):
695
+ minx6 = sol[j][5]
696
+ tmin = t[j]
697
+
698
+ #add minimum of x6 once each cycle ends: (works). then reset minx6 to 99999 for calculating again the minimum
699
+ if (j%ncycle==0 and j>1):
700
+ envx6.append(minx6)
701
+ timesenvx6.append(tmin)
702
+ minx6 = 99999
703
+
704
+ #update w (if 0.005 s. have passed since the last update):
705
+ if (slope<0):
706
+ update = 0
707
+ if (t[j+1] - tlastupdate > 0.005 and slope>0 and update==1): #abs(slope)>0.0001
708
+ # if there are enough points of envelope: calculate slope:
709
+ if (len(envx6)>=3):
710
+ slope = getslope(envx6[-1], envx6[-2], envx6[-3], timesenvx6[-1], timesenvx6[-2], timesenvx6[-3])
711
+ sol[j+1][6] = lastw + c * slope
712
+ #otherwise: take arbitrary rate (see Fig. 16a in simaan2008dynamical)
713
+ else:
714
+ sol[j+1][6] = lastw + 0.005 * slope
715
+ #save w(k) (see formula (8) simaan2008dynamical) and the last time of update t[j] (will have to wait 0.005 s for next update of w)
716
+ tlastupdate = t[j+1]
717
+ lastw = sol[j+1][6]
718
+
719
+ #save functions and print MAP, CO:
720
+ map = 0
721
+ Pao = []
722
+
723
+ for i in range(n):
724
+ result_Vlv.append(sol[i, 0] + Vd)
725
+ result_Plv.append(Plv(sol[i, 0]+Vd, Emax, Emin, t[i], Tc, Vd))
726
+ result_x6.append(sol[i, 5])
727
+ result_x7.append(sol[i, 6])
728
+ Pao.append(sol[i, 3])
729
+
730
+ colors0=np.zeros((len(result_Vlv[65*ncycle:70*ncycle]), 3))
731
+ for col in colors0:
732
+ col[0]=41/255
733
+ col[1]=128/255
734
+ col[2]=205/255
735
+
736
+
737
+ #get co and ef:
738
+ ved = max(result_Vlv[50 * ncycle:52 * ncycle])
739
+ ves = min(result_Vlv[50 * ncycle:52 * ncycle])
740
+ #ves = result_Vlv[50 * ncycle + int(ncycle * 0.2 /Tc + 0.15 * ncycle)]
741
+ ef = (ved-ves)/ved*100
742
+ CO = ((ved - ves) * 60/Tc ) / 1000
743
+
744
+ #get MAP:
745
+ for i in range(n - 5*ncycle, n):
746
+ map += sol[i, 2]
747
+ map = map/(5*ncycle)
748
+
749
+ result_pao = np.array(sol[:, 3])
750
+ pao_ed = min(Pao[50 * ncycle:52 * ncycle])
751
+ pao_es = max(Pao[50 * ncycle:52 * ncycle])
752
+
753
+ return ef, pao_ed, pao_es, CO, map, result_Vlv[65*ncycle:70*ncycle], result_Plv[65*ncycle:70*ncycle]
754
+
755
+ #############################
756
+ ## Demo functions
757
 
758
  def generate_example():
759
  # get random input
 
776
  animated = "prediction.mp4"
777
  return video, animated, Rm, Ra, Emax, Emin, Vd, Tc, start_v
778
 
779
+
780
+ def lvad_plots(Rm, Ra, Emax, Emin, Vd, Tc, start_v, gamma):
781
+
782
+ ncycle = 10000
783
+
784
+ Rs = 1.
785
+ Rc = 0.0398
786
+ Ca= 0.08
787
+ Cs= 1.33
788
+ Cr= 4.4
789
+ Ls=0.0005
790
+
791
+ #get values for periodic loops:
792
+ ef_nolvad, pao_ed, pao_es, co_nolvad, y00, y01, y02, y03, y04, Vlv0, Plv0 = f_nolvad(Tc, start_v, Emax, 0)
793
+ #pao_eds = [pao_ed]
794
+ #pao_ess = [pao_es]
795
+
796
+ #get suction w: (make w go linearly from w0 to w0 + maxtime * 400, and find w at which suction occurs)
797
+ w0 = 5000.
798
+ ratew = 400.
799
+ x60 = 0.
800
+ suctionw = get_suctionw(Tc, start_v, Emax, y00, y01, y02, y03, y04, w0, x60, ratew)
801
+
802
+ #gamma = 1.8
803
+ c = 0.065 #(in simaan2008dynamical: 0.67, but too fast -> 0.061 gives better shape)
804
+ slope0 = 100.
805
+ w0 = suctionw / gamma #if doesn't work (x6 negative), change gamma down to 1.4 or up to 2.1
806
+
807
+ #compute new pv loops and ef with lvad added:
808
+ new_ef, pao_ed, pao_es, CO, MAP, Vlvs, Plvs = f_lvad(Tc, start_v, Emax, c, slope0, w0, x60, y00, y01, y02, y03, y04)
809
+
810
+ print("\nParameters: Tc, start_v, Emax:", Tc, start_v, Emax)
811
+ print("Suction speed:", suctionw)
812
+ print("EF before LVAD:", ef_nolvad)
813
+ print("CO before LVAD:", co_nolvad)
814
+ print("New EF after LVAD:", new_ef, "New CO:", CO, "New MAP:", MAP, "\n\n")
815
+
816
+ plt.plot(Vlv0, Plv0, color='blue', label='No LVAD') #blue
817
+ plt.plot(Vlvs, Plvs, color=(78/255, 192/255, 44/255), label=f"LVAD, ω(0)= {round(w0,2)}r/min") #green
818
+ plt.xlabel('LV volume (ml)')
819
+ plt.ylabel('LV pressure (mmHg)')
820
+ plt.legend(loc='upper left', framealpha=1)
821
+ plt.ylim((0,220))
822
+ plt.xlim((0,250))
823
+
824
+ return plt
825
+
826
  title = "<h1 style='text-align: center; margin-bottom: 1rem'> Non-Invasive Medical Digital Twins using Physics-Informed Self-Supervised Learning </h1>"
827
 
828
  description = """
 
834
 
835
  title2 = "<h3 style='text-align: center'> Physics-based model simulation</h3>"
836
 
837
+ description2 = """
838
+ \n \n
839
  Our model uses a hydraulic analogy model of cardiac function from <a href='https://ieeexplore.ieee.org/document/4729737/keywords#keywords'>Simaan et al 2008</a>. Below you can input values of predicted parameters and output a simulated pressure-volume loop predicted from the <a href='https://ieeexplore.ieee.org/document/4729737/keywords#keywords'>Simaan et al 2008</a> model, which is an ordinary differential equation. Tune parameters and press 'Run simulation.'
840
  """
841
 
842
+ description3 = """
843
+ \n\n
844
+ This model can incorporate a tunable left-ventricular assistance device (LVAD) for in-silico experimentation. Click to view the effect of adding an LVAD to the simulated PV loop.
845
+ """
846
+
847
  gr.Markdown(title)
848
  gr.Markdown(description)
849
 
 
884
  sl5 = gr.Slider(4.0, 25.0, value= 4.0, label="Vd (ml)")
885
  sl6 = gr.Slider(0.4, 1.7, value= 0.4, label="Tc (s)")
886
  sl7 = gr.Slider(0.0, 280.0, value= 140., label="start_v (ml)")
887
+
888
+ gr.Markdown(description2)
889
+
890
+ LVAD_button = gr.Button("Add LVAD")
891
+
892
+ with gr.Row():
893
+ gamma = gr.Slider(1.0, 2.0, value= 1.4, label="Pump speed, ω(0)")
894
 
895
  generate_button.click(fn=generate_example, outputs = [video,plot,Rm,Ra,Emax,Emin,Vd,Tc,start_v])
896
 
 
897
  simulation_button.click(fn=pvloop_simulator_plot_only, inputs = [sl1,sl2,sl3,sl4,sl5,sl6,sl7], outputs = [gr.Plot()])
898
 
899
+ LVAD_button.click(fn=lvad_plots, inputs = [sl1,sl2,sl3,sl4,sl5,sl6,sl7,gamma], outputs = [gr.Plot()])
900
 
901
  demo.launch()
app.py CHANGED
@@ -495,7 +495,265 @@ def pvloop_simulator_plot_only(Rm, Ra, Emax, Emin, Vd, Tc, start_v):
495
  plt.title('Simulated PI-SSL LV Pressure Volume Loop', fontsize=16)
496
  return plot
497
 
498
- ## Demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
  def generate_example():
501
  # get random input
@@ -518,6 +776,53 @@ def generate_example():
518
  animated = "prediction.mp4"
519
  return video, animated, Rm, Ra, Emax, Emin, Vd, Tc, start_v
520
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  title = "<h1 style='text-align: center; margin-bottom: 1rem'> Non-Invasive Medical Digital Twins using Physics-Informed Self-Supervised Learning </h1>"
522
 
523
  description = """
@@ -529,10 +834,16 @@ We demonstrate the ability of our model to predict left ventricular pressure-vol
529
 
530
  title2 = "<h3 style='text-align: center'> Physics-based model simulation</h3>"
531
 
532
- description2 = """\n \n
 
533
  Our model uses a hydraulic analogy model of cardiac function from <a href='https://ieeexplore.ieee.org/document/4729737/keywords#keywords'>Simaan et al 2008</a>. Below you can input values of predicted parameters and output a simulated pressure-volume loop predicted from the <a href='https://ieeexplore.ieee.org/document/4729737/keywords#keywords'>Simaan et al 2008</a> model, which is an ordinary differential equation. Tune parameters and press 'Run simulation.'
534
  """
535
 
 
 
 
 
 
536
  gr.Markdown(title)
537
  gr.Markdown(description)
538
 
@@ -573,12 +884,18 @@ with gr.Blocks() as demo:
573
  sl5 = gr.Slider(4.0, 25.0, value= 4.0, label="Vd (ml)")
574
  sl6 = gr.Slider(0.4, 1.7, value= 0.4, label="Tc (s)")
575
  sl7 = gr.Slider(0.0, 280.0, value= 140., label="start_v (ml)")
576
-
 
 
 
 
 
 
577
 
578
  generate_button.click(fn=generate_example, outputs = [video,plot,Rm,Ra,Emax,Emin,Vd,Tc,start_v])
579
 
580
  simulation_button.click(fn=pvloop_simulator_plot_only, inputs = [sl1,sl2,sl3,sl4,sl5,sl6,sl7], outputs = [gr.Plot()])
581
 
582
-
583
 
584
  demo.launch()
 
495
  plt.title('Simulated PI-SSL LV Pressure Volume Loop', fontsize=16)
496
  return plot
497
 
498
+ #########################################
499
+ # LVAD functions
500
+ # RELU for diodes
501
+ def r(u):
502
+ return max(u, 0.)
503
+
504
+ def heart_ode0(y, t, Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emax, Emin, Tc, Vd):
505
+ x1, x2, x3, x4, x5 = y #here y is a vector of 5 values (not functions), at time t, used for getting (dy/dt)(t)
506
+ P_lv = Plv(x1+Vd,Emax,Emin,t,Tc,Vd)
507
+ dydt = [r(x2-P_lv)/Rm-r(P_lv-x4)/Ra, (x3-x2)/(Rs*Cr)-r(x2-P_lv)/(Cr*Rm), (x2-x3)/(Rs*Cs)+x5/Cs, -x5/Ca+r(P_lv-x4)/(Ca*Ra), (x4-x3-Rc*x5)/Ls]
508
+ return dydt
509
+
510
+ def getslope(y1, y2, y3, x1, x2, x3):
511
+ sum_x = x1 + x2 + x3
512
+ sum_y = y1 + y2 + y3
513
+ sum_xy = x1*y1 + x2*y2 + x3*y3
514
+ sum_xx = x1*x1 + x2*x2 + x3*x3
515
+ # calculate the coefficients of the least-squares line
516
+ n = 3
517
+ slope = (n*sum_xy - sum_x*sum_y) / (n*sum_xx - sum_x*sum_x)
518
+ return slope
519
+
520
+ ### ODE: for each t (here fixed), gives dy/dt as a function of y(t) at that t, so can be used for integrating the vector y over time
521
+ #it is run for each t going from 0 to tmax
522
+ def lvad_ode(y, t, Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emax, Emin, Tc, Vd, ratew):
523
+
524
+ #from simaan2008dynamical:
525
+ Ri = 0.0677
526
+ R0 = 0.0677
527
+ Rk = 0
528
+ x1bar = 1.
529
+ alpha = -3.5
530
+ Li = 0.0127
531
+ L0 = 0.0127
532
+ b0 = -0.296
533
+ b1 = -0.027
534
+ b2 = 9.9025e-7
535
+
536
+ x1, x2, x3, x4, x5, x6, x7 = y #here y is a vector of 5 values (not functions), at time t, used for getting (dy/dt)(t)
537
+
538
+ P_lv = Plv(x1+Vd,Emax,Emin,t,Tc,Vd)
539
+ if (P_lv <= x1bar): Rk = alpha * (P_lv - x1bar)
540
+ Lstar = Li + L0 + b1
541
+ Lstar2 = -Li -L0 +b1
542
+ Rstar = Ri + + R0 + Rk + b0
543
+
544
+ dydt = [-x6 + r(x2-P_lv)/Rm-r(P_lv-x4)/Ra, (x3-x2)/(Rs*Cr)-r(x2-P_lv)/(Cr*Rm), (x2-x3)/(Rs*Cs)+x5/Cs, -x5/Ca+r(P_lv-x4)/(Ca*Ra) + x6/Ca, (x4-x3)/Ls-Rc*x5/Ls, -P_lv / Lstar2 + x4/Lstar2 + (Ri+R0+Rk-b0) / Lstar2 * x6 - b2 / Lstar2 * x7**2, ratew]
545
+
546
+ return dydt
547
+
548
+ #returns pv loop and ef when there is no lvad:
549
+ def f_nolvad(Tc, start_v, Emax, showpvloop):
550
+
551
+ N = 20
552
+ global Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emin, Vd
553
+
554
+ start_pla = float(start_v*Elastance(Emax, Emin, 0, Tc))
555
+ start_pao = 75.
556
+ start_pa = start_pao
557
+ start_qt = 0 #aortic flow is Q_T and is 0 at ED, also see Fig5 in simaan2008dynamical
558
+
559
+ y0 = [start_v, start_pla, start_pa, start_pao, start_qt]
560
+
561
+ t = np.linspace(0, Tc*N, int(60000*N)) #spaced numbers over interval (start, stop, number_of_steps), 60000 time instances for each heart cycle
562
+ #changed to 60000 for having integer positions for Tmax
563
+ #obtain 5D vector solution:
564
+ sol = odeint(heart_ode0, y0, t, args = (Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emax, Emin, Tc,Vd)) #t: list of values
565
+
566
+ result_Vlv = np.array(sol[:, 0]) + Vd
567
+ result_Plv = np.array([Plv(v, Emax, Emin, xi, Tc, Vd) for xi,v in zip(t,sol[:, 0])])
568
+
569
+ #if showpvloop: plt.plot(result_Vlv[18*60000:20*60000], result_Plv[18*60000:20*60000], color='black', label='Without LVAD')
570
+
571
+ ved = sol[19*60000, 0] + Vd
572
+ ves = sol[200*int(60/Tc)+9000+19*60000, 0] + Vd
573
+ ef = (ved-ves)/ved * 100.
574
+ minv = min(result_Vlv[19*60000:20*60000-1])
575
+ minp = min(result_Plv[19*60000:20*60000-1])
576
+
577
+ result_pao = np.array(sol[:, 3])
578
+ pao_ed = min(result_pao[(N-1)*60000:N*60000-1])
579
+ pao_es = max(result_pao[(N-1)*60000:N*60000-1])
580
+
581
+ return ef, pao_ed, pao_es, ((ved - ves) * 60/Tc ) / 1000, sol[19*60000, 0], sol[19*60000, 1], sol[19*60000, 2], sol[19*60000, 3], sol[19*60000, 4], result_Vlv[18*60000:20*60000], result_Plv[18*60000:20*60000]
582
+
583
+ #returns the w at which suction occurs: (i.e. for which the slope of the envelopes of x6 becomes negative)
584
+ def get_suctionw(Tc, start_v, Emax, y00, y01, y02, y03, y04, w0, x60, ratew): #slope is slope0 for w
585
+
586
+ N = 70
587
+ global Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emin, Vd
588
+
589
+ start_pla = float(start_v*Elastance(Emax, Emin, 0, Tc))
590
+ start_pao = 75.
591
+ start_pa = start_pao
592
+ start_qt = 0 #aortic flow is Q_T and is 0 at ED, also see Fig5 in simaan2008dynamical
593
+
594
+ y0 = [start_v, start_pla, start_pa, start_pao, start_qt, x60, w0]
595
+ y0 = [y00, y01, y02, y03, y04, x60, w0]
596
+
597
+ ncycle = 20000
598
+ n = N * ncycle
599
+ sol = np.zeros((n, 7))
600
+ t = np.linspace(0., Tc * N, n)
601
+ for j in range(7):
602
+ sol[0][j] = y0[j]
603
+
604
+ result_Vlv = []
605
+ result_Plv = []
606
+ result_x6 = []
607
+ result_x7 = []
608
+ envx6 = []
609
+ timesenvx6 = []
610
+ slopes = []
611
+ ws = []
612
+
613
+ minx6 = 99999
614
+ tmin = 0
615
+ tlastupdate = 0
616
+ lastw = w0
617
+ update = 1
618
+
619
+ #solve the ODE step by step by adding dydt*dt:
620
+ for j in range(0, n-1):
621
+ #update y with dydt * dt
622
+ y = sol[j]
623
+ dydt = lvad_ode(y, t[j], Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emax, Emin, Tc, Vd, ratew)
624
+ for k in range(7):
625
+ dydt[k] = dydt[k] * (t[j+1] - t[j])
626
+ sol[j+1] = sol[j] + dydt
627
+
628
+ #update the min of x6 in the current cylce. also keep the time at which the min is obtained (for getting the slope later)
629
+ if (minx6 > sol[j][5]):
630
+ minx6 = sol[j][5]
631
+ tmin = t[j]
632
+
633
+ #add minimum of x6 once each cycle ends: (works). then reset minx6 to 99999 for calculating again the minimum
634
+ if (j%ncycle==0 and j>1):
635
+ envx6.append(minx6)
636
+ timesenvx6.append(tmin)
637
+ minx6 = 99999
638
+
639
+ if (len(envx6)>=3):
640
+ slope = getslope(envx6[-1], envx6[-2], envx6[-3], timesenvx6[-1], timesenvx6[-2], timesenvx6[-3])
641
+ slopes.append(slope)
642
+ ws.append(y[6])
643
+
644
+ for i in range(n):
645
+ result_x6.append(sol[i, 5])
646
+ result_x7.append(sol[i, 6])
647
+
648
+ suction_w = 0
649
+ for i in range(2, len(slopes)):
650
+ if (slopes[i] < 0):
651
+ suction_w = ws[i-1]
652
+ break
653
+
654
+ return suction_w
655
+
656
+ def f_lvad(Tc, start_v, Emax, c, slope, w0, x60, y00, y01, y02, y03, y04): #slope is slope0 for w
657
+
658
+ N = 70
659
+ global Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emin, Vd
660
+
661
+ y0 = [y00, y01, y02, y03, y04, x60, w0]
662
+
663
+ ncycle = 10000
664
+ n = N * ncycle
665
+ sol = np.zeros((n, 7))
666
+ t = np.linspace(0., Tc * N, n)
667
+ for j in range(7):
668
+ sol[0][j] = y0[j]
669
+
670
+ result_Vlv = []
671
+ result_Plv = []
672
+ result_x6 = []
673
+ result_x7 = []
674
+ envx6 = []
675
+ timesenvx6 = []
676
+
677
+ minx6 = 99999
678
+ tmin = 0
679
+ tlastupdate = 0
680
+ lastw = w0
681
+ update = 1
682
+ ratew = 0 #6000/60
683
+
684
+ #solve the ODE step by step by adding dydt*dt:
685
+ for j in range(0, n-1):
686
+ #update y with dydt * dt
687
+ y = sol[j]
688
+ dydt = lvad_ode(y, t[j], Rs, Rm, Ra, Rc, Ca, Cs, Cr, Ls, Emax, Emin, Tc, Vd, ratew)
689
+ for k in range(7):
690
+ dydt[k] = dydt[k] * (t[j+1] - t[j])
691
+ sol[j+1] = sol[j] + dydt
692
+
693
+ #update the min of x6 in the current cylce. also keep the time at which the min is obtained (for getting the slope later)
694
+ if (minx6 > sol[j][5]):
695
+ minx6 = sol[j][5]
696
+ tmin = t[j]
697
+
698
+ #add minimum of x6 once each cycle ends: (works). then reset minx6 to 99999 for calculating again the minimum
699
+ if (j%ncycle==0 and j>1):
700
+ envx6.append(minx6)
701
+ timesenvx6.append(tmin)
702
+ minx6 = 99999
703
+
704
+ #update w (if 0.005 s. have passed since the last update):
705
+ if (slope<0):
706
+ update = 0
707
+ if (t[j+1] - tlastupdate > 0.005 and slope>0 and update==1): #abs(slope)>0.0001
708
+ # if there are enough points of envelope: calculate slope:
709
+ if (len(envx6)>=3):
710
+ slope = getslope(envx6[-1], envx6[-2], envx6[-3], timesenvx6[-1], timesenvx6[-2], timesenvx6[-3])
711
+ sol[j+1][6] = lastw + c * slope
712
+ #otherwise: take arbitrary rate (see Fig. 16a in simaan2008dynamical)
713
+ else:
714
+ sol[j+1][6] = lastw + 0.005 * slope
715
+ #save w(k) (see formula (8) simaan2008dynamical) and the last time of update t[j] (will have to wait 0.005 s for next update of w)
716
+ tlastupdate = t[j+1]
717
+ lastw = sol[j+1][6]
718
+
719
+ #save functions and print MAP, CO:
720
+ map = 0
721
+ Pao = []
722
+
723
+ for i in range(n):
724
+ result_Vlv.append(sol[i, 0] + Vd)
725
+ result_Plv.append(Plv(sol[i, 0]+Vd, Emax, Emin, t[i], Tc, Vd))
726
+ result_x6.append(sol[i, 5])
727
+ result_x7.append(sol[i, 6])
728
+ Pao.append(sol[i, 3])
729
+
730
+ colors0=np.zeros((len(result_Vlv[65*ncycle:70*ncycle]), 3))
731
+ for col in colors0:
732
+ col[0]=41/255
733
+ col[1]=128/255
734
+ col[2]=205/255
735
+
736
+
737
+ #get co and ef:
738
+ ved = max(result_Vlv[50 * ncycle:52 * ncycle])
739
+ ves = min(result_Vlv[50 * ncycle:52 * ncycle])
740
+ #ves = result_Vlv[50 * ncycle + int(ncycle * 0.2 /Tc + 0.15 * ncycle)]
741
+ ef = (ved-ves)/ved*100
742
+ CO = ((ved - ves) * 60/Tc ) / 1000
743
+
744
+ #get MAP:
745
+ for i in range(n - 5*ncycle, n):
746
+ map += sol[i, 2]
747
+ map = map/(5*ncycle)
748
+
749
+ result_pao = np.array(sol[:, 3])
750
+ pao_ed = min(Pao[50 * ncycle:52 * ncycle])
751
+ pao_es = max(Pao[50 * ncycle:52 * ncycle])
752
+
753
+ return ef, pao_ed, pao_es, CO, map, result_Vlv[65*ncycle:70*ncycle], result_Plv[65*ncycle:70*ncycle]
754
+
755
+ #############################
756
+ ## Demo functions
757
 
758
  def generate_example():
759
  # get random input
 
776
  animated = "prediction.mp4"
777
  return video, animated, Rm, Ra, Emax, Emin, Vd, Tc, start_v
778
 
779
+
780
+ def lvad_plots(Rm, Ra, Emax, Emin, Vd, Tc, start_v, gamma):
781
+
782
+ ncycle = 10000
783
+
784
+ Rs = 1.
785
+ Rc = 0.0398
786
+ Ca= 0.08
787
+ Cs= 1.33
788
+ Cr= 4.4
789
+ Ls=0.0005
790
+
791
+ #get values for periodic loops:
792
+ ef_nolvad, pao_ed, pao_es, co_nolvad, y00, y01, y02, y03, y04, Vlv0, Plv0 = f_nolvad(Tc, start_v, Emax, 0)
793
+ #pao_eds = [pao_ed]
794
+ #pao_ess = [pao_es]
795
+
796
+ #get suction w: (make w go linearly from w0 to w0 + maxtime * 400, and find w at which suction occurs)
797
+ w0 = 5000.
798
+ ratew = 400.
799
+ x60 = 0.
800
+ suctionw = get_suctionw(Tc, start_v, Emax, y00, y01, y02, y03, y04, w0, x60, ratew)
801
+
802
+ #gamma = 1.8
803
+ c = 0.065 #(in simaan2008dynamical: 0.67, but too fast -> 0.061 gives better shape)
804
+ slope0 = 100.
805
+ w0 = suctionw / gamma #if doesn't work (x6 negative), change gamma down to 1.4 or up to 2.1
806
+
807
+ #compute new pv loops and ef with lvad added:
808
+ new_ef, pao_ed, pao_es, CO, MAP, Vlvs, Plvs = f_lvad(Tc, start_v, Emax, c, slope0, w0, x60, y00, y01, y02, y03, y04)
809
+
810
+ print("\nParameters: Tc, start_v, Emax:", Tc, start_v, Emax)
811
+ print("Suction speed:", suctionw)
812
+ print("EF before LVAD:", ef_nolvad)
813
+ print("CO before LVAD:", co_nolvad)
814
+ print("New EF after LVAD:", new_ef, "New CO:", CO, "New MAP:", MAP, "\n\n")
815
+
816
+ plt.plot(Vlv0, Plv0, color='blue', label='No LVAD') #blue
817
+ plt.plot(Vlvs, Plvs, color=(78/255, 192/255, 44/255), label=f"LVAD, ω(0)= {round(w0,2)}r/min") #green
818
+ plt.xlabel('LV volume (ml)')
819
+ plt.ylabel('LV pressure (mmHg)')
820
+ plt.legend(loc='upper left', framealpha=1)
821
+ plt.ylim((0,220))
822
+ plt.xlim((0,250))
823
+
824
+ return plt
825
+
826
  title = "<h1 style='text-align: center; margin-bottom: 1rem'> Non-Invasive Medical Digital Twins using Physics-Informed Self-Supervised Learning </h1>"
827
 
828
  description = """
 
834
 
835
  title2 = "<h3 style='text-align: center'> Physics-based model simulation</h3>"
836
 
837
+ description2 = """
838
+ \n \n
839
  Our model uses a hydraulic analogy model of cardiac function from <a href='https://ieeexplore.ieee.org/document/4729737/keywords#keywords'>Simaan et al 2008</a>. Below you can input values of predicted parameters and output a simulated pressure-volume loop predicted from the <a href='https://ieeexplore.ieee.org/document/4729737/keywords#keywords'>Simaan et al 2008</a> model, which is an ordinary differential equation. Tune parameters and press 'Run simulation.'
840
  """
841
 
842
+ description3 = """
843
+ \n\n
844
+ This model can incorporate a tunable left-ventricular assistance device (LVAD) for in-silico experimentation. Click to view the effect of adding an LVAD to the simulated PV loop.
845
+ """
846
+
847
  gr.Markdown(title)
848
  gr.Markdown(description)
849
 
 
884
  sl5 = gr.Slider(4.0, 25.0, value= 4.0, label="Vd (ml)")
885
  sl6 = gr.Slider(0.4, 1.7, value= 0.4, label="Tc (s)")
886
  sl7 = gr.Slider(0.0, 280.0, value= 140., label="start_v (ml)")
887
+
888
+ gr.Markdown(description2)
889
+
890
+ LVAD_button = gr.Button("Add LVAD")
891
+
892
+ with gr.Row():
893
+ gamma = gr.Slider(1.0, 2.0, value= 1.4, label="Pump speed, ω(0)")
894
 
895
  generate_button.click(fn=generate_example, outputs = [video,plot,Rm,Ra,Emax,Emin,Vd,Tc,start_v])
896
 
897
  simulation_button.click(fn=pvloop_simulator_plot_only, inputs = [sl1,sl2,sl3,sl4,sl5,sl6,sl7], outputs = [gr.Plot()])
898
 
899
+ LVAD_button.click(fn=lvad_plots, inputs = [sl1,sl2,sl3,sl4,sl5,sl6,sl7,gamma], outputs = [gr.Plot()])
900
 
901
  demo.launch()