def damage_hyphae(self, n_det, n_kill, time, health, grid, fungus: FungusCellList, iron):
        for i in self.alive(self.cell_data['granule_count'] > 0):
            cell = self[i]
            vox = grid.get_voxel(cell['point'])

            # Moore neighborhood, but order partially randomized. Closest to furthest order, but
            # the order of any set of points of equal distance is random
            neighborhood = list(itertools.product(tuple(range(-1 * n_det, n_det + 1)), repeat=3))
            shuffle(neighborhood)
            neighborhood = sorted(neighborhood, key=lambda v: v[0] ** 2 + v[1] ** 2 + v[2] ** 2)

            for dx, dy, dz in neighborhood:
                zi = vox.z + dz
                yj = vox.y + dy
                xk = vox.x + dx
                if grid.is_valid_voxel(Voxel(x=xk, y=yj, z=zi)):
                    index_arr = fungus.get_cells_in_voxel(Voxel(x=xk, y=yj, z=zi))
                    if len(index_arr) > 0:
                        iron[zi, yj, xk] = 0
                    for index in index_arr:
                        if (
                            fungus[index]['form'] == FungusCellData.Form.HYPHAE
                            and cell['granule_count'] > 0
                        ):
                            fungus[index]['health'] -= health * (time / n_kill)
                            cell['granule_count'] -= 1
                            cell['status'] = NeutrophilCellData.Status.GRANULATING
                        elif cell['granule_count'] == 0:
                            cell['status'] = NeutrophilCellData.Status.NONGRANULATING
                            break
    def produce_cytokines(self, m_det, m_n, grid, fungus: FungusCellList,
                          cyto):
        for i in self.alive():
            vox = grid.get_voxel(self[i]['point'])

            hyphae_count = 0

            # Moore neighborhood
            neighborhood = tuple(
                itertools.product(tuple(range(-1 * m_det, m_det + 1)),
                                  repeat=3))

            for dx, dy, dz in neighborhood:
                zi = vox.z + dz
                yj = vox.y + dy
                xk = vox.x + dx
                if grid.is_valid_voxel(Voxel(x=xk, y=yj, z=zi)):
                    index_arr = fungus.get_cells_in_voxel(
                        Voxel(x=xk, y=yj, z=zi))
                    for index in index_arr:
                        if fungus[index]['form'] == FungusCellData.Form.HYPHAE:
                            hyphae_count += 1

            cyto[vox.z, vox.y,
                 vox.x] = cyto[vox.z, vox.y, vox.x] + m_n * hyphae_count
    def internalize_conidia(self, m_det, max_spores, p_in, grid,
                            fungus: FungusCellList):
        for i in self.alive():
            cell = self[i]
            vox = grid.get_voxel(cell['point'])

            # Moore neighborhood, but order partially randomized. Closest to furthest order, but
            # the order of any set of points of equal distance is random
            neighborhood = list(
                itertools.product(tuple(range(-1 * m_det, m_det + 1)),
                                  repeat=3))
            shuffle(neighborhood)
            neighborhood = sorted(neighborhood,
                                  key=lambda v: v[0]**2 + v[1]**2 + v[2]**2)

            for dx, dy, dz in neighborhood:
                zi = vox.z + dz
                yj = vox.y + dy
                xk = vox.x + dx
                if grid.is_valid_voxel(Voxel(x=xk, y=yj, z=zi)):
                    index_arr = fungus.get_cells_in_voxel(
                        Voxel(x=xk, y=yj, z=zi))
                    for index in index_arr:
                        if (fungus[index]['form']
                                == FungusCellData.Form.CONIDIA
                                and not fungus[index]['internalized']
                                and p_in > rg.random()):
                            fungus[index]['internalized'] = True
                            self.append_to_phagosome(i, index, max_spores)
