Packing and influence on cell motility#

Some time ago Hadrien Mary @Hadim_ asked me if tyssue could reproduce this work:

Cellular traffic jam

Illustration is from Lucy Reading @LucyIkkanda

The relevant model is described in Mapeng Bi et al. (nature physics version ($$))

The master equation is the following:

\[\epsilon = \sum_\alpha \left[ (a_\alpha - 1)^2 + \frac{(p_\alpha - p_0)^2}{r}\right]\]

With a unit prefered area and \(p_\alpha\) the cell perimeter.

Imports#

import numpy as np
import pandas as pd

from tyssue import config, Sheet, PlanarGeometry
from tyssue.solvers import QSSolver

from tyssue.solvers.viscous import EulerSolver
from tyssue.draw import sheet_view

from tyssue.dynamics.factory import model_factory
from tyssue.dynamics import effectors, units

from tyssue.utils import to_nd
from tyssue.utils.testing import effector_tester, model_tester
from tyssue.behaviors import EventManager
from tyssue.behaviors.sheet import basic_events

from tyssue.draw import highlight_faces, create_gif
/tmp/ipykernel_3713/1783676346.py:2: DeprecationWarning: 
Pyarrow will become a required dependency of pandas in the next major release of pandas (pandas 3.0),
(to allow more performant data types, such as the Arrow string type, and better interoperability with other libraries)
but was not found to be installed on your system.
If this would cause problems for you,
please provide us feedback at https://github.com/pandas-dev/pandas/issues/54466
        
  import pandas as pd
collision solver could not be imported You may need to install CGAL and re-install tyssue
This module needs ipyvolume to work.
You can install it with:
$ conda install -c conda-forge ipyvolume

Create a 2D patch of cells#

geom = PlanarGeometry

sheet = Sheet.planar_sheet_2d('jam', 15, 15, 1, 1, noise=0.2)
geom.update_all(sheet)

sheet.remove(sheet.cut_out([[0, 8], [0, 8]]), trim_borders=True)
sheet.sanitize()
geom.scale(sheet, sheet.face_df.area.mean()**-0.5, ['x', 'y'])
geom.update_all(sheet)
sheet.reset_index()
sheet.reset_topo()
fig, ax = sheet_view(sheet, mode="quick")
CGAL-based mesh generation utilities not found, you may need to install CGAL and build from source
C++ extensions are not available for this version
../_images/0f31c2ba4e6b9165705f10ce8003e063414ff1b11c37d5125c1d9bee622dbb55.png

Define the relevant mechanical components#

   
# Adding some gigling
class BrownianMotion(effectors.AbstractEffector):
    
    label = 'Brownian Motion'
    element = 'vert'
    specs = {"settings": {"temperature": 1e-3}}
    
    def energy(eptm):
        T = eptm.settings['temperature']
        return np.ones(eptm.Nv) * T / eptm.Nv
    
    def gradient(eptm):
        T = eptm.settings['temperature']
        scale = T/eptm.edge_df.length.mean()
        
        grad = pd.DataFrame(
            data=np.random.normal(0, scale=scale, size=(eptm.Nv, eptm.dim)),
            index=eptm.vert_df.index,
            columns=['g'+c for c in eptm.coords]
        )
        return grad, None
    
    

Quasistatic gradient descent#

With only the conservative potential terms

model = model_factory(
    [effectors.PerimeterElasticity,
     effectors.FaceAreaElasticity])


model_specs = {
    'face': {
        'area_elasticity': 1.,
        'prefered_area': 1.,
        'perimeter_elasticity': 0.1, # 1/2r in the master equation
        'prefered_perimeter': 3.81,
        },
    'edge': {
        'ux': 0,
        'uy': 0,
        'is_active': 1,
        'line_tension': 0.0
    },
    'settings': {'temperature': 1e-2}
}
    
sheet.update_specs(model_specs, reset=True)

res = QSSolver().find_energy_min(sheet, PlanarGeometry, model)

print(res.message)
CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH

Backup so we can play with parameters

bck = sheet.copy()

