Skip to content

Commit

Permalink
Merge branch 'main' into New_docs
Browse files Browse the repository at this point in the history
  • Loading branch information
drodarie authored Jan 7, 2025
2 parents d51b0ac + b27b1da commit 07e30cc
Show file tree
Hide file tree
Showing 10 changed files with 203 additions and 37 deletions.
13 changes: 0 additions & 13 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,16 +157,3 @@ jobs:
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

deploy_ebrains:
runs-on: ubuntu-latest
needs: release
environment: EBRAINS
steps:
- name: Mirror to EBRAINS Gitlab
uses: wei/git-sync@v3
with:
source_repo: dbbs-lab/bsb-core
source_branch: main
destination_repo: https://push:${{ secrets.EBRAINS_GITLAB_ACCESS_TOKEN }}@gitlab.ebrains.eu/robinde/bsb-core.git
destination_branch: main
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
## [v4.5.5] - 2024-12-17
### :bug: Bug Fixes
- [`e65bdd6`](https://github.com/dbbs-lab/bsb-core/commit/e65bdd6367c1e2547cb519143ccccb593963dad4) - parallel arrays *(PR [#903](https://github.com/dbbs-lab/bsb-core/pull/903) by [@drodarie](https://github.com/drodarie))*


## [v4.5.4] - 2024-12-16
### :bug: Bug Fixes
- [`516dc63`](https://github.com/dbbs-lab/bsb-core/commit/516dc631afcb77dbbc3c90c770af20b2f129e6d9) - Update configuration to store whenever an attribute is set *(PR [#900](https://github.com/dbbs-lab/bsb-core/pull/900) by [@filimarc](https://github.com/filimarc))*
- :arrow_lower_right: *fixes issue [#899](https://github.com/dbbs-lab/bsb-core/issues/899) opened by [@drodarie](https://github.com/drodarie)*
- [`5799cf8`](https://github.com/dbbs-lab/bsb-core/commit/5799cf848944207cbf21d7183b75c78f678bbb38) - postprocessing *(PR [#901](https://github.com/dbbs-lab/bsb-core/pull/901) by [@drodarie](https://github.com/drodarie))*
- :arrow_lower_right: *fixes issue [#887](https://github.com/dbbs-lab/bsb-core/issues/887) opened by [@drodarie](https://github.com/drodarie)*


## [v4.5.3] - 2024-10-29
### :bug: Bug Fixes
- [`0dac469`](https://github.com/dbbs-lab/bsb-core/commit/0dac46937c26a078b8b92864321919d8ed2ccac7) - mpi comm *(PR [#896](https://github.com/dbbs-lab/bsb-core/pull/896) by [@drodarie](https://github.com/drodarie))*
Expand Down Expand Up @@ -480,3 +493,5 @@ MorphologyRepositories, morphologies, voxelization and touch detection.
[v4.5.1]: https://github.com/dbbs-lab/bsb-core/compare/v4.5.0...v4.5.1
[v4.5.2]: https://github.com/dbbs-lab/bsb-core/compare/v4.5.1...v4.5.2
[v4.5.3]: https://github.com/dbbs-lab/bsb-core/compare/v4.5.2...v4.5.3
[v4.5.4]: https://github.com/dbbs-lab/bsb-core/compare/v4.5.3...v4.5.4
[v4.5.5]: https://github.com/dbbs-lab/bsb-core/compare/v4.5.4...v4.5.5
2 changes: 1 addition & 1 deletion bsb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
install the `bsb` package instead.
"""

__version__ = "4.5.3"
__version__ = "4.5.5"

import functools
import importlib
Expand Down
2 changes: 2 additions & 0 deletions bsb/config/_attrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,7 @@ def __init__(self, *args, size=None, **kwargs):

def __set__(self, instance, value, _key=None):
_setattr(instance, self.attr_name, self.fill(value, _parent=instance))
self.flag_dirty(instance)

def __populate__(self, instance, value, unique_list=False):
cfglist = _getattr(instance, self.attr_name)
Expand Down Expand Up @@ -755,6 +756,7 @@ def add(self, key, *args, **kwargs):
f"{self.get_node_name()} already contains '{key}'."
+ " Use `node[key] = value` if you want to overwrite it."
)
self._config_attr.flag_dirty(self._config_parent)
self[key] = value = self._elem_type(*args, _parent=self, _key=key, **kwargs)
return value

Expand Down
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
16 changes: 8 additions & 8 deletions bsb/postprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ class AfterPlacementHook(abc.ABC):
name: str = config.attr(key=True)

def queue(self, pool):
pool.queue(
lambda scaffold: scaffold.after_placement[self.name].postprocess(),
submitter=self,
)
def static_function(scaffold, name):
return scaffold.after_placement[name].postprocess()

pool.queue(static_function, (self.name,), submitter=self)

@abc.abstractmethod
def postprocess(self):
Expand All @@ -28,10 +28,10 @@ class AfterConnectivityHook(abc.ABC):
name: str = config.attr(key=True)

def queue(self, pool):
pool.queue(
lambda scaffold: scaffold.after_connectivity[self.name].postprocess(),
submitter=self,
)
def static_function(scaffold, name):
return scaffold.after_connectivity[name].postprocess()

pool.queue(static_function, (self.name,), submitter=self)

@abc.abstractmethod
def postprocess(self):
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ line-length = 90
profile = "black"

[tool.bumpversion]
current_version = "4.5.3"
current_version = "4.5.5"
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
serialize = ["{major}.{minor}.{patch}"]
search = "{current_version}"
Expand Down
23 changes: 23 additions & 0 deletions tests/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -1609,6 +1609,29 @@ def test_booted_root(self):
Scaffold(cfg, self.storage)
self.assertIsNotNone(_attrs._booted_root(cfg), "now it should be booted")

def test_updates(self):
"""Test if tree is updated correctly"""
cfg = Configuration.default()
cfg.morphologies = ["dummy_neuron.swc"]
cfg.partitions.add("base_layer", thickness=100)
cfg.partitions.add("top_layer", thickness=100)
cfg.regions.add(
"brain_region",
type="stack",
children=[
"base_layer",
"top_layer",
],
)
cfg_dict = cfg.__tree__()
self.assertEqual(cfg_dict["morphologies"], ["dummy_neuron.swc"])
self.assertIn("brain_region", cfg_dict["regions"])
cfg.morphologies.pop(0)
cfg.regions.pop("brain_region")
cfg_dict = cfg.__tree__()
self.assertEqual(cfg_dict["morphologies"], [])
self.assertEqual(cfg_dict["regions"], {})


class TestNodeClass(unittest.TestCase):
def test_standalone_node_name(self):
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
87 changes: 87 additions & 0 deletions tests/test_postprocessing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import os
import unittest

from bsb_test import RandomStorageFixture

from bsb import (
MPI,
AfterConnectivityHook,
AfterPlacementHook,
Configuration,
Scaffold,
config,
)


class TestAfterConnectivityHook(
RandomStorageFixture, unittest.TestCase, engine_name="hdf5"
):
def setUp(self):
super().setUp()

@config.node
class TestAfterConn(AfterConnectivityHook):
def postprocess(self):
with open(f"test_after_conn_{MPI.get_rank()}.txt", "a") as f:
# make sure we have access to the scaffold context
f.write(f"{self.scaffold.configuration.name}\n")

self.network = Scaffold(
config=Configuration.default(
name="Test config",
after_connectivity={"test_after_conn": TestAfterConn()},
),
storage=self.storage,
)

def test_after_connectivity_job(self):
self.network.compile()
if MPI.get_rank() == 0:
count_files = 0
for filename in os.listdir():
if filename.startswith("test_after_conn_"):
count_files += 1
with open(filename, "r") as f:
lines = f.readlines()
self.assertEqual(
len(lines), 1, "The postprocess should be called only once."
)
self.assertEqual(lines[0], "Test config\n")
os.remove(filename)
self.assertEqual(count_files, 1)


class TestAfterPlacementHook(RandomStorageFixture, unittest.TestCase, engine_name="hdf5"):
def setUp(self):
super().setUp()

@config.node
class TestAfterPlace(AfterPlacementHook):
def postprocess(self):
with open(f"test_after_place_{MPI.get_rank()}.txt", "a") as f:
# make sure we have access to the scaffold context
f.write(f"{self.scaffold.configuration.name}\n")

self.network = Scaffold(
config=Configuration.default(
name="Test config",
after_placement={"test_after_placement": TestAfterPlace()},
),
storage=self.storage,
)

def test_after_placement_job(self):
self.network.compile()
if MPI.get_rank() == 0:
count_files = 0
for filename in os.listdir():
if filename.startswith("test_after_place_"):
count_files += 1
with open(filename, "r") as f:
lines = f.readlines()
self.assertEqual(
len(lines), 1, "The postprocess should be called only once."
)
self.assertEqual(lines[0], "Test config\n")
os.remove(filename)
self.assertEqual(count_files, 1)

0 comments on commit 07e30cc

Please sign in to comment.