Exemple #4
0
    def cytokine_update(self, s_det, h_det, cyto_rate, m_cyto, n_cyto, fungus,
                        grid):
        for i in self.alive():
            vox = grid.get_voxel(self[i]['point'])

            # spores
            spore_count = 0

            # Moore neighborhood
            neighborhood = tuple(
                itertools.product(tuple(range(-1 * s_det, s_det + 1)),
                                  repeat=3))

            for dx, dy, dz in neighborhood:
                zi = vox.z + dz
                yj = vox.y + dy
                xk = vox.x + dx
                if grid.is_valid_voxel(Voxel(x=xk, y=yj, z=zi)):
                    index_arr = fungus.get_cells_in_voxel(
                        Voxel(x=xk, y=yj, z=zi))
                    for index in index_arr:
                        if fungus[index][
                                'form'] == FungusCellData.Form.CONIDIA and fungus[
                                    index]['status'] in [
                                        FungusCellData.Status.SWOLLEN,
                                        FungusCellData.Status.GERMINATED,
                                    ]:
                            spore_count += 1

            # hyphae_count
            hyphae_count = 0

            # Moore neighborhood
            neighborhood = tuple(
                itertools.product(tuple(range(-1 * h_det, h_det + 1)),
                                  repeat=3))

            for dx, dy, dz in neighborhood:
                zi = vox.z + dz
                yj = vox.y + dy
                xk = vox.x + dx
                if grid.is_valid_voxel(Voxel(x=xk, y=yj, z=zi)):
                    index_arr = fungus.get_cells_in_voxel(
                        Voxel(x=xk, y=yj, z=zi))
                    for index in index_arr:
                        if fungus[index]['form'] == FungusCellData.Form.HYPHAE:
                            hyphae_count += 1

            m_cyto[vox.z, vox.y, vox.x] += cyto_rate * spore_count
            n_cyto[vox.z, vox.y,
                   vox.x] += cyto_rate * (spore_count + hyphae_count)
Exemple #5
0
def periodic_discrete_laplacian(
        grid: RectangularGrid,
        mask: np.ndarray,
        dtype: np.dtype = _dtype_float64) -> csr_matrix:
    """Return a laplacian operator with periodic boundary conditions.

    This computes a standard laplacian operator as a scipy linear operator, except it is
    restricted to a grid mask.  The use case for this is to compute surface diffusion
    on a gridded variable.  The mask is generated from a category on the lung_tissue
    variable.
    """
    graph_shape = len(grid), len(grid)
    z_extent, y_extent, x_extent = grid.shape
    laplacian = dok_matrix(graph_shape, dtype=dtype)

    delta_z = grid.delta(0)
    delta_y = grid.delta(1)
    delta_x = grid.delta(2)

    for k, j, i in zip(*(mask).nonzero()):
        voxel = Voxel(x=i, y=j, z=k)
        voxel_index = grid.get_flattened_index(voxel)

        for offset in [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0),
                       (0, 0, -1), (0, 0, 1)]:
            # voxel coordinate displacements
            dk, dj, di = offset

            # find the neighbor for periodic boundary conditions
            neighbor: Voxel = Voxel(x=(i + di) % x_extent,
                                    y=(j + dj) % y_extent,
                                    z=(k + dk) % z_extent)

            # but maybe it isn't in the mask (i.e. air)
            if not mask[neighbor.z, neighbor.y, neighbor.x]:
                continue

            neighbor_index = grid.get_flattened_index(neighbor)

            # continuous space displacements
            dx = delta_x[k, j, i] * di
            dy = delta_y[k, j, i] * dj
            dz = delta_z[k, j, i] * dk
            inverse_distance2 = 1 / (dx * dx + dy * dy + dz * dz
                                     )  # units: 1/(µm^2)

            laplacian[voxel_index, voxel_index] -= inverse_distance2
            laplacian[voxel_index, neighbor_index] += inverse_distance2

    return laplacian.tocsr()
