import math
import warnings
import numpy as np
import pandas as pd
from scipy import interpolate
from scipy.spatial import Voronoi
from .. import config
from ..core.monolayer import Monolayer
from ..core.objects import Epithelium, get_prev_edges
from ..core.sheet import Sheet, get_outer_sheet
from ..geometry.bulk_geometry import ClosedMonolayerGeometry
from ..geometry.sheet_geometry import (
ClosedSheetGeometry,
EllipsoidGeometry,
SheetGeometry,
)
from ..geometry.utils import update_spherical
from ..topology import type1_transition
from .from_voronoi import from_3d_voronoi
try:
from .cpp import mesh_generation
except ImportError:
print(
"CGAL-based mesh generation utilities not found, you may need to install"
" CGAL and build from source"
)
mesh_generation = None
from ..utils import single_cell, swap_apico_basal
from .modifiers import extrude
[docs]class AnnularSheet(Sheet):
"""2D annular model of a cylinder-like monolayer.
Provides syntactic sugar to access the apical, basal and
lateral segments of the epithlium
"""
[docs] def segment_index(self, segment, element):
df = getattr(self, "{}_df".format(element))
return df[df["segment"] == segment].index
@property
def lateral_edges(self):
return self.segment_index("lateral", "edge")
@property
def apical_edges(self):
return self.segment_index("apical", "edge")
@property
def basal_edges(self):
return self.segment_index("basal", "edge")
@property
def apical_verts(self):
return self.segment_index("apical", "vert")
@property
def basal_verts(self):
return self.segment_index("basal", "vert")
[docs] def reset_topo(self):
super().reset_topo()
self.edge_df["prev"] = get_prev_edges(self)
[docs]def generate_ring(Nf, R_in, R_out, R_vit=None, apical="in"):
"""Generates a 2D tyssue object aranged in a ring of Nf tetragonal cells
with inner diameter R_in and outer diameter R_out
Parameters
----------
Nf : int
The number of cells in the tissue
R_in : float
The inner ring diameter
R_out : float
The outer ring diameter
R_vit : float
The vitelline membrane diameter
(a non strechable membrane around the annulus)
apical : str {'in' | 'out'}
The side of the apical surface
if "in", the apical surface is inside the annulus, facing
the lumen as in an organoid; if 'out': the apical side is facing the
exterior of the tissue, as in an embryo
Returns
-------
eptm : :class:`AnnularSheet`
2D annular tissue. The `R_in` and `R_out` parameters
are stored in the class `settings` attribute.
"""
specs = config.geometry.planar_spec()
specs["settings"] = specs.get("settings", {})
specs["settings"]["R_in"] = R_in
specs["settings"]["R_out"] = R_out
specs["settings"]["R_vit"] = R_vit
Ne = Nf * 4
Nv = Nf * 2
vert_df = pd.DataFrame(
index=pd.Index(range(Nv), name="vert"),
columns=specs["vert"].keys(),
dtype=float,
)
edge_df = pd.DataFrame(
index=pd.Index(range(Ne), name="edge"),
columns=specs["edge"].keys(),
dtype=float,
)
face_df = pd.DataFrame(
index=pd.Index(range(Nf), name="face"),
columns=specs["face"].keys(),
dtype=float,
)
inner_edges = np.array(
[
[f0, v0, v1]
for f0, v0, v1 in zip(range(Nf), range(Nf), np.roll(range(Nf), -1))
]
)
outer_edges = np.zeros_like(inner_edges)
outer_edges[:, 0] = inner_edges[:, 0]
outer_edges[:, 1] = inner_edges[:, 2] + Nf
outer_edges[:, 2] = inner_edges[:, 1] + Nf
left_spokes = np.zeros_like(inner_edges)
left_spokes[:, 0] = inner_edges[:, 0]
left_spokes[:, 1] = outer_edges[:, 2]
left_spokes[:, 2] = inner_edges[:, 1]
right_spokes = np.zeros_like(inner_edges)
right_spokes[:, 0] = inner_edges[:, 0]
right_spokes[:, 1] = inner_edges[:, 2]
right_spokes[:, 2] = outer_edges[:, 1]
edges = np.concatenate([inner_edges, outer_edges, left_spokes, right_spokes])
edge_df[["face", "srce", "trgt"]] = edges
edge_df[["face", "srce", "trgt"]] = edge_df[["face", "srce", "trgt"]].astype(int)
thetas = np.linspace(0, 2 * np.pi, Nf, endpoint=False)
thetas += thetas[1] / 2
thetas = thetas[::-1]
# Setting vertices position (turning clockwise for correct orientation)
vert_df.loc[range(Nf), "x"] = R_in * np.cos(thetas)
vert_df.loc[range(Nf), "y"] = R_in * np.sin(thetas)
vert_df.loc[range(Nf, 2 * Nf), "x"] = R_out * np.cos(thetas)
vert_df.loc[range(Nf, 2 * Nf), "y"] = R_out * np.sin(thetas)
vert_df["segment"] = "basal"
edge_df["segment"] = "basal"
if apical == "out":
edge_df.loc[range(Nf, 2 * Nf), "segment"] = "apical"
vert_df.loc[range(Nf, 2 * Nf), "segment"] = "apical"
elif apical == "in":
edge_df.loc[range(Nf), "segment"] = "apical"
vert_df.loc[range(Nf), "segment"] = "apical"
else:
raise ValueError(
"apical argument not understood, "
f"should be either 'in' or 'out', got {apical}"
)
edge_df.loc[range(2 * Nf, 4 * Nf), "segment"] = "lateral"
datasets = {"vert": vert_df, "edge": edge_df, "face": face_df}
ring = AnnularSheet("ring", datasets, specs, coords=["x", "y"])
ring.reset_topo()
return ring
"""
Ellipsoid 2.5D sheet
"""
[docs]def ellipse_rho(theta, a, b):
return ((a * math.sin(theta)) ** 2 + (b * math.cos(theta)) ** 2) ** 0.5
[docs]def get_ellipsoid_centers(a, b, c, n_zs, pos_err=0.0, phase_err=0.0):
"""
Creates hexagonaly organized points on the surface of an ellipsoid
Parameters
----------
a, b, c: float
ellipsoid radii along the x, y and z axes, respectively
i.e the ellipsoid boounding box will be
`[[-a, a], [-b, b], [-c, c]]`
n_zs : float
number of cells on the z axis, typical
pos_err : float, default 0.
normaly distributed noise of std. dev. pos_err is added
to the centers positions
phase_err : float, default 0.
normaly distributed noise of std. dev. phase_err is added
to the centers angle ϕ
"""
if b != a:
warnings.warn(
"Different half axes length along x and y"
" axes are not supported at the moment"
)
dist = c / n_zs
theta = -np.pi / 2
thetas = []
while theta < np.pi / 2:
theta = theta + dist / ellipse_rho(theta, a, c)
thetas.append(theta)
thetas = np.array(thetas).clip(-np.pi / 2, np.pi / 2)
zs = c * np.sin(thetas)
# np.linspace(-c, c, n_zs, endpoint=False)
# thetas = np.arcsin(zs/c)
av_rhos = (a + b) * np.cos(thetas) / 2
n_cells = np.ceil(av_rhos / dist).astype(int)
phis = np.concatenate(
[
np.linspace(-np.pi, np.pi, nc, endpoint=False) + (np.pi / nc) * (i % 2)
for i, nc in enumerate(n_cells)
]
)
if phase_err > 0:
phis += np.random.normal(scale=phase_err * np.pi, size=phis.shape)
zs = np.concatenate([z * np.ones(nc) for z, nc in zip(zs, n_cells)])
thetas = np.concatenate([theta * np.ones(nc) for theta, nc in zip(thetas, n_cells)])
xs = a * np.cos(thetas) * np.cos(phis)
ys = b * np.cos(thetas) * np.sin(phis)
if pos_err > 0.0:
xs += np.random.normal(scale=pos_err, size=thetas.shape)
ys += np.random.normal(scale=pos_err, size=thetas.shape)
zs += np.random.normal(scale=pos_err, size=thetas.shape)
centers = np.vstack([xs, ys, zs]).T
return centers
[docs]def ellipsoid_sheet(a, b, c, n_zs, **kwargs):
"""Creates an ellipsoidal apical mesh.
Parameters
----------
a, b, c : floats
Size of the ellipsoid half axes in
the x, y, and z directions, respectively
n_zs : int
The (approximate) number of faces along the z axis.
kwargs are passed to `get_ellipsoid_centers`
Returns
-------
eptm : a :class:`Epithelium` object
The mesh returned is an `Epithelium` and not a simpler `Sheet`
so that a unique cell data can hold information on the
whole volume of the ellipsoid.
"""
centers = get_ellipsoid_centers(a, b, c, n_zs, **kwargs)
eptm = sheet_from_cell_centers(centers)
eptm.settings["abc"] = [a, b, c]
EllipsoidGeometry.update_all(eptm)
return eptm
[docs]def spherical_sheet(radius, Nf, Lloyd_relax=False, **kwargs):
"""Returns a spherical sheet with the given radius and (approximately)
the given number of cells
"""
centers = np.array(mesh_generation.make_spherical(Nf))
eptm = sheet_from_cell_centers(centers, **kwargs)
rhos = (eptm.vert_df[eptm.coords] ** 2).sum(axis=1).mean()
ClosedSheetGeometry.scale(eptm, radius / rhos, eptm.coords)
ClosedSheetGeometry.update_all(eptm)
if Lloyd_relax:
eptm = Lloyd_relaxation(
eptm, ClosedSheetGeometry, steps=100, update_method=update_on_sphere
)
return eptm
[docs]def spherical_monolayer(R_in, R_out, Nc, apical="out", Lloyd_relax=False):
"""Returns a spherical monolayer with the given inner and
outer radii, and approximately the gieven number of cells.
The `apical` argument can be 'in' out 'out' to specify wether
the apical face of the cells faces inward or outward, reespectively.
"""
sheet = spherical_sheet(R_in, Nc, Lloyd_relax=Lloyd_relax)
delta_R = R_out - R_in
mono = Monolayer("mono", extrude(sheet.datasets, method="normals", scale=-delta_R))
if apical == "out":
swap_apico_basal(mono)
else:
mono.settings["lumen_side"] = "apical"
ClosedMonolayerGeometry.update_all(mono)
return mono
[docs]def sheet_from_cell_centers(points, noise=0, interp_s=1e-4):
"""Returns a Sheet object from the Voronoï tessalation
of the cell centers.
The strategy is to project the points on a sphere, get the Voronoï
tessalation on this sphere and reproject the vertices on the
original (implicit) surface through linear interpolation of the cell centers.
Works for relatively smooth surfaces (at the very minimum star convex).
Parameters
----------
points : np.ndarray of shape (Nf, 3)
the x, y, z coordinates of the cell centers
noise : float, default 0.0
addiditve normal noise stdev
interp_s : float, default 1e-4
interpolation smoothing factor (might need to set higher)
Returns
-------
sheet : a :class:`Sheet` object with Nf faces
"""
points = points.copy()
if noise:
points += np.random.normal(0, scale=noise, size=points.shape)
points -= points.mean(axis=0)
bbox = np.ptp(points, axis=0)
points /= bbox
rhos = np.linalg.norm(points, axis=1)
thetas = np.arcsin(points[:, 2] / rhos)
phis = np.arctan2(points[:, 0], points[:, 1])
sphere_rad = rhos.max() * 1.1
points_sphere = np.vstack(
(
sphere_rad * np.cos(thetas) * np.cos(phis),
sphere_rad * np.cos(thetas) * np.sin(phis),
sphere_rad * np.sin(thetas),
)
).T
points_sphere = np.concatenate(([[0, 0, 0]], points_sphere))
vor3D = Voronoi(points_sphere)
dsets = from_3d_voronoi(vor3D)
eptm_ = Epithelium("v", dsets)
eptm_ = single_cell(eptm_, 0)
eptm = get_outer_sheet(eptm_)
eptm.reset_index()
eptm.reset_topo()
eptm.vert_df["rho"] = np.linalg.norm(eptm.vert_df[eptm.coords], axis=1)
mean_rho = eptm.vert_df["rho"].mean()
SheetGeometry.scale(eptm, sphere_rad / mean_rho, ["x", "y", "z"])
SheetGeometry.update_all(eptm)
eptm.face_df["phi"] = np.arctan2(eptm.face_df.y, eptm.face_df.x)
eptm.face_df["rho"] = np.linalg.norm(eptm.face_df[["x", "y", "z"]], axis=1)
eptm.face_df["theta"] = np.arcsin(eptm.face_df.z / eptm.face_df["rho"])
_itrp = interpolate.SmoothSphereBivariateSpline(
thetas + np.pi / 2, phis + np.pi, rhos, s=interp_s
)
eptm.face_df["rho"] = _itrp(
eptm.face_df["theta"] + np.pi / 2, eptm.face_df["phi"] + np.pi, grid=False
)
eptm.face_df["x"] = eptm.face_df.eval("rho * cos(theta) * cos(phi)")
eptm.face_df["y"] = eptm.face_df.eval("rho * cos(theta) * sin(phi)")
eptm.face_df["z"] = eptm.face_df.eval("rho * sin(theta)")
eptm.edge_df[["fx", "fy", "fz"]] = eptm.upcast_face(eptm.face_df[["x", "y", "z"]])
eptm.vert_df[["x", "y", "z"]] = eptm.edge_df.groupby("srce")[
["fx", "fy", "fz"]
].mean()
for i, c in enumerate("xyz"):
eptm.vert_df[c] *= bbox[i]
SheetGeometry.update_all(eptm)
eptm.sanitize(trim_borders=True)
eptm.reset_index()
eptm.reset_topo()
SheetGeometry.update_all(eptm)
null_length = eptm.edge_df.query("length == 0")
while null_length.shape[0]:
type1_transition(eptm, null_length.index[0])
SheetGeometry.update_all(eptm)
null_length = eptm.edge_df.query("length == 0")
return eptm
[docs]def Lloyd_relaxation(sheet, geom, steps=10, coords=None, update_method=None):
"""Performs Lloyd relaxation on the sheet."""
if coords is None:
coords = sheet.coords
geom.update_all(sheet)
mean_area0 = sheet.face_df.area.mean()
for _ in range(steps):
if update_method:
centers = update_method(sheet)
else:
centers = sheet.face_df[coords].to_numpy()
sheet_ = sheet_from_cell_centers(centers, interp_s=1e-3)
geom.update_all(sheet)
mean_area = sheet_.face_df.area.mean()
delta = (mean_area0 / mean_area) ** 0.5
geom.scale(sheet_, delta, coords)
sheet = sheet_
geom.update_all(sheet)
return sheet
[docs]def update_on_sphere(sheet):
update_spherical(sheet)
thetas = sheet.face_df["theta"].to_numpy()
phis = sheet.face_df["phi"].to_numpy()
centers = np.array(
[
np.sin(thetas) * np.cos(phis),
np.sin(thetas) * np.sin(phis),
np.cos(thetas),
]
).T
return centers