Source code for vampy.simulation.Womersley

# This file is modified from CBCFLOW

# Copyright (C) 2010-2014 Simula Research Laboratory
#
# This file is part of CBCFLOW.
#
# CBCFLOW is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# CBCFLOW 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with CBCFLOW. If not, see <http://www.gnu.org/licenses/>.

import math

import numpy as np
from dolfin import UserExpression, assemble, Constant, FacetNormal, SpatialCoordinate, Measure
from scipy.integrate import simpson
from scipy.interpolate import UnivariateSpline
from scipy.special import jn


[docs]def x_to_r2(x, c, n): """Compute r**2 from a coordinate x, center point c, and normal vector n. r is defined as the distance from c to x', where x' is the projection of x onto the plane defined by c and n. """ # Steps: rv = x - c rvn = rv.dot(n) rp = rv - rvn * n r2 = rp.dot(rp) return r2
[docs]def compute_boundary_geometry_acrn(mesh, ind, facet_domains): # Some convenient variables assert facet_domains is not None ds = Measure("ds", domain=mesh, subdomain_data=facet_domains) dsi = ds(ind) d = mesh.geometry().dim() x = SpatialCoordinate(mesh) # Compute area of boundary tesselation by integrating 1.0 over all facets A = assemble(Constant(1.0, name="one") * dsi) # assert A > 0.0, "Expecting positive area, probably mismatch between mesh and markers!" if A == 0: return None # Compute barycenter by integrating x components over all facets c = [assemble(x[i] * dsi) / A for i in range(d)] # Compute average normal (assuming boundary is actually flat) n = FacetNormal(mesh) ni = np.array([assemble(n[i] * dsi) for i in range(d)]) n_len = np.sqrt(sum([ni[i] ** 2 for i in range(d)])) # Should always be 1!? normal = ni / n_len # This old estimate is a few % lower because of boundary discretization errors r = np.sqrt(A / math.pi) return A, c, r, normal
[docs]def fourier_coefficients(x, y, T, N): '''From x-array and y-spline and period T, calculate N complex Fourier coefficients.''' omega = 2 * np.pi / T ck = [] ck.append(1 / T * simpson(y(x), x=x)) for n in range(1, N): c = 1 / T * simpson(y(x) * np.exp(-1j * n * omega * x), x=x) # Clamp almost zero real and imag components to zero if 1: cr = c.real ci = c.imag if abs(cr) < 1e-14: cr = 0.0 if abs(ci) < 1e-14: ci = 0.0 c = cr + ci * 1j ck.append(2 * c) return ck
[docs]class WomersleyComponent(UserExpression): # Subclassing the expression class restricts the number of arguments, args # is therefore a dict of arguments. def __init__(self, radius, center, normal, normal_component, period, nu, element, Q=None, V=None): # Spatial args self.radius = radius self.center = center self.normal = normal self.normal_component = normal_component # Temporal args self.period = period if Q is not None: assert V is None, "Cannot provide both Q and V!" self.Qn = Q self.N = len(self.Qn) elif V is not None: self.Vn = V self.N = len(self.Vn) else: raise ValueError("Invalid transient data type, missing argument 'Q' or 'V'.") # Physical args self.nu = nu # Internal state self.t = None self.scale_value = 1.0 # Precomputation self._precompute_bessel_functions() self._all_r_dependent_coeffs = {} super().__init__(element=element) def _precompute_bessel_functions(self): '''Calculate the Bessel functions of the Womersley profile''' self.omega = 2 * np.pi / self.period self.ns = np.arange(1, self.N) # Allocate for 0...N-1 alpha = np.zeros(self.N, dtype=np.complex_) self.beta = np.zeros(self.N, dtype=np.complex_) self.jn0_betas = np.zeros(self.N, dtype=np.complex_) self.jn1_betas = np.zeros(self.N, dtype=np.complex_) # Compute vectorized for 1...N-1 (keeping element 0 in arrays to make indexing work out later) alpha[1:] = self.radius * np.sqrt(self.ns * (self.omega / self.nu)) self.beta[1:] = alpha[1:] * np.sqrt(1j ** 3) self.jn0_betas[1:] = jn(0, self.beta[1:]) self.jn1_betas[1:] = jn(1, self.beta[1:]) def _precompute_r_dependent_coeffs(self, y): pir2 = np.pi * self.radius ** 2 # Compute intermediate terms for womersley function r_dependent_coeffs = np.zeros(self.N, dtype=np.complex_) if hasattr(self, 'Vn'): r_dependent_coeffs[0] = self.Vn[0] * (1 - y ** 2) for n in self.ns: jn0b = self.jn0_betas[n] r_dependent_coeffs[n] = self.Vn[n] * (jn0b - jn(0, self.beta[n] * y)) / (jn0b - 1.0) elif hasattr(self, 'Qn'): r_dependent_coeffs[0] = (2 * self.Qn[0] / pir2) * (1 - y ** 2) for n in self.ns: bn = self.beta[n] j0bn = self.jn0_betas[n] j1bn = self.jn1_betas[n] r_dependent_coeffs[n] = (self.Qn[n] / pir2) * (j0bn - jn(0, bn * y)) / (j0bn - (2.0 / bn) * j1bn) else: raise ValueError("Missing Vn or Qn!") return r_dependent_coeffs def _get_r_dependent_coeffs(self, y): "Look for cached womersley coeffs." key = y r_dependent_coeffs = self._all_r_dependent_coeffs.get(key) if r_dependent_coeffs is None: # Cache miss! Compute coeffs for this coordinate the first time. r_dependent_coeffs = self._precompute_r_dependent_coeffs(y) self._all_r_dependent_coeffs[key] = r_dependent_coeffs return r_dependent_coeffs
[docs] def set_t(self, t): self.t = float(t) % self.period self._expnt = np.exp((self.omega * self.t * 1j) * self.ns)
[docs] def eval(self, value, x): # Compute or get cached complex coefficients that only depend on r y = np.sqrt(x_to_r2(x, self.center, self.normal)) / self.radius coeffs = self._get_r_dependent_coeffs(y) # Multiply complex coefficients for x with complex exponential functions in time wom = (coeffs[0] + np.dot(coeffs[1:], self._expnt)).real # Scale by negative normal direction and scale_value value[0] = -self.normal_component * self.scale_value * wom
[docs]def make_womersley_bcs(t, Q, nu, center, radius, normal, element, coeffstype="Q", N=1001, num_fourier_coefficients=20, Cn=None, **NS_namespace): """ Generate a list of expressions for the components of a Womersley profile. Users can specify either the flow rate or fourier coefficients of the flow rate depending on if Cn is None or not. """ # period is usually the lenght of a cardiac cycle (0.951 s) # If t is a list or numpy array, then period is the maximum value of t # If not, simply assign t as period try: period = max(t) except TypeError: period = t # Compute fourier coefficients of transient profile if Cn is None if Cn is None: # Compute transient profile as interpolation of given coefficients transient_profile = UnivariateSpline(t, Q, s=0, k=1) # Compute fourier coefficients of transient profile timedisc = np.linspace(0, period, N) Cn = fourier_coefficients(timedisc, transient_profile, period, num_fourier_coefficients) # Create Expressions for each direction expressions = [] for ncomp in normal: if coeffstype == "Q": Q = Cn V = None elif coeffstype == "V": V = Cn Q = None expressions.append(WomersleyComponent(radius, center, normal, ncomp, period, nu, element, Q=Q, V=V)) return expressions