Exemple #6
0
    def get_adjacent_voxels(self,
                            voxel: Voxel,
                            corners: bool = False) -> Iterator[Voxel]:
        """Return an iterator over all neighbors of a given voxel.

        Parameters
        ----------
        voxel : simulation.coordinates.Voxel
            The target voxel
        corners : bool
            Include voxels sharing corners and edges in addition to those sharing sides.

        """
        dirs: Iterable[Tuple[int, int, int]]
        if corners:
            dirs = filter(lambda x: x != (0, 0, 0),
                          product([-1, 0, 1], [-1, 0, 1], [-1, 0, 1]))
        else:
            dirs = [(-1, 0, 0), (1, 0, 0), (0, -1, 0), (0, 1, 0), (0, 0, -1),
                    (0, 0, 1)]

        for di, dj, dk in dirs:
            i = voxel.x + di
            j = voxel.y + dj
            k = voxel.z + dk
            neighbor = Voxel(x=i, y=j, z=k)

            if self.is_valid_voxel(neighbor):
                yield neighbor
Exemple #7
0
    def voxel_from_flattened_index(self, index: int) -> 'Voxel':
        """Create a Voxel from flattened index of the grid.

        This is a convenience method that wraps numpy.unravel_index.
        """
        z, y, x = np.unravel_index(index, self.shape)
        return Voxel(x=float(x), y=float(y), z=float(z))
