Vertex Model Solver
Note
To use the Tissue Forge vertex model solver in research, please cite the following:
Sego et al. (2023). “General, open-source vertex modeling in biological applications using Tissue Forge.” Scientific Reports, 13(1), 17886.
The vertex model solver implements the vertex model formalism for modeling biological objects as topologically connected vertices in a mesh. Each vertex is constructed from a Tissue Forge particle. A set of vertices can define a surface for two-dimensional modeling, and a set of surfaces can define a body for three-dimensional modeling. Dynamical models are deployed in simulation by creating actors that implement various property and/or process models and binding them to individual surfaces and/or bodies or their type.
The vertex model solver acts as a subengine along with the rest of Tissue Forge. During each simulation step, the vertex model solver translates vertex model specification and the configuration of a mesh into forces on underlying Tissue Forge particles for integration using overdamped mechanics. After Tissue Forge updates the trajectory of all particles (here vertices), the vertex model solver implements topological transformations, called quality operations, that allow a mesh to adapt its topology according to various kinetic and kinematic criteria. Finally, the vertex model solver uses Tissue Forge module support for mesh rendering and integrating with logging and file I/O support. The vertex model solver fully supports custom model specification and simulation events through the Tissue Forge event system, including mesh object creation, manipulation and destruction.
Each surface is defined by an ordered set of vertices, and each body is defined by an unordered set of connected surfaces. A surface is defined by at least three vertices, and a body is defined by at least four surfaces. The order of the vertices of a surface defines the orientation of the surface normal using the right-hand rule. Each vertex can define an arbitrary number of surfaces, and each surface can, at most, define two bodies. The vertex model solver provides no explicit representation of edges, as they are implicitly defined by the ordered sets of surface vertices. Surfaces are weakly constrained to be flat and convex.
The vertex model solver resolves all vertex model properties and processes, whether they be defined for a surface or body, into properties and processes on the vertices. For example, when defining a force that acts uniformely on the volume of a body, the vertex model solver produces equivalent forces that act on the vertices of the body. As such, while bodies and surfaces naturally have properties like volume and area, respectively, vertices also have measures of mass, area, volume, etc., that depend on the objects that they define. The vertex model solver accomplishes such relationships up and down the hierarchy of objects in a mesh by considering each surface as consisting of a triangulation according to its vertices, where each vertex contributes to two triangles, and all triangles share a point at the centroid of the surface. Each vertex is assumed to contribute to half of the area of each triangle that it defines, and so the area of a vertex is equal to half of the area of all triangles that it defines. Likewise, each vertex is assumed to contribute a volume to a body proportionally to the area of the body that the vertex defines, and so the volume of a vertex is equal to its contribution to all bodies that it defines.
The vertex model solver provides flexible specification and deployment of models representing
object properties and processes through objects called actors.
Like the Tissue Forge potential, an actor is an object that
implements a model, and that can be applied to mesh objects by instance or by type.
Some actors are only applicable to a particular type of object
(e.g., NormalStress
for surfaces), and some actors are applicable to both
two- and three-dimensional objects (e.g., SurfaceAreaConstraint
).
An arbitrary set of actors can be applied to any combination of instances or types by
binding in the typical way, except that
successive calls to binding for the vertex model solver are additive.
While applying two potentials to a particle type requires binding one potential object
that is the sum of the two potentials, two actors can instead be bound to a mesh object
by simply binding each actor in successive calls.
Currently, the vertex model solver provides the following actors that operate on surfaces,
Adhesion
: Models adhesion between pairs of surface instances by typeConvexPolygonConstraint
(automatically applied): Imposes that surfaces are convexEdgeTension
: Models tension between connected verticesFlatSurfaceConstraint
(automatically applied): Imposes that surfaces are flatNormalStress
: Models a stress acting on a surface along its normalPerimeterConstraint
: Imposes a perimeter constraintSurfaceAreaConstraint
: Imposes a surface area constraintSurfaceTraction
: Models a traction force
The vertex model solver provides the following actors that operate on bodies,
Adhesion
: Models adhesion between pairs of body instances by typeBodyForce
: Imposes a body forceSurfaceAreaConstraint
: Imposes a surface area constraintVolumeConstraint
: Imposes a volume constraint
For details on each actor, refer to the Vertex Model Solver API Reference.
The vertex model solver implements quality operations to allow for an automatically dynamic mesh topology. In general, quality operations enforce rules that promote the quality of a mesh, whether kinematically (e.g., no surfaces that are too small) or kinetically (e.g., inserting a vertex to better distribute forces). Current development of the Tissue Forge vertex model solver has primarily targeted quality operations for two-dimensional simulation, and dynamic three-dimensional vertex model meshes are presently an active field of research.
Currently the vertex model solver supports the following quality operations,
Body demote: a body becomes a vertex when its volume is less than a threshold
Edge split: a vertex becomes two vertices when the length of the resulting edge will increase
Surface demote: a surface becomes a vertex when its area is less than a threshold
Vertex insert: a vertex is inserted between two vertices of a surface when it penetrates the perimeter of the surface
Vertex merge: two vertices are merged into one vertex when separated by a distance less than a threshold
In Python, all functionality of the vertex model solver module can be accessed from the
solver
module (models::vertex
namespace in C++),
from tissue_forge.models.vertex import solver as tfv
In C++, the module can be included when building from source with
#include <models/vertex/solver/tfVertexSolver.h>
Before using any functionality of the module and after initialization of Tissue Forge,
the module method init
must be called,
import tissue_forge as tf
tf.init()
tfv.init()
Like the Tissue Forge particle type, the vertex model solver
provides type-based specification of surfaces
and bodies and their creation through
surface and body types, respectively.
Each surface and body is an instance of a surface and body type, respectively,
and each surface and body type is a subclass of SurfaceType
and BodyType
,
respectively.
Surface and body types are dynamic model definitions that can be used to accomplish various
tasks like creating or retrieving instances. In Python, a surface type can easily be created
by deriving from SurfaceTypeSpec
(and likewise from BodyTypeSpec
for a body type),
which has class attributes that, when set, automatically
bind corresponding actors
to the type during registration,
class CellType(tfv.SurfaceTypeSpec):
"""
A surface type that models two-dimensional cells with a surface area constraint
"""
# Surface area constraint
surface_area_val = 1.0 # Constraint value
surface_area_lam = 1.0 # Target area value
A new surface type can be simultaneously instantiated and registered
with the vertex model solver in Python with the class method SurfaceTypeSpec.get
,
cell_type: tfv.SurfaceType = CellType.get() # Get the type
The vertex model solver provides a number of ways to create mesh objects.
SurfaceType
instances can act as a factory that creates
Surface
instances of their type when passed a list of positions in
proper winding order for where to create the vertices that define the surface,
# Create a triangular cell
cell_0 = cell_type(positions=[
tf.Universe.center,
tf.Universe.center + tf.FVector3(1, 0, 0),
tf.Universe.center + tf.FVector3(0, 1, 0)
])
Mesh objects provide methods to modify both the topology and shape of objects in the mesh,
# Make the cell a square by inserting a new vertex
# Get the vertices along the +x and +y directions
v1 = cell_0.find_vertex(dir=tf.FVector3(1, 0, 0))
v2 = cell_0.find_vertex(dir=tf.FVector3(0, 1, 0))
# Create and insert a new vertex
# The new vertex is implicitly added to the surface
v3 = tfv.Vertex.insert_c(pos=tf.Universe.center + tf.FVector3(1, 1, 0), v1=v2, v2=v2)
Mesh objects also provide their own methods for creating new mesh objects, like splitting a surface into two surfaces,
# Split the square cell into two triangular cells
cell_1 = cell_0.split(v1, v2)
BodyType
instances can also act as factories for creating Body
instances, and can be conveniently specified in Python by deriving from BodyTypeSpec
,
class CellBodyType(tfv.BodyTypeSpec):
"""A cell type with a volume constraint"""
# Volume constraint
volume_lam = 1.0 # Constraint value
volume_val = 1.0 # Target volume value
cell_type: tfv.BodyType = CellBodyType.get() # Get the type
A Body can be constructed by passing a set of connected surfaces to a body type. The created body will be an instance of the type with properties that correspond to the shape and topology of the surfaces,
class CellSurfaceType(tfv.SurfaceTypeSpec):
"""A cell surface type"""
cell_surface_type: tfv.SurfaceType = CellSurfaceType.get() # Get the surface type
# Create the surfaces for a cubic body of dimension 1
box_dim = 1
# Define the relative position of each vertex
relative_positions = [
tf.FVector3(0, 0, 0), # Vertex 0
tf.FVector3(0, box_dim, 0), # Vertex 1
tf.FVector3(box_dim, box_dim, 0), # Vertex 2
tf.FVector3(box_dim, 0, 0), # Vertex 3
tf.FVector3(0, 0, box_dim), # Vertex 4
tf.FVector3(box_dim, 0, box_dim), # Vertex 5
tf.FVector3(box_dim, box_dim, box_dim), # Vertex 6
tf.FVector3(0, box_dim, box_dim) # Vertex 7
]
# Define a vertex winding order so that all normals face outward
surface_vertex_indices = [
(0, 3, 2, 1), # Bottom
(4, 5, 6, 7), # Top
(3, 2, 6, 5), # Right
(0, 4, 7, 1), # Left
(0, 3, 5, 4), # Front
(1, 2, 6, 7) # Back
]
o = tf.Universe.center # Choose an origin for Vertex 0
# Create the surfaces
surfs_0 = []
for svi in surface_vertex_indices:
surfs_0.append(cell_surface_type(positions=[o + relative_positions[i] for i in svi]))
tfv.Surface.sew(surfs=surfs_0) # Connect the surfaces according to nearby vertices
body_0 = cell_type(surfs_0) # Create the body
Methods that modify the topology of the mesh are also respected by bodies, such as making two bodies in contact by making them share a surface,
# Create a bigger cubic body and make it in contact with the first body
o = tf.Universe.center + tf.FVector3(box_dim + 0.1, 0, 0) # Choose a new origin for Vertex 0
# Create the surfaces
surfs_1 = []
for svi in surface_vertex_indices:
surfs_1.append(cell_surface_type(positions=[o + relative_positions[i] * 1.25 for i in svi]))
tfv.Surface.sew(surfs=surfs_1) # Connect the surfaces
body_1 = cell_type(surfs_1) # Create another body
# Find the nearest surfaces and merge them into a shared surface
surf_merge_0 = body_0.find_surface(tf.FVector3(1, 0, 0))
surf_merge_1 = body_1.find_surface(tf.FVector3(-1, 0, 0))
surf_merge_0.merge(surf_merge_1, [0.5] * len(surf_merge_0.vertices)) # Merge into a shared surface
The vertex model solver provides generators to quickly construct simple two-dimensional and three-dimensional meshes of mesh objects. For example, a sheet of quadrilaterals can be generated with all surfaces appropriately connected, which can be subsequently transformed to build a cylinder,
import tissue_forge as tf
from tissue_forge.models.vertex import solver as tfv
import numpy as np
# Do initializations
tf.init(dim=[15, 10, 10])
tfv.init()
# Declare cylinder parameters
cyl_radius = 2 # Radius of the cylinder
cyl_len = 8 # Length of the cylinder
num_radius = 100 # No. cells along the perimeter of the cylinder
num_len = 40 # No. cells along the length of the cylinder
cyl_circum = 2 * np.pi * cyl_radius # Cylinder circumference
dim_circum = cyl_circum / num_radius # Cell length along the perimeter of the cylinder
dim_len = cyl_len / num_len # Cell length along the length of the cylinder
# Create and get a surface type
class CellType(tfv.SurfaceTypeSpec):
"""A 2D cell type"""
cell_type: tfv.SurfaceType = CellType.get() # Get the type
# Create a sheet of quadrilateral surfaces
start_pos = (tf.Universe.dim - tf.FVector3(cyl_circum, cyl_len, 0)) * 0.5
surfs = tfv.create_quad_mesh(cell_type, start_pos, num_radius, num_len, dim_circum, dim_len)
The vertices of each surface can be directly manipulated to roll the sheet into a cylinder,
# Roll the sheet into a cylinder
# First, gather all vertices in an array that reflects the sheet topology
verts = []
for i in range(num_radius):
verts.append([])
[verts[-1].append(surfs[i][j].find_vertex(dir=tf.FVector3(-1, -1, 0))) for j in range(num_len)]
verts[-1].append(surfs[i][-1].find_vertex(dir=tf.FVector3(-1, 1, 0)))
verts.append([])
[verts[-1].append(surfs[-1][j].find_vertex(dir=tf.FVector3(1, -1, 0)))]
verts[-1].append(surfs[-1][-1].find_vertex(dir=tf.FVector3(1, 1, 0)))
# Transform vertex coordinates into cylindrical coordinates
for i, vi in enumerate(verts):
ang = -2 * np.pi * i / num_radius
x_coord = tf.Universe.center[0] + cyl_radius * np.sin(ang)
z_coord = tf.Universe.center[2] + cyl_radius * np.cos(ang)
for vj in vi:
vj.position = tf.FVector3(x_coord, vj.position[1], z_coord)
# Sew the seam
[tfv.Surface.sew(s1=surfs[0][j], s2=surfs[-1][j]) for j in range(num_len)]
The vertex model solver also supports constructing mesh objects using data in
3D model formats.
A body can be constructed from a Mesh3DF
instance, and
a surface can be constructed from a Face3DF
instance
or list of them,
# Import a two-dimensional mesh from a 3DF
obj_struct: tf.io.ThreeDFStructure = tf.io.fromFile3DF('my_blender_mesh2d.obj')
# Build a two-dimensional cell for each imported face
cell_type(face_data=obj_struct.faces)
# Verify each newly created vertex position
for s in cell_type:
for v in s.vertices:
print(f'Vertex {v.id}: {v.position}')
Note
Constructing surfaces from a list of imported faces can be performed by either purging (default)
or keeping the topology of the imported faces. When purging the imported topology, new vertices are
created for each imported face, and the topology can be reconstructed with Surface.sew
.
Reconstructing the topology can be slow depending on the details of the imported mesh but can act
as a safeguard against errors in mesh topology that are hard to detect.
Constructing surfaces while keeping the topology of imported faces can be enabled by setting the
keyword argument safe_face_data
to False
.
Mesh objects provide methods for conveniently finding other objects according to their neighborhood and topology. For example, a vertex that defines a surface can be retrieved from the surface by looking in a particular direction with respect to the centroid of the surface,
tf.init(dim=[3, 3, 2])
tfv.init()
class MySurfaceType(tfv.SurfaceTypeSpec):
pass
surface_type = MySurfaceType.get()
# Place a unit square starting at the origin
surf_0 = surface_type(positions=[
tf.FVector3(0, 0, 0),
tf.FVector3(1, 0, 0),
tf.FVector3(1, 1, 0),
tf.FVector3(0, 1, 0)
])
# Get the top-right vertex
vert_topright = surf_0.find_vertex(dir=tf.FVector3(1, 1, 0))
print('Vertex position:', vert_topright.position) # Returns [1, 1, 0]
Mesh objects also provide properties on demand according to their topology, like the area of a vertex (calculated as the area contribution to all surfaces defined by the vertex),
print('Vertex surface area:', vert_topright.area) # Prints 0.25 (1/4 of one unit square)
# Place a second, adjacent surface
vert_botright = surf_0.find_vertex(dir=tf.FVector3(1, -1, 0))
new_vert0 = tfv.Vertex.create(tf.FVector3(2, 0, 0))
new_vert1 = tfv.Vertex.create(tf.FVector3(2, 1, 0))
surf_1 = surface_type([vert_botright, new_vert0, new_vert1, vert_topright])
print('Vertex surface area:', vert_topright.area) # Prints 0.5 (1/4 of two unit squares)
While in Python the SurfaceTypeSpec
and BodyTypeSpec
classes provide
class attributes that indicate actors for their types,
in general, actors can be instantiated and bound to
surfaces, surface types, bodies and body types at any time during simulation.
For example, a sheet of two-dimensional, hexagonal cells can all be modeled with a
surface area constraint according to their type, but also with a few cells
that also experience tension in their edges,
# Do imports and initializations
import tissue_forge as tf
from tissue_forge.models.vertex import solver as tfv
tf.init()
tfv.init()
# Build a 5x4 sheet of hexagonal cells centered at the origin
# Declare a cell type
hex_radius = 1.0
class CellType(tfv.SurfaceTypeSpec):
"""A two-dimensional cell type"""
# Surface area constraint
surface_area_val = 3 / 2 * np.sqrt(3) * hex_radius * hex_radius
surface_area_lam = 1.0
cell_type = CellType.get() # Get the type
# Build the sheet
num_surfs_x, num_surfs_y = 5, 4
start_pos_x = (num_surfs_x + 1) / 2
start_pos_y = (num_surfs_y + 3) * np.cos(np.pi / 3) * np.cos(np.pi / 6)
start_pos = tf.Universe.center - tf.FVector3(start_pos_x, start_pos_y, 0) * hex_radius
surfs = tfv.create_hex2d_mesh(cell_type, start_pos, num_surfs_x, num_surfs_y, hex_radius)
# Bind edge tension to a few surfaces
edge_tension = tfv.EdgeTension(lam=1.5)
[tfv.bind.surface(edge_tension, s) for s in surfs[2][1:3]]
Actors typically have public members that allow dynamic control of their model parameters during simulation, changes to which are instantaneously applied.
# Collapse the surfaces
edge_tension.lam = 2.0
Adhesion is one of a special type of actor that uses two surface or body types in its definition.
An Adhesion
instance can be bound to a pair of types using bind.types
, or
in Python using the class attribute adhesion
, which is a dictionary specifying an adhesion value
with other types by name,
class StickyCellType(tfv.SurfaceTypeSpec):
"""A two-dimensional cell type that aggregates by adhesion"""
adhesion = {
'StickyCellType': -1, # Present in this simulation
'LonelyCellType': 10 # Not present in this simulation, not a problem
}
sticky_cell_type = StickyCellType.get()
tfv.SurfaceTypeSpec.bind_adhesion([StickyCellType]) # Binds for whichever types are registered
Many modeling features from Tissue Forge can also be applied in vertex models. Since each vertex of the vertex model solver has an underlying particle, forces can be applied to all vertices by type.
# Give all vertices some noise
vertex_type = tfv.MeshParticleType_get() # Get the vertex particle type
rf = tf.Force.random(0.1, 0) # Create some noise
tf.bind.force(rf, vertex_type) # Bind noise to the vertex particles
Access to the underlying particle type of all vertices also allows easy setup for two-dimensional simulation,
# Do 2D simulation in the x-y plane
vertex_type.frozen_z = True
Likewise, the underlying particle of each vertex can be accessed to create bonds.
# Make an edge of a surface contractile
surf: tfv.SurfaceHandle
vertex_0, vertex_1 = surf.vertices[0:2]
contractile_pot = tf.Potential.linear(k=1)
tf.Bond.create(contractile_pot, vertex_0.particle(), vertex_1.particle())
When two vertices are merged during mesh quality operations, any bonds connected to one, but not both, vertices is automatically transferred to the remaining vertex. Bonds connected to both vertices are destroyed.
To support vertex modeling at a wide range of resolutions, all mesh quality operations can be customized to adjust how topological transformations occur, or completely disabled.
# Modify some details of mesh quality operations
mesh: tfv.Mesh = tfv.MeshSolver.get_mesh() # The mesh has the quality object
mesh.quality.vertex_merge_distance = 0.1 # Distance below which two vertices merge
mesh.quality.edge_split_distance = 0.2 # Distance between two vertices after a vertex splits
# Or, disable all operations
mesh.quality = None
The vertex model solver provides access to a stream of information about what mesh quality operations occur during a simulation step. A log of operations can be generated by forwarding vertex model solver logging to the Tissue Forge Logger. Vertex model solver logging can be forwarded at a specified logging level, enabled and disabled at any time during simulation.
tf.Logger.enableConsoleLogging(tf.Logger.ERROR)
tfv.Logger.setForwardLogging(True)
tfv.Logger.setLogLevel(tf.Logger.INFORMATION)
Note
The vertex model solver module currently does not support GPU acceleration.