Skip to content

Commit

Permalink
feat: Stack regions and Layer partitions (#868)
Browse files Browse the repository at this point in the history
* fix: Stack regions now orders the partition according to stack index if Layers or position in children array

* fix: Change origin of stack regions based on the origin of the first child in the ordered stack

* feat: add stack_order and anchor to Stack

- Remove stack_index from Layer and related configurations

* fix: drop stack_order and fix documentation

* fix: drop anchor ``backref``, add doc and fix test

* docs: add documentation for Region and Partition classes

* fix: bump bsb-test version

* fix: remove volume_scale from Layer.

Small pass on core.py documentation

* fix: test_cache_survival add delay to read cache items

* docs: update bsb/core.py

Co-authored-by: Robin De Schepper <[email protected]>

---------

Co-authored-by: Robin De Schepper <[email protected]>
  • Loading branch information
drodarie and Helveg authored Sep 13, 2024
1 parent 81b6677 commit 9f4046a
Show file tree
Hide file tree
Showing 17 changed files with 307 additions and 71 deletions.
23 changes: 15 additions & 8 deletions bsb/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ def get_placement(

def get_placement_of(self, *cell_types):
"""
Find all of the placement strategies that given certain cell types.
Find all the placement strategies that involve the given cell types.
:param cell_types: Cell types (or their names) of interest.
:type cell_types: Union[~bsb.cell_types.CellType, str]
Expand All @@ -540,13 +540,15 @@ def get_placement_set(
"""
Return a cell type's placement set from the output formatter.
:param tag: Unique identifier of the placement set in the storage
:type tag: str
:returns: A placement set
:param type: Cell type name
:type type: Union[~bsb.cell_types.CellType, str]
:param chunks: Optionally load a specific list of chunks.
:type chunks: list[tuple[float, float, float]]
:param labels: Labels to filter the placement set by.
:type labels: list[str]
:param morphology_labels: Subcellular labels to apply to the morphologies.
:type morphology_labels: list[str]
:returns: A placement set
:rtype: :class:`~.storage.interfaces.PlacementSet`
"""
if isinstance(type, str):
Expand All @@ -557,7 +559,7 @@ def get_placement_set(

def get_placement_sets(self) -> typing.List["PlacementSet"]:
"""
Return all of the placement sets present in the network.
Return all the placement sets present in the network.
:rtype: List[~bsb.storage.interfaces.PlacementSet]
"""
Expand All @@ -581,9 +583,8 @@ def get_connectivity_sets(self) -> typing.List["ConnectivitySet"]:
"""
Return all connectivity sets from the output formatter.
:param tag: Unique identifier of the connectivity set in the output formatter
:type tag: str
:returns: All connectivity sets
:rtype: List[:class:`~.storage.interfaces.ConnectivitySet`]
"""
return [self._load_cs_types(cs) for cs in self.storage.get_connectivity_sets()]

Expand All @@ -594,10 +595,16 @@ def require_connectivity_set(self, pre, post, tag=None) -> "ConnectivitySet":

def get_connectivity_set(self, tag=None, pre=None, post=None) -> "ConnectivitySet":
"""
Return a connectivity set from the output formatter.
Return a connectivity set from its name according to the output formatter.
The name can be specified directly with tag or with deduced from pre and post
if there is only one connectivity set matching this pair.
:param tag: Unique identifier of the connectivity set in the output formatter
:type tag: str
:param pre: Presynaptic cell type
:type pre: ~bsb.cell_types.CellType
:param post: Postsynaptic cell type
:type post: ~bsb.cell_types.CellType
:returns: A connectivity set
:rtype: :class:`~.storage.interfaces.ConnectivitySet`
"""
Expand Down
56 changes: 32 additions & 24 deletions bsb/topology/partition.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,6 @@
from ..core import Scaffold


def _size_requirements(section):
if "thickness" not in section and "volume_scale" not in section:
raise RequirementError(
"Either a `thickness` or `volume_scale` attribute required"
)


class _backref_property(property):
def __backref__(self, instance, value):
setattr(instance, "_region", value)
Expand All @@ -49,6 +42,10 @@ def __backref__(self, instance, value):
auto_classmap=True,
)
class Partition(abc.ABC):
"""
Abstract class to describe spatial containers for network pieces.
"""

scaffold: "Scaffold"
name: str = config.attr(key=True)

Expand Down Expand Up @@ -162,12 +159,21 @@ def get_layout(self, hint): # pragma: nocover

@config.node
class Rhomboid(Partition, classmap_entry="rhomboid"):
"""
Rectangular cuboid partition defined according to its origin and dimensions.
"""

dimensions: list[float] = config.attr(type=types.list(type=float, size=3))
"""Sizes of the partition for each axis."""
can_scale: bool = config.attr(type=bool, default=True)
"""Boolean flag to authorize rescaling of the partition dimensions"""
origin: list[float] = config.attr(type=types.list(type=float, size=3))
"""Coordinate of the origin of the partition"""
can_move: bool = config.attr(type=bool, default=True)
"""Boolean flag to authorize the translation of the partition"""
orientation: list[float] = config.attr(type=types.list(type=float, size=3))
can_rotate: bool = config.attr(type=bool, default=True)
"""Boolean flag to authorize the rotation of the partition"""

def volume(self, chunk=None):
if chunk is not None:
Expand All @@ -180,10 +186,16 @@ def volume(self, chunk=None):

@property
def mdc(self):
"""
Return the highest coordinate of the partition.
"""
return self._data.mdc

@property
def ldc(self):
"""
Return the lowest coordinate of the partition.
"""
return self._data.ldc

def surface(self, chunk=None):
Expand Down Expand Up @@ -215,7 +227,7 @@ def chunk_to_voxels(self, chunk):
Return an approximation of this partition intersected with a chunk as a list of
voxels.
Default implementation creates a parallellepepid intersection between the
Default implementation creates a parallelepiped intersection between the
LDC, MDC and chunk data.
"""
low = np.maximum(self.ldc, chunk.ldc)
Expand Down Expand Up @@ -246,33 +258,29 @@ def get_layout(self, hint):
if self.dimensions is None:
dim = hint.data.mdc - hint.data.ldc
else:
dim = self.dimensions
dim = np.array(self.dimensions)
if self.origin is None:
orig = hint.data.ldc.copy()
else:
orig = self.origin
orig = np.array(self.origin)
return Layout(RhomboidData(orig, dim + orig), owner=self)


@config.node
class Layer(Rhomboid, classmap_entry="layer"):
"""
Partition that occupies the full space of its containing region
except on a defined axis, where it is limited. This creates a stratum
within the region along the chosen axis.
"""

dimensions = config.unset()
thickness: float = config.attr(type=float, required=_size_requirements)
volume_scale: list[float] = config.attr(
type=types.or_(
types.list(float, size=2),
types.scalar_expand(
float,
lambda x: [x, x],
),
),
default=lambda: [1.0, 1.0],
call_default=True,
)
thickness: float = config.attr(type=float, required=True)
"""Thickness of the layer along its axis"""
axis: typing.Union[typing.Literal["x"], typing.Literal["y"], typing.Literal["z"]] = (
config.attr(type=types.in_(["x", "y", "z"]), default="z")
)
stack_index: float = config.attr(type=float, default=0)
"""Axis along which the layer will be limited."""

def get_layout(self, hint):
axis = ["x", "y", "z"].index(self.axis)
Expand All @@ -281,7 +289,7 @@ def get_layout(self, hint):
if self.origin is None:
orig = hint.data.ldc.copy()
else:
orig = self.origin
orig = np.array(self.origin)
return Layout(RhomboidData(orig, dim + orig), owner=self)

# TODO: Layer scaling
Expand Down
38 changes: 32 additions & 6 deletions bsb/topology/region.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from .. import config
from ..config import refs, types
from ..exceptions import ConfigurationError
from ..reporting import warn
from ._layout import Layout

Expand All @@ -32,6 +33,7 @@ class Region(abc.ABC):
children: list[typing.Union["Region", "Partition"]] = config.reflist(
refs.regional_ref, backref="region", required=True
)
"""Reference to Regions or Partitions belonging to this region."""

@property
def data(self):
Expand Down Expand Up @@ -68,6 +70,12 @@ def scale(self, factors): # pragma: nocover

@config.node
class RegionGroup(Region, classmap_entry="group"):
"""
Base implementation of Region.
Any transformation on the region will be directly
applied to its children (Regions or Partitions).
"""

def rotate(self, rotation):
for child in self.children:
child.rotate(rotation)
Expand All @@ -84,34 +92,52 @@ def scale(self, factors):
@config.node
class Stack(RegionGroup, classmap_entry="stack"):
"""
Stack components on top of each other based on their ``stack_index`` and adjust its
own height accordingly.
Stack components on top of each other and adjust its own height accordingly.
"""

axis: typing.Union[typing.Literal["x"], typing.Literal["y"], typing.Literal["z"]] = (
config.attr(type=types.in_(["x", "y", "z"]), default="z")
)
"""Axis along which the stack's children will be stacked"""
anchor: typing.Union["Region", "Partition"] = config.ref(refs.regional_ref)
"""Reference to one child of the stack, which origin will become the origin of the stack"""

def _resolve_anchor_offset(self, children, axis_idx):
"""
Check if the anchor is one of the children of the stack and
if so, return the offset so the anchor is at the origin of the stack.
"""
children_owners = [child._owner for child in children]
if self.anchor is not None and self.anchor in children_owners:
index = children_owners.index(self.anchor)
return children[index].data.ldc[axis_idx] - sum(
children[i].data.dimensions[axis_idx] for i in range(index)
)
else:
# if anchor is not defined or one of the children
# then the origin of the stack corresponds to the origin of the first child
return children[0].data.ldc[axis_idx]

def get_layout(self, hint):
layout = super().get_layout(hint)
stack_size = 0
axis_idx = ("x", "y", "z").index(self.axis)
trans_eye = np.zeros(3)
trans_eye[axis_idx] = 1

cumul_offset = self._resolve_anchor_offset(layout.children, axis_idx)
for child in layout.children:
if child.data is None:
warn(f"Skipped layout arrangement of {child._owner.name} in {self.name}")
continue
translation = (
layout.data.ldc[axis_idx] + stack_size - child.data.ldc
layout.data.ldc[axis_idx] + cumul_offset - child.data.ldc
) * trans_eye
if not np.allclose(0, translation):
child.propose_translate(translation)
stack_size += child.data.dimensions[axis_idx]
cumul_offset += child.data.dimensions[axis_idx]
ldc = layout.data.ldc
mdc = layout.data.mdc
mdc[axis_idx] = ldc[axis_idx] + stack_size
mdc[axis_idx] = ldc[axis_idx] + cumul_offset
return layout

def rotate(self, rotation):
Expand Down
6 changes: 2 additions & 4 deletions docs/getting-started/getting-started.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,11 @@
"partitions": {
"base_layer": {
"type": "layer",
"thickness": 100,
"stack_index": 0
"thickness": 100
},
"top_layer": {
"type": "layer",
"thickness": 100,
"stack_index": 1
"thickness": 100
}
},
"cell_types": {
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/getting-started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Connectivity

.. literalinclude:: getting-started.json
:language: json
:lines: 56-66
:lines: 54-64

.. literalinclude:: getting_started.py
:language: python
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/getting_started.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
bsb.options.verbosity = 3
config = from_json("network_configuration.json")

config.partitions.add("top_layer", thickness=100, stack_index=1)
config.partitions.add("top_layer", thickness=100)
config.regions.add(
"brain_region",
type="stack",
Expand Down
6 changes: 2 additions & 4 deletions docs/getting-started/include_morphos.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,11 @@
"partitions": {
"base_layer": {
"type": "layer",
"thickness": 100,
"stack_index": 0
"thickness": 100
},
"top_layer": {
"type": "layer",
"thickness": 100,
"stack_index": 1
"thickness": 100
}
},
"cell_types": {
Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started/include_morphos.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
bsb.options.verbosity = 3
config = from_json("network_configuration.json")

config.partitions.add("top_layer", thickness=100, stack_index=1)
config.partitions.add("top_layer", thickness=100)
config.regions["brain_region"] = Stack(
children=[
"base_layer",
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started/include_morphos.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ connection strategies such as :class:`~.connectivity.detailed.voxel_intersection

.. literalinclude:: include_morphos.yaml
:language: yaml
:lines: 56-64
:lines: 54-62

.. literalinclude:: include_morphos.json
:language: json
:lines: 74-84
:lines: 72-82

.. literalinclude:: include_morphos.py
:language: python
Expand Down
2 changes: 0 additions & 2 deletions docs/getting-started/include_morphos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,9 @@ partitions:
base_layer:
type: layer
thickness: 100
stack_index: 0
top_layer:
type: layer
thickness: 100
stack_index: 1
cell_types:
base_type:
spatial:
Expand Down
Loading

0 comments on commit 9f4046a

Please sign in to comment.