Source code for neurorosettes.subcellular

"""This module deals with the two components of the neurons: soma cells and neurites"""
from typing import Tuple
from dataclasses import dataclass

import numpy as np

from neurorosettes import physics
from neurorosettes.utilities import Animator


[docs]class CellBody: """ Class to represent a cell body with physical properties. Parameters ---------- position The coordinates of the cell body centre. mechanics The physical properties of the sphere that defines the cell body object. """ def __init__( self, position: np.ndarray, mechanics: physics.PhysicalProperties ) -> None: self.position = position self.mechanics = mechanics self.force = np.zeros(3) self.force_from_daughter = np.zeros(3) self.force_from_neighbors = np.zeros(3) self.displacement = np.zeros(3) self.sphere = None
[docs] def set_center_position(self, coordinates: np.ndarray) -> None: """ Sets the cell position based on the given coordinates. Parameters ---------- coordinates The cocordinates of the cell body centre. """ self.position[0] = coordinates[0] self.position[1] = coordinates[1] self.position[2] = coordinates[2]
[docs] def set_force_from_daughter(self, force: np.ndarray) -> None: """ Sets the force transmitted by the cell's daughter, if it exists. Parameters ---------- force The force components of the force transmitted by the daughter object. """ self.force_from_daughter[0] = force[0] self.force_from_daughter[1] = force[1] self.force_from_daughter[2] = force[2]
[docs] def set_mechanics(self, mechanics: physics.PhysicalProperties) -> None: """Sets the physical properties of the cell""" self.mechanics = mechanics
[docs] def set_sphere_representation(self, animator: Animator, color="red") -> None: """Creates and sets the sphere representation of the cell""" self.sphere = animator.draw_sphere( self.position, self.mechanics.radius, c=color )
[docs] def update_representation(self) -> None: """Updates the sphere representation of the cell""" self.sphere.pos(self.position)
[docs] def get_neighbor_force( self, neighbor: "CellBody", interaction: physics.ContactForces ) -> np.ndarray: """Returns the interaction force between two cells""" # Compute the vector that connects the centers of the two cells distance_vector, norm = physics.get_distance_components( neighbor.position, self.position ) # Calculate cell-cell adhesion forces magnitude = interaction.compute_adhesion( distance=norm, radius1=self.mechanics.interaction_radius, radius2=neighbor.mechanics.interaction_radius, ) magnitude -= interaction.compute_repulsion( norm, self.mechanics.radius, neighbor.mechanics.radius, ) return magnitude * distance_vector
[docs]class Neurite: """ Class to represent a neurite with physical properties. Parameters ---------- proximal_point The coordinates of the neurite's proximal point. axis The axis of growth used to calculate the neurite's distal point. cylinder_mechanics The physical properties of the sphere that defines the cell body object. """ def __init__( self, proximal_point: np.ndarray, axis: np.ndarray, cylinder_mechanics: physics.CylinderProperties, ): """Initializes the neurite""" self.proximal_point = proximal_point self.distal_point = proximal_point + axis * cylinder_mechanics.default_length self.mechanics = cylinder_mechanics self.cylinder = None self.force = np.zeros(3) self.force_from_daughter = np.zeros(3) self.displacement = np.zeros(3)
[docs] def set_force_from_daughter(self, force): """ Sets the force transmitted by the cell's daughter, if it exists. Parameters ---------- force The force components of the force transmitted by the daughter object. """ self.force_from_daughter = force
[docs] def create_neurite_representation(self, animator: Animator): """Creates a spring+sphere representation of the neurite.""" spring, sphere = animator.draw_spring( self.proximal_point, self.distal_point, self.mechanics.radius ) self.cylinder = (spring, sphere)
[docs] def update_representation(self): """Updates the Animator representation of the neurite.""" self.cylinder[0].stretch(self.proximal_point, self.distal_point) self.cylinder[1].pos(self.distal_point)
@property def tension(self) -> float: """Returns the tension of the spring.""" return self.mechanics.get_spring_tension(self.current_length) @property def spring_axis(self) -> np.ndarray: """Returns the vector that defines the spring.""" return self.distal_point - self.proximal_point @property def current_length(self) -> float: """Returns the current length of the spring.""" return np.linalg.norm(self.spring_axis)
[docs] def move_distal_point(self, coordinates: np.ndarray) -> None: """Moves the distal point of the spring to the given position.""" self.distal_point = coordinates
[docs] def move_proximal_point(self, coordinates) -> None: """Moves the proximal point of the spring to the given position.""" self.proximal_point = coordinates
[docs] def get_growth_force(self, magnitude: float): """Returns the force created by neurite growth""" return magnitude * self.spring_axis
[docs] def get_spring_force(self) -> np.ndarray: """Returns the force created by spring tension""" return -self.tension / self.current_length * self.spring_axis
[docs] def get_cell_neighbor_force( self, neighbor: CellBody, interaction: physics.ContactForces ) -> Tuple[np.ndarray, float]: """ Returns the interaction force between the neurite and a CellBody. Parameters ---------- neighbor The CellBody with which the object is interacting. interactions The contact force to be used to calculate the interaction force. Returns ------- np.ndarray The components of the interaction force between the two objects. float The fraction of the force to be applied to the distal point. """ # Get the point on the cylinder axis closest to the sphere point = physics.get_sphere_cylinder_intersection( center=neighbor.position, base=self.proximal_point, top=self.distal_point ) # Calculate the distance between the two closest points distance_to_point = np.linalg.norm(np.subtract(point, self.proximal_point)) fraction = distance_to_point / self.current_length distance_vector, norm = physics.get_distance_components( neighbor.position, point ) # Calculate cell-cell adhesion forces magnitude = interaction.compute_adhesion( distance=norm, radius1=self.mechanics.interaction_radius, radius2=neighbor.mechanics.interaction_radius, ) magnitude -= interaction.compute_repulsion( norm, self.mechanics.radius, neighbor.mechanics.radius, ) return magnitude * distance_vector, fraction
[docs] def get_neurite_neighbor_force( self, neighbor: "Neurite", interaction: physics.ContactForces ) -> Tuple[np.ndarray, float]: """ Returns the interaction force between the neurite and another Neurite. Parameters ---------- neighbor The other Neurite with which the object is interacting. interactions The contact force to be used to calculate the interaction force. Returns ------- np.ndarray The components of the interaction force between the two objects. float The fraction of the force to be applied to the distal point. """ # Get the closes point on the cylinder axes to the other cylinder point1, point2 = physics.get_cylinder_intersection( base_1=self.proximal_point, top_1=self.distal_point, base_2=neighbor.proximal_point, top_2=neighbor.distal_point, ) # print("Internal check", point1, point2) # Calculate the distance between the two closest points distance_to_point = np.linalg.norm(np.subtract(self.proximal_point, point1)) fraction = distance_to_point / self.current_length distance_vector, norm = physics.get_distance_components(point2, point1) # Calculate cell-cell adhesion forces magnitude = interaction.compute_adhesion( distance=norm, radius1=self.mechanics.interaction_radius, radius2=neighbor.mechanics.interaction_radius, ) magnitude -= interaction.compute_repulsion( distance=norm, radius1=self.mechanics.radius, radius2=neighbor.mechanics.radius, ) return magnitude * distance_vector, fraction
[docs]@dataclass class ObjectFactory: """ Helper class to create instances of cell bodies and neurties based on defined properties. """ cell_radius: float """The cell radius.""" cell_interaction_factor: float """The factor used to calculate the cell's interaction radius.""" neurite_radius: float """The neurite radius.""" neurite_interaction_factor: float """The factor used to calculate the neurite's interaction radius.""" neurite_spring_constant: float """The neurite's spring constant.""" neurite_default_length: float """The initial default length of a new neuritte."""
[docs] def get_cell_body(self, center_position: np.ndarray) -> CellBody: """ Returns a CellBody object centred on the given position. Parameters ---------- center_position The coordinates of the cell body's centre. Returns ------- A CellBody with the given coordinates and the chosen properties. """ cell_mechanics = physics.PhysicalProperties( self.cell_radius, self.cell_interaction_factor ) return CellBody(center_position, cell_mechanics)
[docs] def get_neurite( self, proximal_position: np.ndarray, axis: np.ndarray, restriction_factor: float = 0.9, ) -> Neurite: """ Returns a Neurite object placed on the given position, pointing towards the axis. Parameters ---------- proximal_position The coordinates of the neurite's proximal point. axis The axis of growth of the new neurite. restriction_factor The restriction factor used to compute the direction of a new neurite (the neurite will follow the axis with a given degree of freedom, defined by the restriction factor). Should be between 0 and 1. Returns ------- A Neurite with the given coordinates and the chosen properties. """ if np.linalg.norm(axis) > 1.0: axis = physics.normalize_vector(axis) if not (0 <= restriction_factor <= 1): raise ValueError( "The neurite axis restriction factor should be between 0 and 1." ) axis_angle = np.arctan2(axis[1], axis[0]) temp_angle = ( np.pi * ( (1 - restriction_factor) * np.random.uniform() + (-0.5 + restriction_factor / 2) ) + axis_angle ) new_axis = np.asarray([np.cos(temp_angle), np.sin(temp_angle), 0]) cylinder_mechanics = physics.CylinderProperties( self.neurite_radius, self.neurite_interaction_factor, self.neurite_spring_constant, self.neurite_default_length, ) return Neurite(proximal_position, new_axis, cylinder_mechanics)