Exemple #8
0
    def iron_uptake(self, iron: np.ndarray, iron_max: float, iron_min: float,
                    iron_absorb: float):
        """Absorb iron from external environment."""
        cells = self.cell_data
        for vox_index in np.argwhere(iron > iron_min):
            vox = Voxel(x=vox_index[2], y=vox_index[1], z=vox_index[0])

            cells_here = self.get_cells_in_voxel(vox)

            indices = []
            for index in cells_here:
                if (cells[index]['form'] == FungusCellData.Form.HYPHAE.value
                        and np.invert(cells[index]['internalized'])
                        and cells[index]['iron'] < iron_max):
                    indices.append(index)

            if len(indices) > 0:
                iron_split = iron_absorb * (iron[vox.z, vox.y, vox.x] /
                                            len(indices))
                for cell_index in indices:
                    cells[cell_index]['iron'] += iron_split
                    if cells[cell_index]['iron'] > iron_max:
                        cells[cell_index]['iron'] = iron_max

                iron[vox.z, vox.y,
                     vox.x] = (1 - iron_absorb) * iron[vox.z, vox.y, vox.x]
    def advance(self, state: State, previous_time: float) -> State:
        """Advance the state by a single time step."""
        from nlisim.modules.macrophage import MacrophageState

        hepcidin: HepcidinState = state.hepcidin
        macrophage: MacrophageState = state.macrophage
        voxel_volume: float = state.voxel_volume

        # interaction with macrophages
        activated_voxels = zip(*np.where(
            activation_function(
                x=hepcidin.grid,
                k_d=hepcidin.k_d,
                h=self.time_step / 60,  # units: (min/step) / (min/hour)
                volume=voxel_volume,
                b=1,
            ) > rg.random(size=hepcidin.grid.shape)))
        for z, y, x in activated_voxels:
            for macrophage_cell_index in macrophage.cells.get_cells_in_voxel(
                    Voxel(x=x, y=y, z=z)):
                macrophage_cell = macrophage.cells[macrophage_cell_index]
                macrophage_cell['fpn'] = False
                macrophage_cell['fpn_iteration'] = 0

        # Degrading Hepcidin is done by the "liver"

        # hepcidin does not diffuse

        return state
    def move(self, rec_r, grid, cyto, tissue, fungus: FungusCellList):
        for cell_index in self.alive():
            cell = self[cell_index]
            cell_voxel = grid.get_voxel(cell['point'])

            valid_voxel_offsets = []
            above_threshold_voxel_offsets = []

            # iterate over nearby voxels, recording the cytokine levels
            for dx, dy, dz in itertools.product((-1, 0, 1), repeat=3):
                zi = cell_voxel.z + dz
                yj = cell_voxel.y + dy
                xk = cell_voxel.x + dx
                if grid.is_valid_voxel(Voxel(x=xk, y=yj, z=zi)):
                    if tissue[zi, yj, xk] != TissueType.AIR.value:
                        valid_voxel_offsets.append((dx, dy, dz))
                        if cyto[zi, yj, xk] >= rec_r:
                            above_threshold_voxel_offsets.append(
                                (cyto[zi, yj, xk], (dx, dy, dz)))

            # pick a target for the move
            if len(above_threshold_voxel_offsets) > 0:
                # shuffle + sort (with _only_ 0-key, not lexicographic as tuples) ensures
                # randomization when there are equal top cytokine levels
                # note that numpy's shuffle will complain about ragged arrays
                shuffle(above_threshold_voxel_offsets)
                above_threshold_voxel_offsets = sorted(
                    above_threshold_voxel_offsets,
                    key=lambda x: x[0],
                    reverse=True)
                _, target_voxel_offset = above_threshold_voxel_offsets[0]
            elif len(valid_voxel_offsets) > 0:
                target_voxel_offset = choice(valid_voxel_offsets)
            else:
                raise AssertionError(
                    'This cell has no valid voxel to move to, including the one that it is in!'
                )

            # Some nonsense here, b/c jump is happening at the voxel level, not the point level
            starting_cell_point = Point(x=cell['point'][2],
                                        y=cell['point'][1],
                                        z=cell['point'][0])
            starting_cell_voxel = grid.get_voxel(starting_cell_point)
            ending_cell_voxel = grid.get_voxel(
                Point(
                    x=grid.x[cell_voxel.x + target_voxel_offset[0]],
                    y=grid.y[cell_voxel.y + target_voxel_offset[1]],
                    z=grid.z[cell_voxel.z + target_voxel_offset[2]],
                ))
            ending_cell_point = (starting_cell_point +
                                 grid.get_voxel_center(ending_cell_voxel) -
                                 grid.get_voxel_center(starting_cell_voxel))

            cell['point'] = ending_cell_point
            self.update_voxel_index([cell_index])

            for i in range(0, self.len_phagosome(cell_index)):
                f_index = cell['phagosome'][i]
                fungus[f_index]['point'] = ending_cell_point
                fungus.update_voxel_index([f_index])
    def move(self, rec_r, grid, cyto, tissue):
        for cell_index in self.alive(
            self.cell_data['status'] == NeutrophilCellData.Status.NONGRANULATING
        ):
            # TODO: Algorithm S3.17 says "if degranulating nearby hyphae, do not move" but do
            #  we have the "nearby hyphae" part of this condition?
            cell = self[cell_index]
            cell_voxel = grid.get_voxel(cell['point'])

            valid_voxel_offsets = []
            above_threshold_voxel_offsets = []

            # iterate over nearby voxels, recording the cytokine levels
            for dx, dy, dz in itertools.product((-1, 0, 1), repeat=3):
                zi = cell_voxel.z + dz
                yj = cell_voxel.y + dy
                xk = cell_voxel.x + dx
                if grid.is_valid_voxel(Voxel(x=xk, y=yj, z=zi)):
                    if tissue[zi, yj, xk] != TissueType.AIR.value:
                        valid_voxel_offsets.append((dx, dy, dz))
                        if cyto[zi, yj, xk] >= rec_r:
                            above_threshold_voxel_offsets.append((cyto[zi, yj, xk], (dx, dy, dz)))

            # pick a target for the move
            if len(above_threshold_voxel_offsets) > 0:
                # shuffle + sort (with _only_ 0-key, not lexicographic as tuples) ensures
                # randomization when there are equal top cytokine levels
                # note that numpy's shuffle will complain about ragged arrays
                shuffle(above_threshold_voxel_offsets)
                above_threshold_voxel_offsets = sorted(
                    above_threshold_voxel_offsets, key=lambda x: x[0], reverse=True
                )
                _, target_voxel_offset = above_threshold_voxel_offsets[0]
            elif len(valid_voxel_offsets) > 0:
                target_voxel_offset = choice(valid_voxel_offsets)
            else:
                raise AssertionError(
                    'This cell has no valid voxel to move to, including the one that it is in!'
                )

            # Some nonsense here, b/c jump is happening at the voxel level, not the point level
            starting_cell_point = Point(x=cell['point'][2], y=cell['point'][1], z=cell['point'][0])
            starting_cell_voxel = grid.get_voxel(starting_cell_point)
            ending_cell_voxel = grid.get_voxel(
                Point(
                    x=grid.x[cell_voxel.x + target_voxel_offset[0]],
                    y=grid.y[cell_voxel.y + target_voxel_offset[1]],
                    z=grid.z[cell_voxel.z + target_voxel_offset[2]],
                )
            )
            ending_cell_point = (
                starting_cell_point
                + grid.get_voxel_center(ending_cell_voxel)
                - grid.get_voxel_center(starting_cell_voxel)
            )

            cell['point'] = ending_cell_point
            self.update_voxel_index([cell_index])
