Module molgri.grids
Expand source code
from abc import ABC, abstractmethod
import networkx as nx
import numpy as np
from scipy.constants import pi, golden
from scipy.spatial import distance_matrix
from scipy.spatial.distance import cdist
import pandas as pd
from .analysis import random_sphere_points, unit_dist_on_sphere, random_axes_count_points
from .constants import DEFAULT_SEED, SIX_METHOD_NAMES, UNIQUE_TOL, ENDING_GRID_FILES
from .parsers import NameParser
from .paths import PATH_OUTPUT_ROTGRIDS, PATH_OUTPUT_STAT
from .rotations import grid2quaternion, grid2euler, quaternion2grid, euler2grid
from .wrappers import time_method
class Polytope(ABC):
def __init__(self):
"""
A polytope is a 3-dim object consisting of a set of vertices and connections between them (edges) saved
in self.G property.
"""
self.G = nx.Graph()
self.faces = []
self.current_level = 0
self.side_len = 0
self._create_level0()
@abstractmethod
def _create_level0(self):
# this is implemented by each subclass since they have different edges, vertices and faces
pass
def plot_graph(self):
"""
Plot the networkx graph of self.G.
"""
node_labels = {i: tuple(np.round(i, 3)) for i in self.G.nodes}
nx.draw(self.G, pos=nx.circular_layout(self.G), with_labels=True, labels=node_labels)
def plot_points(self, ax, select_faces: set = None, projection: bool = False):
"""
Plot the points of the icosahedron + possible division points. Colored by level at which the point was added.
Possible to select only one or a few faces on which points are to be plotted for clarity.
Args:
ax: axis
select_faces: a set of face numbers from 0 to (incl) 19, e.g. {0, 5}. If None, all faces are shown.
projection: True if you want to plot the projected points, not the ones on surfaces of polytope
"""
select_faces = set(range(20)) if select_faces is None else select_faces
level_color = ["black", "red", "blue", "green"]
for point in self.G.nodes(data=True):
# select only points that belong to at least one of the chosen select_faces
if len(set(point[1]["face"]).intersection(select_faces)) > 0:
# color selected based on the level of the node
level_node = point[1]["level"]
if projection:
proj_node = point[1]["projection"]
ax.scatter(*proj_node[0], color=level_color[level_node], s=30)
else:
ax.scatter(*point[0], color=level_color[level_node], s=30)
def plot_edges(self, ax, select_faces=None, **kwargs):
"""
Plot the edges between the points. Can select to display only some faces for clarity.
Args:
ax: axis
select_faces: a set of face numbers from 0 to (incl) 19, e.g. {0, 5}. If None, all faces are shown.
**kwargs: other plotting arguments
"""
select_faces = set(range(20)) if select_faces is None else select_faces
for edge in self.G.edges:
faces_edge_1 = set(self.G.nodes[edge[0]]["face"])
faces_edge_2 = set(self.G.nodes[edge[1]]["face"])
# both the start and the end point of the edge must belong to one of the selected faces
if len(faces_edge_1.intersection(select_faces)) > 0 and len(faces_edge_2.intersection(select_faces)) > 0:
ax.plot(*np.array(edge).T, color="black", **kwargs)
def divide_edges(self):
"""
Subdivide once. If previous faces are triangles, adds three points per face (half sides). If they are
squares, adds 4 points per face (half sides + middle). New points will have a higher level and will
be appropriately added to one or more faces (if on edges).
"""
# need to keep track of what to add/remove, since we can't change the network while inside the loop
# consists of (new_point, previous_point_1, previous_point_2) to keep all info
nodes_to_add = []
for node_vector in self.G.nodes():
for neighbour_vector in self.G.neighbors(node_vector):
# this is just to avoid doing everything twice - do it for edge A-B but not B-A
if node_vector < neighbour_vector:
# new point is just the average of the previous two
new_point = tuple((np.array(node_vector)+np.array(neighbour_vector))/2.0)
nodes_to_add.append((new_point, node_vector, neighbour_vector))
# add new nodes, add edges from cont. points to the node and delete the edge between the cont. points
for el in nodes_to_add:
new_point, node_vector, neighbour_vector = el
# new point will have the set of faces that both node and neighbour vector have
faces_node_vector = self.G.nodes[node_vector]["face"]
faces_neighbour_vector = self.G.nodes[neighbour_vector]["face"]
# diagonals get added twice, so this is necessary
if new_point not in self.G.nodes:
self.G.add_node(new_point, level=self.current_level + 1,
face=set(faces_node_vector).intersection(faces_neighbour_vector),
projection=project_grid_on_sphere(np.array(new_point)[np.newaxis, :]))
self.G.add_edge(new_point, neighbour_vector,
length=unit_dist_on_sphere(self.G.nodes[new_point]["projection"],
self.G.nodes[neighbour_vector]["projection"]))
self.G.add_edge(new_point, node_vector,
length=unit_dist_on_sphere(self.G.nodes[new_point]["projection"],
self.G.nodes[neighbour_vector]["projection"]))
# self.G.remove_edge(node_vector, neighbour_vector)
# also add edges between new nodes at distance side_len or sqrt(2)*side_len
new_level = [x for x, y in self.G.nodes(data=True) if y['level'] == self.current_level+1]
for new_node in new_level:
# searching only second neighbours at appropriate level
sec_neighbours = list(second_neighbours(self.G, new_node))
sec_neighbours = [x for x in sec_neighbours if self.G.nodes[x]["level"] == self.current_level+1]
for other_node in sec_neighbours:
node_dist = np.linalg.norm(np.array(new_node)-np.array(other_node))
# check distance criterion
if np.isclose(node_dist, self.side_len) or np.isclose(node_dist, self.side_len*np.sqrt(2)):
self.G.add_edge(new_node, other_node,
length=unit_dist_on_sphere(self.G.nodes[new_node]["projection"],
self.G.nodes[other_node]["projection"])
)
self.current_level += 1
self.side_len = self.side_len / 2
class IcosahedronPolytope(Polytope):
"""
IcosahedronGrid is a graph object, its central feature is self.G (networkx graph). In the beginning, each node is
a vertex of a 3D icosahedron. It is possible to subdivide the sides, in that case a new point always appears in the
middle of each triangle side.
"""
def _create_level0(self):
# DO NOT change order of points - faces will be wrong!
# each face contains numbers - indices of vertices that border on this face
self.faces = [[0, 11, 5],
[0, 5, 1],
[0, 1, 7],
[0, 7, 10],
[0, 10, 11],
[1, 5, 9],
[5, 11, 4],
[11, 10, 2],
[10, 7, 6],
[7, 1, 8],
[3, 9, 4],
[3, 4, 2],
[3, 2, 6],
[3, 6, 8],
[3, 8, 9],
[4, 9, 5],
[2, 4, 11],
[6, 2, 10],
[8, 6, 7],
[9, 8, 1]]
side_len = 1 / np.sin(2 * pi / 5)
self.side_len = side_len
# create vertices
vertices = [(-1, golden, 0), (1, golden, 0), (-1, -golden, 0), (1, -golden, 0),
(0, -1, golden), (0, 1, golden), (0, -1, -golden), (0, 1, -golden),
(golden, 0, -1), (golden, 0, 1), (-golden, 0, -1), (-golden, 0, 1)]
vertices = np.array(vertices) * side_len / 2
# create edges
point_connections = _calc_edges(vertices, side_len)
# add vertices and edges to the graph
for i, vert in enumerate(vertices):
set_of_faces = tuple(faces_i for faces_i, face in enumerate(self.faces) if i in face)
self.G.add_node(tuple(vert), level=self.current_level, face=set_of_faces,
projection=project_grid_on_sphere(vert[np.newaxis, :]))
for key, value in point_connections.items():
for vi in value:
self.G.add_edge(key, tuple(vi),
length=unit_dist_on_sphere(self.G.nodes[key]["projection"],
self.G.nodes[tuple(vi)]["projection"]))
# just to check ...
assert self.G.number_of_nodes() == 12
assert self.G.number_of_edges() == 30
for node in self.G.nodes(data=True):
assert len(node[1]["face"]) == 5 and node[1]["level"] == 0
self.side_len = side_len / 2
class CubePolytope(Polytope):
"""
CubeGrid is a graph object, its central feature is self.G (networkx graph). In the beginning, each node is
a vertex of a 3D cube. It is possible to subdivide the sides, in that case a new point always appears in the
middle of a square and half of previous sides.
"""
def _create_level0(self):
# DO NOT change order of points - faces will be wrong!
# each face contains numbers - indices of vertices that border on this face
self.faces = [[0, 1, 2, 4],
[0, 2, 3, 6],
[0, 1, 3, 5],
[3, 5, 6, 7],
[1, 4, 5, 7],
[2, 4, 6, 7]]
self.side_len = 2 * np.sqrt(1/3)
# create vertices
vertices = [(-self.side_len/2, -self.side_len/2, -self.side_len/2),
(-self.side_len/2, -self.side_len/2, self.side_len/2),
(-self.side_len/2, self.side_len/2, -self.side_len/2),
(self.side_len/2, -self.side_len/2, -self.side_len/2),
(-self.side_len/2, self.side_len/2, self.side_len/2),
(self.side_len/2, -self.side_len/2, self.side_len/2),
(self.side_len/2, self.side_len/2, -self.side_len/2),
(self.side_len/2, self.side_len/2, self.side_len/2)]
vertices = np.array(vertices)
# create edges
point_connections = _calc_edges(vertices, self.side_len)
# add vertices and edges to the graph
for i, vert in enumerate(vertices):
set_of_faces = tuple(faces_i for faces_i, face in enumerate(self.faces) if i in face)
self.G.add_node(tuple(vert), level=self.current_level, face=set_of_faces,
projection=project_grid_on_sphere(vert[np.newaxis, :]))
for key, value in point_connections.items():
for vi in value:
self.G.add_edge(key, tuple(vi),
length=unit_dist_on_sphere(self.G.nodes[key]["projection"],
self.G.nodes[tuple(vi)]["projection"]))
# just to check ...
assert self.G.number_of_nodes() == 8
assert self.G.number_of_edges() == 24
for node in self.G.nodes(data=True):
assert len(node[1]["face"]) == 3 and node[1]["level"] == 0
self.side_len = self.side_len / 2
def _calc_edges(vertices: np.ndarray, side_len: float) -> dict:
"""
Needed for the determination of edges while setting up a polytope. If a bit weird, keep in mind this is a legacy
function. Do not use on its own.
Args:
vertices: an array of polytope vertices
side_len: everything < sqrt(2)*side_len apart will be considered a neighbour
Returns:
a dictionary, key is the vertex, values are its neighbours
"""
dist = distance_matrix(vertices, vertices)
# filter points more than 0 and less than sqrt(2)*side away from each other
# this gives exactly triangles and squares as neighbours
where_result = np.where((dist <= np.sqrt(2)*side_len) & (0 < dist))
indices_min_dist = zip(where_result[0], where_result[1])
# key is the vertex, values are its neighbours
tree_connections = {tuple(vert): [] for vert in vertices}
for i1, i2 in indices_min_dist:
tree_connections[tuple(vertices[i1])].append(vertices[i2])
return tree_connections
def second_neighbours(graph: nx.Graph, node):
"""Yield second neighbors of node in graph.
Neighbors are not not unique!
"""
for neighbor_list in [graph.neighbors(n) for n in graph.neighbors(node)]:
for n in neighbor_list:
yield n
class Grid(ABC):
def __init__(self, N: int, *, ordered: bool = True, use_saved: bool = False, gen_alg: str = None,
time_generation: bool = False):
"""
Generate a grid with one of generation algorithms.
Args:
gen_alg: MUST BE SET IN SUBCLASSES, algorithm name, see names given in SIX_METHOD_NAMES
N: number of grid points
ordered: if True order and truncate, else only truncate to N points
use_saved: if True use saved grids if available
time_generation: if True write out a message about time needed for generation
"""
self.rn_gen = np.random.default_rng(DEFAULT_SEED)
np.random.seed(DEFAULT_SEED)
if gen_alg not in SIX_METHOD_NAMES:
raise ValueError(f"{gen_alg} is not a valid generation algorithm name. Try 'ico', 'cube3D' ...")
self.ordered = ordered
self.N = N
name_properties = {"grid_type": gen_alg, "num_grid_points": N, "ordering": ordered}
self.standard_name = NameParser(name_properties).get_standard_name()
self.decorator_label = f"rotation grid {self.standard_name}"
self.grid = None
self.time = 0
self.nn_dist_arch = None
self.nn_dist_cup = None
self.short_statistics_path = f"{PATH_OUTPUT_STAT}{self.standard_name}_short_stat.txt"
self.statistics_path = f"{PATH_OUTPUT_STAT}{self.standard_name}_full_stat.csv"
gen_func = self.generate_and_time if time_generation else self.generate_grid
# if this option enabled, search first if this grid has already been saved
if use_saved:
try:
self.grid = np.load(f"{PATH_OUTPUT_ROTGRIDS}{self.standard_name}.npy")
except FileNotFoundError:
gen_func()
self.save_grid()
else:
gen_func()
assert isinstance(self.grid, np.ndarray), "A grid must be a numpy array!"
assert self.grid.shape == (N, 3), f"Grid not of correct shape! {self.grid.shape} instead of {(N, 3)}"
assert np.allclose(np.linalg.norm(self.grid, axis=1), 1, atol=10**(-UNIQUE_TOL))
def get_grid(self) -> np.ndarray:
return self.grid
def reduce_N(self, reduce_by=1):
if reduce_by <= 0:
return
self.grid = self.grid[:-reduce_by]
self.N = self.N - reduce_by
self.time = 0
self.nn_dist_arch = None
self.nn_dist_cup = None
@abstractmethod
def generate_grid(self):
# order or truncate
if self.ordered:
self._order()
else:
self.grid = self.grid[:self.N]
@time_method
def generate_and_time(self):
self.generate_grid()
def _order(self):
self.grid = order_grid_points(self.grid, self.N)
def as_quaternion(self) -> np.ndarray:
quaternion_seq = grid2quaternion(self.grid)
assert isinstance(quaternion_seq, np.ndarray), "A quaternion sequence must be a numpy array!"
assert quaternion_seq.shape == (self.N, 4), f"Quaternion sequence not of correct shape!\
{quaternion_seq.shape} instead of {(self.N, 4)}"
return quaternion_seq
def as_euler(self) -> np.ndarray:
euler_seq = grid2euler(self.grid)
assert isinstance(euler_seq, np.ndarray), "An Euler sequence must be a numpy array!"
assert euler_seq.shape == (self.N, 3), f"An Euler sequence not of correct shape!\
{euler_seq.shape} instead of {(self.N, 33)}"
return euler_seq
def save_grid(self):
np.save(f"{PATH_OUTPUT_ROTGRIDS}{self.standard_name}.{ENDING_GRID_FILES}", self.grid)
def save_grid_txt(self):
np.savetxt(f"{PATH_OUTPUT_ROTGRIDS}{self.standard_name}.txt", self.grid)
def save_statistics(self, num_random: int = 100, print_message=False):
# first message (what measure you are using)
newline = "\n"
m1 = f"STATISTICS: Testing the coverage of grid {self.standard_name} using {num_random} " \
f"random points on a sphere."
m2 = f"We select {num_random} random axes and count the number of grid points that fall within the angle" \
f"alpha (selected from [pi / 6, 2 * pi / 6, 3 * pi / 6, 4 * pi / 6, 5 * pi / 6]) of this axis. For an" \
f"ideally uniform grid, we expect the ratio of num_within_alpha/total_num_points to equal the ratio" \
f"area_of_alpha_spherical_cap/area_of_sphere, which we call ideal coverage."
stat_data, full_data = self._generate_statistics(num_rand_points=num_random)
if print_message:
print(m1)
print(stat_data)
# dealing with the file
with open(self.short_statistics_path, "w") as f:
f.writelines([m1, newline, newline, m2, newline, newline])
stat_data.to_csv(self.short_statistics_path, mode="a")
full_data.to_csv(self.statistics_path, mode="w")
def _generate_statistics(self, num_rand_points: int = 100) -> tuple:
# write out short version ("N points", "min", "max", "average", "SD")
columns = ["alphas", "ideal coverages", "min coverage", "avg coverage", "max coverage", "standard deviation"]
ratios_columns = ["coverages", "alphas", "ideal coverage"]
alphas = [pi/6, 2*pi/6, 3*pi/6, 4*pi/6, 5*pi/6]
ratios = [[], [], []]
sphere_surface = 4 * pi
data = np.zeros((len(alphas), 6)) # 5 data columns for: alpha, ideal coverage, min, max, average, sd
for i, alpha in enumerate(alphas):
cone_area = 2 * pi * (1-np.cos(alpha))
ideal_coverage = cone_area / sphere_surface
actual_coverages = random_axes_count_points(self, alpha, num_random_points=num_rand_points)
ratios[0].extend(actual_coverages)
ratios[1].extend([alpha]*num_rand_points)
ratios[2].extend([ideal_coverage]*num_rand_points)
data[i][0] = alpha
data[i][1] = ideal_coverage
data[i][2] = np.min(actual_coverages)
data[i][3] = np.average(actual_coverages)
data[i][4] = np.max(actual_coverages)
data[i][5] = np.std(actual_coverages)
alpha_df = pd.DataFrame(data=data, columns=columns)
alpha_df = alpha_df.set_index("alphas")
ratios_df = pd.DataFrame(data=np.array(ratios).T, columns=ratios_columns)
return alpha_df, ratios_df
def order_grid_points(grid: np.ndarray, N: int, start_i: int = 1) -> np.ndarray:
"""
You are provided with a (possibly) unordered grid and return a grid with N points ordered in such a way that
these N points have the best possible coverage.
Args:
grid: grid, array of shape (L, 3) to be ordered where L should be >= N
N: number of grid points wished at the end
start_i: from which index to start ordering (in case the first i elements already ordered)
Returns:
an array of shape (N, 3) ordered in such a way that these N points have the best possible coverage.
"""
if N > len(grid):
print(f"Warning! N>len(grid)! Only {len(grid)} points will be returned!")
for index in range(start_i, min(len(grid), N)):
grid = select_next_gridpoint(grid, index)
return grid[:N]
def project_grid_on_sphere(grid: np.ndarray) -> np.ndarray:
"""
A grid can be seen as a collection of vectors to gridpoints. If a vector is scaled to 1, it will represent a point
on a unit sphere in d-1 dimensions. This function normalizes the vectors, creating vectors pointing to the
surface of a d-1 dimensional sphere.
Args:
grid: a (N, d) array where each row represents the coordinates of a grid point
Returns:
a (N, d) array where each row has been scaled to length 1
"""
largest_abs = np.max(np.abs(grid), axis=1)[:, np.newaxis]
grid = np.divide(grid, largest_abs)
norms = np.linalg.norm(grid, axis=1)[:, np.newaxis]
return np.divide(grid, norms)
def select_next_gridpoint(set_grid_points, i):
"""
Provide a set of grid points where the first i are already sorted. Find the best next gridpoint out of points
in set_grid_points[i:]
Args:
set_grid_points: grid, array of shape (L, 3) where elements up to i are already ordered
i: index how far the array is already ordered (up to bun not including i).
Returns:
set_grid_points where the ith element in swapped with the best possible next grid point
"""
distances = cdist(set_grid_points[i:], set_grid_points[:i], metric="cosine")
distances.sort()
nn_dist = distances[:, 0]
index_max = np.argmax(nn_dist)
set_grid_points[[i, i + index_max]] = set_grid_points[[i + index_max, i]]
return set_grid_points
class RandomQGrid(Grid):
def __init__(self, N: int, **kwargs):
super().__init__(N, gen_alg="randomQ", **kwargs)
def generate_grid(self):
result = np.zeros((self.N, 4))
random_num = self.rn_gen.random((self.N, 3))
result[:, 0] = np.sqrt(1 - random_num[:, 0]) * np.sin(2 * pi * random_num[:, 1])
result[:, 1] = np.sqrt(1 - random_num[:, 0]) * np.cos(2 * pi * random_num[:, 1])
result[:, 2] = np.sqrt(random_num[:, 0]) * np.sin(2 * pi * random_num[:, 2])
result[:, 3] = np.sqrt(random_num[:, 0]) * np.cos(2 * pi * random_num[:, 2])
self.grid = quaternion2grid(result)
# No super call because ordering not needed for random points and the number of points is exact!
class RandomEGrid(Grid):
def __init__(self, N: int, **kwargs):
super().__init__(N, gen_alg="randomE", **kwargs)
def generate_grid(self):
euler_angles = 2 * pi * self.rn_gen.random((self.N, 3))
self.grid = euler2grid(euler_angles)
# No super call because ordering not needed for random points and the number of points is exact!
class SystemEGrid(Grid):
def __init__(self, N: int, **kwargs):
super().__init__(N, gen_alg="systemE", **kwargs)
def generate_grid(self):
self.grid = []
num_points = 1
while len(self.grid) < self.N:
phis = np.linspace(0, 2 * pi, num_points)
thetas = np.linspace(0, 2 * pi, num_points)
psis = np.linspace(0, 2 * pi, num_points)
euler_meshgrid = np.array(np.meshgrid(*(phis, thetas, psis)), dtype=float)
euler_meshgrid = euler_meshgrid.reshape((3, -1)).T
# convert to a grid
self.grid = euler2grid(euler_meshgrid)
self.grid = np.unique(np.round(self.grid, UNIQUE_TOL), axis=0)
num_points += 1
super().generate_grid()
class Cube4DGrid(Grid):
def __init__(self, N: int, **kwargs):
self.d = 4 # dimensions
super().__init__(N, gen_alg="cube4D", **kwargs)
def generate_grid(self):
self.grid = []
num_divisions = 1
state_before = np.random.get_state()
while len(self.grid) < self.N:
grid_qua = self._full_d_dim_grid()
grid_qua = self._select_only_faces(grid_qua)
grid_qua = project_grid_on_sphere(grid_qua)
# select only half the sphere
grid_qua = grid_qua[grid_qua[:, self.d - 1] >= 0, :]
# convert to grid
self.grid = quaternion2grid(grid_qua)
self.grid = np.unique(np.round(self.grid, UNIQUE_TOL), axis=0)
num_divisions += 1
np.random.set_state(state_before)
super().generate_grid()
def _full_d_dim_grid(self, cheb: bool = False, change_start: float = 0, change_end: float = 0,
dtype=np.float64) -> np.ndarray:
"""
This is a function to create a classical grid of a d-dimensional cube. It creates a grid over the entire
(hyper)volume of the (hyper)cube.
This is a unit cube between -sqrt(1/d) and sqrt(1/d) in all dimensions where d = num of dimensions.
Args:
cheb: use Chebyscheff points instead of equally spaced points
change_start: add or subtract from -sqrt(1/d) as the start of the grid
change_end: add or subtract from sqrt(1/d) as the end of the grid
dtype: forwarded to linspace while creating a grid
Returns:
a meshgrid of dimension (d, n, n, .... n) where n is repeated d times
"""
if cheb:
from numpy.polynomial.chebyshev import chebpts1
side = chebpts1(self.N)
else:
# np.sqrt(1/d)
side = np.linspace(-1 + change_start, 1 - change_end, self.N, dtype=dtype)
# repeat the same n points d times and then make a new line of the array every d elements
sides = np.tile(side, self.d)
sides = sides[np.newaxis, :].reshape((self.d, self.N))
# create a grid by meshing every line of the sides array
return np.array(np.meshgrid(*sides))
def _select_only_faces(self, grid: np.ndarray):
"""
Take a meshgrid (d, n, n, ... n) and return an array of points (N, d) including only the points that
lie on the faces of the grid, so the edge points in at least one of dimensions.
Args:
grid: numpy array (d, n, n, ... n) containing grid points
Returns:
points (N, d) where N is the number of edge points and d the dimension
"""
assert self.d == len(grid)
set_grids = []
for swap_i in range(self.d):
meshgrid_swapped = np.swapaxes(grid, axis1=1, axis2=(1 + swap_i))
set_grids.append(meshgrid_swapped[:, 0, ...])
set_grids.append(meshgrid_swapped[:, -1, ...])
result = np.hstack(set_grids).reshape((self.d, -1)).T
return np.unique(result, axis=0)
class Polyhedron3DGrid(Grid):
def __init__(self, N: int, polyhedron, **kwargs):
self.polyhedron = polyhedron()
super().__init__(N, **kwargs)
def generate_grid(self):
while self.polyhedron.G.number_of_nodes() < self.N:
self.polyhedron.divide_edges()
self.grid = np.array([y["projection"] for x, y in self.polyhedron.G.nodes(data=True)]).squeeze()
np.random.shuffle(self.grid)
super().generate_grid()
class Cube3DGrid(Polyhedron3DGrid):
def __init__(self, N: int, **kwargs):
super().__init__(N, polyhedron=CubePolytope, gen_alg="cube3D", **kwargs)
class IcoGrid(Polyhedron3DGrid):
def __init__(self, N: int, **kwargs):
super().__init__(N, polyhedron=IcosahedronPolytope, gen_alg="ico", **kwargs)
def build_grid(grid_type: str, N: int, **kwargs) -> Grid:
name2grid = {"randomQ": RandomQGrid,
"randomE": RandomEGrid,
"cube4D": Cube4DGrid,
"systemE": SystemEGrid,
"cube3D": Cube3DGrid,
"ico": IcoGrid}
if grid_type not in name2grid.keys():
raise ValueError(f"{grid_type} is not a valid grid type. Try 'ico', 'cube3D' ...")
grid_obj = name2grid[grid_type]
return grid_obj(N, **kwargs)
Functions
def build_grid(grid_type: str, N: int, **kwargs) ‑> Grid
-
Expand source code
def build_grid(grid_type: str, N: int, **kwargs) -> Grid: name2grid = {"randomQ": RandomQGrid, "randomE": RandomEGrid, "cube4D": Cube4DGrid, "systemE": SystemEGrid, "cube3D": Cube3DGrid, "ico": IcoGrid} if grid_type not in name2grid.keys(): raise ValueError(f"{grid_type} is not a valid grid type. Try 'ico', 'cube3D' ...") grid_obj = name2grid[grid_type] return grid_obj(N, **kwargs)
def order_grid_points(grid: numpy.ndarray, N: int, start_i: int = 1) ‑> numpy.ndarray
-
You are provided with a (possibly) unordered grid and return a grid with N points ordered in such a way that these N points have the best possible coverage.
Args
grid
- grid, array of shape (L, 3) to be ordered where L should be >= N
N
- number of grid points wished at the end
start_i
- from which index to start ordering (in case the first i elements already ordered)
Returns
an array of shape (N, 3) ordered in such a way that these N points have the best possible coverage.
Expand source code
def order_grid_points(grid: np.ndarray, N: int, start_i: int = 1) -> np.ndarray: """ You are provided with a (possibly) unordered grid and return a grid with N points ordered in such a way that these N points have the best possible coverage. Args: grid: grid, array of shape (L, 3) to be ordered where L should be >= N N: number of grid points wished at the end start_i: from which index to start ordering (in case the first i elements already ordered) Returns: an array of shape (N, 3) ordered in such a way that these N points have the best possible coverage. """ if N > len(grid): print(f"Warning! N>len(grid)! Only {len(grid)} points will be returned!") for index in range(start_i, min(len(grid), N)): grid = select_next_gridpoint(grid, index) return grid[:N]
def project_grid_on_sphere(grid: numpy.ndarray) ‑> numpy.ndarray
-
A grid can be seen as a collection of vectors to gridpoints. If a vector is scaled to 1, it will represent a point on a unit sphere in d-1 dimensions. This function normalizes the vectors, creating vectors pointing to the surface of a d-1 dimensional sphere.
Args
grid
- a (N, d) array where each row represents the coordinates of a grid point
Returns
a (N, d) array where each row has been scaled to length 1
Expand source code
def project_grid_on_sphere(grid: np.ndarray) -> np.ndarray: """ A grid can be seen as a collection of vectors to gridpoints. If a vector is scaled to 1, it will represent a point on a unit sphere in d-1 dimensions. This function normalizes the vectors, creating vectors pointing to the surface of a d-1 dimensional sphere. Args: grid: a (N, d) array where each row represents the coordinates of a grid point Returns: a (N, d) array where each row has been scaled to length 1 """ largest_abs = np.max(np.abs(grid), axis=1)[:, np.newaxis] grid = np.divide(grid, largest_abs) norms = np.linalg.norm(grid, axis=1)[:, np.newaxis] return np.divide(grid, norms)
def second_neighbours(graph: networkx.classes.graph.Graph, node)
-
Yield second neighbors of node in graph. Neighbors are not not unique!
Expand source code
def second_neighbours(graph: nx.Graph, node): """Yield second neighbors of node in graph. Neighbors are not not unique! """ for neighbor_list in [graph.neighbors(n) for n in graph.neighbors(node)]: for n in neighbor_list: yield n
def select_next_gridpoint(set_grid_points, i)
-
Provide a set of grid points where the first i are already sorted. Find the best next gridpoint out of points in set_grid_points[i:]
Args
set_grid_points
- grid, array of shape (L, 3) where elements up to i are already ordered
i
- index how far the array is already ordered (up to bun not including i).
Returns
set_grid_points where the ith element in swapped with the best possible next grid point
Expand source code
def select_next_gridpoint(set_grid_points, i): """ Provide a set of grid points where the first i are already sorted. Find the best next gridpoint out of points in set_grid_points[i:] Args: set_grid_points: grid, array of shape (L, 3) where elements up to i are already ordered i: index how far the array is already ordered (up to bun not including i). Returns: set_grid_points where the ith element in swapped with the best possible next grid point """ distances = cdist(set_grid_points[i:], set_grid_points[:i], metric="cosine") distances.sort() nn_dist = distances[:, 0] index_max = np.argmax(nn_dist) set_grid_points[[i, i + index_max]] = set_grid_points[[i + index_max, i]] return set_grid_points
Classes
class Cube3DGrid (N: int, **kwargs)
-
Helper class that provides a standard way to create an ABC using inheritance.
Generate a grid with one of generation algorithms.
Args
gen_alg
- MUST BE SET IN SUBCLASSES, algorithm name, see names given in SIX_METHOD_NAMES
N
- number of grid points
ordered
- if True order and truncate, else only truncate to N points
use_saved
- if True use saved grids if available
time_generation
- if True write out a message about time needed for generation
Expand source code
class Cube3DGrid(Polyhedron3DGrid): def __init__(self, N: int, **kwargs): super().__init__(N, polyhedron=CubePolytope, gen_alg="cube3D", **kwargs)
Ancestors
- Polyhedron3DGrid
- Grid
- abc.ABC
class Cube4DGrid (N: int, **kwargs)
-
Helper class that provides a standard way to create an ABC using inheritance.
Generate a grid with one of generation algorithms.
Args
gen_alg
- MUST BE SET IN SUBCLASSES, algorithm name, see names given in SIX_METHOD_NAMES
N
- number of grid points
ordered
- if True order and truncate, else only truncate to N points
use_saved
- if True use saved grids if available
time_generation
- if True write out a message about time needed for generation
Expand source code
class Cube4DGrid(Grid): def __init__(self, N: int, **kwargs): self.d = 4 # dimensions super().__init__(N, gen_alg="cube4D", **kwargs) def generate_grid(self): self.grid = [] num_divisions = 1 state_before = np.random.get_state() while len(self.grid) < self.N: grid_qua = self._full_d_dim_grid() grid_qua = self._select_only_faces(grid_qua) grid_qua = project_grid_on_sphere(grid_qua) # select only half the sphere grid_qua = grid_qua[grid_qua[:, self.d - 1] >= 0, :] # convert to grid self.grid = quaternion2grid(grid_qua) self.grid = np.unique(np.round(self.grid, UNIQUE_TOL), axis=0) num_divisions += 1 np.random.set_state(state_before) super().generate_grid() def _full_d_dim_grid(self, cheb: bool = False, change_start: float = 0, change_end: float = 0, dtype=np.float64) -> np.ndarray: """ This is a function to create a classical grid of a d-dimensional cube. It creates a grid over the entire (hyper)volume of the (hyper)cube. This is a unit cube between -sqrt(1/d) and sqrt(1/d) in all dimensions where d = num of dimensions. Args: cheb: use Chebyscheff points instead of equally spaced points change_start: add or subtract from -sqrt(1/d) as the start of the grid change_end: add or subtract from sqrt(1/d) as the end of the grid dtype: forwarded to linspace while creating a grid Returns: a meshgrid of dimension (d, n, n, .... n) where n is repeated d times """ if cheb: from numpy.polynomial.chebyshev import chebpts1 side = chebpts1(self.N) else: # np.sqrt(1/d) side = np.linspace(-1 + change_start, 1 - change_end, self.N, dtype=dtype) # repeat the same n points d times and then make a new line of the array every d elements sides = np.tile(side, self.d) sides = sides[np.newaxis, :].reshape((self.d, self.N)) # create a grid by meshing every line of the sides array return np.array(np.meshgrid(*sides)) def _select_only_faces(self, grid: np.ndarray): """ Take a meshgrid (d, n, n, ... n) and return an array of points (N, d) including only the points that lie on the faces of the grid, so the edge points in at least one of dimensions. Args: grid: numpy array (d, n, n, ... n) containing grid points Returns: points (N, d) where N is the number of edge points and d the dimension """ assert self.d == len(grid) set_grids = [] for swap_i in range(self.d): meshgrid_swapped = np.swapaxes(grid, axis1=1, axis2=(1 + swap_i)) set_grids.append(meshgrid_swapped[:, 0, ...]) set_grids.append(meshgrid_swapped[:, -1, ...]) result = np.hstack(set_grids).reshape((self.d, -1)).T return np.unique(result, axis=0)
Ancestors
- Grid
- abc.ABC
Methods
def generate_grid(self)
-
Expand source code
def generate_grid(self): self.grid = [] num_divisions = 1 state_before = np.random.get_state() while len(self.grid) < self.N: grid_qua = self._full_d_dim_grid() grid_qua = self._select_only_faces(grid_qua) grid_qua = project_grid_on_sphere(grid_qua) # select only half the sphere grid_qua = grid_qua[grid_qua[:, self.d - 1] >= 0, :] # convert to grid self.grid = quaternion2grid(grid_qua) self.grid = np.unique(np.round(self.grid, UNIQUE_TOL), axis=0) num_divisions += 1 np.random.set_state(state_before) super().generate_grid()
class CubePolytope
-
CubeGrid is a graph object, its central feature is self.G (networkx graph). In the beginning, each node is a vertex of a 3D cube. It is possible to subdivide the sides, in that case a new point always appears in the middle of a square and half of previous sides.
A polytope is a 3-dim object consisting of a set of vertices and connections between them (edges) saved in self.G property.
Expand source code
class CubePolytope(Polytope): """ CubeGrid is a graph object, its central feature is self.G (networkx graph). In the beginning, each node is a vertex of a 3D cube. It is possible to subdivide the sides, in that case a new point always appears in the middle of a square and half of previous sides. """ def _create_level0(self): # DO NOT change order of points - faces will be wrong! # each face contains numbers - indices of vertices that border on this face self.faces = [[0, 1, 2, 4], [0, 2, 3, 6], [0, 1, 3, 5], [3, 5, 6, 7], [1, 4, 5, 7], [2, 4, 6, 7]] self.side_len = 2 * np.sqrt(1/3) # create vertices vertices = [(-self.side_len/2, -self.side_len/2, -self.side_len/2), (-self.side_len/2, -self.side_len/2, self.side_len/2), (-self.side_len/2, self.side_len/2, -self.side_len/2), (self.side_len/2, -self.side_len/2, -self.side_len/2), (-self.side_len/2, self.side_len/2, self.side_len/2), (self.side_len/2, -self.side_len/2, self.side_len/2), (self.side_len/2, self.side_len/2, -self.side_len/2), (self.side_len/2, self.side_len/2, self.side_len/2)] vertices = np.array(vertices) # create edges point_connections = _calc_edges(vertices, self.side_len) # add vertices and edges to the graph for i, vert in enumerate(vertices): set_of_faces = tuple(faces_i for faces_i, face in enumerate(self.faces) if i in face) self.G.add_node(tuple(vert), level=self.current_level, face=set_of_faces, projection=project_grid_on_sphere(vert[np.newaxis, :])) for key, value in point_connections.items(): for vi in value: self.G.add_edge(key, tuple(vi), length=unit_dist_on_sphere(self.G.nodes[key]["projection"], self.G.nodes[tuple(vi)]["projection"])) # just to check ... assert self.G.number_of_nodes() == 8 assert self.G.number_of_edges() == 24 for node in self.G.nodes(data=True): assert len(node[1]["face"]) == 3 and node[1]["level"] == 0 self.side_len = self.side_len / 2
Ancestors
- Polytope
- abc.ABC
Inherited members
class Grid (N: int, *, ordered: bool = True, use_saved: bool = False, gen_alg: str = None, time_generation: bool = False)
-
Helper class that provides a standard way to create an ABC using inheritance.
Generate a grid with one of generation algorithms.
Args
gen_alg
- MUST BE SET IN SUBCLASSES, algorithm name, see names given in SIX_METHOD_NAMES
N
- number of grid points
ordered
- if True order and truncate, else only truncate to N points
use_saved
- if True use saved grids if available
time_generation
- if True write out a message about time needed for generation
Expand source code
class Grid(ABC): def __init__(self, N: int, *, ordered: bool = True, use_saved: bool = False, gen_alg: str = None, time_generation: bool = False): """ Generate a grid with one of generation algorithms. Args: gen_alg: MUST BE SET IN SUBCLASSES, algorithm name, see names given in SIX_METHOD_NAMES N: number of grid points ordered: if True order and truncate, else only truncate to N points use_saved: if True use saved grids if available time_generation: if True write out a message about time needed for generation """ self.rn_gen = np.random.default_rng(DEFAULT_SEED) np.random.seed(DEFAULT_SEED) if gen_alg not in SIX_METHOD_NAMES: raise ValueError(f"{gen_alg} is not a valid generation algorithm name. Try 'ico', 'cube3D' ...") self.ordered = ordered self.N = N name_properties = {"grid_type": gen_alg, "num_grid_points": N, "ordering": ordered} self.standard_name = NameParser(name_properties).get_standard_name() self.decorator_label = f"rotation grid {self.standard_name}" self.grid = None self.time = 0 self.nn_dist_arch = None self.nn_dist_cup = None self.short_statistics_path = f"{PATH_OUTPUT_STAT}{self.standard_name}_short_stat.txt" self.statistics_path = f"{PATH_OUTPUT_STAT}{self.standard_name}_full_stat.csv" gen_func = self.generate_and_time if time_generation else self.generate_grid # if this option enabled, search first if this grid has already been saved if use_saved: try: self.grid = np.load(f"{PATH_OUTPUT_ROTGRIDS}{self.standard_name}.npy") except FileNotFoundError: gen_func() self.save_grid() else: gen_func() assert isinstance(self.grid, np.ndarray), "A grid must be a numpy array!" assert self.grid.shape == (N, 3), f"Grid not of correct shape! {self.grid.shape} instead of {(N, 3)}" assert np.allclose(np.linalg.norm(self.grid, axis=1), 1, atol=10**(-UNIQUE_TOL)) def get_grid(self) -> np.ndarray: return self.grid def reduce_N(self, reduce_by=1): if reduce_by <= 0: return self.grid = self.grid[:-reduce_by] self.N = self.N - reduce_by self.time = 0 self.nn_dist_arch = None self.nn_dist_cup = None @abstractmethod def generate_grid(self): # order or truncate if self.ordered: self._order() else: self.grid = self.grid[:self.N] @time_method def generate_and_time(self): self.generate_grid() def _order(self): self.grid = order_grid_points(self.grid, self.N) def as_quaternion(self) -> np.ndarray: quaternion_seq = grid2quaternion(self.grid) assert isinstance(quaternion_seq, np.ndarray), "A quaternion sequence must be a numpy array!" assert quaternion_seq.shape == (self.N, 4), f"Quaternion sequence not of correct shape!\ {quaternion_seq.shape} instead of {(self.N, 4)}" return quaternion_seq def as_euler(self) -> np.ndarray: euler_seq = grid2euler(self.grid) assert isinstance(euler_seq, np.ndarray), "An Euler sequence must be a numpy array!" assert euler_seq.shape == (self.N, 3), f"An Euler sequence not of correct shape!\ {euler_seq.shape} instead of {(self.N, 33)}" return euler_seq def save_grid(self): np.save(f"{PATH_OUTPUT_ROTGRIDS}{self.standard_name}.{ENDING_GRID_FILES}", self.grid) def save_grid_txt(self): np.savetxt(f"{PATH_OUTPUT_ROTGRIDS}{self.standard_name}.txt", self.grid) def save_statistics(self, num_random: int = 100, print_message=False): # first message (what measure you are using) newline = "\n" m1 = f"STATISTICS: Testing the coverage of grid {self.standard_name} using {num_random} " \ f"random points on a sphere." m2 = f"We select {num_random} random axes and count the number of grid points that fall within the angle" \ f"alpha (selected from [pi / 6, 2 * pi / 6, 3 * pi / 6, 4 * pi / 6, 5 * pi / 6]) of this axis. For an" \ f"ideally uniform grid, we expect the ratio of num_within_alpha/total_num_points to equal the ratio" \ f"area_of_alpha_spherical_cap/area_of_sphere, which we call ideal coverage." stat_data, full_data = self._generate_statistics(num_rand_points=num_random) if print_message: print(m1) print(stat_data) # dealing with the file with open(self.short_statistics_path, "w") as f: f.writelines([m1, newline, newline, m2, newline, newline]) stat_data.to_csv(self.short_statistics_path, mode="a") full_data.to_csv(self.statistics_path, mode="w") def _generate_statistics(self, num_rand_points: int = 100) -> tuple: # write out short version ("N points", "min", "max", "average", "SD") columns = ["alphas", "ideal coverages", "min coverage", "avg coverage", "max coverage", "standard deviation"] ratios_columns = ["coverages", "alphas", "ideal coverage"] alphas = [pi/6, 2*pi/6, 3*pi/6, 4*pi/6, 5*pi/6] ratios = [[], [], []] sphere_surface = 4 * pi data = np.zeros((len(alphas), 6)) # 5 data columns for: alpha, ideal coverage, min, max, average, sd for i, alpha in enumerate(alphas): cone_area = 2 * pi * (1-np.cos(alpha)) ideal_coverage = cone_area / sphere_surface actual_coverages = random_axes_count_points(self, alpha, num_random_points=num_rand_points) ratios[0].extend(actual_coverages) ratios[1].extend([alpha]*num_rand_points) ratios[2].extend([ideal_coverage]*num_rand_points) data[i][0] = alpha data[i][1] = ideal_coverage data[i][2] = np.min(actual_coverages) data[i][3] = np.average(actual_coverages) data[i][4] = np.max(actual_coverages) data[i][5] = np.std(actual_coverages) alpha_df = pd.DataFrame(data=data, columns=columns) alpha_df = alpha_df.set_index("alphas") ratios_df = pd.DataFrame(data=np.array(ratios).T, columns=ratios_columns) return alpha_df, ratios_df
Ancestors
- abc.ABC
Subclasses
Methods
def as_euler(self) ‑> numpy.ndarray
-
Expand source code
def as_euler(self) -> np.ndarray: euler_seq = grid2euler(self.grid) assert isinstance(euler_seq, np.ndarray), "An Euler sequence must be a numpy array!" assert euler_seq.shape == (self.N, 3), f"An Euler sequence not of correct shape!\ {euler_seq.shape} instead of {(self.N, 33)}" return euler_seq
def as_quaternion(self) ‑> numpy.ndarray
-
Expand source code
def as_quaternion(self) -> np.ndarray: quaternion_seq = grid2quaternion(self.grid) assert isinstance(quaternion_seq, np.ndarray), "A quaternion sequence must be a numpy array!" assert quaternion_seq.shape == (self.N, 4), f"Quaternion sequence not of correct shape!\ {quaternion_seq.shape} instead of {(self.N, 4)}" return quaternion_seq
def generate_and_time(self)
-
Expand source code
@time_method def generate_and_time(self): self.generate_grid()
def generate_grid(self)
-
Expand source code
@abstractmethod def generate_grid(self): # order or truncate if self.ordered: self._order() else: self.grid = self.grid[:self.N]
def get_grid(self) ‑> numpy.ndarray
-
Expand source code
def get_grid(self) -> np.ndarray: return self.grid
def reduce_N(self, reduce_by=1)
-
Expand source code
def reduce_N(self, reduce_by=1): if reduce_by <= 0: return self.grid = self.grid[:-reduce_by] self.N = self.N - reduce_by self.time = 0 self.nn_dist_arch = None self.nn_dist_cup = None
def save_grid(self)
-
Expand source code
def save_grid(self): np.save(f"{PATH_OUTPUT_ROTGRIDS}{self.standard_name}.{ENDING_GRID_FILES}", self.grid)
def save_grid_txt(self)
-
Expand source code
def save_grid_txt(self): np.savetxt(f"{PATH_OUTPUT_ROTGRIDS}{self.standard_name}.txt", self.grid)
def save_statistics(self, num_random: int = 100, print_message=False)
-
Expand source code
def save_statistics(self, num_random: int = 100, print_message=False): # first message (what measure you are using) newline = "\n" m1 = f"STATISTICS: Testing the coverage of grid {self.standard_name} using {num_random} " \ f"random points on a sphere." m2 = f"We select {num_random} random axes and count the number of grid points that fall within the angle" \ f"alpha (selected from [pi / 6, 2 * pi / 6, 3 * pi / 6, 4 * pi / 6, 5 * pi / 6]) of this axis. For an" \ f"ideally uniform grid, we expect the ratio of num_within_alpha/total_num_points to equal the ratio" \ f"area_of_alpha_spherical_cap/area_of_sphere, which we call ideal coverage." stat_data, full_data = self._generate_statistics(num_rand_points=num_random) if print_message: print(m1) print(stat_data) # dealing with the file with open(self.short_statistics_path, "w") as f: f.writelines([m1, newline, newline, m2, newline, newline]) stat_data.to_csv(self.short_statistics_path, mode="a") full_data.to_csv(self.statistics_path, mode="w")
class IcoGrid (N: int, **kwargs)
-
Helper class that provides a standard way to create an ABC using inheritance.
Generate a grid with one of generation algorithms.
Args
gen_alg
- MUST BE SET IN SUBCLASSES, algorithm name, see names given in SIX_METHOD_NAMES
N
- number of grid points
ordered
- if True order and truncate, else only truncate to N points
use_saved
- if True use saved grids if available
time_generation
- if True write out a message about time needed for generation
Expand source code
class IcoGrid(Polyhedron3DGrid): def __init__(self, N: int, **kwargs): super().__init__(N, polyhedron=IcosahedronPolytope, gen_alg="ico", **kwargs)
Ancestors
- Polyhedron3DGrid
- Grid
- abc.ABC
class IcosahedronPolytope
-
IcosahedronGrid is a graph object, its central feature is self.G (networkx graph). In the beginning, each node is a vertex of a 3D icosahedron. It is possible to subdivide the sides, in that case a new point always appears in the middle of each triangle side.
A polytope is a 3-dim object consisting of a set of vertices and connections between them (edges) saved in self.G property.
Expand source code
class IcosahedronPolytope(Polytope): """ IcosahedronGrid is a graph object, its central feature is self.G (networkx graph). In the beginning, each node is a vertex of a 3D icosahedron. It is possible to subdivide the sides, in that case a new point always appears in the middle of each triangle side. """ def _create_level0(self): # DO NOT change order of points - faces will be wrong! # each face contains numbers - indices of vertices that border on this face self.faces = [[0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11], [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8], [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9], [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1]] side_len = 1 / np.sin(2 * pi / 5) self.side_len = side_len # create vertices vertices = [(-1, golden, 0), (1, golden, 0), (-1, -golden, 0), (1, -golden, 0), (0, -1, golden), (0, 1, golden), (0, -1, -golden), (0, 1, -golden), (golden, 0, -1), (golden, 0, 1), (-golden, 0, -1), (-golden, 0, 1)] vertices = np.array(vertices) * side_len / 2 # create edges point_connections = _calc_edges(vertices, side_len) # add vertices and edges to the graph for i, vert in enumerate(vertices): set_of_faces = tuple(faces_i for faces_i, face in enumerate(self.faces) if i in face) self.G.add_node(tuple(vert), level=self.current_level, face=set_of_faces, projection=project_grid_on_sphere(vert[np.newaxis, :])) for key, value in point_connections.items(): for vi in value: self.G.add_edge(key, tuple(vi), length=unit_dist_on_sphere(self.G.nodes[key]["projection"], self.G.nodes[tuple(vi)]["projection"])) # just to check ... assert self.G.number_of_nodes() == 12 assert self.G.number_of_edges() == 30 for node in self.G.nodes(data=True): assert len(node[1]["face"]) == 5 and node[1]["level"] == 0 self.side_len = side_len / 2
Ancestors
- Polytope
- abc.ABC
Inherited members
class Polyhedron3DGrid (N: int, polyhedron, **kwargs)
-
Helper class that provides a standard way to create an ABC using inheritance.
Generate a grid with one of generation algorithms.
Args
gen_alg
- MUST BE SET IN SUBCLASSES, algorithm name, see names given in SIX_METHOD_NAMES
N
- number of grid points
ordered
- if True order and truncate, else only truncate to N points
use_saved
- if True use saved grids if available
time_generation
- if True write out a message about time needed for generation
Expand source code
class Polyhedron3DGrid(Grid): def __init__(self, N: int, polyhedron, **kwargs): self.polyhedron = polyhedron() super().__init__(N, **kwargs) def generate_grid(self): while self.polyhedron.G.number_of_nodes() < self.N: self.polyhedron.divide_edges() self.grid = np.array([y["projection"] for x, y in self.polyhedron.G.nodes(data=True)]).squeeze() np.random.shuffle(self.grid) super().generate_grid()
Ancestors
- Grid
- abc.ABC
Subclasses
Methods
def generate_grid(self)
-
Expand source code
def generate_grid(self): while self.polyhedron.G.number_of_nodes() < self.N: self.polyhedron.divide_edges() self.grid = np.array([y["projection"] for x, y in self.polyhedron.G.nodes(data=True)]).squeeze() np.random.shuffle(self.grid) super().generate_grid()
class Polytope
-
Helper class that provides a standard way to create an ABC using inheritance.
A polytope is a 3-dim object consisting of a set of vertices and connections between them (edges) saved in self.G property.
Expand source code
class Polytope(ABC): def __init__(self): """ A polytope is a 3-dim object consisting of a set of vertices and connections between them (edges) saved in self.G property. """ self.G = nx.Graph() self.faces = [] self.current_level = 0 self.side_len = 0 self._create_level0() @abstractmethod def _create_level0(self): # this is implemented by each subclass since they have different edges, vertices and faces pass def plot_graph(self): """ Plot the networkx graph of self.G. """ node_labels = {i: tuple(np.round(i, 3)) for i in self.G.nodes} nx.draw(self.G, pos=nx.circular_layout(self.G), with_labels=True, labels=node_labels) def plot_points(self, ax, select_faces: set = None, projection: bool = False): """ Plot the points of the icosahedron + possible division points. Colored by level at which the point was added. Possible to select only one or a few faces on which points are to be plotted for clarity. Args: ax: axis select_faces: a set of face numbers from 0 to (incl) 19, e.g. {0, 5}. If None, all faces are shown. projection: True if you want to plot the projected points, not the ones on surfaces of polytope """ select_faces = set(range(20)) if select_faces is None else select_faces level_color = ["black", "red", "blue", "green"] for point in self.G.nodes(data=True): # select only points that belong to at least one of the chosen select_faces if len(set(point[1]["face"]).intersection(select_faces)) > 0: # color selected based on the level of the node level_node = point[1]["level"] if projection: proj_node = point[1]["projection"] ax.scatter(*proj_node[0], color=level_color[level_node], s=30) else: ax.scatter(*point[0], color=level_color[level_node], s=30) def plot_edges(self, ax, select_faces=None, **kwargs): """ Plot the edges between the points. Can select to display only some faces for clarity. Args: ax: axis select_faces: a set of face numbers from 0 to (incl) 19, e.g. {0, 5}. If None, all faces are shown. **kwargs: other plotting arguments """ select_faces = set(range(20)) if select_faces is None else select_faces for edge in self.G.edges: faces_edge_1 = set(self.G.nodes[edge[0]]["face"]) faces_edge_2 = set(self.G.nodes[edge[1]]["face"]) # both the start and the end point of the edge must belong to one of the selected faces if len(faces_edge_1.intersection(select_faces)) > 0 and len(faces_edge_2.intersection(select_faces)) > 0: ax.plot(*np.array(edge).T, color="black", **kwargs) def divide_edges(self): """ Subdivide once. If previous faces are triangles, adds three points per face (half sides). If they are squares, adds 4 points per face (half sides + middle). New points will have a higher level and will be appropriately added to one or more faces (if on edges). """ # need to keep track of what to add/remove, since we can't change the network while inside the loop # consists of (new_point, previous_point_1, previous_point_2) to keep all info nodes_to_add = [] for node_vector in self.G.nodes(): for neighbour_vector in self.G.neighbors(node_vector): # this is just to avoid doing everything twice - do it for edge A-B but not B-A if node_vector < neighbour_vector: # new point is just the average of the previous two new_point = tuple((np.array(node_vector)+np.array(neighbour_vector))/2.0) nodes_to_add.append((new_point, node_vector, neighbour_vector)) # add new nodes, add edges from cont. points to the node and delete the edge between the cont. points for el in nodes_to_add: new_point, node_vector, neighbour_vector = el # new point will have the set of faces that both node and neighbour vector have faces_node_vector = self.G.nodes[node_vector]["face"] faces_neighbour_vector = self.G.nodes[neighbour_vector]["face"] # diagonals get added twice, so this is necessary if new_point not in self.G.nodes: self.G.add_node(new_point, level=self.current_level + 1, face=set(faces_node_vector).intersection(faces_neighbour_vector), projection=project_grid_on_sphere(np.array(new_point)[np.newaxis, :])) self.G.add_edge(new_point, neighbour_vector, length=unit_dist_on_sphere(self.G.nodes[new_point]["projection"], self.G.nodes[neighbour_vector]["projection"])) self.G.add_edge(new_point, node_vector, length=unit_dist_on_sphere(self.G.nodes[new_point]["projection"], self.G.nodes[neighbour_vector]["projection"])) # self.G.remove_edge(node_vector, neighbour_vector) # also add edges between new nodes at distance side_len or sqrt(2)*side_len new_level = [x for x, y in self.G.nodes(data=True) if y['level'] == self.current_level+1] for new_node in new_level: # searching only second neighbours at appropriate level sec_neighbours = list(second_neighbours(self.G, new_node)) sec_neighbours = [x for x in sec_neighbours if self.G.nodes[x]["level"] == self.current_level+1] for other_node in sec_neighbours: node_dist = np.linalg.norm(np.array(new_node)-np.array(other_node)) # check distance criterion if np.isclose(node_dist, self.side_len) or np.isclose(node_dist, self.side_len*np.sqrt(2)): self.G.add_edge(new_node, other_node, length=unit_dist_on_sphere(self.G.nodes[new_node]["projection"], self.G.nodes[other_node]["projection"]) ) self.current_level += 1 self.side_len = self.side_len / 2
Ancestors
- abc.ABC
Subclasses
Methods
def divide_edges(self)
-
Subdivide once. If previous faces are triangles, adds three points per face (half sides). If they are squares, adds 4 points per face (half sides + middle). New points will have a higher level and will be appropriately added to one or more faces (if on edges).
Expand source code
def divide_edges(self): """ Subdivide once. If previous faces are triangles, adds three points per face (half sides). If they are squares, adds 4 points per face (half sides + middle). New points will have a higher level and will be appropriately added to one or more faces (if on edges). """ # need to keep track of what to add/remove, since we can't change the network while inside the loop # consists of (new_point, previous_point_1, previous_point_2) to keep all info nodes_to_add = [] for node_vector in self.G.nodes(): for neighbour_vector in self.G.neighbors(node_vector): # this is just to avoid doing everything twice - do it for edge A-B but not B-A if node_vector < neighbour_vector: # new point is just the average of the previous two new_point = tuple((np.array(node_vector)+np.array(neighbour_vector))/2.0) nodes_to_add.append((new_point, node_vector, neighbour_vector)) # add new nodes, add edges from cont. points to the node and delete the edge between the cont. points for el in nodes_to_add: new_point, node_vector, neighbour_vector = el # new point will have the set of faces that both node and neighbour vector have faces_node_vector = self.G.nodes[node_vector]["face"] faces_neighbour_vector = self.G.nodes[neighbour_vector]["face"] # diagonals get added twice, so this is necessary if new_point not in self.G.nodes: self.G.add_node(new_point, level=self.current_level + 1, face=set(faces_node_vector).intersection(faces_neighbour_vector), projection=project_grid_on_sphere(np.array(new_point)[np.newaxis, :])) self.G.add_edge(new_point, neighbour_vector, length=unit_dist_on_sphere(self.G.nodes[new_point]["projection"], self.G.nodes[neighbour_vector]["projection"])) self.G.add_edge(new_point, node_vector, length=unit_dist_on_sphere(self.G.nodes[new_point]["projection"], self.G.nodes[neighbour_vector]["projection"])) # self.G.remove_edge(node_vector, neighbour_vector) # also add edges between new nodes at distance side_len or sqrt(2)*side_len new_level = [x for x, y in self.G.nodes(data=True) if y['level'] == self.current_level+1] for new_node in new_level: # searching only second neighbours at appropriate level sec_neighbours = list(second_neighbours(self.G, new_node)) sec_neighbours = [x for x in sec_neighbours if self.G.nodes[x]["level"] == self.current_level+1] for other_node in sec_neighbours: node_dist = np.linalg.norm(np.array(new_node)-np.array(other_node)) # check distance criterion if np.isclose(node_dist, self.side_len) or np.isclose(node_dist, self.side_len*np.sqrt(2)): self.G.add_edge(new_node, other_node, length=unit_dist_on_sphere(self.G.nodes[new_node]["projection"], self.G.nodes[other_node]["projection"]) ) self.current_level += 1 self.side_len = self.side_len / 2
def plot_edges(self, ax, select_faces=None, **kwargs)
-
Plot the edges between the points. Can select to display only some faces for clarity.
Args
ax
- axis
select_faces
- a set of face numbers from 0 to (incl) 19, e.g. {0, 5}. If None, all faces are shown.
**kwargs
- other plotting arguments
Expand source code
def plot_edges(self, ax, select_faces=None, **kwargs): """ Plot the edges between the points. Can select to display only some faces for clarity. Args: ax: axis select_faces: a set of face numbers from 0 to (incl) 19, e.g. {0, 5}. If None, all faces are shown. **kwargs: other plotting arguments """ select_faces = set(range(20)) if select_faces is None else select_faces for edge in self.G.edges: faces_edge_1 = set(self.G.nodes[edge[0]]["face"]) faces_edge_2 = set(self.G.nodes[edge[1]]["face"]) # both the start and the end point of the edge must belong to one of the selected faces if len(faces_edge_1.intersection(select_faces)) > 0 and len(faces_edge_2.intersection(select_faces)) > 0: ax.plot(*np.array(edge).T, color="black", **kwargs)
def plot_graph(self)
-
Plot the networkx graph of self.G.
Expand source code
def plot_graph(self): """ Plot the networkx graph of self.G. """ node_labels = {i: tuple(np.round(i, 3)) for i in self.G.nodes} nx.draw(self.G, pos=nx.circular_layout(self.G), with_labels=True, labels=node_labels)
def plot_points(self, ax, select_faces: set = None, projection: bool = False)
-
Plot the points of the icosahedron + possible division points. Colored by level at which the point was added. Possible to select only one or a few faces on which points are to be plotted for clarity.
Args
ax
- axis
select_faces
- a set of face numbers from 0 to (incl) 19, e.g. {0, 5}. If None, all faces are shown.
projection
- True if you want to plot the projected points, not the ones on surfaces of polytope
Expand source code
def plot_points(self, ax, select_faces: set = None, projection: bool = False): """ Plot the points of the icosahedron + possible division points. Colored by level at which the point was added. Possible to select only one or a few faces on which points are to be plotted for clarity. Args: ax: axis select_faces: a set of face numbers from 0 to (incl) 19, e.g. {0, 5}. If None, all faces are shown. projection: True if you want to plot the projected points, not the ones on surfaces of polytope """ select_faces = set(range(20)) if select_faces is None else select_faces level_color = ["black", "red", "blue", "green"] for point in self.G.nodes(data=True): # select only points that belong to at least one of the chosen select_faces if len(set(point[1]["face"]).intersection(select_faces)) > 0: # color selected based on the level of the node level_node = point[1]["level"] if projection: proj_node = point[1]["projection"] ax.scatter(*proj_node[0], color=level_color[level_node], s=30) else: ax.scatter(*point[0], color=level_color[level_node], s=30)
class RandomEGrid (N: int, **kwargs)
-
Helper class that provides a standard way to create an ABC using inheritance.
Generate a grid with one of generation algorithms.
Args
gen_alg
- MUST BE SET IN SUBCLASSES, algorithm name, see names given in SIX_METHOD_NAMES
N
- number of grid points
ordered
- if True order and truncate, else only truncate to N points
use_saved
- if True use saved grids if available
time_generation
- if True write out a message about time needed for generation
Expand source code
class RandomEGrid(Grid): def __init__(self, N: int, **kwargs): super().__init__(N, gen_alg="randomE", **kwargs) def generate_grid(self): euler_angles = 2 * pi * self.rn_gen.random((self.N, 3)) self.grid = euler2grid(euler_angles) # No super call because ordering not needed for random points and the number of points is exact!
Ancestors
- Grid
- abc.ABC
Methods
def generate_grid(self)
-
Expand source code
def generate_grid(self): euler_angles = 2 * pi * self.rn_gen.random((self.N, 3)) self.grid = euler2grid(euler_angles) # No super call because ordering not needed for random points and the number of points is exact!
class RandomQGrid (N: int, **kwargs)
-
Helper class that provides a standard way to create an ABC using inheritance.
Generate a grid with one of generation algorithms.
Args
gen_alg
- MUST BE SET IN SUBCLASSES, algorithm name, see names given in SIX_METHOD_NAMES
N
- number of grid points
ordered
- if True order and truncate, else only truncate to N points
use_saved
- if True use saved grids if available
time_generation
- if True write out a message about time needed for generation
Expand source code
class RandomQGrid(Grid): def __init__(self, N: int, **kwargs): super().__init__(N, gen_alg="randomQ", **kwargs) def generate_grid(self): result = np.zeros((self.N, 4)) random_num = self.rn_gen.random((self.N, 3)) result[:, 0] = np.sqrt(1 - random_num[:, 0]) * np.sin(2 * pi * random_num[:, 1]) result[:, 1] = np.sqrt(1 - random_num[:, 0]) * np.cos(2 * pi * random_num[:, 1]) result[:, 2] = np.sqrt(random_num[:, 0]) * np.sin(2 * pi * random_num[:, 2]) result[:, 3] = np.sqrt(random_num[:, 0]) * np.cos(2 * pi * random_num[:, 2]) self.grid = quaternion2grid(result) # No super call because ordering not needed for random points and the number of points is exact!
Ancestors
- Grid
- abc.ABC
Methods
def generate_grid(self)
-
Expand source code
def generate_grid(self): result = np.zeros((self.N, 4)) random_num = self.rn_gen.random((self.N, 3)) result[:, 0] = np.sqrt(1 - random_num[:, 0]) * np.sin(2 * pi * random_num[:, 1]) result[:, 1] = np.sqrt(1 - random_num[:, 0]) * np.cos(2 * pi * random_num[:, 1]) result[:, 2] = np.sqrt(random_num[:, 0]) * np.sin(2 * pi * random_num[:, 2]) result[:, 3] = np.sqrt(random_num[:, 0]) * np.cos(2 * pi * random_num[:, 2]) self.grid = quaternion2grid(result) # No super call because ordering not needed for random points and the number of points is exact!
class SystemEGrid (N: int, **kwargs)
-
Helper class that provides a standard way to create an ABC using inheritance.
Generate a grid with one of generation algorithms.
Args
gen_alg
- MUST BE SET IN SUBCLASSES, algorithm name, see names given in SIX_METHOD_NAMES
N
- number of grid points
ordered
- if True order and truncate, else only truncate to N points
use_saved
- if True use saved grids if available
time_generation
- if True write out a message about time needed for generation
Expand source code
class SystemEGrid(Grid): def __init__(self, N: int, **kwargs): super().__init__(N, gen_alg="systemE", **kwargs) def generate_grid(self): self.grid = [] num_points = 1 while len(self.grid) < self.N: phis = np.linspace(0, 2 * pi, num_points) thetas = np.linspace(0, 2 * pi, num_points) psis = np.linspace(0, 2 * pi, num_points) euler_meshgrid = np.array(np.meshgrid(*(phis, thetas, psis)), dtype=float) euler_meshgrid = euler_meshgrid.reshape((3, -1)).T # convert to a grid self.grid = euler2grid(euler_meshgrid) self.grid = np.unique(np.round(self.grid, UNIQUE_TOL), axis=0) num_points += 1 super().generate_grid()
Ancestors
- Grid
- abc.ABC
Methods
def generate_grid(self)
-
Expand source code
def generate_grid(self): self.grid = [] num_points = 1 while len(self.grid) < self.N: phis = np.linspace(0, 2 * pi, num_points) thetas = np.linspace(0, 2 * pi, num_points) psis = np.linspace(0, 2 * pi, num_points) euler_meshgrid = np.array(np.meshgrid(*(phis, thetas, psis)), dtype=float) euler_meshgrid = euler_meshgrid.reshape((3, -1)).T # convert to a grid self.grid = euler2grid(euler_meshgrid) self.grid = np.unique(np.round(self.grid, UNIQUE_TOL), axis=0) num_points += 1 super().generate_grid()