Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F66133599
bls.py
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Sat, Jun 8, 12:37
Size
28 KB
Mime Type
text/x-python
Expires
Mon, Jun 10, 12:37 (1 d, 23 h)
Engine
blob
Format
Raw Data
Handle
18175505
Attached To
R4670 PySONIC (old)
bls.py
View Options
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Author: Theo Lemaire
# @Date: 2016-09-29 16:16:19
# @Email: theo.lemaire@epfl.ch
# @Last Modified by: Theo Lemaire
# @Last Modified time: 2019-06-09 21:12:38
from
enum
import
Enum
import
os
import
json
import
numpy
as
np
import
pandas
as
pd
import
scipy.integrate
as
integrate
from
scipy.optimize
import
brentq
,
curve_fit
from
.model
import
Model
from
.simulators
import
PeriodicSimulator
from
..utils
import
logger
,
si_format
from
..constants
import
*
class
PmCompMethod
(
Enum
):
''' Enum: types of computation method for the intermolecular pressure '''
direct
=
1
predict
=
2
def
LennardJones
(
x
,
beta
,
alpha
,
C
,
m
,
n
):
''' Generic expression of a Lennard-Jones function, adapted for the context of
symmetric deflection (distance = 2x).
:param x: deflection (i.e. half-distance)
:param beta: x-shifting factor
:param alpha: x-scaling factor
:param C: y-scaling factor
:param m: exponent of the repulsion term
:param n: exponent of the attraction term
:return: Lennard-Jones potential at given distance (2x)
'''
return
C
*
(
np
.
power
((
alpha
/
(
2
*
x
+
beta
)),
m
)
-
np
.
power
((
alpha
/
(
2
*
x
+
beta
)),
n
))
class
BilayerSonophore
(
Model
):
''' This class contains the geometric and mechanical parameters of the
Bilayer Sonophore Model, as well as all the core functions needed to
compute the dynamics (kinetics and kinematics) of the bilayer membrane
cavitation, and run dynamic BLS simulations.
'''
# BIOMECHANICAL PARAMETERS
T
=
309.15
# Temperature (K)
delta0
=
2.0e-9
# Thickness of the leaflet (m)
Delta_
=
1.4e-9
# Initial gap between the two leaflets on a non-charged membrane at equil. (m)
pDelta
=
1.0e5
# Attraction/repulsion pressure coefficient (Pa)
m
=
5.0
# Exponent in the repulsion term (dimensionless)
n
=
3.3
# Exponent in the attraction term (dimensionless)
rhoL
=
1075.0
# Density of the surrounding fluid (kg/m^3)
muL
=
7.0e-4
# Dynamic viscosity of the surrounding fluid (Pa.s)
muS
=
0.035
# Dynamic viscosity of the leaflet (Pa.s)
kA
=
0.24
# Area compression modulus of the leaflet (N/m)
alpha
=
7.56
# Tissue shear loss modulus frequency coefficient (Pa.s)
C0
=
0.62
# Initial gas molar concentration in the surrounding fluid (mol/m^3)
kH
=
1.613e5
# Henry's constant (Pa.m^3/mol)
P0
=
1.0e5
# Static pressure in the surrounding fluid (Pa)
Dgl
=
3.68e-9
# Diffusion coefficient of gas in the fluid (m^2/s)
xi
=
0.5e-9
# Boundary layer thickness for gas transport across leaflet (m)
c
=
1515.0
# Speed of sound in medium (m/s)
# BIOPHYSICAL PARAMETERS
epsilon0
=
8.854e-12
# Vacuum permittivity (F/m)
epsilonR
=
1.0
# Relative permittivity of intramembrane cavity (dimensionless)
tscale
=
'us'
# relevant temporal scale of the model
simkey
=
'MECH'
# keyword used to characterize simulations made with this model
def
__init__
(
self
,
a
,
Cm0
,
Qm0
,
Fdrive
=
None
,
embedding_depth
=
0.0
):
''' Constructor of the class.
:param a: in-plane radius of the sonophore structure within the membrane (m)
:param Cm0: membrane resting capacitance (F/m2)
:param Qm0: membrane resting charge density (C/m2)
:param Fdrive: frequency of acoustic perturbation (Hz)
:param embedding_depth: depth of the embedding tissue around the membrane (m)
'''
# Extract resting constants and geometry
self
.
Cm0
=
Cm0
self
.
Qm0
=
Qm0
self
.
a
=
a
self
.
d
=
embedding_depth
self
.
S0
=
np
.
pi
*
self
.
a
**
2
# Derive frequency-dependent tissue elastic modulus
if
Fdrive
is
not
None
:
G_tissue
=
self
.
alpha
*
Fdrive
# G'' (Pa)
self
.
kA_tissue
=
2
*
G_tissue
*
self
.
d
# kA of the tissue layer (N/m)
else
:
self
.
kA_tissue
=
0.
# Check existence of lookups for derived parameters
lookups
=
self
.
getLookups
()
akey
=
'{:.1f}'
.
format
(
a
*
1e9
)
Qkey
=
'{:.2f}'
.
format
(
Qm0
*
1e5
)
# If no lookup, compute parameters and store them in lookup
if
akey
not
in
lookups
or
Qkey
not
in
lookups
[
akey
]:
# Find Delta that cancels out Pm + Pec at Z = 0 (m)
if
self
.
Qm0
==
0.0
:
D_eq
=
self
.
Delta_
else
:
(
D_eq
,
Pnet_eq
)
=
self
.
findDeltaEq
(
self
.
Qm0
)
assert
Pnet_eq
<
PNET_EQ_MAX
,
'High Pnet at Z = 0 with ∆ =
%.2f
nm'
%
(
D_eq
*
1e9
)
self
.
Delta
=
D_eq
# Find optimal Lennard-Jones parameters to approximate PMavg
(
LJ_approx
,
std_err
,
_
)
=
self
.
LJfitPMavg
()
assert
std_err
<
PMAVG_STD_ERR_MAX
,
'High error in PmAvg nonlinear fit:'
\
' std_err =
%.2f
Pa'
%
std_err
self
.
LJ_approx
=
LJ_approx
if
akey
not
in
lookups
:
lookups
[
akey
]
=
{
Qkey
:
{
'LJ_approx'
:
LJ_approx
,
'Delta_eq'
:
D_eq
}}
else
:
lookups
[
akey
][
Qkey
]
=
{
'LJ_approx'
:
LJ_approx
,
'Delta_eq'
:
D_eq
}
logger
.
debug
(
'Saving BLS derived parameters to lookup file'
)
self
.
saveLookups
(
lookups
)
# If lookup exists, load parameters from it
else
:
logger
.
debug
(
'Loading BLS derived parameters from lookup file'
)
self
.
LJ_approx
=
lookups
[
akey
][
Qkey
][
'LJ_approx'
]
self
.
Delta
=
lookups
[
akey
][
Qkey
][
'Delta_eq'
]
# Compute initial volume and gas content
self
.
V0
=
np
.
pi
*
self
.
Delta
*
self
.
a
**
2
self
.
ng0
=
self
.
gasPa2mol
(
self
.
P0
,
self
.
V0
)
def
__repr__
(
self
):
s
=
'{}({:.1f} nm'
.
format
(
self
.
__class__
.
__name__
,
self
.
a
*
1e9
)
if
self
.
d
>
0.
:
s
+=
', d={}m'
.
format
(
si_format
(
self
.
d
,
precision
=
1
,
space
=
' '
))
return
s
+
')'
def
filecodes
(
self
,
Fdrive
,
Adrive
,
Qm
):
return
{
'simkey'
:
self
.
simkey
,
'a'
:
'{:.0f}nm'
.
format
(
self
.
a
*
1e9
),
'Fdrive'
:
'{:.0f}kHz'
.
format
(
Fdrive
*
1e-3
),
'Adrive'
:
'{:.2f}kPa'
.
format
(
Adrive
*
1e-3
),
'Qm'
:
'{:.1f}nCcm2'
.
format
(
Qm
*
1e5
)
}
def
filecode
(
self
,
Fdrive
,
Adrive
,
Qm
):
return
'_'
.
join
(
self
.
filecodes
(
Fdrive
,
Adrive
,
Qm
)
.
values
())
# return '{}_{:.0f}nm_{:.0f}kHz_{:.1f}kPa_{:.1f}nCcm2'.format(
# self.simkey, self.a * 1e9, Fdrive * 1e-3, Adrive * 1e-3, Qm * 1e5)
def
getLookupsPath
(
self
):
return
os
.
path
.
join
(
os
.
path
.
split
(
__file__
)[
0
],
'bls_lookups.json'
)
def
getLookups
(
self
):
try
:
with
open
(
self
.
getLookupsPath
())
as
fh
:
sample
=
json
.
load
(
fh
)
return
sample
except
FileNotFoundError
:
return
{}
def
saveLookups
(
self
,
lookups
):
with
open
(
self
.
getLookupsPath
(),
'w'
)
as
fh
:
json
.
dump
(
lookups
,
fh
,
indent
=
2
)
def
getPltScheme
(
self
):
return
{
'P_{AC}'
:
[
'Pac'
],
'Z'
:
[
'Z'
],
'n_g'
:
[
'ng'
]
}
def
getPltVars
(
self
,
wrapleft
=
'df["'
,
wrapright
=
'"]'
):
''' Return a dictionary with information about all plot variables related to the model. '''
return
{
'Pac'
:
{
'desc'
:
'acoustic pressure'
,
'label'
:
'P_{AC}'
,
'unit'
:
'kPa'
,
'factor'
:
1e-3
,
'func'
:
'Pacoustic({0}t{1}, meta["Adrive"] * {0}stimstate{1}, meta["Fdrive"])'
.
format
(
wrapleft
,
wrapright
)
},
'Z'
:
{
'desc'
:
'leaflets deflection'
,
'label'
:
'Z'
,
'unit'
:
'nm'
,
'factor'
:
1e9
,
'bounds'
:
(
-
1.0
,
10.0
)
},
'ng'
:
{
'desc'
:
'gas content'
,
'label'
:
'n_g'
,
'unit'
:
'10^{-22}\ mol'
,
'factor'
:
1e22
,
'bounds'
:
(
1.0
,
15.0
)
},
'Pmavg'
:
{
'desc'
:
'average intermolecular pressure'
,
'label'
:
'P_M'
,
'unit'
:
'kPa'
,
'factor'
:
1e-3
,
'func'
:
'PMavgpred({0}Z{1})'
.
format
(
wrapleft
,
wrapright
)
},
'Telastic'
:
{
'desc'
:
'leaflet elastic tension'
,
'label'
:
'T_E'
,
'unit'
:
'mN/m'
,
'factor'
:
1e3
,
'func'
:
'TEleaflet({0}Z{1})'
.
format
(
wrapleft
,
wrapright
)
},
'Cm'
:
{
'desc'
:
'membrane capacitance'
,
'label'
:
'C_m'
,
'unit'
:
'uF/cm^2'
,
'factor'
:
1e2
,
'bounds'
:
(
0.0
,
1.5
),
'func'
:
'v_Capct({0}Z{1})'
.
format
(
wrapleft
,
wrapright
)
}
}
def
curvrad
(
self
,
Z
):
''' Leaflet curvature radius
(signed variable)
:param Z: leaflet apex deflection (m)
:return: leaflet curvature radius (m)
'''
if
Z
==
0.0
:
return
np
.
inf
else
:
return
(
self
.
a
**
2
+
Z
**
2
)
/
(
2
*
Z
)
def
v_curvrad
(
self
,
Z
):
''' Vectorized curvrad function '''
return
np
.
array
(
list
(
map
(
self
.
curvrad
,
Z
)))
def
surface
(
self
,
Z
):
''' Surface area of the stretched leaflet
(spherical cap formula)
:param Z: leaflet apex deflection (m)
:return: stretched leaflet surface (m^2)
'''
return
np
.
pi
*
(
self
.
a
**
2
+
Z
**
2
)
def
volume
(
self
,
Z
):
''' Volume of the inter-leaflet space
(cylinder +/- 2 spherical caps)
:param Z: leaflet apex deflection (m)
:return: bilayer sonophore inner volume (m^3)
'''
return
np
.
pi
*
self
.
a
**
2
*
self
.
Delta
\
*
(
1
+
(
Z
/
(
3
*
self
.
Delta
)
*
(
3
+
Z
**
2
/
self
.
a
**
2
)))
def
arealstrain
(
self
,
Z
):
''' Areal strain of the stretched leaflet
epsilon = (S - S0)/S0 = (Z/a)^2
:param Z: leaflet apex deflection (m)
:return: areal strain (dimensionless)
'''
return
(
Z
/
self
.
a
)
**
2
def
Capct
(
self
,
Z
):
''' Membrane capacitance
(parallel-plate capacitor evaluated at average inter-layer distance)
:param Z: leaflet apex deflection (m)
:return: capacitance per unit area (F/m2)
'''
if
Z
==
0.0
:
return
self
.
Cm0
else
:
return
((
self
.
Cm0
*
self
.
Delta
/
self
.
a
**
2
)
*
(
Z
+
(
self
.
a
**
2
-
Z
**
2
-
Z
*
self
.
Delta
)
/
(
2
*
Z
)
*
np
.
log
((
2
*
Z
+
self
.
Delta
)
/
self
.
Delta
)))
def
v_Capct
(
self
,
Z
):
''' Vectorized Capct function '''
return
np
.
array
(
list
(
map
(
self
.
Capct
,
Z
)))
def
derCapct
(
self
,
Z
,
U
):
''' Evolution of membrane capacitance
:param Z: leaflet apex deflection (m)
:param U: leaflet apex deflection velocity (m/s)
:return: time derivative of capacitance per unit area (F/m2.s)
'''
dCmdZ
=
((
self
.
Cm0
*
self
.
Delta
/
self
.
a
**
2
)
*
((
Z
**
2
+
self
.
a
**
2
)
/
(
Z
*
(
2
*
Z
+
self
.
Delta
))
-
((
Z
**
2
+
self
.
a
**
2
)
*
np
.
log
((
2
*
Z
+
self
.
Delta
)
/
self
.
Delta
))
/
(
2
*
Z
**
2
)))
return
dCmdZ
*
U
def
localdef
(
self
,
r
,
Z
,
R
):
''' Local leaflet deflection at specific radial distance
(signed)
:param r: in-plane distance from center of the sonophore (m)
:param Z: leaflet apex deflection (m)
:param R: leaflet curvature radius (m)
:return: local transverse leaflet deviation (m)
'''
if
np
.
abs
(
Z
)
==
0.0
:
return
0.0
else
:
return
np
.
sign
(
Z
)
*
(
np
.
sqrt
(
R
**
2
-
r
**
2
)
-
np
.
abs
(
R
)
+
np
.
abs
(
Z
))
def
Pacoustic
(
self
,
t
,
Adrive
,
Fdrive
,
phi
=
np
.
pi
):
''' Time-varying acoustic pressure
:param t: time (s)
:param Adrive: acoustic drive amplitude (Pa)
:param Fdrive: acoustic drive frequency (Hz)
:param phi: acoustic drive phase (rad)
'''
return
Adrive
*
np
.
sin
(
2
*
np
.
pi
*
Fdrive
*
t
-
phi
)
def
PMlocal
(
self
,
r
,
Z
,
R
):
''' Local intermolecular pressure
:param r: in-plane distance from center of the sonophore (m)
:param Z: leaflet apex deflection (m)
:param R: leaflet curvature radius (m)
:return: local intermolecular pressure (Pa)
'''
z
=
self
.
localdef
(
r
,
Z
,
R
)
relgap
=
(
2
*
z
+
self
.
Delta
)
/
self
.
Delta_
return
self
.
pDelta
*
((
1
/
relgap
)
**
self
.
m
-
(
1
/
relgap
)
**
self
.
n
)
def
PMavg
(
self
,
Z
,
R
,
S
):
''' Average intermolecular pressure across the leaflet
(computed by quadratic integration)
:param Z: leaflet apex outward deflection value (m)
:param R: leaflet curvature radius (m)
:param S: surface of the stretched leaflet (m^2)
:return: averaged intermolecular resultant pressure (Pa)
.. warning:: quadratic integration is computationally expensive.
'''
# Integrate intermolecular force over an infinitely thin ring of radius r from 0 to a
fTotal
,
_
=
integrate
.
quad
(
lambda
r
,
Z
,
R
:
2
*
np
.
pi
*
r
*
self
.
PMlocal
(
r
,
Z
,
R
),
0
,
self
.
a
,
args
=
(
Z
,
R
))
return
fTotal
/
S
def
v_PMavg
(
self
,
Z
,
R
,
S
):
''' Vectorized PMavg function '''
return
np
.
array
(
list
(
map
(
self
.
PMavg
,
Z
,
R
,
S
)))
def
LJfitPMavg
(
self
):
''' Determine optimal parameters of a Lennard-Jones expression
approximating the average intermolecular pressure.
These parameters are obtained by a nonlinear fit of the
Lennard-Jones function for a range of deflection values
between predetermined Zmin and Zmax.
:return: 3-tuple with optimized LJ parameters for PmAvg prediction (Map) and
the standard and max errors of the prediction in the fitting range (in Pascals)
'''
# Determine lower bound of deflection range: when Pm = Pmmax
PMmax
=
LJFIT_PM_MAX
# Pa
Zminlb
=
-
0.49
*
self
.
Delta
Zminub
=
0.0
Zmin
=
brentq
(
lambda
Z
,
Pmmax
:
self
.
PMavg
(
Z
,
self
.
curvrad
(
Z
),
self
.
surface
(
Z
))
-
PMmax
,
Zminlb
,
Zminub
,
args
=
(
PMmax
),
xtol
=
1e-16
)
# Create vectors for geometric variables
Zmax
=
2
*
self
.
a
Z
=
np
.
arange
(
Zmin
,
Zmax
,
1e-11
)
Pmavg
=
self
.
v_PMavg
(
Z
,
self
.
v_curvrad
(
Z
),
self
.
surface
(
Z
))
# Compute optimal nonlinear fit of custom LJ function with initial guess
x0_guess
=
self
.
delta0
C_guess
=
0.1
*
self
.
pDelta
nrep_guess
=
self
.
m
nattr_guess
=
self
.
n
pguess
=
(
x0_guess
,
C_guess
,
nrep_guess
,
nattr_guess
)
popt
,
_
=
curve_fit
(
lambda
x
,
x0
,
C
,
nrep
,
nattr
:
LennardJones
(
x
,
self
.
Delta
,
x0
,
C
,
nrep
,
nattr
),
Z
,
Pmavg
,
p0
=
pguess
,
maxfev
=
10000
)
(
x0_opt
,
C_opt
,
nrep_opt
,
nattr_opt
)
=
popt
Pmavg_fit
=
LennardJones
(
Z
,
self
.
Delta
,
x0_opt
,
C_opt
,
nrep_opt
,
nattr_opt
)
# Compute prediction error
residuals
=
Pmavg
-
Pmavg_fit
ss_res
=
np
.
sum
(
residuals
**
2
)
N
=
residuals
.
size
std_err
=
np
.
sqrt
(
ss_res
/
N
)
max_err
=
max
(
np
.
abs
(
residuals
))
logger
.
debug
(
'LJ approx: x0 =
%.2f
nm, C =
%.2f
kPa, m =
%.2f
, n =
%.2f
'
,
x0_opt
*
1e9
,
C_opt
*
1e-3
,
nrep_opt
,
nattr_opt
)
LJ_approx
=
{
"x0"
:
x0_opt
,
"C"
:
C_opt
,
"nrep"
:
nrep_opt
,
"nattr"
:
nattr_opt
}
return
(
LJ_approx
,
std_err
,
max_err
)
def
PMavgpred
(
self
,
Z
):
''' Approximated average intermolecular pressure
(using nonlinearly fitted Lennard-Jones function)
:param Z: leaflet apex deflection (m)
:return: predicted average intermolecular pressure (Pa)
'''
return
LennardJones
(
Z
,
self
.
Delta
,
self
.
LJ_approx
[
'x0'
],
self
.
LJ_approx
[
'C'
],
self
.
LJ_approx
[
'nrep'
],
self
.
LJ_approx
[
'nattr'
])
def
Pelec
(
self
,
Z
,
Qm
):
''' Electrical pressure term
:param Z: leaflet apex deflection (m)
:param Qm: membrane charge density (C/m2)
:return: electrical pressure (Pa)
'''
relS
=
self
.
S0
/
self
.
surface
(
Z
)
abs_perm
=
self
.
epsilon0
*
self
.
epsilonR
# F/m
return
-
relS
*
Qm
**
2
/
(
2
*
abs_perm
)
# Pa
def
findDeltaEq
(
self
,
Qm
):
''' Compute the Delta that cancels out the (Pm + Pec) equation at Z = 0
for a given membrane charge density, using the Brent method to refine
the pressure root iteratively.
:param Qm: membrane charge density (C/m2)
:return: equilibrium value (m) and associated pressure (Pa)
'''
def
dualPressure
(
Delta
):
x
=
(
self
.
Delta_
/
Delta
)
return
(
self
.
pDelta
*
(
x
**
self
.
m
-
x
**
self
.
n
)
+
self
.
Pelec
(
0.0
,
Qm
))
Delta_eq
=
brentq
(
dualPressure
,
0.1
*
self
.
Delta_
,
2.0
*
self
.
Delta_
,
xtol
=
1e-16
)
logger
.
debug
(
'∆eq =
%.2f
nm'
,
Delta_eq
*
1e9
)
return
(
Delta_eq
,
dualPressure
(
Delta_eq
))
def
gasFlux
(
self
,
Z
,
P
):
''' Gas molar flux through the sonophore boundary layers
:param Z: leaflet apex deflection (m)
:param P: internal gas pressure (Pa)
:return: gas molar flux (mol/s)
'''
dC
=
self
.
C0
-
P
/
self
.
kH
return
2
*
self
.
surface
(
Z
)
*
self
.
Dgl
*
dC
/
self
.
xi
def
gasmol2Pa
(
self
,
ng
,
V
):
''' Internal gas pressure for a given molar content
:param ng: internal molar content (mol)
:param V: sonophore inner volume (m^3)
:return: internal gas pressure (Pa)
'''
return
ng
*
Rg
*
self
.
T
/
V
def
gasPa2mol
(
self
,
P
,
V
):
''' Internal gas molar content for a given pressure
:param P: internal gas pressure (Pa)
:param V: sonophore inner volume (m^3)
:return: internal gas molar content (mol)
'''
return
P
*
V
/
(
Rg
*
self
.
T
)
def
PtotQS
(
self
,
Z
,
ng
,
Qm
,
Pac
,
Pm_comp_method
):
''' Net quasi-steady pressure for a given acoustic pressure
(Ptot = Pm + Pg + Pec - P0 - Pac)
:param Z: leaflet apex deflection (m)
:param ng: internal molar content (mol)
:param Qm: membrane charge density (C/m2)
:param Pac: acoustic pressure (Pa)
:param Pm_comp_method: computation method for average intermolecular pressure
:return: total balance pressure (Pa)
'''
if
Pm_comp_method
is
PmCompMethod
.
direct
:
Pm
=
self
.
PMavg
(
Z
,
self
.
curvrad
(
Z
),
self
.
surface
(
Z
))
elif
Pm_comp_method
is
PmCompMethod
.
predict
:
Pm
=
self
.
PMavgpred
(
Z
)
return
Pm
+
self
.
gasmol2Pa
(
ng
,
self
.
volume
(
Z
))
-
self
.
P0
-
Pac
+
self
.
Pelec
(
Z
,
Qm
)
def
balancedefQS
(
self
,
ng
,
Qm
,
Pac
=
0.0
,
Pm_comp_method
=
PmCompMethod
.
predict
):
''' Quasi-steady equilibrium deflection for a given acoustic pressure
(computed by approximating the root of quasi-steady pressure)
:param ng: internal molar content (mol)
:param Qm: membrane charge density (C/m2)
:param Pac: external acoustic perturbation (Pa)
:param Pm_comp_method: computation method for average intermolecular pressure
:return: leaflet deflection canceling quasi-steady pressure (m)
'''
lb
,
ub
=
-
0.49
*
self
.
Delta
,
self
.
a
Plb
,
Pub
=
[
self
.
PtotQS
(
x
,
ng
,
Qm
,
Pac
,
Pm_comp_method
)
for
x
in
[
lb
,
ub
]]
assert
(
Plb
>
0
>
Pub
),
'[{}, {}] is not a sign changing interval for PtotQS'
.
format
(
lb
,
ub
)
return
brentq
(
self
.
PtotQS
,
lb
,
ub
,
args
=
(
ng
,
Qm
,
Pac
,
Pm_comp_method
),
xtol
=
1e-16
)
def
TEleaflet
(
self
,
Z
):
''' Elastic tension in leaflet
:param Z: leaflet apex deflection (m)
:return: circumferential elastic tension (N/m)
'''
return
self
.
kA
*
self
.
arealstrain
(
Z
)
def
TEtissue
(
self
,
Z
):
''' Elastic tension in surrounding viscoelastic layer
:param Z: leaflet apex deflection (m)
:return: circumferential elastic tension (N/m)
'''
return
self
.
kA_tissue
*
self
.
arealstrain
(
Z
)
def
TEtot
(
self
,
Z
):
''' Total elastic tension (leaflet + surrounding viscoelastic layer)
:param Z: leaflet apex deflection (m)
:return: circumferential elastic tension (N/m)
'''
return
self
.
TEleaflet
(
Z
)
+
self
.
TEtissue
(
Z
)
def
PEtot
(
self
,
Z
,
R
):
''' Total elastic tension pressure (leaflet + surrounding viscoelastic layer)
:param Z: leaflet apex deflection (m)
:param R: leaflet curvature radius (m)
:return: elastic tension pressure (Pa)
'''
return
-
self
.
TEtot
(
Z
)
/
R
def
PVleaflet
(
self
,
U
,
R
):
''' Viscous stress pressure in leaflet
:param U: leaflet apex deflection velocity (m/s)
:param R: leaflet curvature radius (m)
:return: leaflet viscous stress pressure (Pa)
'''
return
-
12
*
U
*
self
.
delta0
*
self
.
muS
/
R
**
2
def
PVfluid
(
self
,
U
,
R
):
''' Viscous stress pressure in surrounding medium
:param U: leaflet apex deflection velocity (m/s)
:param R: leaflet curvature radius (m)
:return: fluid viscous stress pressure (Pa)
'''
return
-
4
*
U
*
self
.
muL
/
np
.
abs
(
R
)
def
accP
(
self
,
Ptot
,
R
):
''' Leaflet transverse acceleration resulting from pressure imbalance
:param Ptot: net pressure (Pa)
:param R: leaflet curvature radius (m)
:return: pressure-driven acceleration (m/s^2)
'''
return
Ptot
/
(
self
.
rhoL
*
np
.
abs
(
R
))
def
accNL
(
self
,
U
,
R
):
''' Leaflet transverse nonlinear acceleration
:param U: leaflet apex deflection velocity (m/s)
:param R: leaflet curvature radius (m)
:return: nonlinear acceleration term (m/s^2)
.. note:: A simplified version of nonlinear acceleration (neglecting
dR/dH) is used here.
'''
# return - (3/2 - 2*R/H) * U**2 / R
return
-
(
3
*
U
**
2
)
/
(
2
*
R
)
def
derivatives
(
self
,
t
,
y
,
Adrive
,
Fdrive
,
Qm
,
phi
,
Pm_comp_method
=
PmCompMethod
.
predict
):
''' Evolution of the mechanical system
:param t: time instant (s)
:param y: vector of HH system variables at time t
:param Adrive: acoustic drive amplitude (Pa)
:param Fdrive: acoustic drive frequency (Hz)
:param Qm: membrane charge density (F/m2)
:param phi: acoustic drive phase (rad)
:param Pm_comp_method: computation method for average intermolecular pressure
:return: vector of mechanical system derivatives at time t
'''
# Split input vector explicitly
U
,
Z
,
ng
=
y
# Correct deflection value is below critical compression
if
Z
<
-
0.5
*
self
.
Delta
:
logger
.
warning
(
'Deflection out of range: Z =
%.2f
nm'
,
Z
*
1e9
)
Z
=
-
0.49
*
self
.
Delta
# Compute curvature radius
R
=
self
.
curvrad
(
Z
)
# Compute total pressure
Pg
=
self
.
gasmol2Pa
(
ng
,
self
.
volume
(
Z
))
if
Pm_comp_method
is
PmCompMethod
.
direct
:
Pm
=
self
.
PMavg
(
Z
,
self
.
curvrad
(
Z
),
self
.
surface
(
Z
))
elif
Pm_comp_method
is
PmCompMethod
.
predict
:
Pm
=
self
.
PMavgpred
(
Z
)
Ptot
=
(
Pm
+
Pg
-
self
.
P0
-
self
.
Pacoustic
(
t
,
Adrive
,
Fdrive
,
phi
)
+
self
.
PEtot
(
Z
,
R
)
+
self
.
PVleaflet
(
U
,
R
)
+
self
.
PVfluid
(
U
,
R
)
+
self
.
Pelec
(
Z
,
Qm
))
# Compute derivatives
dUdt
=
self
.
accP
(
Ptot
,
R
)
+
self
.
accNL
(
U
,
R
)
dZdt
=
U
dngdt
=
self
.
gasFlux
(
Z
,
Pg
)
# Return derivatives vector
return
[
dUdt
,
dZdt
,
dngdt
]
def
checkInputs
(
self
,
Fdrive
,
Adrive
,
Qm
,
phi
):
''' Check validity of stimulation parameters
:param Fdrive: acoustic drive frequency (Hz)
:param Adrive: acoustic drive amplitude (Pa)
:param phi: acoustic drive phase (rad)
:param Qm: imposed membrane charge density (C/m2)
'''
if
not
all
(
isinstance
(
param
,
float
)
for
param
in
[
Fdrive
,
Adrive
,
Qm
,
phi
]):
raise
TypeError
(
'Invalid stimulation parameters (must be float typed)'
)
if
Fdrive
<=
0
:
raise
ValueError
(
'Invalid US driving frequency: {} kHz (must be strictly positive)'
.
format
(
Fdrive
*
1e-3
))
if
Adrive
<
0
:
raise
ValueError
(
'Invalid US pressure amplitude: {} kPa (must be positive or null)'
.
format
(
Adrive
*
1e-3
))
if
Qm
<
CHARGE_RANGE
[
0
]
or
Qm
>
CHARGE_RANGE
[
1
]:
raise
ValueError
(
'Invalid applied charge: {} nC/cm2 (must be within [{}, {}] interval'
.
format
(
Qm
*
1e5
,
CHARGE_RANGE
[
0
]
*
1e5
,
CHARGE_RANGE
[
1
]
*
1e5
))
if
phi
<
0
or
phi
>=
2
*
np
.
pi
:
raise
ValueError
(
'Invalid US pressure phase: {:.2f} rad (must be within [0, 2 PI[ rad'
.
format
(
phi
))
def
meta
(
self
,
Fdrive
,
Adrive
,
Qm
):
''' Return information about object and simulation parameters.
:param Fdrive: US frequency (Hz)
:param Adrive: acoustic pressure amplitude (Pa)
:param Qm: applied membrane charge density (C/m2)
:return: meta-data dictionary
'''
return
{
'a'
:
self
.
a
,
'd'
:
self
.
d
,
'Cm0'
:
self
.
Cm0
,
'Qm0'
:
self
.
Qm0
,
'Fdrive'
:
Fdrive
,
'Adrive'
:
Adrive
,
'Qm'
:
Qm
}
def
simulate
(
self
,
Fdrive
,
Adrive
,
Qm
,
phi
=
np
.
pi
,
Pm_comp_method
=
PmCompMethod
.
predict
):
''' Simulate system until periodic stabilization for a specific set of ultrasound parameters,
and return output data in a dataframe.
:param Fdrive: acoustic drive frequency (Hz)
:param Adrive: acoustic drive amplitude (Pa)
:param phi: acoustic drive phase (rad)
:param Qm: imposed membrane charge density (C/m2)
:param Pm_comp_method: type of method used to compute average intermolecular pressure
:return: 2-tuple with the output dataframe and computation time.
'''
logger
.
info
(
'
%s
: simulation @ f =
%s
Hz, A =
%s
Pa, Q =
%s
C/cm2'
,
self
,
*
si_format
([
Fdrive
,
Adrive
,
Qm
*
1e-4
],
2
,
space
=
' '
))
# Check validity of stimulation parameters
self
.
checkInputs
(
Fdrive
,
Adrive
,
Qm
,
phi
)
# Determine time step
dt
=
1
/
(
NPC_FULL
*
Fdrive
)
# Compute non-zero deflection value for a small perturbation (solving quasi-steady equation)
Pac
=
self
.
Pacoustic
(
dt
,
Adrive
,
Fdrive
,
phi
)
Z0
=
self
.
balancedefQS
(
self
.
ng0
,
Qm
,
Pac
,
Pm_comp_method
)
# Set initial conditions
y0
=
np
.
array
([
0.
,
Z0
,
self
.
ng0
])
# Initialize simulator and compute solution
simulator
=
PeriodicSimulator
(
lambda
t
,
y
:
self
.
derivatives
(
t
,
y
,
Adrive
,
Fdrive
,
Qm
,
phi
,
Pm_comp_method
),
ivars_to_check
=
[
1
,
2
])
(
t
,
y
,
stim
),
tcomp
=
simulator
(
y0
,
dt
,
1.
/
Fdrive
,
monitor_time
=
True
)
logger
.
debug
(
'completed in
%s
s'
,
si_format
(
tcomp
,
1
))
# Set last stimulation state to zero
stim
[
-
1
]
=
0
# Store output in dataframe
data
=
pd
.
DataFrame
({
't'
:
t
,
'stimstate'
:
stim
,
'Z'
:
y
[:,
1
],
'ng'
:
y
[:,
2
]
})
# Return dataframe and computation time
return
data
,
tcomp
def
getCycleProfiles
(
self
,
Fdrive
,
Adrive
,
Qm
):
''' Simulate mechanical system and compute pressures over the last acoustic cycle
:param Fdrive: acoustic drive frequency (Hz)
:param Adrive: acoustic drive amplitude (Pa)
:param Qm: imposed membrane charge density (C/m2)
:return: dataframe with the time, kinematic and pressure profiles over the last cycle.
'''
# Run default simulation and compute relevant profiles
logger
.
info
(
'Running mechanical simulation (a =
%s
m, f =
%s
Hz, A =
%s
Pa)'
,
si_format
(
self
.
a
,
1
),
si_format
(
Fdrive
,
1
),
si_format
(
Adrive
,
1
))
data
,
_
=
self
.
simulate
(
Fdrive
,
Adrive
,
Qm
,
Pm_comp_method
=
PmCompMethod
.
direct
)
t
,
Z
,
ng
=
[
data
.
loc
[
-
NPC_FULL
:,
key
]
.
values
for
key
in
[
't'
,
'Z'
,
'ng'
]]
dt
=
(
t
[
-
1
]
-
t
[
0
])
/
(
NPC_FULL
-
1
)
t
-=
t
[
0
]
# Compute pressure cyclic profiles
logger
.
info
(
'Computing pressure cyclic profiles'
)
R
=
self
.
v_curvrad
(
Z
)
U
=
np
.
diff
(
Z
)
/
dt
U
=
np
.
hstack
((
U
,
U
[
-
1
]))
data
=
{
't'
:
t
,
'Z'
:
Z
,
'Cm'
:
self
.
v_Capct
(
Z
),
'P_M'
:
self
.
v_PMavg
(
Z
,
R
,
self
.
surface
(
Z
)),
'P_Q'
:
self
.
Pelec
(
Z
,
Qm
),
'P_{VE}'
:
self
.
PEtot
(
Z
,
R
)
+
self
.
PVleaflet
(
U
,
R
),
'P_V'
:
self
.
PVfluid
(
U
,
R
),
'P_G'
:
self
.
gasmol2Pa
(
ng
,
self
.
volume
(
Z
)),
'P_0'
:
-
np
.
ones
(
Z
.
size
)
*
self
.
P0
}
return
pd
.
DataFrame
(
data
,
columns
=
data
.
keys
())
Event Timeline
Log In to Comment