Exemple #12
0
    def get_voxels_in_range(self, point: Point,
                            distance: float) -> Iterator[Tuple[Voxel, float]]:
        """Return an iterator of voxels within a given distance of a point.

        The values returned by the iterator are tuples of `(Voxel, distance)`
        pairs.  For example,

            voxel, distance = next(self.get_voxels_in_range(point, 1))

        where `distance` is the distance from `point` to the center of `voxel`.

        Note: no guarantee is given to the order over which the voxels are
        iterated.

        Parameters
        ----------
        point : simulation.coordinates.Point
            The center point
        distance : float
            Return all voxels with centers less than the distance from the center point

        """
        # Get a hyper-square containing a superset of what we want.  This
        # restricts the set of points that we need to explicitly compute.
        dp = Point(x=distance, y=distance, z=distance)
        z0, y0, x0 = point - dp
        z1, y1, x1 = point + dp

        x0 = max(x0, self.x[0])
        x1 = min(x1, self.x[-1])

        y0 = max(y0, self.y[0])
        y1 = min(y1, self.y[-1])

        z0 = max(z0, self.z[0])
        z1 = min(z1, self.z[-1])

        # get voxel indices of the lower left and upper right corners
        k0, j0, i0 = self.get_voxel(Point(x=x0, y=y0, z=z0))
        k1, j1, i1 = self.get_voxel(Point(x=x1, y=y1, z=z1))

        # get a distance matrix over all voxels in the candidate range
        z, y, x = self.meshgrid
        dx = x[k0:k1 + 1, j0:j1 + 1, i0:i1 + 1] - point.x
        dy = y[k0:k1 + 1, j0:j1 + 1, i0:i1 + 1] - point.y
        dz = z[k0:k1 + 1, j0:j1 + 1, i0:i1 + 1] - point.z
        distances = np.sqrt(dx * dx + dy * dy + dz * dz)

        # iterate over all voxels and yield those in range
        for k in range(distances.shape[0]):
            for j in range(distances.shape[1]):
                for i in range(distances.shape[2]):
                    d = distances[k, j, i]
                    if d <= distance:
                        yield Voxel(x=(i + i0), y=(j + j0), z=(k + k0)), d
Exemple #13
0
def test_get_neighboring_cells(grid: RectangularGrid):
    point = Point(x=4.5, y=4.5, z=4.5)

    raw_cells = [CellData.create_cell(point=point) for _ in range(5)]
    raw_cells[1]['point'] = Point(x=-1, y=4.5, z=4.5)
    raw_cells[4]['point'] = Point(x=4.5, y=4.5, z=-1)

    cells = CellList(grid=grid)
    cells.extend(raw_cells)

    assert_array_equal(cells.get_neighboring_cells(cells[0]), [0, 2, 3])
    assert_array_equal(cells.get_cells_in_voxel(Voxel(x=0, y=0, z=0)),
                       [0, 2, 3])