Pulling on a face#

    
def tract(eptm, face, pull_axis, value, pull_column='line_tension', face_id=None):
    """Modeling face traction as shrinking apical junctions on the neighouring cells,
    
    As if pseudopods where pulling between the cells
    
    
    """
    
    
    pull_axis = np.asarray(pull_axis)
    edges = eptm.edge_df.query(f'face == {face}')
    verts = edges['srce'].values
    r_ai = edges[["r"+c for c in eptm.coords]].values
    proj = (r_ai * pull_axis[None, :]).sum(axis=1)
    
    pulling_vert = verts[np.argmax(proj)]
    
    v_edges = eptm.edge_df.query(
        f'(srce == {pulling_vert}) & (face != {face})'
    )
    pulling_edges = v_edges[~v_edges['trgt'].isin(verts)].index
    eptm.edge_df[pull_column] = 0

    if pulling_edges.size:
        eptm.edge_df.loc[pulling_edges, pull_column] = value
        
        
default_traction_spec = {
    "face": -1,
    "face_id": -1,
    "pull_axis": [0.1, 0.9],
    "value": 4,
    "pull_column": "line_tension"
}


from tyssue.utils.decorators import face_lookup

@face_lookup
def traction(sheet, manager, **kwargs):
    
    traction_spec = default_traction_spec
    traction_spec.update(**kwargs)
    pulling = tract(sheet, **traction_spec)
    
    manager.append(traction, **traction_spec)
pulled = 2

Simple visualisation#

highlight_faces(sheet.face_df, [pulled,], reset_visible=True)
fig, ax = sheet_view(
    sheet, 
    mode="2D", 
    face={"visible": True},
    edge={"head_width": 0.0}, 
    vert={"visible": False})
fig.set_size_inches(12, 12)
../_images/9fd4c7faedd366aa5b8c3a8afb4b7d00ceae54248338be8c52b6083b78b1b474.png

Model with all the components#

model = model_factory(
    [effectors.PerimeterElasticity,
     BrownianMotion,
     effectors.LineTension,
     effectors.FaceAreaElasticity])

Setup the event manager and the solver#

sheet = bck.copy()


# setting up values for the whole epithelium
model_specs = {
    'face': {
        'area_elasticity': 1.,
        'prefered_area': 1.,
        'perimeter_elasticity': 0.05, # 1/2r in the master equation
        'prefered_perimeter': 3.81,
        "id": sheet.face_df.index
        },
    'vert': {
        "viscosity": 1.0
        },
    'edge': {'ux': 0, 'uy': 0},
    'settings': {
        'temperature': 2e-1,
        "p_4": 10.0,
        "p_5p": 1.0,
        "threshold_length": 2e-2
    }
}

sheet.update_specs(model_specs, reset=True)

# This allows to auomaticaly solve topology changes

manager = EventManager("face", )
manager.append(basic_events.reconnect)
manager.append(traction, face_id=pulled)

# Implicit Euler solver

solver = EulerSolver(
    sheet,
    geom,
    model,
    manager=manager,
    bounds=(
        -sheet.edge_df.length.median()/10,
        sheet.edge_df.length.median()/10
    )
)
manager.update()


sheet.face_df['prefered_perimeter'] = 3.8

Run the solver#

    
solver.solve(tf=120.0, dt=0.1)

highlight_faces(sheet.face_df, [pulled,], reset_visible=True)

fig, ax = sheet_view(
    sheet,
    mode="2D",
    face={"visible": True},
    edge={"head_width": 0.0, "color": sheet.edge_df["line_tension"]},
    vert={"visible": False}
)
fig.set_size_inches(6, 6)
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
/tmp/ipykernel_3713/1283920589.py in ?()
----> 1 solver.solve(tf=120.0, dt=0.1)
      2 
      3 highlight_faces(sheet.face_df, [pulled,], reset_visible=True)
      4 

~/checkouts/readthedocs.org/user_builds/tyssue/conda/latest/lib/python3.12/site-packages/tyssue/solvers/viscous.py in ?(self, tf, dt, on_topo_change, topo_change_args)
    120             pos = pos + dot_r * dt
    121             self.set_pos(pos)
    122             self.prev_t = t
    123             if self.manager is not None:
--> 124                 self.manager.execute(self.eptm)
    125                 self.geom.update_all(self.eptm)
    126                 self.manager.update()
    127 

~/checkouts/readthedocs.org/user_builds/tyssue/conda/latest/lib/python3.12/site-packages/tyssue/behaviors/event_manager.py in ?(self, eptm)
    109                 elem_id = kwargs["elem_id"]
    110             else:
    111                 elem_id = -1
    112             logger.debug(f"{self.clock}, {elem_id}, {behavior.__name__}")
