Skip to content

Commit

Permalink
fix: parallel arrays (#903)
Browse files Browse the repository at this point in the history
* fix: check packing on parallel array placement.

* fix: change modulo
  • Loading branch information
drodarie authored Dec 17, 2024
1 parent a373f6e commit e65bdd6
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 14 deletions.
46 changes: 32 additions & 14 deletions bsb/placement/arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from .. import config
from ..config import types
from ..exceptions import ConfigurationError, PackingError
from ..mixins import NotParallel
from ..reporting import report
from .strategy import PlacementStrategy
Expand All @@ -12,39 +13,46 @@
@config.node
class ParallelArrayPlacement(NotParallel, PlacementStrategy):
"""
Implementation of the placement of cells in parallel arrays.
Implementation of the placement of cells in parallel arrays
Cells are placed in rows on the plane defined by 2 selected axes.
"""

spacing_x: float = config.attr(type=float, required=True)
spacing_x: float = config.attr(type=types.float(min=0), required=True)
"""Space in between two cells along the main axis"""
angle: float = config.attr(type=types.deg_to_radian(), required=True)
"""Angle between the second axis and the axis of the rows of cells"""

def boot(self):
if self.angle % (np.pi) == np.pi / 2:
raise ConfigurationError(
f"Parallel array angle should be not a multiple of pi/2 for '{self.name}'. Provided angle: {self.angle}"
)

def place(self, chunk, indicators):
"""
Cell placement: Create a lattice of parallel arrays/lines in the layer's surface.
Cell placement: Create a lattice of parallel arrays/lines in the (x, y) 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.
x_shift = np.random.rand() * self.spacing_x
# Place cells equally spaced over the entire length of the X axis kept apart by the provided space.
# 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
x_pos = np.arange(start=0.0, stop=width, step=self.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
# 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)
n += int((n_arrays * self.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
Expand All @@ -60,11 +68,15 @@ def place(self, chunk, indicators):
# 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
lattice_x = n_arrays * self.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
# Epsilon: open space in the unit cell along the Y axis
epsilon = y_axis_distance / math.cos(self.angle) - radius * 2
if epsilon < 0:
raise PackingError(
f"Not enough space between cells placed on the same row for '{self.name}'."
)
# Storage array for the cells
cells = np.empty((cells_placed, 3))
for i in range(y_pos.shape[0]):
Expand All @@ -75,7 +87,13 @@ def place(self, chunk, indicators):
# 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)
y = (
ldc[1]
+ y_pos[i]
+ epsilon
* (np.random.rand(x.shape[0]) - 0.5)
* math.cos(self.angle)
)
# 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
Expand Down
34 changes: 34 additions & 0 deletions tests/test_placement.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from bsb import (
MPI,
BootError,
CellType,
Chunk,
Configuration,
Expand Down Expand Up @@ -371,6 +372,39 @@ def test_parallel_arrays(self):
self.assertAll(pos[:, 1] <= cfg.partitions.test_layer.data.mdc[1], "not in layer")
self.assertAll(pos[:, 1] >= cfg.partitions.test_layer.data.ldc[1], "not in layer")

def test_packed_arrays(self):
cfg = get_test_config("single")
network = Scaffold(cfg, self.storage)
cfg.placement["test_placement"] = dict(
strategy="bsb.placement.ParallelArrayPlacement",
cell_types=["test_cell"],
partitions=["test_layer"],
spacing_x=150,
angle=0,
)
with self.assertRaises(WorkflowError):
network.compile(clear=True)

def test_wrong_angles(self):
cfg = get_test_config("single")
network = Scaffold(cfg, self.storage)
with self.assertRaises(BootError):
cfg.placement["test_placement"] = dict(
strategy="bsb.placement.ParallelArrayPlacement",
cell_types=["test_cell"],
partitions=["test_layer"],
spacing_x=50,
angle=90,
)
with self.assertRaises(BootError):
cfg.placement["test_placement"] = dict(
strategy="bsb.placement.ParallelArrayPlacement",
cell_types=["test_cell"],
partitions=["test_layer"],
spacing_x=50,
angle=-450,
)

def test_regression_issue_889(self):
cfg = Configuration.default(
regions={
Expand Down

0 comments on commit e65bdd6

Please sign in to comment.