|
""" |
|
Ray factory |
|
|
|
classes that provide vertex and triangle information for rays on spheres |
|
|
|
Example: |
|
|
|
rays = Rays_Tetra(n_level = 4) |
|
|
|
print(rays.vertices) |
|
print(rays.faces) |
|
|
|
""" |
|
from __future__ import print_function, unicode_literals, absolute_import, division |
|
import numpy as np |
|
from scipy.spatial import ConvexHull |
|
import copy |
|
import warnings |
|
|
|
class Rays_Base(object): |
|
def __init__(self, **kwargs): |
|
self.kwargs = kwargs |
|
self._vertices, self._faces = self.setup_vertices_faces() |
|
self._vertices = np.asarray(self._vertices, np.float32) |
|
self._faces = np.asarray(self._faces, int) |
|
self._faces = np.asanyarray(self._faces) |
|
|
|
def setup_vertices_faces(self): |
|
"""has to return |
|
|
|
verts , faces |
|
|
|
verts = ( (z_1,y_1,x_1), ... ) |
|
faces ( (0,1,2), (2,3,4), ... ) |
|
|
|
""" |
|
raise NotImplementedError() |
|
|
|
@property |
|
def vertices(self): |
|
"""read-only property""" |
|
return self._vertices.copy() |
|
|
|
@property |
|
def faces(self): |
|
"""read-only property""" |
|
return self._faces.copy() |
|
|
|
def __getitem__(self, i): |
|
return self.vertices[i] |
|
|
|
def __len__(self): |
|
return len(self._vertices) |
|
|
|
def __repr__(self): |
|
def _conv(x): |
|
if isinstance(x,(tuple, list, np.ndarray)): |
|
return "_".join(_conv(_x) for _x in x) |
|
if isinstance(x,float): |
|
return "%.2f"%x |
|
return str(x) |
|
return "%s_%s" % (self.__class__.__name__, "_".join("%s_%s" % (k, _conv(v)) for k, v in sorted(self.kwargs.items()))) |
|
|
|
def to_json(self): |
|
return { |
|
"name": self.__class__.__name__, |
|
"kwargs": self.kwargs |
|
} |
|
|
|
def dist_loss_weights(self, anisotropy = (1,1,1)): |
|
"""returns the anisotropy corrected weights for each ray""" |
|
anisotropy = np.array(anisotropy) |
|
assert anisotropy.shape == (3,) |
|
return np.linalg.norm(self.vertices*anisotropy, axis = -1) |
|
|
|
def volume(self, dist=None): |
|
"""volume of the starconvex polyhedron spanned by dist (if None, uses dist=1) |
|
dist can be a nD array, but the last dimension has to be of length n_rays |
|
""" |
|
if dist is None: dist = np.ones_like(self.vertices) |
|
|
|
dist = np.asarray(dist) |
|
|
|
if not dist.shape[-1]==len(self.vertices): |
|
raise ValueError("last dimension of dist should have length len(rays.vertices)") |
|
|
|
|
|
|
|
|
|
|
|
dist = np.repeat(np.expand_dims(dist,-1), 3, axis = -1) |
|
|
|
verts = np.broadcast_to(self.vertices, dist.shape) |
|
|
|
|
|
dist = np.moveaxis(dist,-2,0) |
|
verts = np.moveaxis(verts,-2,0) |
|
|
|
|
|
vs = (dist*verts)[self.faces] |
|
|
|
vs = np.moveaxis(vs, 1,-2) |
|
|
|
vs = vs.reshape((len(self.faces)*int(np.prod(dist.shape[1:-1])),3,3)) |
|
d = np.linalg.det(list(vs)).reshape((len(self.faces),)+dist.shape[1:-1]) |
|
|
|
return -1./6*np.sum(d, axis = 0) |
|
|
|
def surface(self, dist=None): |
|
"""surface area of the starconvex polyhedron spanned by dist (if None, uses dist=1)""" |
|
dist = np.asarray(dist) |
|
|
|
if not dist.shape[-1]==len(self.vertices): |
|
raise ValueError("last dimension of dist should have length len(rays.vertices)") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
dist = np.repeat(np.expand_dims(dist,-1), 3, axis = -1) |
|
|
|
verts = np.broadcast_to(self.vertices, dist.shape) |
|
|
|
|
|
dist = np.moveaxis(dist,-2,0) |
|
verts = np.moveaxis(verts,-2,0) |
|
|
|
|
|
vs = (dist*verts)[self.faces] |
|
|
|
vs = np.moveaxis(vs, 1,-2) |
|
|
|
vs = vs.reshape((len(self.faces)*int(np.prod(dist.shape[1:-1])),3,3)) |
|
|
|
pa = vs[...,1,:]-vs[...,0,:] |
|
pb = vs[...,2,:]-vs[...,0,:] |
|
|
|
d = .5*np.linalg.norm(np.cross(list(pa), list(pb)), axis = -1) |
|
d = d.reshape((len(self.faces),)+dist.shape[1:-1]) |
|
return np.sum(d, axis = 0) |
|
|
|
|
|
def copy(self, scale=(1,1,1)): |
|
""" returns a copy whose vertices are scaled by given factor""" |
|
scale = np.asarray(scale) |
|
assert scale.shape == (3,) |
|
res = copy.deepcopy(self) |
|
res._vertices *= scale[np.newaxis] |
|
return res |
|
|
|
|
|
|
|
|
|
def rays_from_json(d): |
|
return eval(d["name"])(**d["kwargs"]) |
|
|
|
|
|
|
|
|
|
class Rays_Explicit(Rays_Base): |
|
def __init__(self, vertices0, faces0): |
|
self.vertices0, self.faces0 = vertices0, faces0 |
|
super().__init__(vertices0=list(vertices0), faces0=list(faces0)) |
|
|
|
def setup_vertices_faces(self): |
|
return self.vertices0, self.faces0 |
|
|
|
|
|
class Rays_Cartesian(Rays_Base): |
|
def __init__(self, n_rays_x=11, n_rays_z=5): |
|
super().__init__(n_rays_x=n_rays_x, n_rays_z=n_rays_z) |
|
|
|
def setup_vertices_faces(self): |
|
"""has to return list of ( (z_1,y_1,x_1), ... ) _""" |
|
n_rays_x, n_rays_z = self.kwargs["n_rays_x"], self.kwargs["n_rays_z"] |
|
dphi = np.float32(2. * np.pi / n_rays_x) |
|
dtheta = np.float32(np.pi / n_rays_z) |
|
|
|
verts = [] |
|
for mz in range(n_rays_z): |
|
for mx in range(n_rays_x): |
|
phi = mx * dphi |
|
theta = mz * dtheta |
|
if mz == 0: |
|
theta = 1e-12 |
|
if mz == n_rays_z - 1: |
|
theta = np.pi - 1e-12 |
|
dx = np.cos(phi) * np.sin(theta) |
|
dy = np.sin(phi) * np.sin(theta) |
|
dz = np.cos(theta) |
|
if mz == 0 or mz == n_rays_z - 1: |
|
dx += 1e-12 |
|
dy += 1e-12 |
|
verts.append([dz, dy, dx]) |
|
|
|
verts = np.array(verts) |
|
|
|
def _ind(mz, mx): |
|
return mz * n_rays_x + mx |
|
|
|
faces = [] |
|
|
|
for mz in range(n_rays_z - 1): |
|
for mx in range(n_rays_x): |
|
faces.append([_ind(mz, mx), _ind(mz + 1, (mx + 1) % n_rays_x), _ind(mz, (mx + 1) % n_rays_x)]) |
|
faces.append([_ind(mz, mx), _ind(mz + 1, mx), _ind(mz + 1, (mx + 1) % n_rays_x)]) |
|
|
|
faces = np.array(faces) |
|
|
|
return verts, faces |
|
|
|
|
|
class Rays_SubDivide(Rays_Base): |
|
""" |
|
Subdivision polyehdra |
|
|
|
n_level = 1 -> base polyhedra |
|
n_level = 2 -> 1x subdivision |
|
n_level = 3 -> 2x subdivision |
|
... |
|
""" |
|
|
|
def __init__(self, n_level=4): |
|
super().__init__(n_level=n_level) |
|
|
|
def base_polyhedron(self): |
|
raise NotImplementedError() |
|
|
|
def setup_vertices_faces(self): |
|
n_level = self.kwargs["n_level"] |
|
verts0, faces0 = self.base_polyhedron() |
|
return self._recursive_split(verts0, faces0, n_level) |
|
|
|
def _recursive_split(self, verts, faces, n_level): |
|
if n_level <= 1: |
|
return verts, faces |
|
else: |
|
verts, faces = Rays_SubDivide.split(verts, faces) |
|
return self._recursive_split(verts, faces, n_level - 1) |
|
|
|
@classmethod |
|
def split(self, verts0, faces0): |
|
"""split a level""" |
|
|
|
split_edges = dict() |
|
verts = list(verts0[:]) |
|
faces = [] |
|
|
|
def _add(a, b): |
|
""" returns index of middle point and adds vertex if not already added""" |
|
edge = tuple(sorted((a, b))) |
|
if not edge in split_edges: |
|
v = .5 * (verts[a] + verts[b]) |
|
v *= 1. / np.linalg.norm(v) |
|
verts.append(v) |
|
split_edges[edge] = len(verts) - 1 |
|
return split_edges[edge] |
|
|
|
for v1, v2, v3 in faces0: |
|
ind1 = _add(v1, v2) |
|
ind2 = _add(v2, v3) |
|
ind3 = _add(v3, v1) |
|
faces.append([v1, ind1, ind3]) |
|
faces.append([v2, ind2, ind1]) |
|
faces.append([v3, ind3, ind2]) |
|
faces.append([ind1, ind2, ind3]) |
|
|
|
return verts, faces |
|
|
|
|
|
class Rays_Tetra(Rays_SubDivide): |
|
""" |
|
Subdivision of a tetrahedron |
|
|
|
n_level = 1 -> normal tetrahedron (4 vertices) |
|
n_level = 2 -> 1x subdivision (10 vertices) |
|
n_level = 3 -> 2x subdivision (34 vertices) |
|
... |
|
""" |
|
|
|
def base_polyhedron(self): |
|
verts = np.array([ |
|
[np.sqrt(8. / 9), 0., -1. / 3], |
|
[-np.sqrt(2. / 9), np.sqrt(2. / 3), -1. / 3], |
|
[-np.sqrt(2. / 9), -np.sqrt(2. / 3), -1. / 3], |
|
[0., 0., 1.] |
|
]) |
|
faces = [[0, 1, 2], |
|
[0, 3, 1], |
|
[0, 2, 3], |
|
[1, 3, 2]] |
|
|
|
return verts, faces |
|
|
|
|
|
class Rays_Octo(Rays_SubDivide): |
|
""" |
|
Subdivision of a tetrahedron |
|
|
|
n_level = 1 -> normal Octahedron (6 vertices) |
|
n_level = 2 -> 1x subdivision (18 vertices) |
|
n_level = 3 -> 2x subdivision (66 vertices) |
|
|
|
""" |
|
|
|
def base_polyhedron(self): |
|
verts = np.array([ |
|
[0, 0, 1], |
|
[0, 1, 0], |
|
[0, 0, -1], |
|
[0, -1, 0], |
|
[1, 0, 0], |
|
[-1, 0, 0]]) |
|
|
|
faces = [[0, 1, 4], |
|
[0, 5, 1], |
|
[1, 2, 4], |
|
[1, 5, 2], |
|
[2, 3, 4], |
|
[2, 5, 3], |
|
[3, 0, 4], |
|
[3, 5, 0], |
|
] |
|
|
|
return verts, faces |
|
|
|
|
|
def reorder_faces(verts, faces): |
|
"""reorder faces such that their orientation points outward""" |
|
def _single(face): |
|
return face[::-1] if np.linalg.det(verts[face])>0 else face |
|
return tuple(map(_single, faces)) |
|
|
|
|
|
class Rays_GoldenSpiral(Rays_Base): |
|
def __init__(self, n=70, anisotropy = None): |
|
if n<4: |
|
raise ValueError("At least 4 points have to be given!") |
|
super().__init__(n=n, anisotropy = anisotropy if anisotropy is None else tuple(anisotropy)) |
|
|
|
def setup_vertices_faces(self): |
|
n = self.kwargs["n"] |
|
anisotropy = self.kwargs["anisotropy"] |
|
if anisotropy is None: |
|
anisotropy = np.ones(3) |
|
else: |
|
anisotropy = np.array(anisotropy) |
|
|
|
|
|
g = (3. - np.sqrt(5.)) * np.pi |
|
phi = g * np.arange(n) |
|
|
|
|
|
|
|
|
|
z = np.linspace(-1, 1, n) |
|
rho = np.sqrt(1. - z ** 2) |
|
verts = np.stack([z, rho * np.sin(phi), rho * np.cos(phi)]).T |
|
|
|
|
|
|
|
|
|
verts = verts/anisotropy |
|
|
|
|
|
hull = ConvexHull(verts) |
|
faces = reorder_faces(verts,hull.simplices) |
|
|
|
verts /= np.linalg.norm(verts, axis=-1, keepdims=True) |
|
|
|
return verts, faces |
|
|