{ "cells": [ { "cell_type": "markdown", "id": "e7b33c59-8a2b-44eb-9b9c-0b4a12ab3baa", "metadata": {}, "source": [ "Monoclonal Conversion in the Colonic Crypt\n", "===========================================\n", "\n", "This example demonstrates constructing a simulation of cell proliferation and differentiation in a two-dimensional representation of a colonic crypt. The example is based on the model described in \n", "\n", " Osborne, James M., et al. \"Comparing individual-based approaches to modelling the self-organization of multicellular tissues.\" PLoS computational biology 13.2 (2017): e1005387." ] }, { "cell_type": "markdown", "id": "015cb97d-d0f2-47dc-a900-c395ad7d67f2", "metadata": {}, "source": [ "Basic Setup\n", "------------\n", "\n", "Define a quasi-two-dimensional domain for simulation of a crypt as if it were unfolded. The crypt wraps around the $x$-direction and the base of the crypt is near the $+y$ boundary. Cells will have a unit diameter, so define a cutoff of 1.5 cell diameters and declare it as a variable for later reference. " ] }, { "cell_type": "code", "execution_count": null, "id": "b0a6f822-6002-4f57-adaa-51f7af345698", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import tissue_forge as tf\n", "\n", "r_max = 1.5\n", "\n", "tf.init(dim=[15, 13, 6], cells=[5, 4, 2], dt=0.005, bc={'y': 'free_slip', 'z': 'free_slip'}, cutoff=r_max)" ] }, { "cell_type": "markdown", "id": "a0040190-fece-4cea-aa1a-2824f865ccc7", "metadata": {}, "source": [ "Particle Type\n", "--------------\n", "\n", "Declare a cell type with unit diameter and overdamped dynamics. Implement two-dimensional displacement and use the Overlapping Sphere potential to implement volume exclusion and intercellular adhesion. " ] }, { "cell_type": "code", "execution_count": null, "id": "da8e3fe0-e83e-4bc9-86a2-3199bd28b5fa", "metadata": {}, "outputs": [], "source": [ "# Define cell type\n", "class CellType(tf.ParticleTypeSpec):\n", "\n", " radius = 0.5\n", " dynamics = tf.Overdamped\n", "\n", "cell_type = CellType.get()\n", "cell_type.frozen_z = True\n", "\n", "# Add volume exclusion and adhesion\n", "tf.bind.types(tf.Potential.overlapping_sphere(mu=50.0, kc=5.0, min=1E-3, max=r_max), cell_type, cell_type)" ] }, { "cell_type": "markdown", "id": "1c8f5f5c-1293-4b2e-8e22-191a9bbcde67", "metadata": {}, "source": [ "Agent Based Model Data\n", "-----------------------\n", "\n", "The agent based model implements the cell cycle for each cell. A cell is in one of the cell cycle phases G1, S, G2, or M for a phase-specific period, where the period of the G1 phase is stochastic for each cell. Each cell of the initial population is assigned a unique clonal identification, and the clone identification is copied to progeny during cell division. To visualize the clonal identification, each cell is assigned a color that corresponds to its clonal identification.\n", "\n", "* Define a dictionary that stores the clonal identification, cell cycle periods, current phase, and a timer for each cell.\n", "* Define an integer label for each phase in order of the cell cycle: G1, S, G2, M, beginning with 0 and incrementing.\n", "* Define a function ``clone_ids`` that returns the current clonal identifications as a list.\n", "* Define a function ``assign_entry`` that adds a new entry to the agent based model data dictionary and uses an optional clonal identification argument. When a clonal identification is not passed, the function should assign a value of one greater than the current maximum. The function should declare phase periods and initialize the entry with a G1 phase. \n", "* Declare a function ``random_style`` that returns a new Tissue Forge ``Style`` object with a randomly selected color." ] }, { "cell_type": "code", "execution_count": null, "id": "b9188d4d-70b1-4db4-beed-31b702a67850", "metadata": {}, "outputs": [], "source": [ "pop_dict = dict()\n", "# id: clone_id, periods, phase, timer\n", "\n", "phase_g1 = 0\n", "phase_s = 1\n", "phase_g2 = 2\n", "phase_m = 3\n", "\n", "def clone_ids():\n", " return list({v[0]: None for v in pop_dict.values()}.keys())\n", "\n", "def assign_entry(_pid: int, clone_id: int = None):\n", " per = {phase_g1: max(0.01, np.random.normal(2.0, 1.0)),\n", " phase_s: 5.0,\n", " phase_g2: 4.0,\n", " phase_m: 1.0}\n", " if clone_id is None:\n", " cids = clone_ids()\n", " clone_id = 0 if not cids else max(cids) + 1\n", " pop_dict[_pid] = [clone_id, per, phase_g1, float(per[phase_g1])]\n", "\n", "def random_style():\n", " return tf.rendering.Style(tf.FVector3(np.random.random(3)))" ] }, { "cell_type": "markdown", "id": "85fefca2-0504-4624-a6a7-7cd7be8a20f1", "metadata": {}, "source": [ "Cell Creation and Destruction\n", "------------------------------\n", "\n", "Agent based model data must be managed along with creating and destroying cells. When creating a cell (e.g., during division), a new entry must be added to the agent based model data dictionary. Likewise when destroying a cell (e.g., when removing at the base of the crypt), the entry in the agent based model data dictionary that corresponds to the destroyed cell must be removed. \n", "\n", "* Define a function ``create`` that creates a new cell at a given position and adds an entry to the agent based model data dictionary. The function should handle optional arguments of a given particle style and clonal identification. When a style is not provided, the function should create a new one using ``random_style``. The function should use ``assign_entry`` to add new data to the agent based model data dictionary.\n", "* Define a function ``destroy`` that destroys a cell according to a given particle id and removes its entry in the agent based model data dictionary. " ] }, { "cell_type": "code", "execution_count": null, "id": "c9cef118-6e3a-48cf-8b99-7551acfde9c5", "metadata": {}, "outputs": [], "source": [ "def create(_position: tf.FVector3, style: tf.rendering.Style = None, clone_id: int = None):\n", " ph = cell_type(position=_position, velocity=tf.FVector3(0))\n", "\n", " if style is None:\n", " style = random_style()\n", " ph.style = style\n", "\n", " assign_entry(ph.id, clone_id=clone_id)\n", " return ph.id\n", "\n", "def destroy(_pid: int):\n", " pop_dict.pop(_pid)\n", " return tf.ParticleHandle(_pid).destroy()" ] }, { "cell_type": "markdown", "id": "8811b447-175a-45b8-af30-d0aac87a669a", "metadata": {}, "source": [ "Crypt Base\n", "-----------\n", "\n", "When a cell reaches a sufficiently high position along the $y$-direction, it is considered as having reached the base of the crypt. A cell that has reached the base of the crypt is removed from the simulation. \n", "\n", "Define a function ``slough`` that removes all cells with a position $y$ component above a threshold, and implement the function as an event that is called at every simulation step. " ] }, { "cell_type": "code", "execution_count": null, "id": "3b4f0c1f-63bf-417e-ac48-3faf2ad59880", "metadata": {}, "outputs": [], "source": [ "def slough():\n", " to_rem = []\n", " for ph in cell_type:\n", " if ph.position[1] > 12.0:\n", " to_rem.append(ph.id)\n", " [destroy(pid) for pid in to_rem]\n", "\n", "\n", "tf.event.on_time(0.1 * tf.Universe.dt, invoke_method=lambda e: slough())" ] }, { "cell_type": "markdown", "id": "9b9951a9-dbb7-406b-b269-ee7b470b7d24", "metadata": {}, "source": [ "Crypt Cellular Dynamics\n", "------------------------\n", "\n", "The cell cycle dynamics of each cell advances when a cell is sufficiently far from the base of the crypt, and when the cell is not too compressed. Each cell advances to the next phase of the cell cycle when its current timer has elapsed, and a cell divides when it progresses from the M phase to the G1 phase.\n", "\n", "* Define a variable ``r_cl`` that describes the ratio of minimum effective area of a cell with a cell cycle that can advance to the target area of the cell (according to its diameter).\n", "* Define a function ``area_eff`` that calculates the effective area of a cell given its id. The effective area for the $i$th cell is calculated from the effective radius $R_i^{eff}$, which, for equally sized cells of radius $R_i$, neighbors $N_i$ and distance $\\textbf{r}_{ij}$ to each $j$th neighbor, is defined as\n", "\n", "$$\n", "R_i^{eff} = \\frac{1}{6}\\left[ \\sum_{j \\in N_i \\left(t\\right)} \\frac{||\\textbf{r}_{ij}||}{2} + R_i \\left(6 - \\textrm{size}\\left(N_i \\left(t\\right) \\right) \\right) \\right].\n", "$$\n", "\n", "* Define a function ``abm`` that implements the following agent based model for each cell:\n", " * The cell cycle does not advance when the cell is too close to the base of the crypt.\n", " * The cell cycle does not advance when the effective area of the cell is too small as measured by ``r_cl``.\n", " * The current timer of the cell cycle decreases according to the simulation time step when the cell cycle advances.\n", " * When the current timer has elapsed, the cell cycle phase increments and a new timer is started according to the new phase.\n", " * When the cell cycle phase increments from the M phase, the cell divides and the new phase of the dividing cell and progeny is G1.\n", " * When a cell divides, its progeny is assigned the same clonal identification and style.\n", "* Implement ``abm`` as an event that is called at every simulation step." ] }, { "cell_type": "code", "execution_count": null, "id": "79457652-97e7-4b56-9b4d-1ef0a79a5ab7", "metadata": {}, "outputs": [], "source": [ "r_cl = 0.7\n", "\n", "def area_eff(_pid: int):\n", " ph = tf.ParticleHandle(_pid)\n", " nbs = ph.neighbors(distance=r_max - 2 * cell_type.radius)\n", " res = sum([0.5 * ph.relativePosition(nb.position).length() for nb in nbs])\n", " r_eff = (res + cell_type.radius * (6.0 - len(nbs))) / 6.0\n", " return np.pi * r_eff * r_eff\n", "\n", "def abm():\n", " for ph in [ph for ph in cell_type]:\n", " # Check whether below vertical threshold\n", " if ph.position[1] > 6.0:\n", " continue\n", " # Check whether contact-inhibited\n", " area_eff_0 = np.pi * cell_type.radius * cell_type.radius * r_cl\n", " if area_eff(ph.id) < area_eff_0:\n", " continue\n", " # Do cell cycle\n", " clone_id, per, phase, timer = pop_dict[ph.id]\n", " timer -= tf.Universe.dt\n", " if timer <= 0:\n", " phase += 1\n", " if phase > phase_m:\n", " disp_ang = np.random.random() * np.pi * 2\n", " disp = tf.FVector3(np.sin(disp_ang), np.cos(disp_ang), 0) * ph.radius * 0.5\n", " create(ph.position + disp, style=ph.style, clone_id=clone_id)\n", " ph.position = ph.position - disp\n", " phase = phase_g1\n", " timer = float(per[phase])\n", " pop_dict[ph.id] = [clone_id, per, phase, timer]\n", "\n", "\n", "tf.event.on_time(period=0.1 * tf.Universe.dt, invoke_method=lambda e: abm())" ] }, { "cell_type": "markdown", "id": "c5d3bb34-d3a1-4925-b694-de66e93a5b72", "metadata": {}, "source": [ "Particle Construction\n", "----------------------\n", "\n", "Initialize a cell population in a hexagonal arrangment. For each created cell, assign a new entry in the agent based model data dictionary using ``assign_entry`` and random style using ``random_style``." ] }, { "cell_type": "code", "execution_count": null, "id": "6de7370f-f628-486e-b35b-9721f4a4a396", "metadata": {}, "outputs": [], "source": [ "uc = tf.lattice.hex2d(0.99, cell_type)\n", "n = [15, 6, 1]\n", "cell_half_size = (uc.a1 + uc.a2 + uc.a3) / 2\n", "extents = n[0] * uc.a1 + n[1] * uc.a2 + n[2] * uc.a3\n", "offset = tf.FVector3(0.0, -2.0, 0.0)\n", "origin = tf.Universe.center + offset - extents / 2 + cell_half_size\n", "tf.lattice.create_lattice(uc, n, origin=origin)\n", "for ph in cell_type:\n", " assign_entry(ph.id)\n", " ph.style = random_style()" ] }, { "cell_type": "code", "execution_count": null, "id": "58a01fb1-d38a-4404-986d-0cb3ae7d3c58", "metadata": {}, "outputs": [], "source": [ "tf.system.camera_view_top()\n", "tf.show()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.8.17" } }, "nbformat": 4, "nbformat_minor": 5 }