def grow_hyphae(self, iron_min_grow, grow_time, p_branch, spacing): """Grow fungal hyphae.""" cells = self.cell_data conidia_indices = self.alive( (cells['form'] == FungusCellData.Form.CONIDIA) & (cells['status'] == FungusCellData.Status.GERMINATED) & (np.invert(cells['internalized']))) hyphae_indices = self.alive( (cells['form'] == FungusCellData.Form.HYPHAE) & (cells['status'] == FungusCellData.Status.GROWABLE) & (np.invert(cells['internalized'])) & (cells['iron'] > iron_min_grow) & (cells['iteration'] > grow_time)) # grow conidia if len(conidia_indices) != 0: cells['status'][conidia_indices] = FungusCellData.Status.GROWN cells['form'][conidia_indices] = FungusCellData.Form.HYPHAE children = FungusCellData(len(conidia_indices), initialize=True) children['iron'] = cells['iron'][conidia_indices] growth = spacing * (rg.random((len(conidia_indices), 3)) * 2 - 1) children['point'] = cells['point'][conidia_indices] + growth self.spawn_hypahael_cell(children) # grow hyphae if len(hyphae_indices) != 0: cells['status'][hyphae_indices] = FungusCellData.Status.GROWN branch_mask = rg.random(len(hyphae_indices)) < p_branch not_branch_indices = (np.invert(branch_mask)).nonzero()[0] branch_indices = branch_mask.nonzero()[0] elongate_children = FungusCellData(len(hyphae_indices), initialize=True) branch_children = FungusCellData(len(branch_indices), initialize=True) elongate_children['iron'] = cells['iron'][hyphae_indices] / 2 growth = spacing * (rg.random((len(hyphae_indices), 3)) * 2 - 1) elongate_children[ 'point'] = cells['point'][hyphae_indices] + growth if len(branch_indices) != 0: elongate_children['iron'][branch_indices] = ( cells['iron'][hyphae_indices[branch_indices]] / 3) branch_children['iron'] = cells['iron'][ hyphae_indices[branch_indices]] / 3 growth = spacing * (rg.random( (len(hyphae_indices[branch_indices]), 3)) * 2 - 1) branch_children['point'] = cells['point'][ hyphae_indices[branch_indices]] + growth # update iron in orignal cells cells['iron'][hyphae_indices[not_branch_indices]] /= 2 cells['iron'][hyphae_indices[branch_indices]] /= 3 self.spawn_hypahael_cell(elongate_children) self.spawn_hypahael_cell(branch_children)
def random_sphere_point() -> np.ndarray: """Generate a random point on the unit 2-sphere in R^3 using Marsaglia's method""" # generate vector in unit disc u: np.ndarray = 2 * rg.random(size=2) - 1 while np.linalg.norm(u) > 1.0: u = 2 * rg.random(size=2) - 1 norm_squared_u = float(np.dot(u, u)) return np.array( [ 2 * u[0] * np.sqrt(1 - norm_squared_u), 2 * u[1] * np.sqrt(1 - norm_squared_u), 1 - 2 * norm_squared_u, ], dtype=np.float64, )
def recruit_new(self, rec_rate_ph, rec_r, p_rec_r, tissue, grid, cyto): num_reps = rec_rate_ph # maximum number of macrophages recruited per time step cyto_index = np.argwhere( np.logical_and(tissue == TissueType.BLOOD.value, cyto >= rec_r)) if len(cyto_index) == 0: # nowhere to place cells return for _ in range(num_reps): if p_rec_r > rg.random(): ii = rg.integers(cyto_index.shape[0]) point = Point( x=grid.x[cyto_index[ii, 2]], y=grid.y[cyto_index[ii, 1]], z=grid.z[cyto_index[ii, 0]], ) # Do we really want these things to always be in the exact center of the voxel? # No we do not. Should not have any effect on model, but maybe some on # visualization. perturbation = rg.multivariate_normal(mean=[0.0, 0.0, 0.0], cov=[[0.25, 0.0, 0.0], [0.0, 0.25, 0.0], [0.0, 0.0, 0.25]]) perturbation_magnitude = np.linalg.norm(perturbation) perturbation /= max(1.0, perturbation_magnitude) point += perturbation self.append(MacrophageCellData.create_cell(point=point))
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 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 change_status(self, p_internal_swell: float, rest_time: int, swell_time: int): cells = self.cell_data indices = self.alive(cells['form'] != FungusCellData.Form.HYPHAE, ) internalized_indices = (cells['internalized'][indices]).nonzero()[0] not_internalized_indices = (np.invert( cells['internalized'][indices])).nonzero()[0] internalized_rest_indices = np.logical_and( cells['status'][internalized_indices] == FungusCellData.Status.RESTING, cells['iteration'][internalized_indices] >= rest_time, ).nonzero()[0] internalized_swollen_indices = np.logical_and( cells['status'][internalized_indices] == FungusCellData.Status.SWOLLEN, cells['iteration'][internalized_indices] >= swell_time, ).nonzero()[0] # internal fungus with REST status swall_mask = rg.random( len(internalized_rest_indices)) < p_internal_swell internalized_rest_indices = swall_mask.nonzero()[0] cells['status'][ internalized_rest_indices] = FungusCellData.Status.SWOLLEN cells['iteration'][internalized_rest_indices] = 0 # internal fungus with SWOLLEN status cells['status'][ internalized_swollen_indices] = FungusCellData.Status.GERMINATED cells['iteration'][internalized_swollen_indices] = 0 rest_indices = np.logical_and( cells['status'][not_internalized_indices] == FungusCellData.Status.RESTING, cells['iteration'][not_internalized_indices] >= rest_time, ).nonzero()[0] swollen_indices = np.logical_and( cells['status'][not_internalized_indices] == FungusCellData.Status.SWOLLEN, cells['iteration'][not_internalized_indices] >= swell_time, ).nonzero()[0] # free fungus with REST status cells['status'][rest_indices] = FungusCellData.Status.SWOLLEN cells['iteration'][rest_indices] = 0 # free fungus with SWOLLEN status cells['status'][swollen_indices] = FungusCellData.Status.GERMINATED cells['iteration'][swollen_indices] = 0
def generate_branch_direction(cell_vec: np.ndarray) -> np.ndarray: """ Generate a direction vector for branches. Parameters ---------- cell_vec : np.ndarray a unit 3-vector Returns ------- np.ndarray a random unit 3-vector at a 45 degree angle to `cell_vec`, sampled from the uniform distribution """ # norm should be approx 1, can delete for performance cell_vec /= np.linalg.norm(cell_vec) # create orthogonal basis adapted to cell's direction # get first orthogonal vector u: np.ndarray epsilon = 0.1 e1 = np.array([1.0, 0.0, 0.0], dtype=np.float64) e2 = np.array([0.0, 1.0, 0.0], dtype=np.float64) # if the cell vector isn't too close to +/- e1, generate the orthogonal vector using the cross # product with e1. otherwise use e2. (we can't be too close to both) u = ( np.cross(cell_vec, e1) if (np.linalg.norm(cell_vec - e1) > epsilon and np.linalg.norm(cell_vec + e1) > epsilon) else np.cross(cell_vec, e2) ) u /= np.linalg.norm(u) # unlike the other normalizations, this is non-optional # get second orthogonal vector, orthogonal to both the cell vec and the first orthogonal vector v = np.cross(cell_vec, u) # norm should be approx 1, can delete for performance v /= np.linalg.norm(v) # change of coordinates matrix p_matrix = np.array([cell_vec, u, v]).T # form a random unit vector on a 45 degree cone theta = rg.random() * 2 * np.pi branch_direction = p_matrix @ np.array([1.0, np.cos(theta), np.sin(theta)]) / np.sqrt(2) # norm should be approx 1, can delete for performance branch_direction /= np.linalg.norm(branch_direction) return branch_direction
def fungus_macrophage_interaction( afumigatus: AfumigatusState, afumigatus_cell: AfumigatusCellData, afumigatus_cell_index: int, macrophage: 'MacrophageState', macrophage_cell: 'MacrophageCellData', macrophage_cell_index: int, iron: IronState, grid: RectangularGrid, ): from nlisim.modules.macrophage import PhagocyteStatus probability_of_interaction = ( afumigatus.pr_ma_hyphae if afumigatus_cell['status'] == AfumigatusCellStatus.HYPHAE else afumigatus.pr_ma_phag ) # return if they do not interact if rg.random() >= probability_of_interaction: return # now they interact interact_with_aspergillus( phagocyte_cell=macrophage_cell, phagocyte_cell_index=macrophage_cell_index, phagocyte_cells=macrophage.cells, aspergillus_cell=afumigatus_cell, aspergillus_cell_index=afumigatus_cell_index, phagocyte=macrophage, phagocytize=afumigatus_cell['status'] != AfumigatusCellStatus.HYPHAE, ) # unlink the fungal cell from its tree if ( afumigatus_cell['status'] == AfumigatusCellStatus.HYPHAE and macrophage_cell['status'] == PhagocyteStatus.ACTIVE ): Afumigatus.kill_fungal_cell( afumigatus, afumigatus_cell, afumigatus_cell_index, iron, grid )
def branch( afumigatus_cell: AfumigatusCellData, afumigatus_cell_index: int, pr_branch: float, afumigatus: AfumigatusState, ): if ( afumigatus_cell['next_branch'] != -1 # if it already has a branch or afumigatus_cell['status'] != AfumigatusCellStatus.HYPHAE or not afumigatus_cell['boolean_network'][NetworkSpecies.LIP] ): return hyphal_length: float = afumigatus.hyphal_length if rg.random() < pr_branch: # now we branch branch_vector = generate_branch_direction(cell_vec=afumigatus_cell['vec']) branch_center_point = ( afumigatus_cell['point'] + (hyphal_length / 2) * afumigatus_cell['vec'] + (hyphal_length / 2) * branch_vector ) # center of new branch is offset by rest (half) of this septa and half of the new septa # create the new septa next_branch: CellData = AfumigatusCellData.create_cell( point=Point( x=branch_center_point[2], y=branch_center_point[1], z=branch_center_point[0] ), vec=branch_vector, growth_iteration=-1, iron_pool=0, status=AfumigatusCellStatus.HYPHAE, state=afumigatus_cell['state'], is_root=False, ) next_branch_id: int = afumigatus.cells.append(next_branch) # link them together afumigatus_cell['next_branch'] = next_branch_id next_branch['previous_septa'] = afumigatus_cell_index
def cell_self_update( afumigatus: AfumigatusState, afumigatus_cell: AfumigatusCellData, afumigatus_cell_index: int, ) -> None: afumigatus_cell['activation_iteration'] += 1 process_boolean_network( afumigatus_cell=afumigatus_cell, steps_to_eval=afumigatus.steps_to_bn_eval, afumigatus=afumigatus, ) # resting conidia become swelling conidia after a number of iterations # (with some probability) if ( afumigatus_cell['status'] == AfumigatusCellStatus.RESTING_CONIDIA and afumigatus_cell['activation_iteration'] >= afumigatus.iter_to_swelling and rg.random() < afumigatus.pr_aspergillus_change ): afumigatus_cell['status'] = AfumigatusCellStatus.SWELLING_CONIDIA afumigatus_cell['activation_iteration'] = 0 elif ( afumigatus_cell['status'] == AfumigatusCellStatus.SWELLING_CONIDIA and afumigatus_cell['activation_iteration'] >= afumigatus.iter_to_germinate ): afumigatus_cell['status'] = AfumigatusCellStatus.GERM_TUBE afumigatus_cell['activation_iteration'] = 0 # TODO: verify this, 1 turn on internalizing then free? if afumigatus_cell['state'] in { AfumigatusCellState.INTERNALIZING, AfumigatusCellState.RELEASING, }: afumigatus_cell['state'] = AfumigatusCellState.FREE # Distribute iron evenly within fungal tree. # Note: called for every cell, but a no-op on non-root cells. diffuse_iron(afumigatus_cell_index, afumigatus)
def lip_activation(afumigatus: AfumigatusState, iron_pool: float) -> bool: molar_concentration = iron_pool / afumigatus.hyphae_volume activation = 1 - np.exp(-molar_concentration / afumigatus.kd_lip) return bool(rg.random() < activation)
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