# Copyright 2017. Allen Institute. All rights reserved
#
# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
# following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided with the distribution.
#
# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote
# products derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANwhile (itercount < max_iter) and (n_pass < np.ceil(ndraws_tot / 5000)):Y EXPRESS OR IMPLIED WARRANTIES,
# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
import numpy as np
import math
# import nrrd
import matplotlib.pyplot as plt
from collections import defaultdict
from types import SimpleNamespace
try:
from sklearn.neighbors import KDTree
except ImportError:
pass
try:
import nrrd
except ImportError:
pass
[docs]class CellLocations(object):
# Added class to facilitate checking of new locations against already placed locations in other populations for
# the purpose of ensuring a minimum distance.
# Only a unique minimum distance is allowed per network (populations sharing the same physical space) and must be
# set before adding placements
# Original functions outside of class are retained for compatibility with continued usage of non-class approach
# for simpler situations.
def __init__(self, name, dmin=0.00):
# Currently only allowing a unique dmin
self._name = name
self._dmin = dmin
self._CCF_orientation = True
#self._all_positions = [np.array([]).reshape(0, 3)]
self._all_positions =[]
self._all_pop_names = []
@property
def dmin(self):
return self._dmin
@property
def CCF_orientation(self):
return self._CCF_orientation
'''
@property
def pop_positions(self,pop_name):
return
'''
@dmin.setter
def dmin(self, value):
self._dmin = value
@CCF_orientation.setter
def CCF_orientation(self, value):
self._CCF_orientation = value
[docs] def add_positions_nrrd(self, nrrd_filename, max_dens_per_mm3, pop_names, partitions=[1], split_bilateral=None,
method='prog', verbose = False):
if self._all_positions:
existing_positions = np.vstack(self._all_positions)
else:
existing_positions = np.array([]).reshape(0, 3)
positions = positions_nrrd(nrrd_filename, max_dens_per_mm3, split_bilateral, self._dmin,
CCF_orientation=self._CCF_orientation, plot=False,
method=method, existing_positions=existing_positions, verbose=verbose)
# list of position arrays
positions_pop = partition_locations(positions, partitions)
self.add_positions(pop_names, positions_pop)
[docs] def add_positions_columnar(self, pop_names, partitions=[1], N=1, center=[0.0, 50.0, 0.0], height = 100.0,
min_radius = 0.0, max_radius = 1.0, method='prog', verbose=False):
if sum(partitions)!=1:
raise ValueError('The elements of `partitions` must add up to 1.')
if self._all_positions:
existing_positions = np.vstack(self._all_positions)
else:
existing_positions = np.array([]).reshape(0, 3)
vol = np.pi * (max_radius ** 2 - min_radius ** 2) * height / 1000**3
if method == 'prog':
def sampling_func(ndraws):
positions = positions_columinar(N=ndraws, center=center, height=height, min_radius=min_radius,
max_radius=max_radius)
return positions
positions = positions_dmin_prog(N=N, vol_tot=vol, sampling_func=sampling_func,
dmin=self._dmin, existing_positions=existing_positions, verbose=verbose)
elif method == 'lattice':
box_dims = np.array([2*max_radius, height, 2*max_radius])
position_scale = np.eye(3)*box_dims
mat = np.array([[[N/vol]]])
def filter_func(x, y, z):
inside = (np.sqrt((x-box_dims[0]/2)**2 + (z-box_dims[2]/2)**2) <= max_radius) & \
(np.sqrt((x-box_dims[0]/2)**2 + (z-box_dims[2]/2)**2) >= min_radius) & \
(np.abs(y-box_dims[1]/2) < height/2)
return inside
positions = positions_dmin_lattice(mat, position_scale=position_scale, N=N, vol_tot=vol, dmin=self._dmin,
existing_positions=existing_positions, filter_func=filter_func,
verbose=verbose)
positions = positions - box_dims/2 + center
positions_pop = partition_locations(positions, partitions)
self.add_positions(pop_names, positions_pop)
[docs] def add_positions_rect_prism(self, pop_names, partitions=[1], N=1, center=[0.0, 50.0, 0.0], height=20.0,
x_length=100.0, z_length=100.0, method='prog', verbose=False):
if sum(partitions) != 1:
raise ValueError('The elements of `partitions` must add up to 1.')
if self._all_positions:
existing_positions = np.vstack(self._all_positions)
else:
existing_positions = np.array([]).reshape(0, 3)
vol = x_length * z_length * height / 1000**3
if method == 'prog':
def sampling_func(ndraws):
positions = positions_rect_prism(N=ndraws, center=center, height=height, x_length=x_length,
z_length=z_length)
return positions
positions = positions_dmin_prog(N=N, vol_tot=vol, sampling_func=sampling_func,
dmin=self._dmin, existing_positions=existing_positions,
verbose=verbose)
elif method == 'lattice':
box_dims = np.array([x_length, height, z_length])
position_scale = np.eye(3) * box_dims
mat = np.array([[[N / vol]]])
positions = positions_dmin_lattice(mat, position_scale=position_scale, N=N, vol_tot=vol, dmin=self._dmin,
existing_positions=existing_positions)
positions = positions - box_dims / 2 + center
positions_pop = partition_locations(positions, partitions)
self.add_positions(pop_names, positions_pop)
[docs] def add_positions_ellipsoid(self, pop_names, partitions=[1], N=1, center=[0.0, 50.0, 0.0], height=50.0,
x_length=100.0, z_length=200.0, method='prog', verbose=False):
if sum(partitions) != 1:
raise ValueError('The elements of `partitions` must add up to 1.')
if self._all_positions:
existing_positions = np.vstack(self._all_positions)
else:
existing_positions = np.array([]).reshape(0, 3)
vol = 4 / 3 * np.pi * (x_length/2) * (z_length/2) * (height/2) / 1000**3
if method == 'prog':
def sampling_func(ndraws):
positions = positions_ellipsoid(N=ndraws, center=center, height=height, x_length=x_length,
z_length=z_length)
return positions
positions = positions_dmin_prog(N=N, vol_tot=vol, sampling_func=sampling_func,
dmin=self._dmin, existing_positions=existing_positions, verbose=verbose)
elif method == 'lattice':
box_dims = np.array([x_length, height, z_length])
position_scale = np.eye(3) * box_dims
mat = np.array([[[N / vol]]])
def filter_func(x, y, z):
inside = ((x - box_dims[0] / 2) ** 2 / (x_length/2) ** 2
+ (y - box_dims[1] / 2) ** 2 / (height/2) ** 2
+ (z - box_dims[2] / 2) ** 2 / (z_length/2) ** 2) <= 1
return inside
positions = positions_dmin_lattice(mat, position_scale=position_scale, N=N, vol_tot=vol, dmin=self._dmin,
existing_positions=existing_positions, filter_func=filter_func)
positions = positions - box_dims / 2 + center
'''
elif method == 'lattice':
box_dims = np.array([2*max_radius, height, 2*max_radius])
position_scale = np.eye(3)*box_dims
mat = np.array([[[N/(np.prod(box_dims)/1000**3)]]])
def filter_func(x, y, z):
inside = (np.sqrt((x-box_dims[0]/2)**2 + (z-box_dims[2]/2)**2) <= max_radius) & \
(np.sqrt((x-box_dims[0]/2)**2 + (z-box_dims[2]/2)**2) >= min_radius) & \
(np.abs(y-box_dims[1]/2) < height/2)
return inside
positions = positions_dmin_lattice(mat, position_scale=position_scale, dmin=self._dmin,
existing_positions=existing_positions, filter_func=filter_func)
positions = positions - box_dims/2 + center
'''
positions_pop = partition_locations(positions, partitions)
self.add_positions(pop_names, positions_pop)
[docs] def plot_locs (self):
fig, ax = plt.subplots(1,2,figsize=(20,15), subplot_kw={'projection':'3d'})
plt.style.use('seaborn-bright')
if self._CCF_orientation:
ax[0].invert_zaxis()
for p, pop_name in enumerate(self._all_pop_names):
# x,y,z axis orientations depending on function and orientation parameters
x = self._all_positions[p][:,0]
y = self._all_positions[p][:,1]
z = self._all_positions[p][:,2]
if self._CCF_orientation:
plot_positions(x, z, y, ax[0], labels=['X', 'Z', 'Y'], pop_name=pop_name)
ax[0].set_title('Posterior view')
ax[0].view_init(elev=10., azim=0)
plot_positions(x, z, y, ax[1], labels=['X', 'Z', 'Y'], pop_name=pop_name)
ax[1].set_title('Top view')
ax[1].view_init(elev=90., azim=0)
ax[1].legend(loc="upper right", markerscale=2, prop={'size': 15})
else:
plot_positions(x, y, z, ax[0], labels=['X','Y','Z'], pop_name=pop_name)
ax.view_init(elev=10., azim=0)
ax[0].set_title('Side view')
ax[0].view_init(elev=10., azim=-90)
plot_positions(x, y, z, ax[1], labels=['X', 'Y', 'Z'], pop_name=pop_name)
ax[1].set_title('Top view')
ax[1].view_init(elev=90., azim=0)
ax[1].legend(loc="upper right", markerscale=2, prop={'size': 15})
plt.tight_layout()
plt.show()
[docs] def add_positions(self, pop_names, positions_pop):
if isinstance(pop_names, str):
pop_names = [pop_names]
for p, pop_name in enumerate(pop_names):
self._all_positions.append(positions_pop[p])
self._all_pop_names.append(pop_name)
myvars = vars(self)
myvars[pop_name] = SimpleNamespace()
myvars[pop_name].positions = self._all_positions[p]
myvars[pop_name].N = self._all_positions[p].shape[0]
[docs]def positions_columinar(N=1, center=[0.0, 50.0, 0.0], height=100.0, min_radius=0.0, max_radius=1.0, plot=False):
"""Returns a set of random x,y,z coordinates within a given cylinder or cylindrical ring.
Height is given as the y (index 1) coordinates.
:param N: Number of points to return
:param center: center of sphere
:param height: maximum length of sphere (y coord)
:param min_radius: minimum horizontal radius on x-z plane
:param max_radius: maximum horizontal radius on x-z plane
:return: A (N, 3) matrix
"""
phi = 2.0 * math.pi * np.random.random([N])
r = np.sqrt((min_radius**2 - max_radius**2) * np.random.random([N]) + max_radius**2)
x = center[0] + r * np.cos(phi)
z = center[2] + r * np.sin(phi)
y = center[1] + height * (np.random.random([N]) - 0.5)
if plot:
fig, ax = plt.subplots(1,2,figsize=(9,12), subplot_kw={'projection':'3d'})
plot_positions(x, z, y, ax[0], labels=['X','Z','Y'])
ax[0].set_title('Side view')
ax[0].view_init(elev=5., azim=-90)
plot_positions(x, z, y, ax[1], labels=['X','Z','Y'])
ax[1].set_title('Top view')
ax[1].view_init(elev=90., azim=-90)
return np.column_stack((x, y, z))
positions_columnar = positions_columinar
[docs]def positions_rect_prism(N=1, center=[0.0, 50.0, 0.0], height=20.0, x_length=100.0, z_length=100.0, plot=False):
"""Returns a set of random x,y,z coordinates within a rectangular prism.
Height is given as the y (index 1) coordinates.
:param N: Number of points to return
:param center: center of rectangular prism
:param height: height of prism (y-coordinate)
:param x_length: length of prism in x
:param z_length: length of prism in z
:return: A (N, 3) matrix
"""
x = center[0] + x_length * (np.random.random([N]) - 0.5)
z = center[2] + z_length * (np.random.random([N]) - 0.5)
y = center[1] + height * (np.random.random([N]) - 0.5)
if plot:
fig, ax = plt.subplots(1,2,figsize=(9,12), subplot_kw={'projection':'3d'})
plot_positions(x, z, y, ax[0], labels=['X','Z','Y'])
ax[0].set_title('Side view')
ax[0].view_init(elev=5., azim=-90)
plot_positions(x, z, y, ax[1], labels=['X','Z','Y'])
ax[1].set_title('Top view')
ax[1].view_init(elev=90., azim=-90)
return np.column_stack((x, y, z))
[docs]def positions_ellipsoid (N=1, center=[0.0, 50.0, 0.0], height=50, x_length=100.0, z_length=200.0, plot=False):
"""Returns a set of random x,y,z coordinates within an ellipsoid. Height is given as the y (index 1) coordinates.
:param N: Number of points to return
:param center: center of ellipsoid
:param height: height of ellipsoid (y-coordinate)
:param x_length: length of ellipsoid in x
:param z_length: length of ellipsoid in z
:return: A (N, 3) matrix
"""
# Generate prism bounding the ellipsoid
positions = positions_rect_prism (3*N, center, height, x_length, z_length, plot=False)
val = ((positions[:,0]-center[0])**2/(x_length/2)**2)\
+((positions[:,1]-center[1])**2/(height/2)**2)\
+((positions[:,2]-center[2])**2/(z_length/2)**2)
positions = np.squeeze(positions [np.where(val<1),:])
positions = positions [:N,:]
if plot:
fig, ax = plt.subplots(1,2,figsize=(9,12), subplot_kw={'projection':'3d'})
plot_positions(positions[:,0], positions[:,2], positions[:,1], ax[0], labels=['X','Z','Y'])
ax[0].set_title('Side view')
ax[0].view_init(elev=5., azim=-90)
plot_positions(positions[:,0], positions[:,2], positions[:,1], ax[1], labels=['X','Z','Y'])
ax[1].set_title('Top view')
ax[1].view_init(elev=90., azim=-90)
return positions
[docs]def positions_cuboid(N=1, center=[0.0, 0.0, 0.0], height=100.0, xside_length=100.0, yside_length=100.0, min_dist=20.0,
plot = False):
"""This function distributes the cells in a 3D cuboid (x,y,z sides may have different lengths). The method used
assures cells cannot be placed too close to one another (must be > min_dist apart)
WARNING: If cell density is high and there is more than 1 population of cells, there is a high chance cells will be
placed on top of one another. You can use positions_list() to avoid this...
Written by Ben Latimer at University of Missouri (latimerb@missouri.edu)
:return: A (N, 3) matrix
"""
# Create the possible x,y,z coordinates
x_grid = np.arange(center[0], xside_length+min_dist, min_dist)
y_grid = np.arange(center[1], yside_length+min_dist, min_dist)
z_grid = np.arange(center[2], height+min_dist, min_dist)
xx, yy, zz = np.meshgrid(x_grid, y_grid, z_grid)
positions = np.vstack([xx.ravel(), yy.ravel(), zz.ravel()]).T
# Pick N indices for the positions matrix randomly, without replacement (so coordinates are unique)
inds = np.random.choice(np.arange(0, np.size(positions, 0)), N, replace=False)
# Assign positions
x = positions[inds][:, 0]
y = positions[inds][:, 1]
z = positions[inds][:, 2]
if plot:
fig, ax = plt.subplots(1,2,figsize=(9,12), subplot_kw={'projection':'3d'})
plot_positions(x, y, z, ax[0], labels=['X','Y','Z'])
ax[0].set_title('Side view')
ax[0].view_init(elev=5., azim=-90)
plot_positions(x, y, z, ax[1], labels=['X','Y','Z'])
ax[1].set_title('Top view')
ax[1].view_init(elev=90., azim=-90)
return np.column_stack((x, y, z))
[docs]def positions_list(positions=np.array([(0, 0, 0), (0, 0, 1)])):
"""This function is designed to be used with an externally supplied array of x,y,z coordinates. It is useful to
avoid cell overlap in high density situations or to make some unique geometry. After you create each population,
delete those positions from the "master" list of coordinates. This will assure that no cells are placed on top of
one another.
Written by Ben Latimer at University of Missouri (latimerb@missouri.edu)
:param positions:
:return:
"""
x = positions[:, 0]
y = positions[:, 1]
z = positions[:, 2]
return np.column_stack((x, y, z))
[docs]def positions_density_matrix(mat, position_scale=np.array([[1,0,0],[0,1,0],[0,0,1]]), origin=np.array([0,0,0]),
plot=False, CCF_orientation=False, dmin=0.0, method='prog',
existing_positions=np.array([]).reshape(0, 3), verbose=False):
"""This function places random x,y,z coordinates according to a supplied 3D array of densities (cells/mm^3).
The optional position_scale parameter defines a transformation matrix A to physical space such that::
[x_phys, y_phys, z_phys] = A * [x_mat, y_mat, z_mat]
Note: position_scale and output coordinates are in units of microns, while density is specified in mm^3.
:param mat: A 3-dimensional matrix of densities (cells/mm^3)
:param position_scale: A (3, 3) matrix (microns)
:param plot: Generates a plot of the cell locations when set to True
:param CCF_orientation: if True, plot will be oriented for Allen atlas common coordinate framework
See https://help.brain-map.org/display/mouseconnectivity/API#API-DownloadAtlas3-DReferenceModels
:param dmin: Minimum distance allowed between cell centers - using this function can severely slow down cell
placement, especially as we approach the maximal possible density for this minimum distance
:param verbose: If set to true, prints every 100 units placed for monitoring progress
:return: A (N, 3) matrix (microns)
"""
ind_nz = np.nonzero(mat)
if method == 'prog':
# Use batch progressive sampling
def sampling_func(ndraws):
rand_pos_new = np.random.uniform(size=(3, ndraws)) # Or for simple geometries, draw over max_dims
rand_nz_vox_new = np.random.choice(range(ind_nz[0].shape[0]), ndraws)
positions = position_scale.dot([ind_nz[0][rand_nz_vox_new].astype(float) + rand_pos_new[0, :],
ind_nz[1][rand_nz_vox_new].astype(float) + rand_pos_new[1, :],
ind_nz[2][rand_nz_vox_new].astype(float) + rand_pos_new[2, :]])
positions = positions.T
return positions
positions = positions_dmin_prog(mat, position_scale=position_scale, sampling_func=sampling_func, dmin=dmin,
existing_positions=existing_positions, verbose=verbose)
elif method == 'lattice':
# Use lattice jittering
vol_per_voxel = np.abs(np.linalg.det(position_scale.astype(float))) / (1000) ** 3
vol = vol_per_voxel * len(ind_nz[0])
N = math.floor(np.max(mat)*vol) # Final desired N
positions = positions_dmin_lattice(mat, position_scale=position_scale, N=N, vol_tot=vol, dmin=dmin,
existing_positions=existing_positions, verbose=verbose)
else:
raise ValueError("The 'method' argument can be either 'prog' for progressive sampling "
"or 'lattice' for lattice jittering")
# Remove cells to achieve non-uniform density, if applicable
temp = set(mat.flat)
temp.discard(0)
print('positions:', positions.shape)
inds = np.matmul(np.linalg.inv(position_scale),(positions.T)).T
inds = np.floor(inds).astype(int)
indx = inds[:,0]
indy = inds[:,1]
indz = inds[:,2]
max_dens = np.max(mat)
if len(temp) > 1:
rand_keep = np.random.uniform(size=positions.shape[0])
for j in range(len(rand_keep)):
vox_dens = mat[indx[j], indy[j], indz[j]]
if rand_keep[j] >= vox_dens / max_dens:
positions[j,:] = [np.nan, np.nan, np.nan]
positions = positions[~np.isnan(positions[:,0]),:]
# Shift re: origin
positions += origin
x = positions[:,0]
y = positions[:,1]
z = positions[:,2]
if plot:
fig, ax = plt.subplots(figsize=(11, 11), subplot_kw={'projection':'3d'})
if CCF_orientation:
plot_positions(x, z, y, ax, labels=['X','Z','Y'])
ax.invert_zaxis()
ax.view_init(elev=10., azim=0)
else:
plot_positions(x, y, z, ax, labels=['X','Y','Z'])
ax.view_init(elev=10., azim=0)
return positions
[docs]def positions_dmin_prog(mat=None, position_scale=np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]), N=None, vol_tot=None, sampling_func=None,
dmin=0.0, existing_positions=np.array([]).reshape(0, 3), verbose=False):
"""This function places random x,y,z coordinates with a minimal distance according to a supplied 3D array of
densities (cells/mm^3) using progressive sampling.
The optional position_scale parameter defines a transformation matrix A to physical space such that:
[x_phys, y_phys, z_phys] = A * [x_mat, y_mat, z_mat]
Note: position_scale and output coordinates are in units of microns, while density is specified in mm^3.
:param mat: A 3-dimensional matrix of densities (cells/mm^3)
:param position_scale: A (3, 3) matrix (microns)
:param plot: Generates a plot of the cell locations when set to True
:param CCF_orientation: if True, plot will be oriented for Allen atlas common coordinate framework
See https://help.brain-map.org/display/mouseconnectivity/API#API-DownloadAtlas3-DReferenceModels
:param dmin: Minimum distance allowed between cell centers (microns) - using this function can severely slow down cell
placement, especially as we approach the maximal possible density for this minimum distance
:return: A (N, 3) matrix (microns)
"""
# Can later put this inside check function?
if (dmin < 0):
raise ValueError('Minimum distance between cell centers (dmin) must not be negative')
if isinstance(mat, np.ndarray): # Density matrix
max_dens=np.max(mat)
vol_per_voxel = np.abs(np.linalg.det(position_scale.astype(float))) / (1000) ** 3
mat = mat.astype(float) * vol_per_voxel
max_dens_n = np.max(mat)
n_vox_nz = np.nonzero(mat)[0].shape[0]
ndraws_tot = int(max_dens_n * n_vox_nz)
vol_tot = vol_per_voxel * n_vox_nz
elif N is not None: # Desired total N
ndraws_tot = N
max_dens = N/(vol_tot)
else:
raise Exception('Must specify either a cell density matrix (mat) or a total number of cells (N)')
if dmin != 0:
# Number of spheres at FCC/HCP packing: 0.74*V/V_sphere
# Random close packing: 0.64*V/V_sphere
V_sphere = 4 / 3 * np.pi * (dmin/1000/2) ** 3
dens_lim = 0.63 / V_sphere # in points per mm^3
print('Density limit:{:.3f}'.format(dens_lim))
print('Max density requested:{:.3f}'.format(max_dens))
if max_dens > dens_lim:
raise ValueError('Requested density is incompatible with minimum distance: either reduce the density or dmin.')
thresh = np.ceil(vol_tot * 0.0001 / V_sphere)
else:
thresh = 1
# First distribute uniformly in non zero voxels at max_dens
ndraws = ndraws_tot
positions = np.array([]).reshape(0, 3)
len_existing = existing_positions.shape[0]
max_iter = 1
past_iters = np.full(3, np.nan)
pi_ind = 0
while positions.shape[0] < ndraws_tot:
positions_new = []
itercount = 0
n_pass = 0
while (itercount < max_iter) and (n_pass < thresh):
positions_new.append(sampling_func(ndraws))
if dmin>0:
# Check against existing tree
# Find nearest neighbor
positions_all = np.vstack((existing_positions, positions))
if positions_all.shape[0] > 0:
tree = KDTree(positions_all, leaf_size=2)
dists, d_inds = tree.query(positions_new[-1], k=1)
conf_inds = np.squeeze(dists < dmin)
positions_new[-1][conf_inds,:] = [np.nan, np.nan, np.nan]
notna = ~np.isnan(positions_new[-1][:, 0])
positions_new[-1] = positions_new[-1][notna, :]
itercount += 1
n_pass += positions_new[-1].shape[0]
if pi_ind >= len(past_iters):
# Get mean number of iterations for past 3 points and use to set new maxiter
max_iter = 1.5 * np.max(past_iters)
pi_ind = 0
past_iters[pi_ind] = itercount
pi_ind += 1
if n_pass < int(ndraws_tot / 2000):
# Replace some of existing points
# Draw a small fraction of the total number of points
positions_new.append(sampling_func(ndraws))
# Remove fraction of points in conflict with new points (exclude pre-existing points from other populations)
inds2 = tree.query_radius(positions_new[-1], r=dmin)
lens = np.array([np.nan if np.any(a<len_existing) else len(a) for a in inds2])
k = np.min([math.ceil(0.005*positions_all.shape[0]), np.count_nonzero(lens <= 1)])
noconf = np.nonzero(lens == 0)[0]
swap = np.argpartition(lens, k)[:k] # k indices with smallest number of conflicts, nan's sorted to largest
inds3 = np.unique(np.hstack(inds2[swap]))
inds3 = inds3[inds3>=len_existing]-len_existing
positions[inds3, :] = [np.nan, np.nan, np.nan]
positions = positions[~np.isnan(positions[:, 0]),:]
positions_new[-1] = positions_new[-1][np.concatenate((swap, noconf)),:]
positions_new = np.vstack(positions_new)
# Check new points that are retained against each other
if (dmin > 0) and positions_new.shape[0] > 1:
tree = KDTree(positions_new, leaf_size=2)
dists, d_inds = tree.query(positions_new, k=2)
# Overremoves by checking against removed cells, but tracking removed cells is not faster
conf_inds = np.squeeze(dists[:,1] < dmin)
positions_new[conf_inds, :] = [np.nan, np.nan, np.nan]
notna = ~np.isnan(positions_new[:, 0])
positions_new = positions_new[notna, :]
positions = np.concatenate((positions, positions_new), axis=0) # Add new positions
if verbose:
print(f'{positions.shape[0]}/{ndraws_tot} cells placed')
ndraws = ndraws_tot
positions = positions[:ndraws,:]
if verbose:
print(f'{positions.shape[0]} cells')
return positions
[docs]def positions_dmin_lattice(mat, position_scale=np.array([0,0,0]), N=1, vol_tot=None, dmin=0.0,
existing_positions=np.array([]).reshape(0, 3), filter_func=None, verbose=False):
'''Packing with a minimum distance, starting from a hexagonal close packing lattice
:param x_box, y_box, z_box: size of each dimension of lattice box to be generated (microns)
:param N: number of points to be placed
'''
if (dmin < 0):
raise ValueError('Minimum distance between cell centers (dmin) must not be negative')
if existing_positions.shape[0] > 0:
raise RuntimeError("The 'lattice' method cannot be used with already placed points. "
"Use the partition inputs to place multiple populations of cells into the same space, "
"or use the 'prog' method.")
max_dens=N/(vol_tot)
V_sphere = 4 / 3 * np.pi * (dmin / 1000 / 2) ** 3
dens_lim = 0.74 / V_sphere # in points per mm^3
print('Density limit:{:.3f}'.format(dens_lim))
print('Max density requested:{:.3f}'.format(max_dens))
if max_dens > dens_lim:
raise ValueError('Requested density is incompatible with minimum distance: either reduce the density or dmin.')
ind_nz = np.nonzero(mat)
minx = np.min(ind_nz[0])
miny = np.min(ind_nz[1])
minz = np.min(ind_nz[2])
maxx = np.max(ind_nz[0])
maxy = np.max(ind_nz[1])
maxz = np.max(ind_nz[2])
corners = [v.flatten() for v in (np.meshgrid([minx, maxx+1], [miny, maxy+1], [minz, maxz+1]))]
corners = np.vstack(corners)
corners_t = (position_scale.astype(float).dot(corners))
minx2 = np.min(corners_t[0])
miny2 = np.min(corners_t[1])
minz2 = np.min(corners_t[2])
maxx2 = np.max(corners_t[0])
maxy2 = np.max(corners_t[1])
maxz2 = np.max(corners_t[2])
# max_dims
# Calc additional origin
origin2 = [minx2, miny2, minz2]
x_box = maxx2 - minx2
y_box = maxy2 - miny2
z_box = maxz2 - minz2
# Calc box N
N_box = math.floor(x_box * y_box * z_box * np.max(mat) / (1000 ** 3)) # N for lattice box
vol_box = x_box * y_box * z_box
r = (vol_box * 0.74 / N_box * 3 / 4 / np.pi) ** (1. / 3)
x_n = int((x_box-r) / (2 * r)) + 1
y_n = int(y_box / (r) / np.sqrt(3) - 1/3) + 1
z_n = int(z_box / (2 * r) / np.sqrt(6) * 3) + 1
x, y, z = hcp(x_n, y_n, z_n, r)
# Randomly remove units in excess of N
# Set to NaN rather than drop to keep lattice in register
x_vec = x.flatten()
y_vec = y.flatten()
z_vec = z.flatten()
todrop = np.random.choice(range(len(x_vec)), np.max([0,len(x_vec) - N_box]), replace=False)
x_vec[todrop] = np.nan
y_vec[todrop] = np.nan
z_vec[todrop] = np.nan
positions = np.vstack((x_vec, y_vec, z_vec))
# Remove unneeded units by mat or geometry function
position_scale_inv = np.linalg.inv(position_scale)
inds = np.matmul(position_scale_inv, (positions+np.reshape(origin2, (-1, 1)))).T
inds = np.floor(inds)
mask = ~np.isnan(inds)
mat_vals = np.full(inds.shape[0], fill_value=np.nan)
mat_vals[mask[:,0]] = mat[inds[mask[:, 0],0].astype(int),inds[mask[:, 0],1].astype(int),inds[mask[:, 0],2].astype(int)]
mat_vals = mat_vals==0
mask = ~np.isnan(mat_vals)
positions = positions.T
positions [mat_vals[mask],:] = [np.nan, np.nan, np.nan]
x_vec = positions[:,0]
y_vec = positions[:,1]
z_vec = positions[:,2]
x = x_vec.reshape((x_n, y_n, z_n))
y = y_vec.reshape((x_n, y_n, z_n))
z = z_vec.reshape((x_n, y_n, z_n))
# Jitter by up to 2*r-dmin and check if okay,if not, redraw jitter
jitr_max = 2 * r - dmin
for i in range(x.shape[0]):
for j in range(x.shape[1]):
for k in range(x.shape[2]):
# Don't need it to be uniformly distributed - the outer points are less likely to qualify
if np.isnan(x[i, j, k]):
# Eliminated unit, skip
continue
pt_old = [x[i, j, k], y[i, j, k], z[i, j, k]]
x[i, j, k] = float('inf')
y[i, j, k] = float('inf')
z[i, j, k] = float('inf')
keep = False
q = 2
indsi = np.arange(max(i - q, 0), min(i + q, x.shape[0]))
indsj = np.arange(max(j - q, 0), min(j + q, x.shape[1]))
indsk = np.arange(max(k - q, 0), min(k + q, x.shape[2]))
near_inds = np.meshgrid(indsi, indsj, indsk)
while not keep:
jitx = np.random.uniform(-jitr_max, jitr_max)
jity = np.random.uniform(-jitr_max, jitr_max)
jitz = np.random.uniform(-jitr_max, jitr_max)
x_new = pt_old[0] + jitx
y_new = pt_old[1] + jity
z_new = pt_old[2] + jitz
dists = np.sqrt((x_new - x[near_inds[0], near_inds[1], near_inds[2]]) ** 2 +
(y_new - y[near_inds[0], near_inds[1], near_inds[2]]) ** 2 +
(z_new - z[near_inds[0], near_inds[1], near_inds[2]]) ** 2)
d = np.nanmin(np.append(dists.flatten(), float('inf')))
keep = d > dmin
# Replace existing
x[i, j, k] = x_new
y[i, j, k] = y_new
z[i, j, k] = z_new
x, y, z = x.flatten(), y.flatten(), z.flatten()
x = x[~np.isnan(x)]
y = y[~np.isnan(y)]
z = z[~np.isnan(z)]
before_trim_len = x.shape[0]
if filter_func is None:
# Trim any points that have been jittered out of permitted region
inside = (x <= x_box) & (x >= 0) & (y <= y_box) & (y >= 0) & (z <= z_box) & (z >= 0)
else:
inside = filter_func(x, y, z)
x = x[inside]
y = y[inside]
z = z[inside]
positions = np.vstack((x, y, z)).T
if positions.shape[0] > N:
# Trim excess N, points are ordered, so pick randomly
todrop = np.random.choice(range(len(x_vec)), positions.shape[0]-N, replace=False)
positions[todrop,:] = np.nan
while positions.shape[0] < N:
if verbose==True:
print(f'{positions.shape[0]}/{N} cells placed')
tree = KDTree(positions, leaf_size=2)
rand_pos_new = np.random.uniform(size=(3, before_trim_len))
rand_nz_vox_new = np.random.choice(range(ind_nz[0].shape[0]), before_trim_len)
positions_new = position_scale.dot([(ind_nz[0]-minx)[rand_nz_vox_new].astype(float) + rand_pos_new[0, :],
(ind_nz[1]-miny)[rand_nz_vox_new].astype(float) + rand_pos_new[1, :],
(ind_nz[2]-minz)[rand_nz_vox_new].astype(float) + rand_pos_new[2, :]])
positions_new = positions_new.T
dists, d_inds = tree.query(positions_new, k=1)
conf_inds = np.squeeze(dists < dmin)
positions_new[conf_inds, :] = [np.nan, np.nan, np.nan]
positions_new = positions_new[~np.isnan(positions_new[:, 0]), :]
if positions_new.shape[0] > 1:
tree = KDTree(positions_new, leaf_size=2)
dists, d_inds = tree.query(positions_new, k=2)
conf_inds = np.squeeze(dists[:,1] < dmin)
positions_new[conf_inds, :] = [np.nan, np.nan, np.nan]
notna = ~np.isnan(positions_new[:, 0])
positions_new = positions_new[notna, :]
if filter_func is not None:
inside = filter_func(positions_new[:,0], positions_new[:,1], positions_new[:,2])
positions_new = positions_new[inside]
positions = np.concatenate((positions, positions_new), axis=0) # Add new positions
positions = positions[:N, :]
# If N rather than density, replace any missing
x = x[~np.isnan(x)]
y = y[~np.isnan(y)]
z = z[~np.isnan(z)]
# Shift re: origin
x += origin2[0]
y += origin2[1]
z += origin2[2]
return positions
[docs]def positions_nrrd (nrrd_filename, max_dens_per_mm3, split_bilateral=None, dmin = 0.0, CCF_orientation=True, plot=False, method='prog',
existing_positions=np.array([]).reshape(0, 3), verbose=False):
'''Generates random cell positions based on a .nrrd file. Matrix values are interpreted as cell densities
for each voxel. The maximum density is scaled to max_dens_per_mm3 (cells/mm^3).
If the .nrrd file is a structural mask, cells will be placed uniformly at max_dens_per_mm3 within the
structure geometry.
By default, only one hemisphere is shown, but this can be disabled by setting bilateral=True.
:param nrrd_filename: path to .nrrd file
:param max_dens_per_mm3: desired density at maximum value of nrrd array (cells/mm^3)
:param split_bilateral: return only unilateral structure by removing half of the array along the given axis.
If no splitting is desired, pass in None
:return: A (N, 3) matrix (microns)
'''
readdata, header = nrrd.read(nrrd_filename)
space_dir = header['space directions']
origin = header['space origin']
readdata_scaled = readdata / np.max(readdata) * max_dens_per_mm3
if split_bilateral is not None:
if split_bilateral=='x':
readdata_scaled = readdata_scaled[:int(readdata_scaled.shape[0]/2), :, :]
elif split_bilateral=='y':
readdata_scaled = readdata_scaled[:, :int(readdata_scaled.shape[1]/2), :]
elif split_bilateral=='z':
readdata_scaled = readdata_scaled[:, :, :int(readdata_scaled.shape[2]/2)]
else:
error("split_bilateral must take values of 'x', 'y', or 'z', or 'None' if no processing is desired")
# For testing non-uniform density
#t = np.arange(0, readdata_scaled.shape[2], 1)
#s = 1+np.sin(2*np.pi*0.1*t)
#readdata_scaled = readdata_scaled * s
positions = positions_density_matrix(readdata_scaled, position_scale=space_dir, origin = origin, plot=plot,
CCF_orientation=CCF_orientation, dmin=dmin, method=method,
existing_positions=existing_positions, verbose=verbose)
return positions
[docs]def xiter_random(N=1, min_x=0.0, max_x=1.0):
return np.random.uniform(low=min_x, high=max_x, size=(N,))
[docs]def plot_positions(x,y,z,ax,labels,pop_name=None):
# Do plot a verification for each of these helper functions
ax.scatter(x,y,z, marker='.', s=5, label=pop_name)
ax.set_xlabel(labels[0])
ax.set_ylabel(labels[1])
ax.set_zlabel(labels[2])
[docs]def hcp(x_n, y_n, z_n, r):
# Hexagonal close packing lattice generation
i = np.arange(x_n)
j = np.arange(y_n)
k = np.arange(z_n)
get_x = lambda i, j, k, r: 2 * r * i + r * ((j + k) % 2)
x = get_x(i[:, None, None], j[None, :, None], k[None, None, :], r)
get_y = lambda i, j, k, r: r * np.sqrt(3) * (j + 1 / 3 * (k % 2)) + 0 * i
y = get_y(i[:, None, None], j[None, :, None], k[None, None, :], r)
get_z = lambda i, j, k, r: 2 * r * np.sqrt(6) / 3 * k + 0 * i + 0 * j
z = get_z(i[:, None, None], j[None, :, None], k[None, None, :], r)
return x, y, z
[docs]def partition_locations(positions, partitions):
# Assign positions to subpopulations based on probabilities
pop_positions = []
pop_assigned = np.random.choice(range(len(partitions)), positions.shape[0], p=partitions)
for i in range(len(partitions)):
pop_positions.append(positions[pop_assigned==i,:])
return pop_positions