import math
import numpy as np
from .. import config
from ..config import types
from ..mixins import NotParallel
from ..reporting import report
from .strategy import PlacementStrategy
[docs]
@config.node
class ParallelArrayPlacement(NotParallel, PlacementStrategy):
"""
Implementation of the placement of cells in parallel arrays.
"""
spacing_x: float = config.attr(type=float, required=True)
angle: float = config.attr(type=types.deg_to_radian(), required=True)
[docs]
def place(self, chunk, indicators):
"""
Cell placement: Create a lattice of parallel arrays/lines in the layer's surface.
"""
for indicator in indicators.values():
cell_type = indicator.cell_type
radius = indicator.get_radius()
for prt in self.partitions:
width, depth, height = prt.data.mdc - prt.data.ldc
ldc = prt.data.ldc
# Extension of a single array in the X dimension
spacing_x = self.spacing_x
# Add a random shift to the starting points of the arrays for variation.
x_shift = np.random.rand() * spacing_x
# Place purkinje cells equally spaced over the entire length of the X axis kept apart by their dendritic trees.
# They are placed in straight lines, tilted by a certain angle by adding a shifting value.
x_pos = np.arange(start=0.0, stop=width, step=spacing_x) + x_shift
if x_pos.shape[0] == 0:
# When the spacing_x of is larger than the simulation volume,
# place a single row on a random position along the x axis
x_pos = np.array([x_shift])
# Amount of parallel arrays of cells
n_arrays = x_pos.shape[0]
# Number of cells
n = np.sum(indicator.guess(prt.data))
# Add extra cells to fill the lattice error volume which will be pruned
n += int((n_arrays * spacing_x % width) / width * n)
# cells to distribute along the rows
cells_per_row = round(n / n_arrays)
# The rounded amount of cells that will be placed
cells_placed = cells_per_row * n_arrays
# Calculate the position of the cells along the z-axis.
y_pos, y_axis_distance = np.linspace(
start=0.0,
stop=depth - radius,
num=cells_per_row,
retstep=True,
endpoint=False,
)
# Center the cell soma center to the middle of the unit cell
y_pos += radius + y_axis_distance / 2
# The length of the X axis rounded up to a multiple of the unit cell size.
lattice_x = n_arrays * spacing_x
# The length of the X axis where cells can be placed in.
bounded_x = lattice_x - radius * 2
# Epsilon: open space in the unit cell along the z-axis
epsilon = y_axis_distance - radius * 2
# Storage array for the cells
cells = np.empty((cells_placed, 3))
for i in range(y_pos.shape[0]):
# Shift the arrays at an angle
angleShift = y_pos[i] * math.tan(self.angle)
# Apply shift and offset
x = x_pos + angleShift
# Place the cells in a bounded lattice with a little modulus magic
x = ldc[0] + x % bounded_x + radius
# Place the cells in their y-position with jitter
y = ldc[1] + y_pos[i] + epsilon * (np.random.rand(x.shape[0]) - 0.5)
# Place them at a uniformly random height throughout the partition.
z = ldc[2] + np.random.uniform(radius, height - radius, x.shape[0])
# Store this stack's cells
cells[(i * len(x)) : ((i + 1) * len(x)), 0] = x
cells[(i * len(x)) : ((i + 1) * len(x)), 1] = y
cells[(i * len(x)) : ((i + 1) * len(x)), 2] = z
# Place all the cells in 1 batch (more efficient)
positions = cells[cells[:, 0] < width - radius]
# Determine in which chunks the cells must be placed
cs = self.scaffold.configuration.network.chunk_size
chunks_list = np.array(
[chunk.data + np.floor_divide(p, cs[0]) for p in positions]
)
unique_chunks_list = np.unique(chunks_list, axis=0)
# For each chunk, place the cells
for c in unique_chunks_list:
idx = np.where((chunks_list == c).all(axis=1))
pos_current_chunk = positions[idx]
self.place_cells(indicator, pos_current_chunk, chunk=c)
report(f"Placed {len(positions)} {cell_type.name} in {prt.name}", level=3)
__all__ = ["ParallelArrayPlacement"]