import numba
import numpy as np
_float_1D_array = numba.types.Array(numba.float64, 1, "C")
_float_2D_array = numba.types.Array(numba.float64, 2, "C")
[docs]
@numba.njit(_float_1D_array(_float_1D_array))
def normalize(u: np.ndarray) -> np.ndarray:
"""L2-Normalize vector with
Parameters
----------
u : np.ndarray
Vector
Returns
-------
np.ndarray
Normalized vector
"""
u_norm = np.linalg.norm(u)
if u_norm > 0.0:
return u / u_norm
return u
[docs]
@numba.njit(_float_1D_array(_float_1D_array, _float_1D_array, numba.float64))
def slerp(q1: np.ndarray, q2: np.ndarray, t: float) -> np.ndarray:
"""Spherical linear interpolation from `q1` to `q2` at `t`
Parameters
----------
q1 : np.ndarray
Source quaternion
q2 : np.ndarray
Target quaternion
t : float
Interpolation factor, between 0 and 1
Returns
-------
np.ndarray
The spherical linear interpolation between `q1` and `q2` at `t`
"""
dot = q1.dot(q2)
q3 = q2
if dot < 0.0:
dot = -dot
q3 = -q2
if dot < 0.9999:
angle = np.arccos(dot)
a = np.sin(angle * (1 - t)) / np.sin(angle)
b = np.sin(angle * t) / np.sin(angle)
return a * q1 + b * q3
# Angle is close to zero - do linear interpolation
return q1 * (1 - t) + q3 * t
@numba.njit(_float_1D_array(_float_2D_array))
def rot2quat(Q: np.ndarray) -> np.ndarray:
t = Q[0][0] + Q[1][1] + Q[2][2]
if t > 0:
r = np.sqrt(1.0 + t)
s = 0.5 / r
a = 0.5 * r
b = (Q[2][1] - Q[1][2]) * s
c = (Q[0][2] - Q[2][0]) * s
d = (Q[1][0] - Q[0][1]) * s
elif Q[0][0] > Q[1][1] and Q[0][0] > Q[2][2]:
s = 2 * np.sqrt(1.0 + Q[0][0] - Q[1][1] - Q[2][2])
a = (Q[2][1] - Q[1][2]) / s
b = 0.25 * s
c = (Q[0][1] + Q[1][0]) / s
d = (Q[0][2] + Q[2][0]) / s
elif Q[1][1] > Q[0][0] and Q[1][1] > Q[2][2]:
s = 2 * np.sqrt(1.0 + Q[1][1] - Q[0][0] - Q[2][2])
a = (Q[0][2] - Q[2][0]) / s
b = (Q[0][1] + Q[1][0]) / s
c = 0.25 * s
d = (Q[1][2] + Q[2][1]) / s
else:
s = 2 * np.sqrt(1.0 + Q[2][2] - Q[0][0] - Q[1][1])
a = (Q[1][0] - Q[0][1]) / s
b = (Q[0][2] + Q[2][0]) / s
c = (Q[1][2] + Q[2][1]) / s
d = 0.25 * s
return normalize(np.array([a, b, c, d]))
[docs]
@numba.njit(_float_2D_array(_float_1D_array))
def quat2rot(q: np.ndarray) -> np.ndarray:
"""Convert quaternion to rotation matrix
Parameters
----------
q : np.ndarray
Quaternion
Returns
-------
np.ndarray
Rotation matrix
"""
Q = np.zeros((3, 3))
w = q[0]
x = q[1]
y = q[2]
z = q[3]
x2 = x * x
y2 = y * y
z2 = z * z
wx = w * x
wy = w * y
wz = w * z
xy = x * y
xz = x * z
yz = y * z
Q[0][0] = 1.0 - 2.0 * y2 - 2.0 * z2
Q[1][0] = 2.0 * xy + 2.0 * wz
Q[2][0] = 2.0 * xz - 2.0 * wy
Q[0][1] = 2.0 * xy - 2.0 * wz
Q[1][1] = 1.0 - 2.0 * x2 - 2.0 * z2
Q[2][1] = 2.0 * yz + 2.0 * wx
Q[0][2] = 2.0 * xz + 2.0 * wy
Q[1][2] = 2.0 * yz - 2.0 * wx
Q[2][2] = 1.0 - 2.0 * x2 - 2.0 * y2
return Q
[docs]
@numba.njit(_float_2D_array(_float_2D_array, _float_2D_array, numba.float64))
def bislerp(
Qa: np.ndarray,
Qb: np.ndarray,
t: float,
) -> np.ndarray:
r"""
Linear interpolation of two orthogonal matrices.
Assume that :math:`Q_a` and :math:`Q_b` refers to
timepoint :math:`0` and :math:`1` respectively.
Using spherical linear interpolation (slerp) find the
orthogonal matrix at timepoint :math:`t`.
"""
# if ~Qa.any() and ~Qb.any():
# return np.zeros((3, 3))
# if ~Qa.any():
# return Qb
# if ~Qb.any():
# return Qa
# tol = 1e-5
# if t < tol:
# return Qa
# if t > 1 - tol:
# return Qb
qa = rot2quat(Qa)
qb = rot2quat(Qb)
a = qa[0]
b = qa[1]
c = qa[2]
d = qa[3]
# i_qa = np.array([-b, a, -d, c])
# j_qa = np.array([-c, d, a, -b])
# k_qa = np.array([-d, -c, b, a])
qa_i = np.array([-b, a, d, -c])
qa_j = np.array([-c, -d, a, b])
qa_k = np.array([-d, c, -b, a])
# quat_array = [
# qa,
# i_qa,
# j_qa,
# k_qa,
# ]
quat_array = [
qa,
qa_i,
qa_j,
qa_k,
]
qm = quat_array[0]
max_dot = abs(qm.dot(qb))
for v in quat_array[1:]:
dot = abs(v.dot(qb))
if dot > max_dot:
max_dot = dot
qm = v
qm_slerp = slerp(qm, qb, t)
return quat2rot(qm_slerp)
[docs]
@numba.njit(_float_2D_array(_float_1D_array, _float_1D_array))
def axis(u: np.ndarray, v: np.ndarray) -> np.ndarray:
r"""
Construct the fiber orientation coordinate system.
Given two vectors :math:`u` and :math:`v` in the apicobasal
and transmural direction respectively return a matrix that
represents an orthonormal basis in the circumferential (first),
apicobasal (second) and transmural (third) direction.
"""
e1 = normalize(u)
e2 = v - (e1.dot(v)) * e1
e2 = normalize(e2)
e0 = np.cross(e1, e2)
e0 = normalize(e0)
Q = np.zeros((3, 3))
Q[:, 0] = e0
Q[:, 1] = e1
Q[:, 2] = e2
return Q
[docs]
@numba.njit(_float_2D_array(_float_2D_array, numba.float64, numba.float64))
def orient(Q: np.ndarray, alpha: float, beta: float) -> np.ndarray:
r"""
Define the orthotropic fiber orientations.
Given a coordinate system :math:`Q`, in the canonical
basis, rotate it in order to align with the fiber, sheet
and sheet-normal axis determine by the angles :math:`\alpha`
(fiber) and :math:`\beta` (sheets).
"""
A = np.zeros((3, 3))
A[0, :] = [np.cos(np.radians(alpha)), -np.sin(np.radians(alpha)), 0]
A[1, :] = [np.sin(np.radians(alpha)), np.cos(np.radians(alpha)), 0]
A[2, :] = [0, 0, 1]
B = np.zeros((3, 3))
B[0, :] = [1, 0, 0]
B[1, :] = [0, np.cos(np.radians(beta)), np.sin(np.radians(beta))]
B[2, :] = [0, -np.sin(np.radians(beta)), np.cos(np.radians(beta))]
C = np.dot(Q.real, A).dot(B)
return C
@numba.njit
def compute_fiber_sheet_system(
f0,
s0,
n0,
xdofs,
ydofs,
zdofs,
sdofs,
lv_scalar,
rv_scalar,
epi_scalar,
lv_rv_scalar,
lv_gradient,
rv_gradient,
epi_gradient,
apex_gradient,
marker_scalar,
alpha_endo_lv,
alpha_epi_lv,
alpha_endo_rv,
alpha_epi_rv,
alpha_endo_sept,
alpha_epi_sept,
beta_endo_lv,
beta_epi_lv,
beta_endo_rv,
beta_epi_rv,
beta_endo_sept,
beta_epi_sept,
epi_only=False,
):
grad_lv = np.zeros(3)
grad_rv = np.zeros(3)
grad_epi = np.zeros(3)
grad_ab = np.zeros(3)
for i in range(len(xdofs)):
lv = lv_scalar[sdofs[i]]
rv = rv_scalar[sdofs[i]]
epi = epi_scalar[sdofs[i]]
lv_rv = lv_rv_scalar[sdofs[i]]
grad_lv[0] = lv_gradient[xdofs[i]]
grad_lv[1] = lv_gradient[ydofs[i]]
grad_lv[2] = lv_gradient[zdofs[i]]
grad_rv[0] = rv_gradient[xdofs[i]]
grad_rv[1] = rv_gradient[ydofs[i]]
grad_rv[2] = rv_gradient[zdofs[i]]
grad_epi[0] = epi_gradient[xdofs[i]]
grad_epi[1] = epi_gradient[ydofs[i]]
grad_epi[2] = epi_gradient[zdofs[i]]
grad_ab[0] = apex_gradient[xdofs[i]]
grad_ab[1] = apex_gradient[ydofs[i]]
grad_ab[2] = apex_gradient[zdofs[i]]
if epi > 0.5:
# We are not in the septum
if lv_rv >= 0.5:
# We are in the LV region
marker_scalar[sdofs[i]] = 1
alpha_endo = alpha_endo_lv
beta_endo = beta_endo_lv
alpha_epi = alpha_epi_lv
beta_epi = beta_epi_lv
else:
# We are in the RV region
marker_scalar[sdofs[i]] = 2
alpha_endo = alpha_endo_rv
beta_endo = beta_endo_rv
alpha_epi = alpha_epi_rv
beta_epi = beta_epi_rv
else:
if lv_rv >= 0.9:
# We are in the LV region
marker_scalar[sdofs[i]] = 1
alpha_endo = alpha_endo_lv
beta_endo = beta_endo_lv
alpha_epi = alpha_epi_lv
beta_epi = beta_epi_lv
elif lv_rv <= 0.1:
# We are in the RV region
marker_scalar[sdofs[i]] = 2
alpha_endo = alpha_endo_rv
beta_endo = beta_endo_rv
alpha_epi = alpha_epi_rv
beta_epi = beta_epi_rv
else:
# We are in the septum
marker_scalar[sdofs[i]] = 3
alpha_endo = alpha_endo_sept
beta_endo = beta_endo_sept
alpha_epi = alpha_epi_sept
beta_epi = beta_epi_sept
if lv + rv < 1e-12:
depth = 0.5
else:
depth = rv / (lv + rv)
alpha_s = alpha_endo * (1 - depth) - alpha_endo * depth
alpha_w = alpha_endo * (1 - epi) + alpha_epi * epi
beta_s = beta_endo * (1 - depth) - beta_endo * depth
beta_w = beta_endo * (1 - epi) + beta_epi * epi
Q_lv = axis(grad_ab, -grad_lv)
Q_lv = orient(Q_lv, alpha_s, beta_s)
Q_rv = axis(grad_ab, grad_rv)
Q_rv = orient(Q_rv, alpha_s, beta_s)
Q_epi = axis(grad_ab, grad_epi)
Q_epi = orient(Q_epi, alpha_w, beta_w)
if epi_only:
Q_fiber = Q_epi
else:
Q_endo = bislerp(Q_lv, Q_rv, depth)
Q_fiber = bislerp(Q_endo, Q_epi, epi)
f0[xdofs[i]] = Q_fiber[0, 0]
f0[ydofs[i]] = Q_fiber[1, 0]
f0[zdofs[i]] = Q_fiber[2, 0]
s0[xdofs[i]] = Q_fiber[0, 1]
s0[ydofs[i]] = Q_fiber[1, 1]
s0[zdofs[i]] = Q_fiber[2, 1]
n0[xdofs[i]] = Q_fiber[0, 2]
n0[ydofs[i]] = Q_fiber[1, 2]
n0[zdofs[i]] = Q_fiber[2, 2]