--> 113             behavior(eptm, self, **kwargs)

~/checkouts/readthedocs.org/user_builds/tyssue/conda/latest/lib/python3.12/site-packages/tyssue/utils/decorators.py in ?(*args, **kwargs)
     62         face = sheet.idx_lookup(face_id, "face")
     63         if face is None:
     64             return
     65         kwargs["face"] = face
---> 66         return func(*args, **kwargs)

/tmp/ipykernel_3713/3300601360.py in ?(sheet, manager, **kwargs)
     40 def traction(sheet, manager, **kwargs):
     41 
     42     traction_spec = default_traction_spec
     43     traction_spec.update(**kwargs)
---> 44     pulling = tract(sheet, **traction_spec)
     45 
     46     manager.append(traction, **traction_spec)

/tmp/ipykernel_3713/3300601360.py in ?(eptm, face, pull_axis, value, pull_column, face_id)
     18     v_edges = eptm.edge_df.query(
     19         f'(srce == {pulling_vert}) & (face != {face})'
     20     )
     21     pulling_edges = v_edges[~v_edges['trgt'].isin(verts)].index
---> 22     eptm.edge_df[pull_column] = 0
     23 
     24     if pulling_edges.size:
     25         eptm.edge_df.loc[pulling_edges, pull_column] = value

~/checkouts/readthedocs.org/user_builds/tyssue/conda/latest/lib/python3.12/site-packages/pandas/core/frame.py in ?(self, key, value)
   4295             # Column to set is duplicated
   4296             self._setitem_array([key], value)
   4297         else:
   4298             # set column
-> 4299             self._set_item(key, value)

~/checkouts/readthedocs.org/user_builds/tyssue/conda/latest/lib/python3.12/site-packages/pandas/core/frame.py in ?(self, key, value)
   4522                 if isinstance(existing_piece, DataFrame):
   4523                     value = np.tile(value, (len(existing_piece.columns), 1)).T
   4524                     refs = None
   4525 
-> 4526         self._set_item_mgr(key, value, refs)

~/checkouts/readthedocs.org/user_builds/tyssue/conda/latest/lib/python3.12/site-packages/pandas/core/frame.py in ?(self, key, value, refs)
   4474         except KeyError:
   4475             # This item wasn't present, just insert at end
   4476             self._mgr.insert(len(self._info_axis), key, value, refs)
   4477         else:
-> 4478             self._iset_item_mgr(loc, value, refs=refs)
   4479 
   4480         # check if we are modifying a copy
   4481         # try to set first as we want an invalid

~/checkouts/readthedocs.org/user_builds/tyssue/conda/latest/lib/python3.12/site-packages/pandas/core/frame.py in ?(self, loc, value, inplace, refs)
   4463         refs: BlockValuesRefs | None = None,
   4464     ) -> None:
   4465         # when called from _set_item_mgr loc can be anything returned from get_loc
   4466         self._mgr.iset(loc, value, inplace=inplace, refs=refs)
-> 4467         self._clear_item_cache()

~/checkouts/readthedocs.org/user_builds/tyssue/conda/latest/lib/python3.12/site-packages/pandas/core/frame.py in ?(self)
-> 4611     def _clear_item_cache(self) -> None:
   4612         self._item_cache.clear()

KeyboardInterrupt: 

Define a simple drawing function#

def view(sheet):
    highlight_faces(sheet.face_df, [pulled], reset_visible=True)
    geom.update_all(sheet)
    if sheet.edge_df['line_tension'].max():
        ecolor = sheet.edge_df['line_tension']
    else:
        ecolor = "#aaaaaa"
        
    fig, ax = sheet_view(
        sheet,
        mode="2D",
        face={"visible": True},
        edge={"head_width": 0.0, "color": ecolor, "width": 2},
        vert={"visible": False}
    )
    fig.set_size_inches(8, 8)
    ax.set_xticks([])
    ax.set_yticks([])
    
    return fig, ax

Create a gif of the resulting simulation#

create_gif(solver.history, "demo.gif", draw_func=view, num_frames=120)
solver.history.time_stamps
array([0.000e+00, 1.000e-01, 2.000e-01, ..., 1.198e+02, 1.199e+02,
       1.200e+02])
from IPython.display import Image
Image("demo.gif")
../_images/a8add4a25e51f61a395d8f6ce73cdcb7a2800afc187b0ecabaefb863b8261d85.gif