Exemple #14
0
    def get_voxel(self, point: Point) -> Voxel:
        """Return the voxel containing the given point.

        For points outside of the grid, this method will return invalid
        indices.  For example, given vertex coordinates `[1.5, 2.7, 6.5]` and point
        `-1.5` or `7.1`, this method will return `-1` and `3`, respectively.  Call the
        the `is_valid_voxel` method to determine if the voxel is valid.
        """
        # For some reason, extracting fields from a recordarray results in a
        # transposed point object (shape (1,3) rather than (3,)).  This code
        # ensures the representation is as expected.
        point = point.ravel().view(Point)
        assert len(point) == 3, 'This method does not handle arrays of points'

        ix = self._find_dimension_index(self.xv, point.x)
        iy = self._find_dimension_index(self.yv, point.y)
        iz = self._find_dimension_index(self.zv, point.z)
        return Voxel(x=ix, y=iy, z=iz)
Exemple #15
0
def discrete_laplacian(grid: RectangularGrid,
                       mask: np.ndarray,
                       dtype: np.dtype = np.float64) -> csr_matrix:
    """Return a discrete laplacian operator for the given restricted grid.

    This computes a standard laplacian operator as a scipy linear operator, except it is
    restricted to a grid mask.  The use case for this is to compute surface diffusion
    on a gridded variable.  The mask is generated from a category on the lung_tissue
    variable.
    """
    graph_shape = len(grid), len(grid)
    laplacian = dok_matrix(graph_shape)

    delta_z = grid.delta(0)
    delta_y = grid.delta(1)
    delta_x = grid.delta(2)

    for k, j, i in zip(*(mask).nonzero()):
        voxel = Voxel(x=i, y=j, z=k)
        voxel_index = grid.get_flattened_index(voxel)
        normalization = 0

        for neighbor in grid.get_adjecent_voxels(voxel, corners=False):
            ni = neighbor.x
            nj = neighbor.y
            nk = neighbor.z

            if not mask[nk, nj, ni]:
                continue

            neighbor_index = grid.get_flattened_index(neighbor)

            dx = delta_x[k, j, i] * (i - ni)
            dy = delta_y[k, j, i] * (j - nj)
            dz = delta_z[k, j, i] * (k - nk)
            distance2 = 1 / (dx * dx + dy * dy + dz * dz)

            normalization -= distance2
            laplacian[voxel_index, neighbor_index] = distance2

        laplacian[voxel_index, voxel_index] = normalization

    return laplacian.tocsr()
Exemple #16
0
    def chemotaxis(
        self,
        molecule,
        drift_lambda,
        drift_bias,
        tissue,
        grid: RectangularGrid,
    ):
        # 'molecule' = state.'molecule'.concentration
        # prob = 0-1 random number to determine which voxel is chosen to move

        # 1. Get cells that are alive
        for index in self.alive():
            prob = rg.random()

            # 2. Get voxel for each cell to get molecule in that voxel
            cell = self[index]
            vox = grid.get_voxel(cell['point'])

            # 3. Set prob for neighboring voxels
            p = []
            vox_list = []
            p_tot = 0.0
            i = -1

            # calculate individual probability
            for x in [0, 1, -1]:
                for y in [0, 1, -1]:
                    for z in [0, 1, -1]:
                        p.append(0.0)
                        vox_list.append([x, y, z])
                        i += 1
                        zk = vox.z + z
                        yj = vox.y + y
                        xi = vox.x + x
                        if grid.is_valid_voxel(Voxel(x=xi, y=yj, z=zk)):
                            if tissue[zk, yj, xi] in [
                                    TissueType.SURFACTANT.value,
                                    TissueType.BLOOD.value,
                                    TissueType.EPITHELIUM.value,
                                    TissueType.PORE.value,
                            ]:
                                p[i] = logistic(molecule[zk, yj, xi],
                                                drift_lambda, drift_bias)
                                p_tot += p[i]

            # scale to sum of probabilities
            if p_tot:
                for i in range(len(p)):
                    p[i] = p[i] / p_tot

            # chose vox from neighbors
            cum_p = 0.0
            for i in range(len(p)):
                cum_p += p[i]
                if prob <= cum_p:
                    cell['point'] = Point(
                        x=random.uniform(grid.xv[vox.x + vox_list[i][0]],
                                         grid.xv[vox.x + vox_list[i][0] + 1]),
                        y=random.uniform(grid.yv[vox.y + vox_list[i][1]],
                                         grid.yv[vox.y + vox_list[i][1] + 1]),
                        z=random.uniform(grid.zv[vox.z + vox_list[i][2]],
                                         grid.zv[vox.z + vox_list[i][2] + 1]),
                    )
                    self.update_voxel_index([index])
                    break
