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)
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)
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()
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
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))
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])
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
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])
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)
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()
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
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)