diff --git a/hypertiling/generative/generative_reflection.py b/hypertiling/generative/generative_reflection.py index 4abfe357da0e147375f94543ec512e8be701e8cc..f3fc3af8cd3bc0588bf06553ae4b3c97eefdf9f8 100644 --- a/hypertiling/generative/generative_reflection.py +++ b/hypertiling/generative/generative_reflection.py @@ -3,7 +3,8 @@ import numpy as np import hypertiling.generative.generative_reflection_util as util from hypertiling.generative.generative_reflection_util import PI2 from hypertiling.kernel_abc import AbstractKernelBase -import hypertiling.arraytransformation as trans +import hypertiling.transformation as transform +import hypertiling.arraytransformation as arraytransform import hypertiling.distance as distance """ @@ -57,7 +58,7 @@ class KernelGenerativeReflection(AbstractKernelBase): self.mangle = mangle / 360 * PI2 # estimate some other technical attributes - if n > 1: + if n != 0: lengths = util.get_reflection_n_estimation(p, q, n) # n self._sector_lengths = np.ceil(lengths / p).astype(np.uint32) # n else: @@ -109,7 +110,14 @@ class KernelGenerativeReflection(AbstractKernelBase): phis = np.array([dphi * i for i in range(1, self.p)]) for i, angle in enumerate(phis): for poly in polys: - yield poly * np.exp(angle * 1j) + if self._sector_polys[0, 0] == 0: + yield poly * np.exp(angle * 1j) + else: + poly_c = np.copy(poly) + arraytransform.morigin(self.p, self._sector_polys[0, 0], poly_c) + poly_c *= np.exp(angle * 1j) + arraytransform.morigin_inv(self.p, self._sector_polys[0, 0], poly_c) + yield poly_c def _wiggle_index(self, index1: int, index2: int, tol: int = 1) -> int: """ @@ -420,7 +428,14 @@ class KernelGenerativeReflection(AbstractKernelBase): phis = np.array([dphi * i for i in range(1, self.p)]) for i, angle in enumerate(phis): for poly in self._sector_polys[1:]: - yield poly * np.exp(angle * 1j) + if self._sector_polys[0, 0] == 0: + yield poly * np.exp(angle * 1j) + else: + poly_c = np.copy(poly) + arraytransform.morigin(self.p, self._sector_polys[0, 0], poly_c) + poly_c *= np.exp(angle * 1j) + arraytransform.morigin_inv(self.p, self._sector_polys[0, 0], poly_c) + yield poly_c def __getitem__(self, index: int) -> np.array: """ @@ -445,7 +460,16 @@ class KernelGenerativeReflection(AbstractKernelBase): if phi == 0: return poly - return poly * np.exp(phi * 1j) + if self._sector_polys[0, 0] == 0: + return poly * np.exp(phi * 1j) + + else: + poly_c = np.copy(poly) + arraytransform.morigin(self.p, self._sector_polys[0, 0], poly_c) + poly_c *= np.exp(phi * 1j) + arraytransform.morigin_inv(self.p, self._sector_polys[0, 0], poly_c) + return poly_c + # Basics ########################################################################################################### # API ############################################################################################################## @@ -833,7 +857,7 @@ class KernelGenerativeReflection(AbstractKernelBase): """ if not isinstance(function, np.vectorize): function = np.vectorize(function) - function(self._sector_polys) + self._sector_polys = function(self._sector_polys) def rotate(self, angle: float): """ @@ -842,7 +866,7 @@ class KernelGenerativeReflection(AbstractKernelBase): Time-complexity: O(m / p) :return: void """ - self.transform(lambda x: trans.mrotate(x.shape[0], -angle, x)) + self.transform(lambda x: transform.moeb_rotate_trafo(-angle, x)) def translate(self, z: np.complex128): """ @@ -851,7 +875,7 @@ class KernelGenerativeReflection(AbstractKernelBase): Time-complexity: O(m / p) :return: void """ - self.transform(lambda x: trans.morigin(x.shape[0], z, x)) + self.transform(lambda x: transform.moeb_origin_trafo(z, x)) # Transformations ################################################################################################## @@ -860,13 +884,12 @@ if __name__ == "__main__": import matplotlib.pyplot as plt import matplotlib as mpl import time - from sys import getsizeof fig_ax = plt.subplots() fig_ax[1].set_xlim(-1, 1) fig_ax[1].set_ylim(-1, 1) t1 = time.time() - tiling = KernelGenerativeReflection(7, 3, 5) + tiling = KernelGenerativeReflection(7, 3, 3) t2 = time.time() tiling.map_nbrs() @@ -878,11 +901,19 @@ if __name__ == "__main__": # tiling.check_integrity() colors = ["#FF000080", "#00FF0080", "#0000FF80"] + prob = [2 / (i + 1) for i in range(9)] + + tiling.translate(tiling[1][0]) + for polygon_index, pgon in enumerate(tiling): + print(polygon_index) + # print(polygon_index, pgon) # poly_layer = tiling.get_layer(polygon_index) poly_layer = tiling.get_reflection_level(polygon_index) + facecolor = colors[poly_layer % len(colors)] patch = mpl.patches.Polygon(np.array([(np.real(e), np.imag(e)) for e in pgon[1:]]), - color=colors[poly_layer % len(colors)]) + facecolor=facecolor, edgecolor="#FFFFFF") fig_ax[1].add_patch(patch) - # fig_ax[1].text(np.real(pgon[0]), np.imag(pgon[0]), str(poly_layer)) + fig_ax[1].text(np.real(pgon[0]), np.imag(pgon[0]), str(polygon_index)) + plt.show() diff --git a/hypertiling/graphics/animator.py b/hypertiling/graphics/animator.py index dd56ce74a469ed3728a5091e3f449954f550d86b..87f781d5fdc6b80a16fb504c157a9da37cdd51b4 100644 --- a/hypertiling/graphics/animator.py +++ b/hypertiling/graphics/animator.py @@ -1,4 +1,11 @@ from matplotlib import animation +from hypertiling.graphics.plot import convert_polygons_to_patches +from hypertiling.transformation import mymoeb +import numpy as np + + +def moeb_rotate_trafo(phi, z): + return z * (np.cos(phi) + 1j * np.sin(phi)) """ Wrapper which specializes matplotlibs FuncAnimation for hyperbolic tilings @@ -22,29 +29,31 @@ from matplotlib import animation """ + class animate_live: - + def __init__(self, state, fig, pgons, step, stepargs={}, animargs={}): - self.initstate = state + self.initstate = state self.stepargs = stepargs - self.anim = animation.FuncAnimation(fig, self._update, init_func=self._init, **animargs) + self.anim = animation.FuncAnimation(fig, self._update, init_func=self._init, **animargs) self.nextstate = step self.pgons = pgons - + def _init(self): self.state = self.initstate self.pgons.set_array(self.state) return self.pgons, - def _update(self,i): + def _update(self, i): self.state = self.nextstate(self.state, **self.stepargs) self.pgons.set_array(self.state) return self.pgons, - + def save(self, path, fps=5, codec=None): writer = animation.FFMpegWriter(fps, codec) self.anim.save(path, writer) - + + """ Wrapper which specializes matplotlibs FuncAnimation for hyperbolic tilings @@ -62,28 +71,177 @@ class animate_live: additional kwargs to be passed to the FuncAnimator """ - + + class animate_list: - + def __init__(self, data, fig, pgons, animargs={}): if "frames" in animargs: if animargs["frames"] > len(data): animargs["frames"] = len(data) else: animargs["frames"] = len(data) - - self.anim = animation.FuncAnimation(fig, self._update, init_func=self._init, **animargs) + + self.anim = animation.FuncAnimation(fig, self._update, init_func=self._init, **animargs) self.data = data self.pgons = pgons - + def _init(self): self.pgons.set_array(self.data[0]) return self.pgons, - def _update(self,i): + def _update(self, i): self.pgons.set_array(self.data[i]) return self.pgons, + + def save(self, path, fps=5, codec=None): + writer = animation.FFMpegWriter(fps, codec) + self.anim.save(path, writer) + + +""" + Wrapper which specializes matplotlibs FuncAnimation for hyperbolic tilings + + use this if you have a pre-computed array of polygon states and want to animate a moving tiling + Arguments + --------- + data : 2d array-like + list of polygon states to be traversed through during the animation + fig : matplotlib.Figure + the figure to be animated + ax : matplotlib.axes + the axes in which the animation is shown + tiling : hypertiling.HyperbolicTiling + the tiling to be animated + path : 1d or 2d array-like + points in the Poincare disk which will be moved to the center throughout the animation. + if path is 1d and the elements are integers, the points are expected to be polygon id's. + if path is 1d and the elements are complex, the points are expected to be coordinates on the Poincare disk + if path is 2d and the elements are floats, the points are expected to be coordinates in the poincare disk, + where the 1st dimension hold the real part and the 2nd the complex + path_frames : int, default: 32 + how many frames it takes to go from one point to the next in path + data_frames : int, default: None + how many frames it takes to go from one state to the next in data + if no value is given, data_frames takes the same value as path_frames + kwargs : dict, optional + additional matplotlib.Patch kwargs + animargs : dict, optional + additional kwargs to be passed to the FuncAnimator + +""" + + +class HyperanimatorPath: + + def __init__(self, data, fig, ax, tiling, path, path_frames=32, data_frames=None, kwargs={}, animargs={}): + self.tiling = tiling + self.ax = ax + + ### Check whether path has entries of type int or complex/2d float + ### If int: entries correspond to polygon IDs + ### If complex or 2d float: entries correspond to coordinates + if isinstance(path, list): + path = np.array(path) + if path.ndim == 2 and isinstance(path[0].item(), float): + self.coords = path + 1j * path + elif path.ndim == 1 and isinstance(path[0].item(), complex): + self.coords = path + elif path.ndim == 1 and isinstance(path[0].item(), int): + self.coords = self._poly_id_to_coords(path) + else: + print(" some kind of error message ") + return + + self.path_frames = path_frames + if not data_frames: + self.data_frames = self.path_frames + else: + self.data_frames = data_frames + self.s_coords = self._stretch_coords_geodesic(self.coords, self.path_frames) + self.s_data = self._stretch_data(data, self.data_frames, len(self.s_coords)) + self.frames = np.min([len(self.s_coords), len(self.s_data)]) + + self.anim = animation.FuncAnimation(fig, self._update, frames=self.frames, **animargs) + self.kwargs = kwargs + + def _update(self, i): + self.ax.clear() + self.tiling.translate(self.s_coords[i]) + self.s_coords = mymoeb(-self.s_coords[i], self.s_coords) + pgons = convert_polygons_to_patches(self.tiling, self.s_data[i], **self.kwargs) + self.ax.add_collection(pgons) + + self.ax.set_xlim(-1, 1) + self.ax.set_ylim(-1, 1) + self.ax.axis("off") + self.ax.set_aspect('equal') + + return self.ax + + def _poly_id_to_coords(self, path): + # Takes list of polygon id's and returns list containing the respective coordinates + coords = np.zeros(len(path), dtype=np.complex128) + for i in range(len(path)): + coords[i] = self.tiling.get_center(path[i]) + return coords + + def _stretch_pair_geodesic(self, pair, factor): + # Takes a list containing two coordinates and divides the path between them into "factor" geodesic parts + + # t = translated + # r = rotated + # g = geodesic + + # Translate first entry to the origin + t_pair = mymoeb(-pair[0], pair) + + # Rotate second entry on to the real axis + angle = np.angle(t_pair[1]) + r_t_pair = moeb_rotate_trafo(-angle, t_pair) + + # Go to geodesic length to calculate equal path slices of length diff + g_r_t_stretched = np.zeros(factor, dtype=np.complex128) + diff = np.arctanh(r_t_pair[1]) / factor + + # Diff gets added 'factor'-times to the first entry + for i in range(1, factor): + g_r_t_stretched[i] = g_r_t_stretched[i - 1] + diff + + # Go back to poincare + r_t_stretched = np.tanh(g_r_t_stretched) + # Rotate back + t_stretched = moeb_rotate_trafo(angle, r_t_stretched) + # Translate everything back + stretched = mymoeb(pair[0], t_stretched) + + return stretched + + def _stretch_coords_geodesic(self, coords, factor): + # Takes list of all coordinates to be visited and stretches it "factor" times + + stretched_path = [] + + # Replicate the first position + for i in range((factor + 1) // 2): + stretched_path.append(coords[0]) + # Stretch the in between positions + for i in range(coords[:-1].size): + stretched_path.extend(self._stretch_pair_geodesic(coords[i:i + 2], factor)) + # Replicate the last position + for i in range((factor + 1) // 2): + stretched_path.append(coords[-1]) + + return np.array(stretched_path) + + def _stretch_data(self, data, data_frames, len_coords): + try: + len(data[0]) + return np.repeat(data, (data_frames + 1), axis=0) + except: + return np.tile(data, len_coords).reshape(len_coords, len(data)) + def save(self, path, fps=5, codec=None): writer = animation.FFMpegWriter(fps, codec) self.anim.save(path, writer) diff --git a/hypertiling/ion.py b/hypertiling/ion.py index 88cfa7e5fbc92f900df32afc8e3149891dea8b6f..b632111df08ef8c522002bc8cfb685c2d8cb2f42 100644 --- a/hypertiling/ion.py +++ b/hypertiling/ion.py @@ -3,6 +3,7 @@ import os import numpy as np from .geodesics import geodesic_arc import matplotlib.lines as mlines +from matplotlib import cm def to_px(z, factor=100, offset=1): @@ -29,7 +30,7 @@ def to_px(z, factor=100, offset=1): -def write_svg(fname, tiling, edgecolor="black", facecolor="transparent", lw=.5, link=''): +def write_svg(fname, tiling, edgecolor="black", facecolor="transparent", lw=.5, link='', cmap=None): """ Saves a plot of the geodesic edges as a .svg-file. @@ -67,19 +68,34 @@ def write_svg(fname, tiling, edgecolor="black", facecolor="transparent", lw=.5, svg.write(pattern + "\r\n") if facecolor == 'transparent': facecolor = '' + fill_individual = False + if hasattr(facecolor, "__len__") and len(facecolor) == len(tiling): + fill_individual = True + if not cmap: + cmap = cm.get_cmap("RdYlGn") + else: + cmap = cm.get_cmap(f"{cmap}") + colors = array_to_rgb(norm_0_1(facecolor), cmap) + else: + print("facecolor must be either an SVG fill command or an array like of size len(tiling) " + "containing ints or floats") + return - pi2 = 2 * np.pi vs = [_ for _ in range(1, tiling.p)] + [0] - for pgon in tiling.polygons: - start = f"