Exemple #17
0
    def advance(self, state: State, previous_time: float):
        erythrocyte: ErythrocyteState = state.erythrocyte
        molecules: MoleculesState = state.molecules
        hemoglobin: HemoglobinState = state.hemoglobin
        hemolysin: HemolysinState = state.hemolysin
        macrophage: MacrophageState = state.macrophage
        afumigatus: AfumigatusState = state.afumigatus
        grid: RectangularGrid = state.grid
        voxel_volume: float = state.voxel_volume

        shape = erythrocyte.cells['count'].shape

        # erythrocytes replenish themselves
        avg_number_of_new_erythrocytes = (1 - molecules.turnover_rate) * (
            1 - erythrocyte.cells['count'] / erythrocyte.max_erythrocyte_voxel)
        mask = avg_number_of_new_erythrocytes > 0
        erythrocyte.cells['count'][mask] += np.random.poisson(
            avg_number_of_new_erythrocytes[mask],
            avg_number_of_new_erythrocytes[mask].shape)

        # ---------- interactions

        # uptake hemoglobin
        erythrocyte.cells['hemoglobin'] += hemoglobin.grid
        hemoglobin.grid.fill(0.0)

        # interact with hemolysin. pop goes the blood cell
        # TODO: avg? variable name improvement?
        avg_lysed_erythrocytes = erythrocyte.cells[
            'count'] * activation_function(
                x=hemolysin.grid,
                k_d=erythrocyte.kd_hemo,
                h=self.time_step / 60,  # units: (min/step) / (min/hour)
                volume=voxel_volume,
                b=1,
            )
        number_lysed = np.minimum(
            np.random.poisson(avg_lysed_erythrocytes, shape),
            erythrocyte.cells['count'])
        erythrocyte.cells[
            'hemoglobin'] += number_lysed * erythrocyte.hemoglobin_quantity
        erythrocyte.cells['count'] -= number_lysed

        # interact with Macrophage
        erythrocytes_to_hemorrhage = erythrocyte.cells[
            'hemorrhage'] * np.random.poisson(
                erythrocyte.pr_macrophage_phagocytize_erythrocyte *
                erythrocyte.cells['count'], shape)

        for z, y, x in zip(*np.where(erythrocytes_to_hemorrhage > 0)):
            local_macrophages = macrophage.cells.get_cells_in_voxel(
                Voxel(x=x, y=y, z=z))
            num_local_macrophages = len(local_macrophages)
            for macrophage_index in local_macrophages:
                macrophage_cell = macrophage.cells[macrophage_index]
                if macrophage_cell['dead']:
                    continue
                macrophage_cell['iron_pool'] += (
                    4  # number of iron atoms in hemoglobin
                    * erythrocyte.hemoglobin_quantity *
                    erythrocytes_to_hemorrhage[z, y, x] /
                    num_local_macrophages)
        erythrocyte.cells['count'] -= erythrocytes_to_hemorrhage

        # interact with fungus
        for fungal_cell_index in afumigatus.cells.alive():
            fungal_cell = afumigatus.cells[fungal_cell_index]
            if fungal_cell['status'] == AfumigatusCellStatus.HYPHAE:
                fungal_voxel: Voxel = grid.get_voxel(fungal_cell['point'])
                erythrocyte.cells['hemorrhage'][tuple(fungal_voxel)] = True

        return state
def v(x: int, y: int, z: int) -> Voxel:
    return Voxel(x=x, y=y, z=z)