Module nlisim.modules.macrophage
Expand source code
import math
import random
from typing import Any, Dict, Tuple
import attr
from attr import attrs
import numpy as np
from nlisim.cell import CellData, CellFields, CellList
from nlisim.coordinates import Point, Voxel
from nlisim.grid import RectangularGrid
from nlisim.modules.phagocyte import (
PhagocyteCellData,
PhagocyteModel,
PhagocyteModuleState,
PhagocyteState,
PhagocyteStatus,
)
from nlisim.random import rg
from nlisim.state import State
from nlisim.util import choose_voxel_by_prob
class MacrophageCellData(PhagocyteCellData):
MACROPHAGE_FIELDS: CellFields = [
('status', np.uint8),
('state', np.uint8),
('fpn', bool),
('fpn_iteration', np.int64),
('tf', bool), # TODO: descriptive name, transferrin?
('tnfa', bool),
('iron_pool', np.float64),
('status_iteration', np.uint64),
('velocity', np.float64, 3),
]
dtype = np.dtype(
CellData.FIELDS + PhagocyteCellData.PHAGOCYTE_FIELDS + MACROPHAGE_FIELDS, align=True
) # type: ignore
@classmethod
def create_cell_tuple(
cls,
**kwargs,
) -> Tuple:
initializer = {
'status': kwargs.get('status', PhagocyteStatus.RESTING),
'state': kwargs.get('state', PhagocyteState.FREE),
'fpn': kwargs.get('fpn', True),
'fpn_iteration': kwargs.get('fpn_iteration', 0),
'tf': kwargs.get('tf', False),
'tnfa': kwargs.get('tnfa', False),
'iron_pool': kwargs.get('iron_pool', 0.0),
'status_iteration': kwargs.get('status_iteration', 0),
'velocity': kwargs.get('root', np.zeros(3, dtype=np.float64)),
}
# ensure that these come in the correct order
return PhagocyteCellData.create_cell_tuple(**kwargs) + tuple(
[initializer[key] for key, *_ in MacrophageCellData.MACROPHAGE_FIELDS]
)
@attrs(kw_only=True, frozen=True, repr=False)
class MacrophageCellList(CellList):
CellDataClass = MacrophageCellData
def cell_list_factory(self: 'MacrophageState') -> MacrophageCellList:
return MacrophageCellList(grid=self.global_state.grid)
@attr.s(kw_only=True)
class MacrophageState(PhagocyteModuleState):
cells: MacrophageCellList = attr.ib(default=attr.Factory(cell_list_factory, takes_self=True))
time_to_rest: float # units: min
iter_to_rest: int # units: steps
time_to_change_state: float # units: hours
iter_to_change_state: int # units: steps
ma_internal_iron: float # units: atto-mols
prob_death_per_timestep: float # units: probability * step^-1
max_ma: int # units: count
min_ma: int # units: count
init_num_macrophages: int # units: count
recruitment_rate: float
rec_bias: float
drift_bias: float
ma_move_rate_act: float # µm/min
ma_move_rate_rest: float # µm/min
half_life: float # units: hours
# UNUSED:
# kd_ma_iron: float
# ma_vol: float # units: pL
class Macrophage(PhagocyteModel):
name = 'macrophage'
StateClass = MacrophageState
def initialize(self, state: State):
from nlisim.util import TissueType
macrophage: MacrophageState = state.macrophage
lung_tissue = state.lung_tissue
time_step_size: float = self.time_step
macrophage.max_conidia = self.config.getint(
'max_conidia'
) # (from phagocyte model) units: count
macrophage.time_to_rest = self.config.getint('time_to_rest') # units: min
macrophage.time_to_change_state = self.config.getint('time_to_change_state') # units: hours
macrophage.ma_internal_iron = self.config.getfloat('ma_internal_iron') # units: atto-mols
macrophage.max_ma = self.config.getint('max_ma') # units: count
macrophage.min_ma = self.config.getint('min_ma') # units: count
macrophage.init_num_macrophages = self.config.getint('init_num_macrophages') # units: count
macrophage.recruitment_rate = self.config.getfloat('recruitment_rate')
macrophage.rec_bias = self.config.getfloat('rec_bias')
macrophage.drift_bias = self.config.getfloat('drift_bias')
macrophage.ma_move_rate_act = self.config.getfloat('ma_move_rate_act') # µm/min
macrophage.ma_move_rate_rest = self.config.getfloat('ma_move_rate_rest') # µm/min
macrophage.half_life = self.config.getfloat('ma_half_life') # units: hours
# UNUSED:
# macrophage.kd_ma_iron = self.config.getfloat('kd_ma_iron')
# macrophage.ma_vol = self.config.getfloat('ma_vol')
# computed values
macrophage.iter_to_rest = int(
macrophage.time_to_rest / self.time_step
) # units: min / (min/step) = steps
macrophage.iter_to_change_state = int(
macrophage.time_to_change_state * (60 / time_step_size)
) # units: hours * (min/hour) / (min/step) = step
macrophage.prob_death_per_timestep = -math.log(0.5) / (
macrophage.half_life * (60 / time_step_size)
) # units: 1/( hours * (min/hour) / (min/step) ) = 1/step
# initialize cells, placing them randomly
locations = list(zip(*np.where(lung_tissue != TissueType.AIR)))
dz_field: np.ndarray = state.grid.delta(axis=0)
dy_field: np.ndarray = state.grid.delta(axis=1)
dx_field: np.ndarray = state.grid.delta(axis=2)
for vox_z, vox_y, vox_x in random.choices(locations, k=macrophage.init_num_macrophages):
# the x,y,z coordinates are in the centers of the grids
z = state.grid.z[vox_z]
y = state.grid.y[vox_y]
x = state.grid.x[vox_x]
dz = dz_field[vox_z, vox_y, vox_x]
dy = dy_field[vox_z, vox_y, vox_x]
dx = dx_field[vox_z, vox_y, vox_x]
self.create_macrophage(
state=state,
x=x + rg.uniform(-dx / 2, dx / 2),
y=y + rg.uniform(-dy / 2, dy / 2),
z=z + rg.uniform(-dz / 2, dz / 2),
iron_pool=macrophage.ma_internal_iron,
)
return state
def advance(self, state: State, previous_time: float):
"""Advance the state by a single time step."""
macrophage: MacrophageState = state.macrophage
for macrophage_cell_index in macrophage.cells.alive():
macrophage_cell = macrophage.cells[macrophage_cell_index]
num_cells_in_phagosome = np.sum(macrophage_cell['phagosome'] >= 0)
self.update_status(state, macrophage_cell, num_cells_in_phagosome)
if (
num_cells_in_phagosome == 0
and rg.uniform() < macrophage.prob_death_per_timestep
and len(macrophage.cells.alive()) > macrophage.min_ma
):
macrophage_cell['status'] = PhagocyteStatus.DEAD
macrophage_cell['dead'] = True
if not macrophage_cell['fpn']:
if macrophage_cell['fpn_iteration'] >= macrophage.iter_to_change_state:
macrophage_cell['fpn_iteration'] = 0
macrophage_cell['fpn'] = True
else:
macrophage_cell['fpn_iteration'] += 1
# Movement
if macrophage_cell['status'] == PhagocyteStatus.ACTIVE:
max_move_step = (
macrophage.ma_move_rate_act * self.time_step
) # (µm/min) * (min/step) = µm * step
else:
max_move_step = (
macrophage.ma_move_rate_rest * self.time_step
) # (µm/min) * (min/step) = µm * step
move_step: int = rg.poisson(max_move_step)
# move the cell 1 µm, move_step number of times
for _ in range(move_step):
self.single_step_move(
state, macrophage_cell, macrophage_cell_index, macrophage.cells
)
# Recruitment
self.recruit_macrophages(state)
return state
def summary_stats(self, state: State) -> Dict[str, Any]:
macrophage: MacrophageState = state.macrophage
live_macrophages = macrophage.cells.alive()
max_index = max(map(int, PhagocyteStatus))
status_counts = np.bincount(
np.fromiter(
(
macrophage.cells[macrophage_cell_index]['status']
for macrophage_cell_index in live_macrophages
),
dtype=np.uint8,
),
minlength=max_index + 1,
)
tnfa_active = int(
np.sum(
np.fromiter(
(
macrophage.cells[macrophage_cell_index]['tnfa']
for macrophage_cell_index in live_macrophages
),
dtype=bool,
)
)
)
return {
'count': len(live_macrophages),
'inactive': int(status_counts[PhagocyteStatus.INACTIVE]),
'inactivating': int(status_counts[PhagocyteStatus.INACTIVATING]),
'resting': int(status_counts[PhagocyteStatus.RESTING]),
'activating': int(status_counts[PhagocyteStatus.ACTIVATING]),
'active': int(status_counts[PhagocyteStatus.ACTIVE]),
'apoptotic': int(status_counts[PhagocyteStatus.APOPTOTIC]),
'necrotic': int(status_counts[PhagocyteStatus.NECROTIC]),
'anergic': int(status_counts[PhagocyteStatus.ANERGIC]),
'interacting': int(status_counts[PhagocyteStatus.INTERACTING]),
'TNFa active': tnfa_active,
}
def visualization_data(self, state: State):
return 'cells', state.macrophage.cells
def recruit_macrophages(self, state: State) -> None:
"""
Recruit macrophages based on MIP1b activation
Parameters
----------
state : State
global simulation state
Returns
-------
nothing
"""
from nlisim.modules.mip1b import MIP1BState
from nlisim.util import TissueType, activation_function
macrophage: MacrophageState = state.macrophage
mip1b: MIP1BState = state.mip1b
voxel_volume: float = state.voxel_volume
space_volume: float = state.space_volume
lung_tissue = state.lung_tissue
# 1. compute number of macrophages to recruit
num_live_macrophages = len(macrophage.cells.alive())
avg = (
macrophage.recruitment_rate
* np.sum(mip1b.grid)
* (1 - num_live_macrophages / macrophage.max_ma)
/ (mip1b.k_d * space_volume)
)
number_to_recruit = max(
np.random.poisson(avg) if avg > 0 else 0, macrophage.min_ma - num_live_macrophages
)
# 2. get voxels for new macrophages, based on activation
if number_to_recruit > 0:
activation_voxels = zip(
*np.where(
np.logical_and(
activation_function(
x=mip1b.grid,
k_d=mip1b.k_d,
h=self.time_step / 60,
volume=voxel_volume,
b=macrophage.rec_bias,
)
< rg.uniform(size=mip1b.grid.shape),
lung_tissue != TissueType.AIR,
)
)
)
dz_field: np.ndarray = state.grid.delta(axis=0)
dy_field: np.ndarray = state.grid.delta(axis=1)
dx_field: np.ndarray = state.grid.delta(axis=2)
for coordinates in rg.choice(
tuple(activation_voxels), size=number_to_recruit, replace=True
):
vox_z, vox_y, vox_x = coordinates
# the x,y,z coordinates are in the centers of the grids
z = state.grid.z[vox_z]
y = state.grid.y[vox_y]
x = state.grid.x[vox_x]
dz = dz_field[vox_z, vox_y, vox_x]
dy = dy_field[vox_z, vox_y, vox_x]
dx = dx_field[vox_z, vox_y, vox_x]
self.create_macrophage(
state=state,
x=x + rg.uniform(-dx / 2, dx / 2),
y=y + rg.uniform(-dy / 2, dy / 2),
z=z + rg.uniform(-dz / 2, dz / 2),
)
def single_step_probabilistic_drift(
self, state: State, cell: PhagocyteCellData, voxel: Voxel
) -> Point:
"""
Calculate a 1µm movement of a macrophage
Parameters
----------
state : State
global simulation state
cell : MacrophageCellData
a macrophage cell
voxel : Voxel
current voxel position of the macrophage
Returns
-------
Point
the new position of the macrophage
"""
# macrophages are attracted by MIP1b
from nlisim.modules.mip1b import MIP1BState
from nlisim.util import TissueType, activation_function
macrophage: MacrophageState = state.macrophage
mip1b: MIP1BState = state.mip1b
grid: RectangularGrid = state.grid
lung_tissue: np.ndarray = state.lung_tissue
voxel_volume: float = state.voxel_volume
# compute chemokine influence on velocity, with some randomness.
# macrophage has a non-zero probability of moving into non-air voxels.
# if not any of these, stay in place. This could happen if e.g. you are
# somehow stranded in air.
nearby_voxels: Tuple[Voxel, ...] = tuple(grid.get_adjacent_voxels(voxel, corners=True))
weights = np.array(
[
0.0
if lung_tissue[tuple(vxl)] == TissueType.AIR
else activation_function(
x=mip1b.grid[tuple(vxl)],
k_d=mip1b.k_d,
h=self.time_step / 60, # units: (min/step) / (min/hour)
volume=voxel_volume,
b=1,
)
+ macrophage.drift_bias
for vxl in nearby_voxels
],
dtype=np.float64,
)
voxel_movement_direction: Voxel = choose_voxel_by_prob(
voxels=nearby_voxels, default_value=voxel, weights=weights
)
# get normalized direction vector
dp_dt: np.ndarray = grid.get_voxel_center(voxel_movement_direction) - grid.get_voxel_center(
voxel
)
norm = np.linalg.norm(dp_dt)
if norm > 0.0:
dp_dt /= norm
# average and re-normalize with existing velocity
dp_dt += cell['velocity']
norm = np.linalg.norm(dp_dt)
if norm > 0.0:
dp_dt /= norm
# we need to determine if this movement will put us into an air voxel. This can happen
# when pushed there by momentum. If that happens, we stay in place and zero out the
# momentum. Otherwise, velocity is updated to dp/dt and movement is as expected.
new_position = cell['point'] + dp_dt
new_voxel: Voxel = grid.get_voxel(new_position)
if state.lung_tissue[tuple(new_voxel)] == TissueType.AIR:
cell['velocity'][:] = np.zeros(3, dtype=np.float64)
return cell['point']
else:
cell['velocity'][:] = dp_dt
return new_position
@staticmethod
def create_macrophage(*, state: State, x: float, y: float, z: float, **kwargs) -> None:
"""
Create a new macrophage cell
Parameters
----------
state : State
global simulation state
x : float
y : float
z : float
coordinates of created macrophage
kwargs
parameters for macrophage, will give
Returns
-------
nothing
"""
macrophage: MacrophageState = state.macrophage
# use default value of iron pool if not present
iron_pool = kwargs.get('iron_pool', macrophage.ma_internal_iron)
kwargs.pop('iron_pool', None)
macrophage.cells.append(
MacrophageCellData.create_cell(
point=Point(x=x, y=y, z=z),
iron_pool=iron_pool,
**kwargs,
)
)
def update_status(
self, state: State, macrophage_cell: MacrophageCellData, num_cells_in_phagosome
) -> None:
"""
Update the status of the cell, progressing between states after a certain number of ticks.
Parameters
----------
state : State
global simulation state
macrophage_cell : MacrophageCellData
num_cells_in_phagosome
Returns
-------
nothing
"""
macrophage: MacrophageState = state.macrophage
if macrophage_cell['status'] == PhagocyteStatus.NECROTIC:
# TODO: what about APOPTOTIC?
self.release_phagosome(state, macrophage_cell)
elif num_cells_in_phagosome > macrophage.max_conidia:
# TODO: how do we get here?
macrophage_cell['status'] = PhagocyteStatus.NECROTIC
elif macrophage_cell['status'] == PhagocyteStatus.ACTIVE:
if macrophage_cell['status_iteration'] >= macrophage.iter_to_rest:
macrophage_cell['status_iteration'] = 0
macrophage_cell['tnfa'] = False
macrophage_cell['status'] = PhagocyteStatus.RESTING
else:
macrophage_cell['status_iteration'] += 1
elif macrophage_cell['status'] == PhagocyteStatus.INACTIVE:
if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state:
macrophage_cell['status_iteration'] = 0
macrophage_cell['status'] = PhagocyteStatus.RESTING
else:
macrophage_cell['status_iteration'] += 1
elif macrophage_cell['status'] == PhagocyteStatus.ACTIVATING:
if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state:
macrophage_cell['status_iteration'] = 0
macrophage_cell['status'] = PhagocyteStatus.ACTIVE
else:
macrophage_cell['status_iteration'] += 1
elif macrophage_cell['status'] == PhagocyteStatus.INACTIVATING:
if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state:
macrophage_cell['status_iteration'] = 0
macrophage_cell['status'] = PhagocyteStatus.INACTIVE
else:
macrophage_cell['status_iteration'] += 1
elif macrophage_cell['status'] == PhagocyteStatus.ANERGIC:
if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state:
macrophage_cell['status_iteration'] = 0
macrophage_cell['status'] = PhagocyteStatus.RESTING
else:
macrophage_cell['status_iteration'] += 1
Functions
def cell_list_factory(self: MacrophageState) ‑> MacrophageCellList
-
Expand source code
def cell_list_factory(self: 'MacrophageState') -> MacrophageCellList: return MacrophageCellList(grid=self.global_state.grid)
Classes
class Macrophage (config: SimulationConfig)
-
Expand source code
class Macrophage(PhagocyteModel): name = 'macrophage' StateClass = MacrophageState def initialize(self, state: State): from nlisim.util import TissueType macrophage: MacrophageState = state.macrophage lung_tissue = state.lung_tissue time_step_size: float = self.time_step macrophage.max_conidia = self.config.getint( 'max_conidia' ) # (from phagocyte model) units: count macrophage.time_to_rest = self.config.getint('time_to_rest') # units: min macrophage.time_to_change_state = self.config.getint('time_to_change_state') # units: hours macrophage.ma_internal_iron = self.config.getfloat('ma_internal_iron') # units: atto-mols macrophage.max_ma = self.config.getint('max_ma') # units: count macrophage.min_ma = self.config.getint('min_ma') # units: count macrophage.init_num_macrophages = self.config.getint('init_num_macrophages') # units: count macrophage.recruitment_rate = self.config.getfloat('recruitment_rate') macrophage.rec_bias = self.config.getfloat('rec_bias') macrophage.drift_bias = self.config.getfloat('drift_bias') macrophage.ma_move_rate_act = self.config.getfloat('ma_move_rate_act') # µm/min macrophage.ma_move_rate_rest = self.config.getfloat('ma_move_rate_rest') # µm/min macrophage.half_life = self.config.getfloat('ma_half_life') # units: hours # UNUSED: # macrophage.kd_ma_iron = self.config.getfloat('kd_ma_iron') # macrophage.ma_vol = self.config.getfloat('ma_vol') # computed values macrophage.iter_to_rest = int( macrophage.time_to_rest / self.time_step ) # units: min / (min/step) = steps macrophage.iter_to_change_state = int( macrophage.time_to_change_state * (60 / time_step_size) ) # units: hours * (min/hour) / (min/step) = step macrophage.prob_death_per_timestep = -math.log(0.5) / ( macrophage.half_life * (60 / time_step_size) ) # units: 1/( hours * (min/hour) / (min/step) ) = 1/step # initialize cells, placing them randomly locations = list(zip(*np.where(lung_tissue != TissueType.AIR))) dz_field: np.ndarray = state.grid.delta(axis=0) dy_field: np.ndarray = state.grid.delta(axis=1) dx_field: np.ndarray = state.grid.delta(axis=2) for vox_z, vox_y, vox_x in random.choices(locations, k=macrophage.init_num_macrophages): # the x,y,z coordinates are in the centers of the grids z = state.grid.z[vox_z] y = state.grid.y[vox_y] x = state.grid.x[vox_x] dz = dz_field[vox_z, vox_y, vox_x] dy = dy_field[vox_z, vox_y, vox_x] dx = dx_field[vox_z, vox_y, vox_x] self.create_macrophage( state=state, x=x + rg.uniform(-dx / 2, dx / 2), y=y + rg.uniform(-dy / 2, dy / 2), z=z + rg.uniform(-dz / 2, dz / 2), iron_pool=macrophage.ma_internal_iron, ) return state def advance(self, state: State, previous_time: float): """Advance the state by a single time step.""" macrophage: MacrophageState = state.macrophage for macrophage_cell_index in macrophage.cells.alive(): macrophage_cell = macrophage.cells[macrophage_cell_index] num_cells_in_phagosome = np.sum(macrophage_cell['phagosome'] >= 0) self.update_status(state, macrophage_cell, num_cells_in_phagosome) if ( num_cells_in_phagosome == 0 and rg.uniform() < macrophage.prob_death_per_timestep and len(macrophage.cells.alive()) > macrophage.min_ma ): macrophage_cell['status'] = PhagocyteStatus.DEAD macrophage_cell['dead'] = True if not macrophage_cell['fpn']: if macrophage_cell['fpn_iteration'] >= macrophage.iter_to_change_state: macrophage_cell['fpn_iteration'] = 0 macrophage_cell['fpn'] = True else: macrophage_cell['fpn_iteration'] += 1 # Movement if macrophage_cell['status'] == PhagocyteStatus.ACTIVE: max_move_step = ( macrophage.ma_move_rate_act * self.time_step ) # (µm/min) * (min/step) = µm * step else: max_move_step = ( macrophage.ma_move_rate_rest * self.time_step ) # (µm/min) * (min/step) = µm * step move_step: int = rg.poisson(max_move_step) # move the cell 1 µm, move_step number of times for _ in range(move_step): self.single_step_move( state, macrophage_cell, macrophage_cell_index, macrophage.cells ) # Recruitment self.recruit_macrophages(state) return state def summary_stats(self, state: State) -> Dict[str, Any]: macrophage: MacrophageState = state.macrophage live_macrophages = macrophage.cells.alive() max_index = max(map(int, PhagocyteStatus)) status_counts = np.bincount( np.fromiter( ( macrophage.cells[macrophage_cell_index]['status'] for macrophage_cell_index in live_macrophages ), dtype=np.uint8, ), minlength=max_index + 1, ) tnfa_active = int( np.sum( np.fromiter( ( macrophage.cells[macrophage_cell_index]['tnfa'] for macrophage_cell_index in live_macrophages ), dtype=bool, ) ) ) return { 'count': len(live_macrophages), 'inactive': int(status_counts[PhagocyteStatus.INACTIVE]), 'inactivating': int(status_counts[PhagocyteStatus.INACTIVATING]), 'resting': int(status_counts[PhagocyteStatus.RESTING]), 'activating': int(status_counts[PhagocyteStatus.ACTIVATING]), 'active': int(status_counts[PhagocyteStatus.ACTIVE]), 'apoptotic': int(status_counts[PhagocyteStatus.APOPTOTIC]), 'necrotic': int(status_counts[PhagocyteStatus.NECROTIC]), 'anergic': int(status_counts[PhagocyteStatus.ANERGIC]), 'interacting': int(status_counts[PhagocyteStatus.INTERACTING]), 'TNFa active': tnfa_active, } def visualization_data(self, state: State): return 'cells', state.macrophage.cells def recruit_macrophages(self, state: State) -> None: """ Recruit macrophages based on MIP1b activation Parameters ---------- state : State global simulation state Returns ------- nothing """ from nlisim.modules.mip1b import MIP1BState from nlisim.util import TissueType, activation_function macrophage: MacrophageState = state.macrophage mip1b: MIP1BState = state.mip1b voxel_volume: float = state.voxel_volume space_volume: float = state.space_volume lung_tissue = state.lung_tissue # 1. compute number of macrophages to recruit num_live_macrophages = len(macrophage.cells.alive()) avg = ( macrophage.recruitment_rate * np.sum(mip1b.grid) * (1 - num_live_macrophages / macrophage.max_ma) / (mip1b.k_d * space_volume) ) number_to_recruit = max( np.random.poisson(avg) if avg > 0 else 0, macrophage.min_ma - num_live_macrophages ) # 2. get voxels for new macrophages, based on activation if number_to_recruit > 0: activation_voxels = zip( *np.where( np.logical_and( activation_function( x=mip1b.grid, k_d=mip1b.k_d, h=self.time_step / 60, volume=voxel_volume, b=macrophage.rec_bias, ) < rg.uniform(size=mip1b.grid.shape), lung_tissue != TissueType.AIR, ) ) ) dz_field: np.ndarray = state.grid.delta(axis=0) dy_field: np.ndarray = state.grid.delta(axis=1) dx_field: np.ndarray = state.grid.delta(axis=2) for coordinates in rg.choice( tuple(activation_voxels), size=number_to_recruit, replace=True ): vox_z, vox_y, vox_x = coordinates # the x,y,z coordinates are in the centers of the grids z = state.grid.z[vox_z] y = state.grid.y[vox_y] x = state.grid.x[vox_x] dz = dz_field[vox_z, vox_y, vox_x] dy = dy_field[vox_z, vox_y, vox_x] dx = dx_field[vox_z, vox_y, vox_x] self.create_macrophage( state=state, x=x + rg.uniform(-dx / 2, dx / 2), y=y + rg.uniform(-dy / 2, dy / 2), z=z + rg.uniform(-dz / 2, dz / 2), ) def single_step_probabilistic_drift( self, state: State, cell: PhagocyteCellData, voxel: Voxel ) -> Point: """ Calculate a 1µm movement of a macrophage Parameters ---------- state : State global simulation state cell : MacrophageCellData a macrophage cell voxel : Voxel current voxel position of the macrophage Returns ------- Point the new position of the macrophage """ # macrophages are attracted by MIP1b from nlisim.modules.mip1b import MIP1BState from nlisim.util import TissueType, activation_function macrophage: MacrophageState = state.macrophage mip1b: MIP1BState = state.mip1b grid: RectangularGrid = state.grid lung_tissue: np.ndarray = state.lung_tissue voxel_volume: float = state.voxel_volume # compute chemokine influence on velocity, with some randomness. # macrophage has a non-zero probability of moving into non-air voxels. # if not any of these, stay in place. This could happen if e.g. you are # somehow stranded in air. nearby_voxels: Tuple[Voxel, ...] = tuple(grid.get_adjacent_voxels(voxel, corners=True)) weights = np.array( [ 0.0 if lung_tissue[tuple(vxl)] == TissueType.AIR else activation_function( x=mip1b.grid[tuple(vxl)], k_d=mip1b.k_d, h=self.time_step / 60, # units: (min/step) / (min/hour) volume=voxel_volume, b=1, ) + macrophage.drift_bias for vxl in nearby_voxels ], dtype=np.float64, ) voxel_movement_direction: Voxel = choose_voxel_by_prob( voxels=nearby_voxels, default_value=voxel, weights=weights ) # get normalized direction vector dp_dt: np.ndarray = grid.get_voxel_center(voxel_movement_direction) - grid.get_voxel_center( voxel ) norm = np.linalg.norm(dp_dt) if norm > 0.0: dp_dt /= norm # average and re-normalize with existing velocity dp_dt += cell['velocity'] norm = np.linalg.norm(dp_dt) if norm > 0.0: dp_dt /= norm # we need to determine if this movement will put us into an air voxel. This can happen # when pushed there by momentum. If that happens, we stay in place and zero out the # momentum. Otherwise, velocity is updated to dp/dt and movement is as expected. new_position = cell['point'] + dp_dt new_voxel: Voxel = grid.get_voxel(new_position) if state.lung_tissue[tuple(new_voxel)] == TissueType.AIR: cell['velocity'][:] = np.zeros(3, dtype=np.float64) return cell['point'] else: cell['velocity'][:] = dp_dt return new_position @staticmethod def create_macrophage(*, state: State, x: float, y: float, z: float, **kwargs) -> None: """ Create a new macrophage cell Parameters ---------- state : State global simulation state x : float y : float z : float coordinates of created macrophage kwargs parameters for macrophage, will give Returns ------- nothing """ macrophage: MacrophageState = state.macrophage # use default value of iron pool if not present iron_pool = kwargs.get('iron_pool', macrophage.ma_internal_iron) kwargs.pop('iron_pool', None) macrophage.cells.append( MacrophageCellData.create_cell( point=Point(x=x, y=y, z=z), iron_pool=iron_pool, **kwargs, ) ) def update_status( self, state: State, macrophage_cell: MacrophageCellData, num_cells_in_phagosome ) -> None: """ Update the status of the cell, progressing between states after a certain number of ticks. Parameters ---------- state : State global simulation state macrophage_cell : MacrophageCellData num_cells_in_phagosome Returns ------- nothing """ macrophage: MacrophageState = state.macrophage if macrophage_cell['status'] == PhagocyteStatus.NECROTIC: # TODO: what about APOPTOTIC? self.release_phagosome(state, macrophage_cell) elif num_cells_in_phagosome > macrophage.max_conidia: # TODO: how do we get here? macrophage_cell['status'] = PhagocyteStatus.NECROTIC elif macrophage_cell['status'] == PhagocyteStatus.ACTIVE: if macrophage_cell['status_iteration'] >= macrophage.iter_to_rest: macrophage_cell['status_iteration'] = 0 macrophage_cell['tnfa'] = False macrophage_cell['status'] = PhagocyteStatus.RESTING else: macrophage_cell['status_iteration'] += 1 elif macrophage_cell['status'] == PhagocyteStatus.INACTIVE: if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state: macrophage_cell['status_iteration'] = 0 macrophage_cell['status'] = PhagocyteStatus.RESTING else: macrophage_cell['status_iteration'] += 1 elif macrophage_cell['status'] == PhagocyteStatus.ACTIVATING: if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state: macrophage_cell['status_iteration'] = 0 macrophage_cell['status'] = PhagocyteStatus.ACTIVE else: macrophage_cell['status_iteration'] += 1 elif macrophage_cell['status'] == PhagocyteStatus.INACTIVATING: if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state: macrophage_cell['status_iteration'] = 0 macrophage_cell['status'] = PhagocyteStatus.INACTIVE else: macrophage_cell['status_iteration'] += 1 elif macrophage_cell['status'] == PhagocyteStatus.ANERGIC: if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state: macrophage_cell['status_iteration'] = 0 macrophage_cell['status'] = PhagocyteStatus.RESTING else: macrophage_cell['status_iteration'] += 1
Ancestors
Static methods
def create_macrophage(*, state: State, x: float, y: float, z: float, **kwargs) ‑> None
-
Create a new macrophage cell
Parameters
state
:State
- global simulation state
x
:float
y
:float
z
:float
- coordinates of created macrophage
kwargs
- parameters for macrophage, will give
Returns
nothing
Expand source code
@staticmethod def create_macrophage(*, state: State, x: float, y: float, z: float, **kwargs) -> None: """ Create a new macrophage cell Parameters ---------- state : State global simulation state x : float y : float z : float coordinates of created macrophage kwargs parameters for macrophage, will give Returns ------- nothing """ macrophage: MacrophageState = state.macrophage # use default value of iron pool if not present iron_pool = kwargs.get('iron_pool', macrophage.ma_internal_iron) kwargs.pop('iron_pool', None) macrophage.cells.append( MacrophageCellData.create_cell( point=Point(x=x, y=y, z=z), iron_pool=iron_pool, **kwargs, ) )
Methods
def advance(self, state: State, previous_time: float)
-
Advance the state by a single time step.
Expand source code
def advance(self, state: State, previous_time: float): """Advance the state by a single time step.""" macrophage: MacrophageState = state.macrophage for macrophage_cell_index in macrophage.cells.alive(): macrophage_cell = macrophage.cells[macrophage_cell_index] num_cells_in_phagosome = np.sum(macrophage_cell['phagosome'] >= 0) self.update_status(state, macrophage_cell, num_cells_in_phagosome) if ( num_cells_in_phagosome == 0 and rg.uniform() < macrophage.prob_death_per_timestep and len(macrophage.cells.alive()) > macrophage.min_ma ): macrophage_cell['status'] = PhagocyteStatus.DEAD macrophage_cell['dead'] = True if not macrophage_cell['fpn']: if macrophage_cell['fpn_iteration'] >= macrophage.iter_to_change_state: macrophage_cell['fpn_iteration'] = 0 macrophage_cell['fpn'] = True else: macrophage_cell['fpn_iteration'] += 1 # Movement if macrophage_cell['status'] == PhagocyteStatus.ACTIVE: max_move_step = ( macrophage.ma_move_rate_act * self.time_step ) # (µm/min) * (min/step) = µm * step else: max_move_step = ( macrophage.ma_move_rate_rest * self.time_step ) # (µm/min) * (min/step) = µm * step move_step: int = rg.poisson(max_move_step) # move the cell 1 µm, move_step number of times for _ in range(move_step): self.single_step_move( state, macrophage_cell, macrophage_cell_index, macrophage.cells ) # Recruitment self.recruit_macrophages(state) return state
def recruit_macrophages(self, state: State) ‑> None
-
Recruit macrophages based on MIP1b activation
Parameters
state
:State
- global simulation state
Returns
nothing
Expand source code
def recruit_macrophages(self, state: State) -> None: """ Recruit macrophages based on MIP1b activation Parameters ---------- state : State global simulation state Returns ------- nothing """ from nlisim.modules.mip1b import MIP1BState from nlisim.util import TissueType, activation_function macrophage: MacrophageState = state.macrophage mip1b: MIP1BState = state.mip1b voxel_volume: float = state.voxel_volume space_volume: float = state.space_volume lung_tissue = state.lung_tissue # 1. compute number of macrophages to recruit num_live_macrophages = len(macrophage.cells.alive()) avg = ( macrophage.recruitment_rate * np.sum(mip1b.grid) * (1 - num_live_macrophages / macrophage.max_ma) / (mip1b.k_d * space_volume) ) number_to_recruit = max( np.random.poisson(avg) if avg > 0 else 0, macrophage.min_ma - num_live_macrophages ) # 2. get voxels for new macrophages, based on activation if number_to_recruit > 0: activation_voxels = zip( *np.where( np.logical_and( activation_function( x=mip1b.grid, k_d=mip1b.k_d, h=self.time_step / 60, volume=voxel_volume, b=macrophage.rec_bias, ) < rg.uniform(size=mip1b.grid.shape), lung_tissue != TissueType.AIR, ) ) ) dz_field: np.ndarray = state.grid.delta(axis=0) dy_field: np.ndarray = state.grid.delta(axis=1) dx_field: np.ndarray = state.grid.delta(axis=2) for coordinates in rg.choice( tuple(activation_voxels), size=number_to_recruit, replace=True ): vox_z, vox_y, vox_x = coordinates # the x,y,z coordinates are in the centers of the grids z = state.grid.z[vox_z] y = state.grid.y[vox_y] x = state.grid.x[vox_x] dz = dz_field[vox_z, vox_y, vox_x] dy = dy_field[vox_z, vox_y, vox_x] dx = dx_field[vox_z, vox_y, vox_x] self.create_macrophage( state=state, x=x + rg.uniform(-dx / 2, dx / 2), y=y + rg.uniform(-dy / 2, dy / 2), z=z + rg.uniform(-dz / 2, dz / 2), )
def single_step_probabilistic_drift(self, state: State, cell: PhagocyteCellData, voxel: Voxel) ‑> Point
-
Calculate a 1µm movement of a macrophage
Parameters
state
:State
- global simulation state
cell
:MacrophageCellData
- a macrophage cell
voxel
:Voxel
- current voxel position of the macrophage
Returns
Point
- the new position of the macrophage
Expand source code
def single_step_probabilistic_drift( self, state: State, cell: PhagocyteCellData, voxel: Voxel ) -> Point: """ Calculate a 1µm movement of a macrophage Parameters ---------- state : State global simulation state cell : MacrophageCellData a macrophage cell voxel : Voxel current voxel position of the macrophage Returns ------- Point the new position of the macrophage """ # macrophages are attracted by MIP1b from nlisim.modules.mip1b import MIP1BState from nlisim.util import TissueType, activation_function macrophage: MacrophageState = state.macrophage mip1b: MIP1BState = state.mip1b grid: RectangularGrid = state.grid lung_tissue: np.ndarray = state.lung_tissue voxel_volume: float = state.voxel_volume # compute chemokine influence on velocity, with some randomness. # macrophage has a non-zero probability of moving into non-air voxels. # if not any of these, stay in place. This could happen if e.g. you are # somehow stranded in air. nearby_voxels: Tuple[Voxel, ...] = tuple(grid.get_adjacent_voxels(voxel, corners=True)) weights = np.array( [ 0.0 if lung_tissue[tuple(vxl)] == TissueType.AIR else activation_function( x=mip1b.grid[tuple(vxl)], k_d=mip1b.k_d, h=self.time_step / 60, # units: (min/step) / (min/hour) volume=voxel_volume, b=1, ) + macrophage.drift_bias for vxl in nearby_voxels ], dtype=np.float64, ) voxel_movement_direction: Voxel = choose_voxel_by_prob( voxels=nearby_voxels, default_value=voxel, weights=weights ) # get normalized direction vector dp_dt: np.ndarray = grid.get_voxel_center(voxel_movement_direction) - grid.get_voxel_center( voxel ) norm = np.linalg.norm(dp_dt) if norm > 0.0: dp_dt /= norm # average and re-normalize with existing velocity dp_dt += cell['velocity'] norm = np.linalg.norm(dp_dt) if norm > 0.0: dp_dt /= norm # we need to determine if this movement will put us into an air voxel. This can happen # when pushed there by momentum. If that happens, we stay in place and zero out the # momentum. Otherwise, velocity is updated to dp/dt and movement is as expected. new_position = cell['point'] + dp_dt new_voxel: Voxel = grid.get_voxel(new_position) if state.lung_tissue[tuple(new_voxel)] == TissueType.AIR: cell['velocity'][:] = np.zeros(3, dtype=np.float64) return cell['point'] else: cell['velocity'][:] = dp_dt return new_position
def update_status(self, state: State, macrophage_cell: MacrophageCellData, num_cells_in_phagosome) ‑> None
-
Update the status of the cell, progressing between states after a certain number of ticks.
Parameters
state
:State
- global simulation state
macrophage_cell
:MacrophageCellData
num_cells_in_phagosome
Returns
nothing
Expand source code
def update_status( self, state: State, macrophage_cell: MacrophageCellData, num_cells_in_phagosome ) -> None: """ Update the status of the cell, progressing between states after a certain number of ticks. Parameters ---------- state : State global simulation state macrophage_cell : MacrophageCellData num_cells_in_phagosome Returns ------- nothing """ macrophage: MacrophageState = state.macrophage if macrophage_cell['status'] == PhagocyteStatus.NECROTIC: # TODO: what about APOPTOTIC? self.release_phagosome(state, macrophage_cell) elif num_cells_in_phagosome > macrophage.max_conidia: # TODO: how do we get here? macrophage_cell['status'] = PhagocyteStatus.NECROTIC elif macrophage_cell['status'] == PhagocyteStatus.ACTIVE: if macrophage_cell['status_iteration'] >= macrophage.iter_to_rest: macrophage_cell['status_iteration'] = 0 macrophage_cell['tnfa'] = False macrophage_cell['status'] = PhagocyteStatus.RESTING else: macrophage_cell['status_iteration'] += 1 elif macrophage_cell['status'] == PhagocyteStatus.INACTIVE: if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state: macrophage_cell['status_iteration'] = 0 macrophage_cell['status'] = PhagocyteStatus.RESTING else: macrophage_cell['status_iteration'] += 1 elif macrophage_cell['status'] == PhagocyteStatus.ACTIVATING: if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state: macrophage_cell['status_iteration'] = 0 macrophage_cell['status'] = PhagocyteStatus.ACTIVE else: macrophage_cell['status_iteration'] += 1 elif macrophage_cell['status'] == PhagocyteStatus.INACTIVATING: if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state: macrophage_cell['status_iteration'] = 0 macrophage_cell['status'] = PhagocyteStatus.INACTIVE else: macrophage_cell['status_iteration'] += 1 elif macrophage_cell['status'] == PhagocyteStatus.ANERGIC: if macrophage_cell['status_iteration'] >= macrophage.iter_to_change_state: macrophage_cell['status_iteration'] = 0 macrophage_cell['status'] = PhagocyteStatus.RESTING else: macrophage_cell['status_iteration'] += 1
Inherited members
class MacrophageCellData (arg: Union[int, Iterable[ForwardRef('CellData')]], initialize: bool = False, **kwargs)
-
A low-level data contain for an array cells.
This class is a subtype of numpy.recarray containing the lowest level representation of a list of "cells" in a simulation. The underlying data format of this type are identical to a simple array of C structures with the fields given in the static "dtype" variable.
The base class contains only a single coordinate representing the location of the center of the cell. Most implementations will want to override this class to append more fields. Subclasses must also override the base implementation of
create_cell
to construct a single record containing the additional fields.For example, the following derived class adds an addition floating point value associated with each cell.
class DerivedCell(CellData): FIELDS = CellData.FIELDS + [ ('iron_content', 'f8') ] dtype = np.dtype(CellData.FIELDS, align=True) @classmethod def create_cell_tuple(cls, iron_content=0, **kwargs) -> Tuple: return CellData.create_cell_tuple(**kwargs) + (iron_content,)
Expand source code
class MacrophageCellData(PhagocyteCellData): MACROPHAGE_FIELDS: CellFields = [ ('status', np.uint8), ('state', np.uint8), ('fpn', bool), ('fpn_iteration', np.int64), ('tf', bool), # TODO: descriptive name, transferrin? ('tnfa', bool), ('iron_pool', np.float64), ('status_iteration', np.uint64), ('velocity', np.float64, 3), ] dtype = np.dtype( CellData.FIELDS + PhagocyteCellData.PHAGOCYTE_FIELDS + MACROPHAGE_FIELDS, align=True ) # type: ignore @classmethod def create_cell_tuple( cls, **kwargs, ) -> Tuple: initializer = { 'status': kwargs.get('status', PhagocyteStatus.RESTING), 'state': kwargs.get('state', PhagocyteState.FREE), 'fpn': kwargs.get('fpn', True), 'fpn_iteration': kwargs.get('fpn_iteration', 0), 'tf': kwargs.get('tf', False), 'tnfa': kwargs.get('tnfa', False), 'iron_pool': kwargs.get('iron_pool', 0.0), 'status_iteration': kwargs.get('status_iteration', 0), 'velocity': kwargs.get('root', np.zeros(3, dtype=np.float64)), } # ensure that these come in the correct order return PhagocyteCellData.create_cell_tuple(**kwargs) + tuple( [initializer[key] for key, *_ in MacrophageCellData.MACROPHAGE_FIELDS] )
Ancestors
- PhagocyteCellData
- CellData
- numpy.ndarray
Class variables
var MACROPHAGE_FIELDS : List[Union[Tuple[str, numpy.dtype], Tuple[str, Type[Any]], Tuple[str, Type[Any], int], Tuple[str, str], Tuple[str, str, int]]]
Inherited members
class MacrophageCellList (*, grid: RectangularGrid, max_cells: int = 1000000, cell_data: CellData = NOTHING)
-
A python view on top of a CellData array.
This class represents a pythonic interface to the data contained in a CellData array. Because the CellData class is a low-level object, it does not allow dynamically appending new elements. Objects of this class get around this limitation by pre-allocating a large block of memory that is transparently available. User-facing properties are sliced to make it appear as if the extra data is not there.
Subclassed types are expected to set the
CellDataClass
attribute to a subclass ofCellData
. This provides information about the underlying low-level array.Parameters
grid
:simulation.grid.RectangularGrid
max_cells
:int
, optionalcells
:simulation.cell.CellData
, optional
Method generated by attrs for class MacrophageCellList.
Expand source code
class MacrophageCellList(CellList): CellDataClass = MacrophageCellData
Ancestors
Class variables
var grid : RectangularGrid
var max_cells : int
Inherited members
class MacrophageState (*, global_state: State, cells: MacrophageCellList = NOTHING)
-
Base type intended to store the state for simulation modules.
This class contains serialization support for basic types (float, int, str, bool) and numpy arrays of those types. Modules containing more complicated state must override the serialization mechanism with custom behavior.
Method generated by attrs for class MacrophageState.
Expand source code
class MacrophageState(PhagocyteModuleState): cells: MacrophageCellList = attr.ib(default=attr.Factory(cell_list_factory, takes_self=True)) time_to_rest: float # units: min iter_to_rest: int # units: steps time_to_change_state: float # units: hours iter_to_change_state: int # units: steps ma_internal_iron: float # units: atto-mols prob_death_per_timestep: float # units: probability * step^-1 max_ma: int # units: count min_ma: int # units: count init_num_macrophages: int # units: count recruitment_rate: float rec_bias: float drift_bias: float ma_move_rate_act: float # µm/min ma_move_rate_rest: float # µm/min half_life: float # units: hours # UNUSED: # kd_ma_iron: float # ma_vol: float # units: pL
Ancestors
Class variables
var cells : MacrophageCellList
var drift_bias : float
var half_life : float
var init_num_macrophages : int
var iter_to_change_state : int
var iter_to_rest : int
var ma_internal_iron : float
var ma_move_rate_act : float
var ma_move_rate_rest : float
var max_ma : int
var min_ma : int
var prob_death_per_timestep : float
var rec_bias : float
var recruitment_rate : float
var time_to_change_state : float
var time_to_rest : float
Inherited members