├── .gitignore ├── LICENSE ├── README.md ├── groupy ├── __init__.py ├── garray │ ├── C4_array.py │ ├── D4_array.py │ ├── Z2_array.py │ ├── __init__.py │ ├── finitegroup.py │ ├── garray.py │ ├── matrix_garray.py │ ├── p4_array.py │ ├── p4m_array.py │ └── test_garray.py ├── gconv │ ├── __init__.py │ ├── chainer_gconv │ │ ├── __init__.py │ │ ├── kernels │ │ │ ├── __init__.py │ │ │ ├── integer_indexing_cuda_kernel.py │ │ │ └── test_integer_indexing_cuda_kernel.py │ │ ├── p4_conv.py │ │ ├── p4m_conv.py │ │ ├── pooling │ │ │ ├── __init__.py │ │ │ └── plane_group_spatial_max_pooling.py │ │ ├── splitgconv2d.py │ │ ├── test_gconv.py │ │ ├── test_transform_filter.py │ │ └── transform_filter.py │ ├── make_gconv_indices.py │ ├── tensorflow_gconv │ │ ├── __init__.py │ │ ├── check_gconv2d.py │ │ ├── check_transform_filter.py │ │ ├── splitgconv2d.py │ │ └── transform_filter.py │ └── theano_gconv │ │ └── __init__.py └── gfunc │ ├── __init__.py │ ├── gfuncarray.py │ ├── p4func_array.py │ ├── p4mfunc_array.py │ ├── plot │ ├── __init__.py │ ├── plot_p4.py │ ├── plot_p4m.py │ └── plot_z2.py │ ├── test_gfuncarray.py │ └── z2func_array.py ├── p4_anim.gif ├── p4_fmaps.png ├── p4m_fmaps.png ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Current project only: 2 | ####################### 3 | # ignore cython-generated files 4 | *.c 5 | 6 | # Compiled source # 7 | ################### 8 | *.com 9 | *.class 10 | *.dll 11 | *.exe 12 | *.o 13 | *.so 14 | *.pyc 15 | 16 | # LaTeX # 17 | ######### 18 | *.aux 19 | *.glo 20 | *.idx 21 | *.log 22 | *.toc 23 | *.ist 24 | *.acn 25 | *.acr 26 | *.alg 27 | *.bbl 28 | *.blg 29 | *.dvi 30 | *.glg 31 | *.gls 32 | *.ilg 33 | *.ind 34 | *.lof 35 | *.lot 36 | *.maf 37 | *.mtc 38 | *.mtc1 39 | *.out 40 | *.synctex.gz 41 | 42 | # Packages # 43 | ############ 44 | # it's better to unpack these files and commit the raw source 45 | # git has its own built in compression methods 46 | *.7z 47 | *.dmg 48 | *.gz 49 | *.iso 50 | *.jar 51 | *.rar 52 | *.tar 53 | *.zip 54 | 55 | # Logs and databases # 56 | ###################### 57 | *.log 58 | *.sql 59 | *.sqlite 60 | 61 | # OS generated files # 62 | ###################### 63 | .DS_Store 64 | .DS_Store? 65 | ._* 66 | .Spotlight-V100 67 | .Trashes 68 | Icon? 69 | ehthumbs.db 70 | Thumbs.db 71 | 72 | # Emacs # 73 | ######### 74 | *~ 75 | \#*\# 76 | /.emacs.desktop 77 | /.emacs.desktop.lock 78 | .elc 79 | auto-save-list 80 | tramp 81 | .\#* 82 | 83 | # Others # 84 | ########## 85 | # Python-pickled data files 86 | *.pkl 87 | *.npy 88 | *.imc 89 | *.mat 90 | *.idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LICENSE CONDITIONS 2 | 3 | Copyright (2016) Taco Cohen 4 | All rights reserved. 5 | 6 | For details, see the paper: 7 | T.S. Cohen, M. Welling, 8 | Group Equivariant Convolutional Networks. 9 | Proceedings of the International Conference on Machine Learning (ICML), 2016 10 | 11 | Permission to use, copy, modify, and distribute this software and its documentation for educational, research, and non-commercial purposes, without fee and without a signed licensing agreement, is hereby granted, provided that the above copyright notice and this paragraph appear in all copies, modifications, and distributions. 12 | 13 | Any commercial use or any redistribution of this software requires a license. For further details, contact Taco Cohen (taco.cohen@gmail.com). 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ### Note: If you are looking for a PyTorch implementation, please have a look at the pull requests by Jorn Peters and Adam Bielski (https://github.com/tscohen/GrouPy/pulls). 3 | 4 | # GrouPy 5 | 6 | GrouPy is a python library that implements group equivariant convolutional neural networks [\[Cohen & Welling, 2016\]](#gcnn) in Chainer and TensorFlow, and supports other numerical computations involving transformation groups. 7 | 8 | GrouPy consists of the following modules: 9 | 10 | - garray: an array of transformation variables ("group elements") 11 | - gfunc: an array of functions on a group of transformations 12 | - gconv: group convolutions for use in group equivariant convolutional networks 13 | 14 | The modules garray and gfunc are used in a quick precomputation stage and run on CPU, while gconv is used to train and test the neural network, and runs on GPU. 15 | 16 | We have mostly worked with the Chainer implementation (see [experiments](https://github.com/tscohen/gconv_experiments)) but a unit-tested tensorflow implementation is available, and the code is written so that porting to theano, torch, or other frameworks is relatively easy. Most of the complexity of the code is in a precomputation step that generates indices used for transforming the filters, and this step can be shared by every deep learning framework. The rest is a basic indexing operation. 17 | 18 | 19 | ## Setup 20 | 21 | Install scientific python stack + nosetests 22 | ``` 23 | $ pip install numpy scipy matplotlib nose 24 | ``` 25 | 26 | Install [chainer](http://chainer.org/) with CUDNN and HDF5 or install [tensorflow](https://www.tensorflow.org/) 27 | 28 | Clone the latest GrouPy from github and run setup.py 29 | 30 | ``` 31 | $ python setup.py install 32 | ``` 33 | 34 | To run the tests, navigate to the groupy directory and run 35 | 36 | ``` 37 | $ nosetests -v 38 | ``` 39 | 40 | ## Getting Started 41 | 42 | ### TensorFlow 43 | 44 | ``` 45 | import numpy as np 46 | import tensorflow as tf 47 | from groupy.gconv.tensorflow_gconv.splitgconv2d import gconv2d, gconv2d_util 48 | 49 | # Construct graph 50 | x = tf.placeholder(tf.float32, [None, 9, 9, 3]) 51 | 52 | gconv_indices, gconv_shape_info, w_shape = gconv2d_util( 53 | h_input='Z2', h_output='D4', in_channels=3, out_channels=64, ksize=3) 54 | w = tf.Variable(tf.truncated_normal(w_shape, stddev=1.)) 55 | y = gconv2d(input=x, filter=w, strides=[1, 1, 1, 1], padding='SAME', 56 | gconv_indices=gconv_indices, gconv_shape_info=gconv_shape_info) 57 | 58 | gconv_indices, gconv_shape_info, w_shape = gconv2d_util( 59 | h_input='D4', h_output='D4', in_channels=64, out_channels=64, ksize=3) 60 | w = tf.Variable(tf.truncated_normal(w_shape, stddev=1.)) 61 | y = gconv2d(input=y, filter=w, strides=[1, 1, 1, 1], padding='SAME', 62 | gconv_indices=gconv_indices, gconv_shape_info=gconv_shape_info) 63 | 64 | # Compute 65 | init = tf.global_variables_initializer() 66 | sess = tf.Session() 67 | sess.run(init) 68 | y = sess.run(y, feed_dict={x: np.random.randn(10, 9, 9, 3)}) 69 | sess.close() 70 | 71 | print y.shape # (10, 9, 9, 512) 72 | ``` 73 | 74 | ### Chainer 75 | 76 | ``` 77 | from chainer import Variable 78 | import cupy as cp 79 | from groupy.gconv.chainer_gconv import P4ConvZ2, P4ConvP4 80 | 81 | # Construct G-Conv layers and copy to GPU 82 | C1 = P4ConvZ2(in_channels=3, out_channels=64, ksize=3, stride=1, pad=1).to_gpu() 83 | C2 = P4ConvP4(in_channels=64, out_channels=64, ksize=3, stride=1, pad=1).to_gpu() 84 | 85 | # Create 10 images with 3 channels and 9x9 pixels: 86 | x = Variable(cp.random.randn(10, 3, 9, 9).astype('float32')) 87 | 88 | # fprop 89 | y = C2(C1(x)) 90 | print y.data.shape # (10, 64, 4, 9, 9) 91 | ``` 92 | 93 | 94 | ## Functionality 95 | 96 | The following describes the main modules of GrouPy. For usage examples, see the various unit tests. 97 | 98 | ### garray 99 | 100 | The garray module contains a base class GArray as well as subclasses for various groups G. A GArray represents an array (just like numpy.ndarray) that contains transformations instead of scalars. Elementwise multiplication of two GArrays results in an elementwise composition of transformations. The GArray supports most functionality of a numpy.ndarray, including indexing, broadcasting, reshaping, etc. 101 | 102 | Each GArray subclass implements the group operation (composition) for the corresponding group, as well as the action of the given group on various spaces (e.g. a rotation acting on points in the plane). 103 | 104 | In addition, each GArray may have multiple parameterizations, which is convenient because the composition is typically most easily implemented as a matrix multiplication, while the transformation of a function on the group (see gfunc) requires that we associate each transformation with some number of integer indices. 105 | 106 | 107 | ### gfunc 108 | 109 | The gfunc module contains a base class GFuncArray as well as subclasses for various groups G. A GFuncArray is an array of functions on a group G. Like the GArray, this class mimicks the numpy.ndarray. 110 | 111 | Additionally, a GFuncArray can be transformed by group elements stored in a GArray. The GFuncArray associates each cell in the array storing the function values with its *coordinate*, which is an element of the group G. When a GFuncArray is transformed, we apply the transformation to the coordinates, and do a lookup in the cells associated with the transformed coordinates, to produce the values of the transformed function. 112 | 113 | The transformation behaviour for a function on the rotation-translation group (p4) and the rotation-flip-translation group (p4m) is shown below. This function could represent a feature map or filter in a G-CNN. 114 | 115 | ![p4_anim](./p4_anim.gif) 116 | 117 | A rotating function on p4. Rotating a function on p4 amounts to rolling the 4 patches (in counterclockwise direction). "Rolling" means that each square patch moves to the next one (indicated by the red arrow), while simultaneously undergoing a 90 degree rotation. For visual clarity, the animation contains frames at multiples of 45 degrees, but it should be noted that only rotations by multiples of 90 degrees are part of the group p4. 118 | 119 | ![p4m_fmap_e](./p4m_fmaps.png) 120 | 121 | A function on p4m, its rotation by 90 degrees, and its vertical reflection. Patches follow the red rotation arrows (while rotating) or the blue mirroring lines (while flipping). 122 | 123 | For more details, see section 4.4 of [\[Cohen & Welling, 2016\]](#gcnn). 124 | 125 | The gfunc.plot module contains code for plotting the [Cayley](https://en.wikipedia.org/wiki/Cayley_graph)-style graphs shown above. 126 | 127 | 128 | ### Convolution 129 | 130 | The gconv module contains group convolution layers for use in neural networks. The TensorFlow implementation is in gconv.tensorflow_gconv.splitgconv2d.py and the Chainer implementation is in gconv.chainer_gconv.p4m_conv.py and similar files. 131 | 132 | 133 | ## Implementation notes 134 | 135 | ### Porting to other frameworks 136 | 137 | To port the gconv to a new deep learning framework, we must implement two computations: 138 | 139 | 1. *Filter transformation*: a simple indexing operation (see gconv.chainer_gconv.transform_filter and gconv.tensorflow_gconv.transform_filter) 140 | 2. *Planar convolution*: standard convolution using the filters returned by the filter transformation step (see gconv.chainer_gconv.splitgconv2d) 141 | 142 | For details, see [\[Cohen & Welling, 2016\]](#gcnn), section 7 "Efficient Implementation". 143 | 144 | 145 | ### Adding new groups 146 | 147 | The garray and gfunc modules are written to facilitate easy implementation of the group convolution for new groups. 148 | The group convolution for a new group can be implemented as follows: 149 | 150 | 1. Subclass GArray for the new group and the corresponding stabilizer (see e.g. garray.C4_array and garray.p4_array) 151 | 2. Subclass GFuncArray for the new group (see e.g. garray.gfunc.p4func_array) 152 | 3. Add a function to gconv.make_gconv_indices to precompute the indices used by the group convolution GPU kernel. 153 | 4. For the Chainer implementation, subclass gconv.chainer_gconv.splitgconv2d (see e.g. gconv.chainer_gconv.p4_conv) 154 | 155 | These subclasses can easily be tested against the group axioms and other mathematical properties (see test_garray, test_gfuncarray, test_transform_filter, test_gconv). 156 | 157 | 158 | ## References 159 | 160 | 1. T.S. Cohen, M. Welling, [Group Equivariant Convolutional Networks](http://www.jmlr.org/proceedings/papers/v48/cohenc16.pdf). Proceedings of the International Conference on Machine Learning (ICML), 2016. -------------------------------------------------------------------------------- /groupy/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscohen/GrouPy/c6f40f2c07418c940e08b5297525478e3b3a824b/groupy/__init__.py -------------------------------------------------------------------------------- /groupy/garray/C4_array.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from groupy.garray.matrix_garray import MatrixGArray 3 | from groupy.garray.finitegroup import FiniteGroup 4 | from groupy.garray.p4_array import P4Array 5 | from groupy.garray.Z2_array import Z2Array 6 | 7 | 8 | class C4Array(MatrixGArray): 9 | 10 | parameterizations = ['int', 'mat', 'hmat'] 11 | _g_shapes = {'int': (1,), 'mat': (2, 2), 'hmat': (3, 3)} 12 | _left_actions = {} 13 | _reparameterizations = {} 14 | _group_name = 'C4' 15 | 16 | def __init__(self, data, p='int'): 17 | data = np.asarray(data) 18 | assert data.dtype == np.int 19 | 20 | self._left_actions[C4Array] = self.__class__.left_action_mat 21 | self._left_actions[P4Array] = self.__class__.left_action_hmat 22 | self._left_actions[Z2Array] = self.__class__.left_action_vec 23 | 24 | super(C4Array, self).__init__(data, p) 25 | 26 | def int2mat(self, int_data): 27 | r = int_data[..., 0] 28 | out = np.zeros(int_data.shape[:-1] + (2, 2), dtype=np.int) 29 | out[..., 0, 0] = np.cos(0.5 * np.pi * r) 30 | out[..., 0, 1] = -np.sin(0.5 * np.pi * r) 31 | out[..., 1, 0] = np.sin(0.5 * np.pi * r) 32 | out[..., 1, 1] = np.cos(0.5 * np.pi * r) 33 | return out 34 | 35 | def mat2int(self, mat_data): 36 | s = mat_data[..., 1, 0] 37 | c = mat_data[..., 1, 1] 38 | r = ((np.arctan2(s, c) / np.pi * 2) % 4).astype(np.int) 39 | out = np.zeros(mat_data.shape[:-2] + (1,), dtype=np.int) 40 | out[..., 0] = r 41 | return out 42 | 43 | 44 | class C4Group(FiniteGroup, C4Array): 45 | 46 | def __init__(self): 47 | C4Array.__init__( 48 | self, 49 | data=np.arange(4)[:, None], 50 | p='int' 51 | ) 52 | FiniteGroup.__init__(self, C4Array) 53 | 54 | def factory(self, *args, **kwargs): 55 | return C4Array(*args, **kwargs) 56 | 57 | 58 | C4 = C4Group() 59 | 60 | # Generators & special elements 61 | r = C4Array(data=np.array([1]), p='int') 62 | e = C4Array(data=np.array([0]), p='int') 63 | 64 | 65 | def identity(shape=(), p='int'): 66 | e = C4Array(np.zeros(shape + (1,), dtype=np.int), 'int') 67 | return e.reparameterize(p) 68 | 69 | 70 | def rand(size=()): 71 | data = np.zeros(size + (1,), dtype=np.int64) 72 | data[..., 0] = np.random.randint(0, 4, size) 73 | return C4Array(data=data, p='int') 74 | -------------------------------------------------------------------------------- /groupy/garray/D4_array.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from groupy.garray.garray import GArray 3 | from groupy.garray.finitegroup import FiniteGroup 4 | from groupy.garray.p4m_array import P4MArray 5 | from groupy.garray.Z2_array import Z2Array 6 | 7 | from groupy.garray.matrix_garray import MatrixGArray 8 | 9 | 10 | class D4Array(MatrixGArray): 11 | 12 | parameterizations = ['int', 'mat', 'hmat'] 13 | _g_shapes = {'int': (2,), 'mat': (2, 2), 'hmat': (3, 3)} 14 | _left_actions = {} 15 | _reparameterizations = {} 16 | _group_name = 'D4' 17 | 18 | def __init__(self, data, p='int'): 19 | data = np.asarray(data) 20 | assert data.dtype == np.int 21 | 22 | self._left_actions[D4Array] = self.__class__.left_action_mat 23 | self._left_actions[P4MArray] = self.__class__.left_action_hmat 24 | self._left_actions[Z2Array] = self.__class__.left_action_vec 25 | 26 | super(D4Array, self).__init__(data, p) 27 | 28 | def int2mat(self, int_data): 29 | m = int_data[..., 0] 30 | r = int_data[..., 1] 31 | out = np.zeros(int_data.shape[:-1] + self._g_shapes['mat'], dtype=np.int) 32 | out[..., 0, 0] = np.cos(0.5 * np.pi * r) * (-1) ** m 33 | out[..., 0, 1] = -np.sin(0.5 * np.pi * r) * (-1) ** m 34 | out[..., 1, 0] = np.sin(0.5 * np.pi * r) 35 | out[..., 1, 1] = np.cos(0.5 * np.pi * r) 36 | return out 37 | 38 | def mat2int(self, mat_data): 39 | neg_det_r = mat_data[..., 1, 0] * mat_data[..., 0, 1] - mat_data[..., 0, 0] * mat_data[..., 1, 1] 40 | s = mat_data[..., 1, 0] 41 | c = mat_data[..., 1, 1] 42 | m = (neg_det_r + 1) // 2 43 | r = ((np.arctan2(s, c) / np.pi * 2) % 4).astype(np.int) 44 | 45 | out = np.zeros(mat_data.shape[:-2] + self._g_shapes['int'], dtype=np.int) 46 | out[..., 0] = m 47 | out[..., 1] = r 48 | return out 49 | 50 | 51 | class D4Group(FiniteGroup, D4Array): 52 | 53 | def __init__(self): 54 | D4Array.__init__( 55 | self, 56 | data=np.array([[0, 0], [0, 1], [0, 2], [0, 3], [1, 0], [1, 1], [1, 2], [1, 3]]), 57 | p='int' 58 | ) 59 | FiniteGroup.__init__(self, D4Array) 60 | 61 | def factory(self, *args, **kwargs): 62 | return D4Array(*args, **kwargs) 63 | 64 | 65 | D4 = D4Group() 66 | 67 | # Generators & special elements 68 | r = D4Array(data=np.array([0, 1]), p='int') 69 | m = D4Array(data=np.array([1, 0]), p='int') 70 | e = D4Array(data=np.array([0, 0]), p='int') 71 | 72 | 73 | def identity(shape=(), p='int'): 74 | e = D4Array(np.zeros(shape + (2,), dtype=np.int), 'int') 75 | return e.reparameterize(p) 76 | 77 | 78 | def rand(size=()): 79 | data = np.zeros(size + (2,), dtype=np.int64) 80 | data[..., 0] = np.random.randint(0, 2, size) 81 | data[..., 1] = np.random.randint(0, 4, size) 82 | return D4Array(data=data, p='int') 83 | -------------------------------------------------------------------------------- /groupy/garray/Z2_array.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | 4 | from groupy.garray.garray import GArray 5 | 6 | 7 | class Z2Array(GArray): 8 | 9 | parameterizations = ['int'] 10 | _left_actions = {} 11 | _reparameterizations = {} 12 | _g_shapes = {'int': (2,)} 13 | _group_name = 'Z2' 14 | 15 | def __init__(self, data, p='int'): 16 | data = np.asarray(data) 17 | assert data.dtype == np.int 18 | self._left_actions[Z2Array] = self.__class__.z2_composition 19 | super(Z2Array, self).__init__(data, p) 20 | 21 | def z2_composition(self, other): 22 | return Z2Array(self.data + other.data) 23 | 24 | def inv(self): 25 | return Z2Array(-self.data) 26 | 27 | def __repr__(self): 28 | return 'Z2\n' + self.data.__repr__() 29 | 30 | def reparameterize(self, p): 31 | assert p == 'int' 32 | return self 33 | 34 | 35 | def identity(shape=()): 36 | e = Z2Array(np.zeros(shape + (2,), dtype=np.int), 'int') 37 | return e 38 | 39 | 40 | def rand(minu, maxu, minv, maxv, size=()): 41 | data = np.zeros(size + (2,), dtype=np.int64) 42 | data[..., 0] = np.random.randint(minu, maxu, size) 43 | data[..., 1] = np.random.randint(minv, maxv, size) 44 | return Z2Array(data=data, p='int') 45 | 46 | 47 | def u_range(start=-1, stop=2, step=1): 48 | m = np.zeros((stop - start, 2), dtype=np.int) 49 | m[:, 0] = np.arange(start, stop, step) 50 | return Z2Array(m) 51 | 52 | 53 | def v_range(start=-1, stop=2, step=1): 54 | m = np.zeros((stop - start, 2), dtype=np.int) 55 | m[:, 1] = np.arange(start, stop, step) 56 | return Z2Array(m) 57 | 58 | 59 | def meshgrid(u=u_range(), v=v_range()): 60 | u = Z2Array(u.data[:, None, ...], p=u.p) 61 | v = Z2Array(v.data[None, :, ...], p=v.p) 62 | return u * v 63 | 64 | 65 | # def gmeshgrid(*args): 66 | # out = identity() 67 | # for i in range(len(args)): 68 | # slices = [None if j != i else slice(None) for j in range(len(args))] + [Ellipsis] 69 | # d = args[i].data[slices] 70 | # print i, slices, d.shape 71 | # out *= P4MArray(d, p=args[i].p) 72 | # 73 | # return out 74 | -------------------------------------------------------------------------------- /groupy/garray/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from groupy.garray.Z2_array import Z2Array 3 | from groupy.garray.p4_array import P4Array 4 | from groupy.garray.p4m_array import P4MArray 5 | from groupy.garray.C4_array import C4Array, C4Group 6 | from groupy.garray.D4_array import D4Array, D4Group 7 | -------------------------------------------------------------------------------- /groupy/garray/finitegroup.py: -------------------------------------------------------------------------------- 1 | 2 | #TODO check axioms in unit test instead of in constructor 3 | 4 | 5 | class FiniteGroup(object): 6 | 7 | def __init__(self, garray_type): 8 | 9 | if not isinstance(self, garray_type): 10 | raise TypeError('A subclass of FiniteGroup should derive from a subclass of GArray and pass' 11 | ' that GArray subclass as garray_type to the FiniteGroup constructor.') 12 | self.garray_type = garray_type 13 | 14 | # Any subclass of FiniteGroup should also inherit from a subclass of GArray. 15 | # Assume the subclass has already called the constructor of the GArray subclass from which it is derived. 16 | if not self.shape[0] == self.size: 17 | raise ValueError('Group should be a flat GArray. Got shape ' + str(self.shape)) 18 | 19 | # Check group axioms 20 | for g in self: 21 | # Inverse must be in G 22 | if not g.inv() in self: 23 | raise ValueError('FiniteGroup not closed under inverses: inv(' + str(g) + ') = ' + str(g.inv())) 24 | 25 | for h in self: 26 | if not g * h in self: 27 | raise ValueError('FiniteGroup not closed under products: ' + str(g) + str(h) + ' = ' + str(g * h)) 28 | 29 | def __eq__(self, other): 30 | if isinstance(other, self.__class__): 31 | # Any instance of the same group should be equal 32 | return True 33 | else: 34 | return super(FiniteGroup, self).__eq__(other) 35 | 36 | def __ne__(self, other): 37 | if isinstance(other, self.__class__): 38 | # Any instance of the same group should be equal 39 | return False 40 | else: 41 | return super(FiniteGroup, self).__ne__(other) 42 | -------------------------------------------------------------------------------- /groupy/garray/garray.py: -------------------------------------------------------------------------------- 1 | 2 | import copy 3 | import numpy as np 4 | 5 | # TODO: add checks in constructor to make sure data argument is well formed (for the given parameterization). 6 | # TODO: for example, for a finite group, when p=='int', we want data >= 0 and data <= order_of_G 7 | 8 | 9 | class GArray(object): 10 | """ 11 | GArray is a wrapper of numpy.ndarray that can store group elements instead of numbers. 12 | Subclasses of GArray implement the needed functionality for specific groups G. 13 | 14 | A GArray has a shape (how many group elements are in the array), 15 | and a g_shape, which is the shape used to store group element itself (e.g. (3, 3) for a 3x3 matrix). 16 | The user of a GArray usually doesn't need to know the g_shape, or even the group G. 17 | GArrays should be fully gufunc compatible; i.e. they support broadcasting according to the rules of numpy. 18 | A GArray of a given shape broadcasts just like a numpy array of that shape, regardless of the g_shape. 19 | 20 | A group may have multiple parameterizations, each with its own g_shape. 21 | Group elements can be composed and compared (using the * and == operators) irrespective of their parameterization. 22 | """ 23 | 24 | # To be set in subclass 25 | parameterizations = [] 26 | _g_shapes = {} 27 | _left_actions = {} 28 | _reparameterizations = {} 29 | _group_name = 'GArray Base Class' 30 | 31 | def __init__(self, data, p): 32 | 33 | if not isinstance(data, np.ndarray): 34 | raise TypeError('data should be of type np.ndarray, got ' + str(type(data)) + ' instead.') 35 | 36 | if p not in self.parameterizations: 37 | raise ValueError('Unknown parameterization: ' + str(p)) 38 | 39 | self.data = data 40 | self.p = p 41 | self.g_shape = self._g_shapes[p] 42 | self.shape = data.shape[:data.ndim - self.g_ndim] 43 | 44 | if self.data.shape[self.ndim:] != self.g_shape: 45 | raise ValueError('Invalid data shape. Expected shape ' + str(self.g_shape) + 46 | ' for parameterization ' + str(p) + 47 | '. Got data shape ' + str(self.data.shape[self.ndim:]) + ' instead.') 48 | 49 | def inv(self): 50 | """ 51 | Compute the inverse of the group elements 52 | 53 | :return: GArray of the same shape as self, containing inverses of each element in self. 54 | """ 55 | raise NotImplementedError() 56 | 57 | def reparameterize(self, p): 58 | """ 59 | Return a GArray containing the same group elements in the requested parameterization p. 60 | If p is the same as the current parameterization, this function returns self. 61 | 62 | :param p: the requested parameterization. Must be an element of self.parameterizations 63 | :return: GArray subclass with reparameterized elements. 64 | """ 65 | if p == self.p: 66 | return self 67 | 68 | if p not in self.parameterizations: 69 | raise ValueError('Unknown parameterization:' + str(p)) 70 | 71 | if not (self.p, p) in self._reparameterizations: 72 | return ValueError('No reparameterization implemented for ' + self.p + ' -> ' + str(p)) 73 | 74 | new_data = self._reparameterizations[(self.p, p)](self.data) 75 | return self.factory(data=new_data, p=p) 76 | 77 | def reshape(self, *shape): 78 | shape = shape[0] if isinstance(shape[0], tuple) else shape 79 | full_shape = shape + self.g_shape 80 | new = copy.copy(self) 81 | new.data = self.data.reshape(full_shape) 82 | new.shape = shape 83 | return new 84 | 85 | def flatten(self): 86 | return self.reshape(np.prod(self.shape)) 87 | 88 | def __mul__(self, other): 89 | """ 90 | Act on another GArray from the left. 91 | 92 | If the arrays do not have the same shape for the loop dimensions, they are broadcast together. 93 | 94 | The left action is chosen from self.left_actions depending on the type of other; 95 | this way, a GArray subclass can act on various other compatible GArray subclasses. 96 | 97 | This function will still work if self and other have a different parameterization. 98 | The output is always returned in the other's parameterization. 99 | 100 | :param other: 101 | :return: 102 | """ 103 | for garray_type in self._left_actions: 104 | if isinstance(other, garray_type): 105 | return self._left_actions[garray_type](self, other) 106 | return NotImplemented 107 | 108 | def __eq__(self, other): 109 | """ 110 | Elementwise equality test of GArrays. 111 | Group elements are considered equal if, after reparameterization, they are numerically identical. 112 | 113 | :param other: GArray to be compared to 114 | :return: a boolean numpy.ndarray of shape self.shape 115 | """ 116 | if isinstance(other, self.__class__) or isinstance(self, other.__class__): 117 | return (self.data == other.reparameterize(self.p).data).all(axis=-1) 118 | else: 119 | return NotImplemented 120 | 121 | def __ne__(self, other): 122 | """ 123 | Elementwise inequality test of GArrays. 124 | Group elements are considered equal if, after reparameterization, they are numerically identical. 125 | 126 | :param other: GArray to be compared to 127 | :return: a boolean numpy.ndarray of shape self.shape 128 | """ 129 | if isinstance(other, self.__class__) or isinstance(self, other.__class__): 130 | return (self.data != other.reparameterize(self.p).data).any(axis=-1) 131 | else: 132 | return NotImplemented 133 | 134 | def __len__(self): 135 | if len(self.shape) > 0: 136 | return self.shape[0] 137 | else: 138 | return 1 139 | 140 | def __getitem__(self, key): 141 | # We return a factory here instead of self.__class__(..) so that a subclass 142 | # can decide what type the result should have. 143 | # For instance, a FiniteGroup may wish to return an instance of a different GArray instead of a FiniteGroup. 144 | return self.factory(data=self.data[key], p=self.p) 145 | 146 | # def __setitem__(self, key, value): 147 | # raise NotImplementedError() # TODO 148 | 149 | def __delitem__(self, key): 150 | # Raise an error to mimic the behaviour of numpy.ndarray 151 | raise ValueError('cannot delete garray elements') 152 | 153 | def __iter__(self): 154 | for i in range(self.shape[0]): 155 | yield self[i] 156 | 157 | def __contains__(self, item): 158 | return (self == item).any() 159 | 160 | # Factory is used to create new instances from a given instance, e.g. when using __getitem__ or inv() 161 | # In some cases (e.g. FiniteGroup), we may wish to instantiate a superclass instead of self.__class__ 162 | # Example: D4Group instantiates a D4Array when an element is selected. 163 | def factory(self, *args, **kwargs): 164 | return self.__class__(*args, **kwargs) 165 | 166 | @property 167 | def size(self): 168 | # Usually, np.prod(self.shape) returns an int because self.shape contains ints. 169 | # However, if self.shape == (), np.prod(self.shape) returns the float 1.0, 170 | # so we convert to int. 171 | return int(np.prod(self.shape)) 172 | 173 | @property 174 | def g_ndim(self): 175 | """ 176 | The shape of each group element in this GArray, for the current parameterization. 177 | 178 | :return: 179 | """ 180 | return len(self.g_shape) 181 | 182 | @property 183 | def ndim(self): 184 | return len(self.shape) 185 | 186 | def __repr__(self): 187 | return self._group_name + self.data.__repr__() -------------------------------------------------------------------------------- /groupy/garray/matrix_garray.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | 4 | from groupy.garray.garray import GArray 5 | 6 | 7 | class MatrixGArray(GArray): 8 | """ 9 | Base class for matrix group GArrays. 10 | Composition, inversion and the action on vectors is implemented as 11 | matrix multiplication, matrix inversion and matrix-vector multiplication, respectively. 12 | """ 13 | 14 | def __init__(self, data, p='int'): 15 | data = np.asarray(data) 16 | 17 | if p == 'int' and data.dtype != np.int: 18 | raise ValueError('data.dtype must be int when integer parameterization is used.') 19 | 20 | if 'mat' not in self.parameterizations and 'hmat' not in self.parameterizations: 21 | raise AssertionError('Subclasses of MatrixGArray should always have a "mat" and/or "hmat" parameterization') 22 | 23 | if 'mat' in self.parameterizations: 24 | self._reparameterizations[('int', 'mat')] = self.int2mat 25 | self._reparameterizations[('mat', 'int')] = self.mat2int 26 | 27 | if 'hmat' in self.parameterizations: 28 | self._reparameterizations[('int', 'hmat')] = self.int2hmat 29 | self._reparameterizations[('hmat', 'int')] = self.hmat2int 30 | 31 | if 'mat' in self.parameterizations and 'hmat' in self.parameterizations: 32 | self._reparameterizations[('hmat', 'mat')] = self.hmat2mat 33 | self._reparameterizations[('mat', 'hmat')] = self.mat2hmat 34 | 35 | super(MatrixGArray, self).__init__(data, p) 36 | 37 | def inv(self): 38 | mat_p = 'mat' if 'mat' in self.parameterizations else 'hmat' 39 | self_mat = self.reparameterize(mat_p).data 40 | self_mat_inv = np.linalg.inv(self_mat) 41 | self_mat_inv = np.round(self_mat_inv, 0).astype(self_mat.dtype) 42 | return self.factory(data=self_mat_inv, p=mat_p).reparameterize(self.p) 43 | 44 | def left_action_mat(self, other): 45 | self_mat = self.reparameterize('mat').data 46 | other_mat = other.reparameterize('mat').data 47 | c_mat = np.einsum('...ij,...jk->...ik', self_mat, other_mat) 48 | return other.factory(data=c_mat, p='mat').reparameterize(other.p) 49 | 50 | def left_action_hmat(self, other): 51 | self_hmat = self.reparameterize('hmat').data 52 | other_hmat = other.reparameterize('hmat').data 53 | c_hmat = np.einsum('...ij,...jk->...ik', self_hmat, other_hmat) 54 | return other.factory(data=c_hmat, p='hmat').reparameterize(other.p) 55 | 56 | def left_action_vec(self, other): 57 | self_mat = self.reparameterize('mat').data 58 | assert other.p == 'int' # TODO 59 | out = np.einsum('...ij,...j->...i', self_mat, other.data) 60 | return other.factory(data=out, p=other.p) 61 | 62 | def left_action_hvec(self, other): 63 | self_hmat = self.reparameterize('hmat').data 64 | assert other.p == 'int' # TODO 65 | self_mat = self_hmat[..., :-1, :-1] 66 | out = np.einsum('...ij,...j->...i', self_mat, other.data) + self_hmat[..., :-1, -1] 67 | return other.factory(data=out, p=other.p) 68 | 69 | def int2mat(self, int_data): 70 | raise NotImplementedError() 71 | 72 | def mat2int(self, mat_data): 73 | raise NotImplementedError() 74 | 75 | def mat2hmat(self, mat_data): 76 | n, m = self._g_shapes['mat'] 77 | out = np.zeros(mat_data.shape[:-2] + (n + 1, m + 1), dtype=mat_data.dtype) 78 | out[..., :n, :m] = mat_data 79 | return out 80 | 81 | def hmat2mat(self, hmat_data): 82 | return hmat_data[..., :-1, :-1] 83 | 84 | def int2hmat(self, int_data): 85 | return self.mat2hmat(self.int2mat(int_data)) 86 | 87 | def hmat2int(self, hmat_data): 88 | return self.mat2int(self.hmat2mat(hmat_data)) 89 | -------------------------------------------------------------------------------- /groupy/garray/p4_array.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from groupy.garray.matrix_garray import MatrixGArray 4 | from groupy.garray.Z2_array import Z2Array 5 | 6 | # A transformation in p4 can be coded using three integers: 7 | # r in {0, 1, 2, 3}, the rotation index 8 | # u, translation along the first spatial axis 9 | # v, translation along the second spatial axis 10 | # We will always store these in the order (r, u, v). 11 | # This is called the 'int' parameterization of p4. 12 | 13 | # A matrix representation of this group is given by 14 | # T(u, v) R(r) 15 | # where 16 | # T = [[ 1, 0, u], 17 | # [ 0, 1, v], 18 | # [ 0, 0, 1]] 19 | # R = [[ cos(r pi / 2), -sin(r pi /2), 0], 20 | # [ sin(r pi / 2), cos(r pi / 2), 0], 21 | # [ 0, 0, 1]] 22 | # This is called the 'hmat' (homogeneous matrix) parameterization of p4. 23 | 24 | # The matrix representation is easier to work with when multiplying and inverting group elements, 25 | # while the integer parameterization is required when indexing gfunc on p4. 26 | 27 | 28 | class P4Array(MatrixGArray): 29 | 30 | parameterizations = ['int', 'hmat'] 31 | _g_shapes = {'int': (3,), 'hmat': (3, 3)} 32 | _left_actions = {} 33 | _reparameterizations = {} 34 | _group_name = 'p4' 35 | 36 | def __init__(self, data, p='int'): 37 | data = np.asarray(data) 38 | assert data.dtype == np.int 39 | self._left_actions[P4Array] = self.__class__.left_action_hmat 40 | self._left_actions[Z2Array] = self.__class__.left_action_hvec 41 | super(P4Array, self).__init__(data, p) 42 | 43 | def int2hmat(self, int_data): 44 | r = int_data[..., 0] 45 | u = int_data[..., 1] 46 | v = int_data[..., 2] 47 | out = np.zeros(int_data.shape[:-1] + (3, 3), dtype=np.int) 48 | out[..., 0, 0] = np.cos(0.5 * np.pi * r) 49 | out[..., 0, 1] = -np.sin(0.5 * np.pi * r) 50 | out[..., 0, 2] = u 51 | out[..., 1, 0] = np.sin(0.5 * np.pi * r) 52 | out[..., 1, 1] = np.cos(0.5 * np.pi * r) 53 | out[..., 1, 2] = v 54 | out[..., 2, 2] = 1. 55 | return out 56 | 57 | def hmat2int(self, mat_data): 58 | s = mat_data[..., 1, 0] 59 | c = mat_data[..., 1, 1] 60 | u = mat_data[..., 0, 2] 61 | v = mat_data[..., 1, 2] 62 | r = ((np.arctan2(s, c) / np.pi * 2) % 4).astype(np.int) 63 | 64 | out = np.zeros(mat_data.shape[:-2] + (3,), dtype=np.int) 65 | out[..., 0] = r 66 | out[..., 1] = u 67 | out[..., 2] = v 68 | return out 69 | 70 | 71 | # Generators 72 | r = P4Array(data=np.array([1, 0, 0]), p='int') 73 | u = P4Array(data=np.array([0, 1, 0]), p='int') 74 | v = P4Array(data=np.array([0, 0, 1]), p='int') 75 | 76 | 77 | def identity(shape=(), p='int'): 78 | e = P4Array(np.zeros(shape + (3,), dtype=np.int), 'int') 79 | return e.reparameterize(p) 80 | 81 | 82 | def rand(minu, maxu, minv, maxv, size=()): 83 | data = np.zeros(size + (3,), dtype=np.int64) 84 | data[..., 0] = np.random.randint(0, 4, size) 85 | data[..., 1] = np.random.randint(minu, maxu, size) 86 | data[..., 2] = np.random.randint(minv, maxv, size) 87 | return P4Array(data=data, p='int') 88 | 89 | 90 | def rotation(r, center=(0, 0)): 91 | r = np.asarray(r) 92 | center = np.asarray(center) 93 | 94 | rdata = np.zeros(r.shape + (3,), dtype=np.int) 95 | rdata[..., 0] = r 96 | r0 = P4Array(rdata) 97 | 98 | tdata = np.zeros(center.shape[:-1] + (3,), dtype=np.int) 99 | tdata[..., 1:] = center 100 | t = P4Array(tdata) 101 | 102 | return t * r0 * t.inv() 103 | 104 | 105 | def translation(t): 106 | t = np.asarray(t) 107 | tdata = np.zeros(t.shape[:-1] + (3,), dtype=np.int) 108 | tdata[..., 1:] = t 109 | return P4Array(tdata) 110 | 111 | 112 | def r_range(start=0, stop=4, step=1): 113 | assert stop > 0 114 | assert stop <= 4 115 | assert start >= 0 116 | assert start < 4 117 | assert start < stop 118 | m = np.zeros((stop - start, 3), dtype=np.int) 119 | m[:, 0] = np.arange(start, stop, step) 120 | return P4Array(m) 121 | 122 | 123 | def u_range(start=-1, stop=2, step=1): 124 | m = np.zeros((stop - start, 3), dtype=np.int) 125 | m[:, 1] = np.arange(start, stop, step) 126 | return P4Array(m) 127 | 128 | 129 | def v_range(start=-1, stop=2, step=1): 130 | m = np.zeros((stop - start, 3), dtype=np.int) 131 | m[:, 2] = np.arange(start, stop, step) 132 | return P4Array(m) 133 | 134 | 135 | def meshgrid(r=r_range(), u=u_range(), v=v_range()): 136 | r = P4Array(r.data[:, None, None, ...], p=r.p) 137 | u = P4Array(u.data[None, :, None, ...], p=u.p) 138 | v = P4Array(v.data[None, None, :, ...], p=v.p) 139 | return u * v * r 140 | 141 | 142 | # When rotating even-sized filters, rotating around the origin would not map the filter onto itself. 143 | # For example, take a 2x2 filter 144 | # [[a, b], 145 | # [c, d]] 146 | # To rotate this filter, we want to rotate about its center, which is not a point in the grid Z^2. 147 | # The following subgroup contains all 4 rotations around the point (-0.5, -0.5), which we can take as the center of 148 | # the filter. 149 | # C4_halfshift = P4Array(data=np.array([[0, 0, 0], 150 | # [1, 1, 0], 151 | # [2, 1, 1], 152 | # [3, 0, 1]]), p='int') 153 | C4_halfshift = P4Array(data=np.array([[0, 0, 0], 154 | [1, -1, 0], 155 | [2, -1, -1], 156 | [3, 0, -1]]), p='int') 157 | 158 | # def gmeshgrid(*args): 159 | # out = identity() 160 | # for i in range(len(args)): 161 | # slices = [None if j != i else slice(None) for j in range(len(args))] + [Ellipsis] 162 | # d = args[i].data[slices] 163 | # print i, slices, d.shape 164 | # out *= P4MArray(d, p=args[i].p) 165 | # 166 | # return out 167 | -------------------------------------------------------------------------------- /groupy/garray/p4m_array.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | from groupy.garray.matrix_garray import MatrixGArray 4 | from groupy.garray.Z2_array import Z2Array 5 | 6 | # A transformation in p4m can be coded using four integers: 7 | # m in {0, 1}, mirror reflection in the second translation axis or not 8 | # r in {0, 1, 2, 3}, the rotation index 9 | # u, translation along the first spatial axis 10 | # v, translation along the second spatial axis 11 | # We will always store these in the order (m, r, u, v). 12 | # This is called the 'int' parameterization of p4m. 13 | 14 | # A matrix representation of this group is given by 15 | # T(u, v) M(m) R(r) 16 | # where 17 | # T = [[ 1, 0, u], 18 | # [ 0, 1, v], 19 | # [ 0, 0, 1]] 20 | # M = [[ (-1) ** m, 0, 0], 21 | # [ 0, 1, 0], 22 | # [ 0, 0, 1]] 23 | # R = [[ cos(r pi / 2), -sin(r pi /2), 0], 24 | # [ sin(r pi / 2), cos(r pi / 2), 0], 25 | # [ 0, 0, 1]] 26 | # This is called the 'hmat' (homogeneous matrix) parameterization of p4m. 27 | 28 | # The matrix representation is easier to work with when multiplying and inverting group elements, 29 | # while the integer parameterization is required when indexing gfunc on p4m. 30 | 31 | 32 | class P4MArray(MatrixGArray): 33 | 34 | parameterizations = ['int', 'hmat'] 35 | _g_shapes = {'int': (4,), 'hmat': (3, 3)} 36 | _left_actions = {} 37 | _reparameterizations = {} 38 | _group_name = 'p4m' 39 | 40 | def __init__(self, data, p='int'): 41 | data = np.asarray(data) 42 | assert data.dtype == np.int 43 | assert (p == 'int' and data.shape[-1] == 4) or (p == 'hmat' and data.shape[-2:] == (3, 3)) 44 | 45 | self._left_actions[P4MArray] = self.__class__.left_action_hmat 46 | self._left_actions[Z2Array] = self.__class__.left_action_hvec 47 | 48 | super(P4MArray, self).__init__(data, p) 49 | 50 | def int2hmat(self, int_data): 51 | m = int_data[..., 0] 52 | r = int_data[..., 1] 53 | u = int_data[..., 2] 54 | v = int_data[..., 3] 55 | out = np.zeros(int_data.shape[:-1] + (3, 3), dtype=np.int) 56 | out[..., 0, 0] = np.cos(0.5 * np.pi * r) * (-1) ** m 57 | out[..., 0, 1] = -np.sin(0.5 * np.pi * r) * (-1) ** m 58 | out[..., 0, 2] = u 59 | out[..., 1, 0] = np.sin(0.5 * np.pi * r) 60 | out[..., 1, 1] = np.cos(0.5 * np.pi * r) 61 | out[..., 1, 2] = v 62 | out[..., 2, 2] = 1. 63 | return out 64 | 65 | def hmat2int(self, hmat_data): 66 | neg_det_r = hmat_data[..., 1, 0] * hmat_data[..., 0, 1] - hmat_data[..., 0, 0] * hmat_data[..., 1, 1] 67 | s = hmat_data[..., 1, 0] 68 | c = hmat_data[..., 1, 1] 69 | u = hmat_data[..., 0, 2] 70 | v = hmat_data[..., 1, 2] 71 | m = (neg_det_r + 1) // 2 72 | r = ((np.arctan2(s, c) / np.pi * 2) % 4).astype(np.int) 73 | 74 | out = np.zeros(hmat_data.shape[:-2] + (4,), dtype=np.int) 75 | out[..., 0] = m 76 | out[..., 1] = r 77 | out[..., 2] = u 78 | out[..., 3] = v 79 | return out 80 | 81 | 82 | def identity(shape=(), p='int'): 83 | e = P4MArray(np.zeros(shape + (4,), dtype=np.int), 'int') 84 | return e.reparameterize(p) 85 | 86 | 87 | def rand(minu, maxu, minv, maxv, size=()): 88 | data = np.zeros(size + (4,), dtype=np.int64) 89 | data[..., 0] = np.random.randint(0, 2, size) 90 | data[..., 1] = np.random.randint(0, 4, size) 91 | data[..., 2] = np.random.randint(minu, maxu, size) 92 | data[..., 3] = np.random.randint(minv, maxv, size) 93 | return P4MArray(data=data, p='int') 94 | 95 | 96 | def rotation(r, center=(0, 0)): 97 | r = np.asarray(r) 98 | center = np.asarray(center) 99 | 100 | rdata = np.zeros(r.shape + (4,), dtype=np.int) 101 | rdata[..., 1] = r 102 | r0 = P4MArray(rdata) 103 | 104 | tdata = np.zeros(center.shape[:-1] + (4,), dtype=np.int) 105 | tdata[..., 2:] = center 106 | t = P4MArray(tdata) 107 | 108 | return t * r0 * t.inv() 109 | 110 | 111 | def mirror_u(shape=None): 112 | shape = shape if shape is not None else () 113 | mdata = np.zeros(shape + (4,), dtype=np.int) 114 | mdata[0] = 1 115 | return P4MArray(mdata) 116 | 117 | 118 | def mirror_v(shape=None): 119 | hm = mirror_u(shape) 120 | r = rotation(1) 121 | return r * hm * r.inv() 122 | 123 | 124 | def m_range(start=0, stop=2): 125 | assert stop > 0 126 | assert stop <= 2 127 | assert start >= 0 128 | assert start < 2 129 | assert start < stop 130 | m = np.zeros((stop - start, 4), dtype=np.int) 131 | m[:, 0] = np.arange(start, stop) 132 | return P4MArray(m) 133 | 134 | 135 | def r_range(start=0, stop=4, step=1): 136 | assert stop > 0 137 | assert stop <= 4 138 | assert start >= 0 139 | assert start < 4 140 | assert start < stop 141 | m = np.zeros((stop - start, 4), dtype=np.int) 142 | m[:, 1] = np.arange(start, stop, step) 143 | return P4MArray(m) 144 | 145 | 146 | def u_range(start=-1, stop=2, step=1): 147 | m = np.zeros((stop - start, 4), dtype=np.int) 148 | m[:, 2] = np.arange(start, stop, step) 149 | return P4MArray(m) 150 | 151 | 152 | def v_range(start=-1, stop=2, step=1): 153 | m = np.zeros((stop - start, 4), dtype=np.int) 154 | m[:, 3] = np.arange(start, stop, step) 155 | return P4MArray(m) 156 | 157 | 158 | def meshgrid(m=m_range(), r=r_range(), u=u_range(), v=v_range()): 159 | m = P4MArray(m.data[:, None, None, None, ...], p=m.p) 160 | r = P4MArray(r.data[None, :, None, None, ...], p=r.p) 161 | u = P4MArray(u.data[None, None, :, None, ...], p=u.p) 162 | v = P4MArray(v.data[None, None, None, :, ...], p=v.p) 163 | return u * v * m * r 164 | 165 | 166 | # def gmeshgrid(*args): 167 | # out = identity() 168 | # for i in range(len(args)): 169 | # slices = [None if j != i else slice(None) for j in range(len(args))] + [Ellipsis] 170 | # d = args[i].data[slices] 171 | # print i, slices, d.shape 172 | # out *= P4MArray(d, p=args[i].p) 173 | # 174 | # return out 175 | -------------------------------------------------------------------------------- /groupy/garray/test_garray.py: -------------------------------------------------------------------------------- 1 | 2 | # TODO: reshaping / flattening tests, check updating of shape, g_shape, ndim, g_ndim 3 | # TODO: test all left_actions, not just composition in group 4 | 5 | 6 | def test_p4_array(): 7 | from groupy.garray import p4_array 8 | check_wallpaper_group(p4_array, p4_array.P4Array) 9 | 10 | 11 | def test_p4m_array(): 12 | from groupy.garray import p4m_array 13 | check_wallpaper_group(p4m_array, p4m_array.P4MArray) 14 | 15 | 16 | def test_z2_array(): 17 | from groupy.garray import Z2_array 18 | check_wallpaper_group(Z2_array, Z2_array.Z2Array) 19 | 20 | 21 | def test_c4_array(): 22 | from groupy.garray import C4_array 23 | check_finite_group(C4_array, C4_array.C4Array, C4_array.C4) 24 | 25 | 26 | def test_d4_array(): 27 | from groupy.garray import D4_array 28 | check_finite_group(D4_array, D4_array.D4Array, D4_array.D4) 29 | 30 | 31 | def check_wallpaper_group(garray_module, garray_class): 32 | 33 | a = garray_module.rand(minu=-1, maxu=2, minv=-1, maxv=2, size=(2, 3)) 34 | b = garray_module.rand(minu=-1, maxu=2, minv=-1, maxv=2, size=(2, 3)) 35 | c = garray_module.rand(minu=-1, maxu=2, minv=-1, maxv=2, size=(2, 3)) 36 | 37 | check_associative(a, b, c) 38 | check_identity(garray_module, a) 39 | check_inverse(garray_module, a) 40 | 41 | check_reparameterize_invertible(garray_class, a) 42 | 43 | m = garray_module.meshgrid( 44 | u=garray_module.u_range(-1, 2), 45 | v=garray_module.v_range(-1, 2) 46 | ) 47 | check_closed_inverse(m) 48 | 49 | 50 | def check_finite_group(garray_module, garray_class, G): 51 | 52 | a = garray_module.rand() 53 | b = garray_module.rand() 54 | c = garray_module.rand() 55 | 56 | check_associative(a, b, c) 57 | check_identity(garray_module, a) 58 | check_inverse(garray_module, a) 59 | 60 | check_reparameterize_invertible(garray_class, a) 61 | 62 | check_closed_composition(G) 63 | check_closed_inverse(G) 64 | 65 | 66 | def check_associative(a, b, c): 67 | ab = a * b 68 | ab_c = ab * c 69 | bc = b * c 70 | a_bc = a * bc 71 | assert (ab_c == a_bc).all() 72 | 73 | 74 | def check_identity(garray_module, a): 75 | e = garray_module.identity() 76 | assert (e * a == a).all() 77 | assert (a * e == a).all() 78 | 79 | 80 | def check_inverse(garray_module, a): 81 | e = garray_module.identity() 82 | assert (a * a.inv() == e).all() 83 | assert (a.inv().inv() == a).all() 84 | 85 | 86 | def check_garray_equal_as_sets(G, H): 87 | """ 88 | Check that two GArrays G and H are equal as sets, 89 | i.e. that every element in G is in H and vice versa. 90 | """ 91 | Gf = G.flatten() 92 | Hf = H.flatten() 93 | 94 | for i in range(Gf.size): 95 | gi = Gf[i] 96 | assert (gi == H).sum() > 0 97 | 98 | for i in range(Hf.size): 99 | hi = Hf[i] 100 | assert (hi == G).sum() > 0 101 | 102 | 103 | def check_closed_composition(G): 104 | """ 105 | Check that a finite group G is closed under the group operation. 106 | This function computes an "outer product" of the GArray G, 107 | i.e. each element of G is multiplied with each other element. 108 | Then, we check that the resulting elements are all in G, 109 | and that each row and column of the outer product is equal to G as a set. 110 | 111 | :param G: a GArray containing every element of a finite group. 112 | """ 113 | 114 | Gf = G.flatten() 115 | outer = Gf[:, None] * Gf[None, :] 116 | 117 | for i in range(outer.shape[0]): 118 | Gi = outer[i, :] 119 | assert Gi.size == G.size 120 | check_garray_equal_as_sets(G, Gi) 121 | 122 | Gi = outer[:, i] 123 | assert Gi.size == G.size 124 | check_garray_equal_as_sets(G, Gi) 125 | 126 | 127 | def check_closed_inverse(G): 128 | """ 129 | Check that a finite group G is closed under the inverses. 130 | This function computes the inverse of each element in G, 131 | and then checks that the resulting set is equal to G as a set. 132 | 133 | Note: this function can be used on finite groups G, 134 | but also on "symmetric sets" in infinite groups. 135 | I define a symmetric set as a subset of a group that is closed under inverses, 136 | but not necessarily under composition. 137 | An example are the translations by up to and including 1 unit in x and y direction, 138 | composed with every rotation in the group p4. 139 | 140 | :param G: a GArray containing every element of a finite group. 141 | """ 142 | 143 | Gf = G.flatten() 144 | Ginv = Gf.inv() 145 | check_garray_equal_as_sets(G, Ginv) 146 | 147 | 148 | def check_reparameterize_invertible(garray_class, a): 149 | import copy 150 | 151 | for p1 in garray_class.parameterizations: 152 | 153 | b = copy.deepcopy(a) 154 | bp1 = b.reparameterize(p1) 155 | bp1data = bp1.data.copy() 156 | 157 | for p2 in garray_class.parameterizations: 158 | bp2 = bp1.reparameterize(p2) 159 | bp21 = bp2.reparameterize(p1) 160 | assert (bp1data == bp21.data).all() 161 | -------------------------------------------------------------------------------- /groupy/gconv/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from groupy.gconv.chainer_gconv.p4_conv import P4ConvZ2, P4ConvP4 3 | from groupy.gconv.chainer_gconv.p4m_conv import P4MConvZ2, P4MConvP4M 4 | -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/kernels/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscohen/GrouPy/c6f40f2c07418c940e08b5297525478e3b3a824b/groupy/gconv/chainer_gconv/kernels/__init__.py -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/kernels/integer_indexing_cuda_kernel.py: -------------------------------------------------------------------------------- 1 | # Chainer implementation of the indexing kernels used in the G-conv 2 | 3 | # These kernels take an input array containing filters, as well as an array of indices, 4 | # and produce a set of transformed filters. 5 | 6 | # The shapes are as follows 7 | # Filter shape: (output_channels, input_channels, input_transforms, nu, nv) 8 | # Index shape (one per coordinate t, u, v): (output_transforms, input_transforms, nu, nv) 9 | # Result shape: (output_channels, output_transforms, input_channels, input_transforms, nu, nv) 10 | # Note that there is one index array per group coordinate (t, u, v). 11 | 12 | # A Z2 filter is viewed as a function on G that is right-invariant to the stabilizer of the origin H 13 | # For example, for the P4 (rotation-translation) conv, the input image is a function on Z2, 14 | # which we may think of as a function on P4 that is right-invariant to rotation. 15 | # A right-rotation-invariant P4 function has the same value at (r, u, v) as it has at (r', u, v). 16 | # Naturally, we don't store this invariant P4 function, but we store an array with a length-1 axis for the rotation 17 | # coordinate. 18 | # This is consistent with the numpy convention that lenght-1 axes get broadcast automatically. 19 | # So for Z2 filters, we get the following shapes: 20 | # Filter shape: (output_channels, input_channels, 1, nu, nv) 21 | # Index shape (one per coordinate t, u, v): (output_transforms, 1, nu, nv) 22 | # Result shape: (output_channels, output_transforms, input_channels, 1, nu, nv) 23 | 24 | 25 | import cupy 26 | from cupy.core.core import compile_with_cache 27 | 28 | x = cupy.arange(2, dtype='f') # WORKAROUND - currently, cupy compile_with_cache fails if no cupy code is executed first 29 | 30 | # This computes input[..., T, U, V].swapaxes(1, 2) 31 | _index_group_func_str = \ 32 | """ 33 | extern "C" __global__ void indexing_kernel( 34 | CArray<{0}, 5> input, 35 | CArray T, 36 | CArray U, 37 | CArray V, 38 | CArray<{0}, 6> output) 39 | {{ 40 | CUPY_FOR(i, output.size()) {{ 41 | 42 | const int* oshape = output.shape(); 43 | const int* ostrides = output.strides(); 44 | 45 | // The flat index i corresponds to the following multi-index in the output array: 46 | // (output_channel, output_transform, input_channel, input_transform, u, v) 47 | const int output_channel = (sizeof({0}) * i / ostrides[0]) % oshape[0]; 48 | const int output_transform = (sizeof({0}) * i / ostrides[1]) % oshape[1]; 49 | const int input_channel = (sizeof({0}) * i / ostrides[2]) % oshape[2]; 50 | const int input_transform = (sizeof({0}) * i / ostrides[3]) % oshape[3]; 51 | const int u = (sizeof({0}) * i / ostrides[4]) % oshape[4]; 52 | const int v = (sizeof({0}) * i / ostrides[5]) % oshape[5]; 53 | 54 | int indexTUV[4] = {{output_transform, input_transform, u, v}}; 55 | int index[5] = {{output_channel, input_channel, T[indexTUV], U[indexTUV], V[indexTUV]}}; 56 | output[i] = input[index]; 57 | }} 58 | }} 59 | """ 60 | 61 | _index_group_func_kernel32 = compile_with_cache(_index_group_func_str.format('float')).get_function('indexing_kernel') 62 | _index_group_func_kernel64 = compile_with_cache(_index_group_func_str.format('double')).get_function('indexing_kernel') 63 | 64 | 65 | def index_group_func_kernel(input, T, U, V, output): 66 | if input.dtype == 'float32': 67 | _index_group_func_kernel32.linear_launch( 68 | size=output.size, 69 | args=(input, T, U, V, output) 70 | ) 71 | elif input.dtype == 'float64': 72 | _index_group_func_kernel64.linear_launch( 73 | size=output.size, 74 | args=(input, T, U, V, output) 75 | ) 76 | else: 77 | raise ValueError() 78 | 79 | 80 | _grad_index_group_func_str_double = \ 81 | """ 82 | // atomicAdd for doubles is not implemented in cuda, so have to add it here 83 | __device__ double my_atomicAdd(double* address, double val) 84 | {{ 85 | unsigned long long int* address_as_ull = 86 | (unsigned long long int*)address; 87 | unsigned long long int old = *address_as_ull, assumed; 88 | 89 | do {{ 90 | assumed = old; 91 | old = atomicCAS(address_as_ull, assumed, 92 | __double_as_longlong(val + 93 | __longlong_as_double(assumed))); 94 | 95 | // Note: uses integer comparison to avoid hang in case of NaN (since NaN != NaN) 96 | }} while (assumed != old); 97 | 98 | return __longlong_as_double(old); 99 | }} 100 | 101 | extern "C" __global__ void grad_indexing_kernel( 102 | CArray<{0}, 6> grad_output, 103 | CArray T, 104 | CArray U, 105 | CArray V, 106 | CArray<{0}, 5> grad_input) 107 | {{ 108 | CUPY_FOR(i, grad_output.size()) {{ 109 | 110 | const int* oshape = grad_output.shape(); 111 | const int* ostrides = grad_output.strides(); 112 | 113 | // The flat index i corresponds to the following multi-index in the output array: 114 | // (output_channel, output_transform, input_channel, input_transform, u, v) 115 | const int output_channel = (sizeof({0}) * i / ostrides[0]) % oshape[0]; 116 | const int output_transform = (sizeof({0}) * i / ostrides[1]) % oshape[1]; 117 | const int input_channel = (sizeof({0}) * i / ostrides[2]) % oshape[2]; 118 | const int input_transform = (sizeof({0}) * i / ostrides[3]) % oshape[3]; 119 | const int u = (sizeof({0}) * i / ostrides[4]) % oshape[4]; 120 | const int v = (sizeof({0}) * i / ostrides[5]) % oshape[5]; 121 | 122 | int indexTUV[4] = {{output_transform, input_transform, u, v}}; 123 | int index[5] = {{output_channel, input_channel, T[indexTUV], U[indexTUV], V[indexTUV]}}; 124 | my_atomicAdd(&grad_input[index], grad_output[i]); 125 | }} 126 | }} 127 | """ 128 | 129 | _grad_index_group_func_str_float = \ 130 | """ 131 | extern "C" __global__ void grad_indexing_kernel( 132 | CArray<{0}, 6> grad_output, 133 | CArray T, 134 | CArray U, 135 | CArray V, 136 | CArray<{0}, 5> grad_input) 137 | {{ 138 | CUPY_FOR(i, grad_output.size()) {{ 139 | 140 | const int* oshape = grad_output.shape(); 141 | const int* ostrides = grad_output.strides(); 142 | 143 | // The flat index i corresponds to the following multi-index in the output array: 144 | // (output_channel, output_transform, input_channel, input_transform, u, v) 145 | const int output_channel = (sizeof({0}) * i / ostrides[0]) % oshape[0]; 146 | const int output_transform = (sizeof({0}) * i / ostrides[1]) % oshape[1]; 147 | const int input_channel = (sizeof({0}) * i / ostrides[2]) % oshape[2]; 148 | const int input_transform = (sizeof({0}) * i / ostrides[3]) % oshape[3]; 149 | const int u = (sizeof({0}) * i / ostrides[4]) % oshape[4]; 150 | const int v = (sizeof({0}) * i / ostrides[5]) % oshape[5]; 151 | 152 | int indexTUV[4] = {{output_transform, input_transform, u, v}}; 153 | int index[5] = {{output_channel, input_channel, T[indexTUV], U[indexTUV], V[indexTUV]}}; 154 | atomicAdd(&grad_input[index], grad_output[i]); 155 | }} 156 | }} 157 | """ 158 | 159 | _grad_index_group_func_kernel32 = compile_with_cache( 160 | #_grad_index_group_func_str.format('float') 161 | _grad_index_group_func_str_float.format('float') 162 | ).get_function('grad_indexing_kernel') 163 | _grad_index_group_func_kernel64 = compile_with_cache( 164 | #_grad_index_group_func_str.format('double') 165 | _grad_index_group_func_str_double.format('double') 166 | ).get_function('grad_indexing_kernel') 167 | 168 | 169 | def grad_index_group_func_kernel(grad_output, T, U, V, grad_input): 170 | if grad_output.dtype == 'float32': 171 | _grad_index_group_func_kernel32.linear_launch( 172 | size=grad_output.size, 173 | args=(grad_output, T, U, V, grad_input) 174 | ) 175 | elif grad_output.dtype == 'float64': 176 | _grad_index_group_func_kernel64.linear_launch( 177 | size=grad_output.size, 178 | args=(grad_output, T, U, V, grad_input) 179 | ) 180 | else: 181 | raise ValueError() 182 | -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/kernels/test_integer_indexing_cuda_kernel.py: -------------------------------------------------------------------------------- 1 | 2 | from groupy.gconv.chainer_gconv.kernels.integer_indexing_cuda_kernel import index_group_func_kernel 3 | 4 | 5 | def test_index_group_func(): 6 | import numpy as np 7 | import cupy as cp 8 | from chainer import cuda 9 | input = np.random.randn(2, 3, 4, 5, 6) 10 | I = np.random.randint(0, 4, (7, 8, 9, 10)) 11 | J = np.random.randint(0, 5, (7, 8, 9, 10)) 12 | K = np.random.randint(0, 6, (7, 8, 9, 10)) 13 | 14 | output = input[..., I, J, K].swapaxes(1, 2) 15 | 16 | cpoutput = cp.zeros(output.shape) 17 | cpinput = cuda.to_gpu(input) 18 | cpI = cuda.to_gpu(I) 19 | cpJ = cuda.to_gpu(J) 20 | cpK = cuda.to_gpu(K) 21 | 22 | index_group_func_kernel(cpinput, cpI, cpJ, cpK, cpoutput) 23 | 24 | cpoutput = cuda.to_cpu(cpoutput) 25 | 26 | error = np.abs(cpoutput - output).sum() 27 | print(error) 28 | assert np.isclose(error, 0.) 29 | 30 | -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/p4_conv.py: -------------------------------------------------------------------------------- 1 | from groupy.gconv.chainer_gconv.splitgconv2d import SplitGConv2D 2 | from groupy.gconv.make_gconv_indices import make_c4_z2_indices, make_c4_p4_indices 3 | 4 | 5 | class P4ConvZ2(SplitGConv2D): 6 | 7 | input_stabilizer_size = 1 8 | output_stabilizer_size = 4 9 | 10 | def make_transformation_indices(self, ksize): 11 | return make_c4_z2_indices(ksize=ksize) 12 | 13 | 14 | class P4ConvP4(SplitGConv2D): 15 | 16 | input_stabilizer_size = 4 17 | output_stabilizer_size = 4 18 | 19 | def make_transformation_indices(self, ksize): 20 | return make_c4_p4_indices(ksize=ksize) -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/p4m_conv.py: -------------------------------------------------------------------------------- 1 | from groupy.gconv.chainer_gconv.splitgconv2d import SplitGConv2D 2 | from groupy.gconv.make_gconv_indices import make_d4_z2_indices, make_d4_p4m_indices 3 | 4 | 5 | class P4MConvZ2(SplitGConv2D): 6 | 7 | input_stabilizer_size = 1 8 | output_stabilizer_size = 8 9 | 10 | def make_transformation_indices(self, ksize): 11 | return make_d4_z2_indices(ksize=ksize) 12 | 13 | 14 | class P4MConvP4M(SplitGConv2D): 15 | 16 | input_stabilizer_size = 8 17 | output_stabilizer_size = 8 18 | 19 | def make_transformation_indices(self, ksize): 20 | return make_d4_p4m_indices(ksize=ksize) 21 | -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/pooling/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscohen/GrouPy/c6f40f2c07418c940e08b5297525478e3b3a824b/groupy/gconv/chainer_gconv/pooling/__init__.py -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/pooling/plane_group_spatial_max_pooling.py: -------------------------------------------------------------------------------- 1 | import chainer.functions as F 2 | 3 | 4 | def plane_group_spatial_max_pooling(x, ksize, stride=None, pad=0, cover_all=True, use_cudnn=True): 5 | xs = x.data.shape 6 | x = F.reshape(x, (xs[0], xs[1] * xs[2], xs[3], xs[4])) 7 | x = F.max_pooling_2d(x, ksize, stride, pad, cover_all, use_cudnn) 8 | x = F.reshape(x, (xs[0], xs[1], xs[2], x.data.shape[2], x.data.shape[3])) 9 | return x 10 | -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/splitgconv2d.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import numpy as np 4 | 5 | import chainer 6 | import chainer.functions as F 7 | from chainer import Variable 8 | from chainer.utils import type_check 9 | 10 | from groupy.gconv.chainer_gconv.transform_filter import TransformGFilter 11 | 12 | # Implementation note: 13 | # The standard operation computed by chainer's Convolution2D is the correlation with filter psi on the right: 14 | # output(x) = psi \corr f(t) = sum_T psi(T) f(t + T) = sum_T f(T) psi(T - t) 15 | # This operation is equivariant: psi \corr [L_t f] = L_t [psi \corr f] 16 | # What we want to compute is the following: 17 | # o(r, t) = int_T f(T) [L_tr psi](T) dT 18 | # = int_T f(T) [L_r psi](T - t) dT 19 | # This is exactly a Convolution2D correlation of f with the rotated filter [L_r psi]. 20 | 21 | 22 | class SplitGConv2D(chainer.Link): 23 | """ 24 | Group convolution base class for split plane groups. 25 | 26 | A plane group (aka wallpaper group) is a group of distance-preserving transformations that includes two independent 27 | discrete translations. 28 | 29 | A group is called split (or symmorphic) if every element in this group can be written as the composition of an 30 | element from the "stabilizer of the origin" and a translation. The stabilizer of the origin consists of those 31 | transformations in the group that leave the origin fixed. For example, the stabilizer in the rotation-translation 32 | group p4 is the set of rotations around the origin, which is (isomorphic to) the group C4. 33 | 34 | Most plane groups are split, but some include glide-reflection generators; such groups are not split. 35 | For split groups G, the G-conv can be split into a "filter transform" and "translational convolution" part. 36 | 37 | Different subclasses of this class implement the filter transform for various groups, while this class implements 38 | the common functionality. 39 | """ 40 | 41 | # To be set in subclass; the size of the stabilizer for the input and output space. 42 | # For example: for Z2, this is 1, for P4, this is 4, for P4M, this is 8. 43 | input_stabilizer_size = None 44 | output_stabilizer_size = None 45 | 46 | def __init__(self, 47 | in_channels, 48 | out_channels, 49 | ksize=3, 50 | filter_mask=None, 51 | flat_channels=False, 52 | stride=1, 53 | pad=0, 54 | wscale=1, 55 | nobias=False, 56 | use_cudnn=True, 57 | initialW=None, 58 | initial_bias=None, 59 | dtype=np.float32): 60 | """ 61 | :param in_channels: 62 | :param out_channels: 63 | :param ksize: 64 | :param filter_mask: 65 | :param stride: 66 | :param pad: 67 | :param wscale: 68 | :param nobias: 69 | :param use_cudnn: 70 | :param initialW: 71 | :param initial_bias: 72 | :param dtype: 73 | :return: 74 | """ 75 | super(SplitGConv2D, self).__init__() 76 | 77 | self.dtype = np.dtype(dtype) 78 | if self.dtype != np.float32 and use_cudnn: 79 | raise FloatingPointError('float64 cudnn convolutions are buggy, see chainer issue #519') 80 | 81 | if not isinstance(ksize, int): 82 | raise TypeError('ksize must be an integer (only square filters are supported).') 83 | 84 | self.in_channels = in_channels 85 | self.out_channels = out_channels 86 | self.ksize = ksize 87 | self.stride = stride if hasattr(stride, '__getitem__') else (stride, stride) 88 | self.pad = pad if hasattr(pad, '__getitem__') else (pad, pad) 89 | self.use_cudnn = use_cudnn 90 | self.flat_channels = flat_channels 91 | 92 | w_shape = (self.out_channels, self.in_channels, self.input_stabilizer_size, self.ksize, self.ksize) 93 | self.add_param(name='W', shape=w_shape, dtype=self.dtype) 94 | 95 | if initialW is not None: 96 | assert initialW.shape == w_shape 97 | assert isinstance(initialW, self.xp.ndarray) 98 | self.W.data[:] = initialW.astype(self.dtype) 99 | else: 100 | self.W.data[:] = self.xp.random.normal( 101 | 0, wscale * math.sqrt(1. / (self.input_stabilizer_size * self.ksize ** 2 * self.in_channels)), 102 | w_shape 103 | ).astype(self.dtype) 104 | 105 | self.usebias = not nobias 106 | if self.usebias: 107 | self.add_param( 108 | name='b', 109 | shape=self.out_channels, 110 | dtype=self.dtype 111 | ) 112 | 113 | if initial_bias is not None: # Todo: update in accordance with outcome of #525 114 | assert initial_bias.shape == (self.out_channels,) 115 | assert isinstance(initial_bias, self.xp.ndarray) 116 | self.b.data[:] = initial_bias.astype(self.dtype) 117 | elif not nobias: 118 | self.b.data[:] = self.xp.repeat(self.dtype.type(0.), self.out_channels) 119 | 120 | if filter_mask is not None: 121 | if not filter_mask.shape == (self.out_channels, self.in_channels, self.input_stabilizer_size): 122 | raise ValueError('Invalid filter_mask shape. Got: ' + str(filter_mask.shape) + 123 | '. Expected: ' + str((self.out_channels, self.in_channels, self.input_stabilizer_size))) 124 | 125 | filter_mask = filter_mask[..., None, None].astype(dtype) 126 | 127 | self.add_persistent('filter_mask', filter_mask) 128 | else: 129 | self.filter_mask = None 130 | 131 | self.add_persistent(name='inds', value=self.make_transformation_indices(ksize=self.ksize)) 132 | 133 | def make_transformation_indices(self, ksize): 134 | raise NotImplementedError() 135 | 136 | def __call__(self, x): 137 | 138 | # Apply a mask to the filters (optional) 139 | if self.filter_mask is not None: 140 | w, m = F.broadcast(self.W, Variable(self.filter_mask)) 141 | w = w * m 142 | # w = self.W * Variable(self.filter_mask) 143 | else: 144 | w = self.W 145 | 146 | # Transform the filters 147 | # w.shape == (out_channels, in_channels, input_stabilizer_size, ksize, ksize) 148 | # tw.shape == (out_channels, output_stabilizer_size, in_channels, input_stabilizer_size, ksize, ksize) 149 | tw = TransformGFilter(self.inds)(w) 150 | 151 | # Fold the transformed filters 152 | tw_shape = (self.out_channels * self.output_stabilizer_size, 153 | self.in_channels * self.input_stabilizer_size, 154 | self.ksize, self.ksize) 155 | tw = F.Reshape(tw_shape)(tw) 156 | 157 | # If flat_channels is False, we need to flatten the input feature maps to have a single 1d feature dimension. 158 | if not self.flat_channels: 159 | batch_size = x.data.shape[0] 160 | in_ny, in_nx = x.data.shape[-2:] 161 | x = F.reshape(x, (batch_size, self.in_channels * self.input_stabilizer_size, in_ny, in_nx)) 162 | 163 | # Perform the 2D convolution 164 | y = F.convolution_2d(x, tw, b=None, stride=self.stride, pad=self.pad, use_cudnn=self.use_cudnn) 165 | 166 | # Unfold the output feature maps 167 | # We do this even if flat_channels is True, because we need to add the same bias to each G-feature map 168 | batch_size, _, ny_out, nx_out = y.data.shape 169 | y = F.reshape(y, (batch_size, self.out_channels, self.output_stabilizer_size, ny_out, nx_out)) 170 | 171 | # Add a bias to each G-feature map 172 | if self.usebias: 173 | bb = F.Reshape((1, self.out_channels, 1, 1, 1))(self.b) 174 | y, b = F.broadcast(y, bb) 175 | y = y + b 176 | 177 | # Flatten feature channels if needed 178 | if self.flat_channels: 179 | n, nc, ng, nx, ny = y.data.shape 180 | y = F.reshape(y, (n, nc * ng, nx, ny)) 181 | 182 | return y 183 | -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/test_gconv.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from chainer import cuda, Variable 3 | 4 | 5 | def test_p4_net_equivariance(): 6 | from groupy.gfunc import Z2FuncArray, P4FuncArray 7 | import groupy.garray.C4_array as c4a 8 | from groupy.gconv.chainer_gconv.p4_conv import P4ConvZ2, P4ConvP4 9 | 10 | im = np.random.randn(1, 1, 11, 11).astype('float32') 11 | check_equivariance( 12 | im=im, 13 | layers=[ 14 | P4ConvZ2(in_channels=1, out_channels=2, ksize=3), 15 | P4ConvP4(in_channels=2, out_channels=3, ksize=3) 16 | ], 17 | input_array=Z2FuncArray, 18 | output_array=P4FuncArray, 19 | point_group=c4a, 20 | ) 21 | 22 | 23 | def test_p4m_net_equivariance(): 24 | from groupy.gfunc import Z2FuncArray, P4MFuncArray 25 | import groupy.garray.D4_array as d4a 26 | from groupy.gconv.chainer_gconv.p4m_conv import P4MConvZ2, P4MConvP4M 27 | 28 | im = np.random.randn(1, 1, 11, 11).astype('float32') 29 | check_equivariance( 30 | im=im, 31 | layers=[ 32 | P4MConvZ2(in_channels=1, out_channels=2, ksize=3), 33 | P4MConvP4M(in_channels=2, out_channels=3, ksize=3) 34 | ], 35 | input_array=Z2FuncArray, 36 | output_array=P4MFuncArray, 37 | point_group=d4a, 38 | ) 39 | 40 | 41 | def test_g_z2_conv_equivariance(): 42 | from groupy.gfunc import Z2FuncArray, P4FuncArray, P4MFuncArray 43 | import groupy.garray.C4_array as c4a 44 | import groupy.garray.D4_array as d4a 45 | from groupy.gconv.chainer_gconv.p4_conv import P4ConvZ2 46 | from groupy.gconv.chainer_gconv.p4m_conv import P4MConvZ2 47 | 48 | im = np.random.randn(1, 1, 11, 11).astype('float32') 49 | check_equivariance( 50 | im=im, 51 | layers=[P4ConvZ2(1, 2, 3)], 52 | input_array=Z2FuncArray, 53 | output_array=P4FuncArray, 54 | point_group=c4a, 55 | ) 56 | 57 | check_equivariance( 58 | im=im, 59 | layers=[P4MConvZ2(1, 2, 3)], 60 | input_array=Z2FuncArray, 61 | output_array=P4MFuncArray, 62 | point_group=d4a, 63 | ) 64 | 65 | 66 | def test_p4_p4_conv_equivariance(): 67 | from groupy.gfunc import P4FuncArray 68 | import groupy.garray.C4_array as c4a 69 | from groupy.gconv.chainer_gconv.p4_conv import P4ConvP4 70 | 71 | im = np.random.randn(1, 1, 4, 11, 11).astype('float32') 72 | check_equivariance( 73 | im=im, 74 | layers=[P4ConvP4(1, 2, 3)], 75 | input_array=P4FuncArray, 76 | output_array=P4FuncArray, 77 | point_group=c4a, 78 | ) 79 | 80 | 81 | def test_p4m_p4m_conv_equivariance(): 82 | from groupy.gfunc import P4MFuncArray 83 | import groupy.garray.D4_array as d4a 84 | from groupy.gconv.chainer_gconv.p4m_conv import P4MConvP4M 85 | 86 | im = np.random.randn(1, 1, 8, 11, 11).astype('float32') 87 | check_equivariance( 88 | im=im, 89 | layers=[P4MConvP4M(1, 2, 3)], 90 | input_array=P4MFuncArray, 91 | output_array=P4MFuncArray, 92 | point_group=d4a, 93 | ) 94 | 95 | 96 | def check_equivariance(im, layers, input_array, output_array, point_group): 97 | 98 | # Transform the image 99 | f = input_array(im) 100 | g = point_group.rand() 101 | gf = g * f 102 | im1 = gf.v 103 | 104 | # Apply layers to both images 105 | im = Variable(cuda.to_gpu(im)) 106 | im1 = Variable(cuda.to_gpu(im1)) 107 | 108 | fmap = im 109 | fmap1 = im1 110 | for layer in layers: 111 | layer.to_gpu() 112 | fmap = layer(fmap) 113 | fmap1 = layer(fmap1) 114 | 115 | # Transform the computed feature maps 116 | fmap1_garray = output_array(cuda.to_cpu(fmap1.data)) 117 | r_fmap1_data = (g.inv() * fmap1_garray).v 118 | 119 | fmap_data = cuda.to_cpu(fmap.data) 120 | assert np.allclose(fmap_data, r_fmap1_data, rtol=1e-5, atol=1e-3) 121 | -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/test_transform_filter.py: -------------------------------------------------------------------------------- 1 | import cupy as cp 2 | import numpy as np 3 | from chainer import Variable 4 | from chainer import cuda 5 | 6 | # TODO: check that sequential transforms match the application of a composition of transforms: g (h f) = (gh) f 7 | # TODO: check that applying a transformation and its inverse leaves the signal invariant g^-1 (g f) = f 8 | 9 | from groupy.gconv.make_gconv_indices import make_c4_z2_indices, make_c4_p4_indices,\ 10 | make_d4_z2_indices, make_d4_p4m_indices 11 | from groupy.gconv.chainer_gconv.transform_filter import TransformGFilter 12 | 13 | 14 | def test_transform_grad(): 15 | for dtype, toll in [('float32', 1e-3), ('float64', 1e-10)]: 16 | check_transform_c4_z2_grad(dtype, toll) 17 | check_transform_c4_p4_grad(dtype, toll) 18 | check_transform_d4_p4m_grad(dtype, toll) 19 | check_transform_d4_z2_grad(dtype, toll) 20 | 21 | 22 | def check_transform_c4_z2_grad(dtype='float64', toll=1e-10): 23 | inds = make_c4_z2_indices(ksize=5) 24 | w = cp.random.randn(3, 2, 1, 5, 5) 25 | check_transform_grad(inds, w, TransformGFilter, dtype, toll) 26 | 27 | 28 | def check_transform_c4_p4_grad(dtype='float64', toll=1e-10): 29 | inds = make_c4_p4_indices(ksize=3) 30 | w = cp.random.randn(1, 2, 4, 3, 3) 31 | check_transform_grad(inds, w, TransformGFilter, dtype, toll) 32 | 33 | 34 | def check_transform_d4_z2_grad(dtype='float64', toll=1e-10): 35 | inds = make_d4_z2_indices(ksize=5) 36 | w = cp.random.randn(3, 2, 1, 5, 5) 37 | check_transform_grad(inds, w, TransformGFilter, dtype, toll) 38 | 39 | 40 | def check_transform_d4_p4m_grad(dtype='float64', toll=1e-10): 41 | inds = make_d4_p4m_indices(ksize=3) 42 | w = cp.random.randn(1, 2, 8, 3, 3) 43 | check_transform_grad(inds, w, TransformGFilter, dtype, toll) 44 | 45 | 46 | def check_transform_grad(inds, w, transformer, dtype, toll): 47 | from chainer import gradient_check 48 | 49 | inds = cuda.to_gpu(inds) 50 | 51 | W = Variable(w.astype(dtype)) 52 | R = transformer(inds) 53 | 54 | RW = R(W) 55 | 56 | RW.grad = cp.random.randn(*RW.data.shape).astype(dtype) 57 | RW.backward(retain_grad=True) 58 | 59 | func = RW.creator 60 | fn = lambda: func.forward((W.data,)) 61 | gW, = gradient_check.numerical_grad(fn, (W.data,), (RW.grad,)) 62 | 63 | gan = cuda.to_cpu(gW) 64 | gat = cuda.to_cpu(W.grad) 65 | 66 | relerr = np.max(np.abs(gan - gat) / np.maximum(np.abs(gan), np.abs(gat))) 67 | 68 | print (dtype, toll, relerr) 69 | assert relerr < toll 70 | -------------------------------------------------------------------------------- /groupy/gconv/chainer_gconv/transform_filter.py: -------------------------------------------------------------------------------- 1 | 2 | # Chainer Functions for rotating filters or feature maps 3 | 4 | from chainer import cuda 5 | from chainer import function 6 | from chainer.utils import type_check 7 | 8 | from groupy.gconv.chainer_gconv.kernels.integer_indexing_cuda_kernel import grad_index_group_func_kernel 9 | from groupy.gconv.chainer_gconv.kernels.integer_indexing_cuda_kernel import index_group_func_kernel 10 | 11 | 12 | class TransformGFilter(function.Function): 13 | """ 14 | Transform a set of filters defined on a split (symmorphic) plane group G. 15 | 16 | The input filterbank w has shape (no, ni, nt, n, n), where: 17 | no: the number of output channels 18 | ni: the number of input channels 19 | nt: the number of transformations in the stabilizer of the origin in G 20 | n: the filter width and height 21 | 22 | The output filterbank rotated_w has shape (no, nt, ni, nt, n, n), where a length-nt axis is added. 23 | The filter at rotated_w[o, t, i] is the filter w[o, i] transformed by t. 24 | """ 25 | 26 | def __init__(self, inds): 27 | assert inds.dtype == 'int32' 28 | assert inds.ndim == 5 29 | self.T = inds[..., 0] 30 | self.U = inds[..., 1] 31 | self.V = inds[..., 2] 32 | 33 | def check_type_forward(self, in_types): 34 | w_type, = in_types 35 | type_check.expect(w_type.ndim == 5) 36 | # TODO: check x_type is float or double 37 | 38 | def forward_gpu(self, inputs): 39 | 40 | w, = inputs 41 | xp = cuda.get_array_module(w) 42 | och, ich, _, ny, nx = w.shape 43 | 44 | nto, nti = self.T.shape[:2] 45 | rotated_w = xp.empty((och, nto, ich, nti, ny, nx), dtype=w.dtype) 46 | 47 | index_group_func_kernel( 48 | input=w, 49 | T=self.T, 50 | U=self.U, 51 | V=self.V, 52 | output=rotated_w 53 | ) 54 | 55 | return rotated_w, 56 | 57 | def backward_gpu(self, inputs, grad_output): 58 | 59 | w, = inputs 60 | grad_rotated_w, = grad_output 61 | xp = cuda.get_array_module(w) 62 | 63 | # Gradient must be initialized with zeros, 64 | # because the kernel accumulates the gradient instead of overwriting it 65 | grad_w = xp.zeros_like(w) 66 | 67 | grad_index_group_func_kernel( 68 | grad_output=grad_rotated_w, 69 | T=self.T, 70 | U=self.U, 71 | V=self.V, 72 | grad_input=grad_w 73 | ) 74 | 75 | return grad_w, 76 | -------------------------------------------------------------------------------- /groupy/gconv/make_gconv_indices.py: -------------------------------------------------------------------------------- 1 | 2 | # Code for generating indices used in G-convolutions for various groups G. 3 | # The indices created by these functions are used to rotate and flip filters on the plane or on a group. 4 | # These indices depend only on the filter size, so they are created only once at the beginning of training. 5 | 6 | import numpy as np 7 | 8 | from groupy.garray.C4_array import C4 9 | from groupy.garray.D4_array import D4 10 | from groupy.garray.p4_array import C4_halfshift 11 | from groupy.gfunc.z2func_array import Z2FuncArray 12 | from groupy.gfunc.p4func_array import P4FuncArray 13 | from groupy.gfunc.p4mfunc_array import P4MFuncArray 14 | 15 | 16 | def make_c4_z2_indices(ksize): 17 | x = np.random.randn(1, ksize, ksize) 18 | f = Z2FuncArray(v=x) 19 | 20 | if ksize % 2 == 0: 21 | uv = f.left_translation_indices(C4_halfshift[:, None, None, None]) 22 | else: 23 | uv = f.left_translation_indices(C4[:, None, None, None]) 24 | r = np.zeros(uv.shape[:-1] + (1,)) 25 | ruv = np.c_[r, uv] 26 | return ruv.astype('int32') 27 | 28 | 29 | def make_c4_p4_indices(ksize): 30 | x = np.random.randn(4, ksize, ksize) 31 | f = P4FuncArray(v=x) 32 | 33 | if ksize % 2 == 0: 34 | li = f.left_translation_indices(C4_halfshift[:, None, None, None]) 35 | else: 36 | li = f.left_translation_indices(C4[:, None, None, None]) 37 | return li.astype('int32') 38 | 39 | 40 | def make_d4_z2_indices(ksize): 41 | assert ksize % 2 == 1 # TODO 42 | x = np.random.randn(1, ksize, ksize) 43 | f = Z2FuncArray(v=x) 44 | uv = f.left_translation_indices(D4.flatten()[:, None, None, None]) 45 | mr = np.zeros(uv.shape[:-1] + (1,)) 46 | mruv = np.c_[mr, uv] 47 | return mruv.astype('int32') 48 | 49 | 50 | def make_d4_p4m_indices(ksize): 51 | assert ksize % 2 == 1 # TODO 52 | x = np.random.randn(8, ksize, ksize) 53 | f = P4MFuncArray(v=x) 54 | li = f.left_translation_indices(D4.flatten()[:, None, None, None]) 55 | return li.astype('int32') 56 | 57 | 58 | def flatten_indices(inds): 59 | """ 60 | The Chainer implementation of G-Conv uses indices into a 5D filter tensor (with an additional axis for the 61 | transformations H. For the tensorflow implementation it was more convenient to flatten the filter tensor into 62 | a 3D tensor with shape (output channels, input channels, transformations * width * height). 63 | 64 | This function takes indices in the format required for Chainer and turns them into indices into the flat array 65 | used by tensorflow. 66 | 67 | :param inds: np.ndarray of shape (output transformations, input transformations, n, n, 3), as output by 68 | the functions like make_d4_p4m_indices(n). 69 | :return: np.ndarray of shape (output transformations, input transformations, n, n) 70 | """ 71 | n = inds.shape[-2] 72 | nti = inds.shape[1] 73 | T = inds[..., 0] # shape (nto, nti, n, n) 74 | U = inds[..., 1] # shape (nto, nti, n, n) 75 | V = inds[..., 2] # shape (nto, nti, n, n) 76 | # inds_flat = T * n * n + U * n + V 77 | inds_flat = U * n * nti + V * nti + T 78 | return inds_flat -------------------------------------------------------------------------------- /groupy/gconv/tensorflow_gconv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscohen/GrouPy/c6f40f2c07418c940e08b5297525478e3b3a824b/groupy/gconv/tensorflow_gconv/__init__.py -------------------------------------------------------------------------------- /groupy/gconv/tensorflow_gconv/check_gconv2d.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import tensorflow as tf 4 | 5 | from groupy.gconv.tensorflow_gconv.splitgconv2d import gconv2d_util, gconv2d 6 | from groupy.gfunc.z2func_array import Z2FuncArray 7 | from groupy.gfunc.p4func_array import P4FuncArray 8 | from groupy.gfunc.p4mfunc_array import P4MFuncArray 9 | import groupy.garray.C4_array as C4a 10 | import groupy.garray.D4_array as D4a 11 | 12 | # NOTE: it seems like loading tensorflow and Chainer in the same session is likely to result in problems. 13 | # I've disabled these tests for now (renamed to check_... instead of test_... so they are ignored by nose) 14 | # They should still work if you run these in a separate session 15 | 16 | 17 | def check_c4_z2_conv_equivariance(): 18 | im = np.random.randn(2, 5, 5, 1) 19 | x, y = make_graph('Z2', 'C4') 20 | check_equivariance(im, x, y, Z2FuncArray, P4FuncArray, C4a) 21 | 22 | 23 | def check_c4_c4_conv_equivariance(): 24 | im = np.random.randn(2, 5, 5, 4) 25 | x, y = make_graph('C4', 'C4') 26 | check_equivariance(im, x, y, P4FuncArray, P4FuncArray, C4a) 27 | 28 | 29 | def check_d4_z2_conv_equivariance(): 30 | im = np.random.randn(2, 5, 5, 1) 31 | x, y = make_graph('Z2', 'D4') 32 | check_equivariance(im, x, y, Z2FuncArray, P4MFuncArray, D4a) 33 | 34 | 35 | def check_d4_d4_conv_equivariance(): 36 | im = np.random.randn(2, 5, 5, 8) 37 | x, y = make_graph('D4', 'D4') 38 | check_equivariance(im, x, y, P4MFuncArray, P4MFuncArray, D4a) 39 | 40 | 41 | def make_graph(h_input, h_output): 42 | gconv_indices, gconv_shape_info, w_shape = gconv2d_util( 43 | h_input=h_input, h_output=h_output, in_channels=1, out_channels=1, ksize=3) 44 | nti = gconv_shape_info[-2] 45 | x = tf.placeholder(tf.float32, [None, 5, 5, 1 * nti]) 46 | w = tf.Variable(tf.truncated_normal(w_shape, stddev=1.)) 47 | y = gconv2d(input=x, filter=w, strides=[1, 1, 1, 1], padding='SAME', 48 | gconv_indices=gconv_indices, gconv_shape_info=gconv_shape_info) 49 | return x, y 50 | 51 | 52 | def check_equivariance(im, input, output, input_array, output_array, point_group): 53 | 54 | # Transform the image 55 | f = input_array(im.transpose((0, 3, 1, 2))) 56 | g = point_group.rand() 57 | gf = g * f 58 | im1 = gf.v.transpose((0, 2, 3, 1)) 59 | 60 | # Compute 61 | init = tf.global_variables_initializer() 62 | sess = tf.Session() 63 | sess.run(init) 64 | yx = sess.run(output, feed_dict={input: im}) 65 | yrx = sess.run(output, feed_dict={input: im1}) 66 | sess.close() 67 | 68 | # Transform the computed feature maps 69 | fmap1_garray = output_array(yrx.transpose((0, 3, 1, 2))) 70 | r_fmap1_data = (g.inv() * fmap1_garray).v.transpose((0, 2, 3, 1)) 71 | 72 | print (np.abs(yx - r_fmap1_data).sum()) 73 | assert np.allclose(yx, r_fmap1_data, rtol=1e-5, atol=1e-3) 74 | -------------------------------------------------------------------------------- /groupy/gconv/tensorflow_gconv/check_transform_filter.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import tensorflow as tf 4 | 5 | from groupy.gconv.make_gconv_indices import make_c4_z2_indices, make_c4_p4_indices 6 | from groupy.gconv.tensorflow_gconv.transform_filter import transform_filter_2d_nchw, transform_filter_2d_nhwc 7 | 8 | from groupy.gconv.make_gconv_indices import make_c4_z2_indices, make_c4_p4_indices,\ 9 | make_d4_z2_indices, make_d4_p4m_indices, flatten_indices 10 | 11 | # NOTE: it seems like loading tensorflow and Chainer in the same session is likely to result in problems. 12 | # I've disabled these tests for now (renamed to check_... instead of test_... so they are ignored by nose) 13 | # They should still work if you run these in a separate session 14 | 15 | 16 | def check_c4_z2(): 17 | inds = make_c4_z2_indices(ksize=3) 18 | w = np.random.randn(6, 7, 1, 3, 3) 19 | 20 | rt = tf_trans_filter(w, inds) 21 | rc = ch_trans_filter(w, inds) 22 | 23 | diff = np.abs(rt - rc).sum() 24 | print ('>>>>> DIFFERENCE:', diff) 25 | assert diff == 0 26 | 27 | 28 | def check_c4_p4(): 29 | inds = make_c4_p4_indices(ksize=3) 30 | w = np.random.randn(6, 7, 4, 3, 3) 31 | 32 | rt = tf_trans_filter(w, inds) 33 | rc = ch_trans_filter(w, inds) 34 | 35 | diff = np.abs(rt - rc).sum() 36 | print ('>>>>> DIFFERENCE:', diff) 37 | assert diff == 0 38 | 39 | 40 | def check_d4_z2(): 41 | inds = make_d4_z2_indices(ksize=3) 42 | w = np.random.randn(6, 7, 1, 3, 3) 43 | 44 | rt = tf_trans_filter(w, inds) 45 | rc = ch_trans_filter(w, inds) 46 | 47 | diff = np.abs(rt - rc).sum() 48 | print ('>>>>> DIFFERENCE:', diff) 49 | assert diff == 0 50 | 51 | 52 | def check_d4_p4m(): 53 | inds = make_d4_p4m_indices(ksize=3) 54 | w = np.random.randn(6, 7, 8, 3, 3) 55 | 56 | rt = tf_trans_filter(w, inds) 57 | rc = ch_trans_filter(w, inds) 58 | 59 | diff = np.abs(rt - rc).sum() 60 | print ('>>>>> DIFFERENCE:', diff) 61 | assert diff == 0 62 | 63 | 64 | def tf_trans_filter(w, inds): 65 | 66 | flat_inds = flatten_indices(inds) 67 | no, ni, nti, n, _ = w.shape 68 | shape_info = (no, inds.shape[0], ni, nti, n) 69 | 70 | w = w.transpose((3, 4, 2, 1, 0)).reshape((n, n, nti * ni, no)) 71 | 72 | wt = tf.constant(w) 73 | rwt = transform_filter_2d_nhwc(wt, flat_inds, shape_info) 74 | 75 | sess = tf.Session() 76 | rwt = sess.run(rwt) 77 | sess.close() 78 | 79 | nto = inds.shape[0] 80 | rwt = rwt.transpose(3, 2, 0, 1).reshape(no, nto, ni, nti, n, n) 81 | return rwt 82 | 83 | def tf_trans_filter2(w, inds): 84 | 85 | flat_inds = flatten_indices(inds) 86 | no, ni, nti, n, _ = w.shape 87 | shape_info = (no, inds.shape[0], ni, nti, n) 88 | 89 | w = w.reshape(no, ni * nti, n, n) 90 | 91 | wt = tf.constant(w) 92 | rwt = transform_filter_2d_nchw(wt, flat_inds, shape_info) 93 | 94 | sess = tf.Session() 95 | rwt = sess.run(rwt) 96 | sess.close() 97 | 98 | nto = inds.shape[0] 99 | rwt = rwt.reshape(no, nto, ni, nti, n, n) 100 | return rwt 101 | 102 | def ch_trans_filter(w, inds): 103 | from chainer import cuda, Variable 104 | from groupy.gconv.chainer_gconv.transform_filter import TransformGFilter 105 | 106 | w_gpu = cuda.to_gpu(w) 107 | inds_gpu = cuda.to_gpu(inds) 108 | 109 | wv = Variable(w_gpu) 110 | rwv = TransformGFilter(inds_gpu)(wv) 111 | 112 | return cuda.to_cpu(rwv.data) 113 | -------------------------------------------------------------------------------- /groupy/gconv/tensorflow_gconv/splitgconv2d.py: -------------------------------------------------------------------------------- 1 | 2 | import tensorflow as tf 3 | 4 | from groupy.gconv.make_gconv_indices import make_c4_z2_indices, make_c4_p4_indices,\ 5 | make_d4_z2_indices, make_d4_p4m_indices, flatten_indices 6 | from groupy.gconv.tensorflow_gconv.transform_filter import transform_filter_2d_nchw, transform_filter_2d_nhwc 7 | 8 | 9 | def gconv2d(input, filter, strides, padding, gconv_indices, gconv_shape_info, 10 | use_cudnn_on_gpu=None, data_format='NHWC', name=None): 11 | """ 12 | Tensorflow implementation of the group convolution. 13 | This function has the same interface as the standard convolution nn.conv2d, except for two new parameters, 14 | gconv_indices and gconv_shape_info. These can be obtained from gconv2d_util(), and are described below 15 | 16 | :param input: a tensor with (batch, height, width, in channels) axes. 17 | :param filter: a tensor with (ksize, ksize, in channels * in transformations, out channels) axes. 18 | The shape for filter can be obtained from gconv2d_util(). 19 | :param strides: A list of ints. 1-D of length 4. The stride of the sliding window for each dimension of input. 20 | Must be in the same order as the dimension specified with format. 21 | :param padding: A string from: "SAME", "VALID". The type of padding algorithm to use. 22 | :param gconv_indices: indices used in the filter transformation step of the G-Conv. 23 | Can be obtained from gconv2d_util() or using a command like flatten_indices(make_d4_p4m_indices(ksize=3)). 24 | :param gconv_shape_info: a tuple containing 25 | (num output channels, num output transformations, num input channels, num input transformations, kernel size) 26 | Can be obtained from gconv2d_util() 27 | :param use_cudnn_on_gpu: an optional bool. Defaults to True. 28 | :param data_format: the order of axes. Currently only NCHW is supported 29 | :param name: a name for the operation (optional) 30 | :return: tensor with (batch, out channels, height, width) axes. 31 | """ 32 | 33 | if data_format != 'NHWC': 34 | raise NotImplemented('Currently only NHWC data_format is supported. Got:' + str(data_format)) 35 | 36 | # Transform the filters 37 | transformed_filter = transform_filter_2d_nhwc(w=filter, flat_indices=gconv_indices, shape_info=gconv_shape_info) 38 | 39 | # Convolve input with transformed filters 40 | conv = tf.nn.conv2d(input=input, filter=transformed_filter, strides=strides, padding=padding, 41 | use_cudnn_on_gpu=use_cudnn_on_gpu, data_format=data_format, name=name) 42 | 43 | return conv 44 | 45 | 46 | def gconv2d_util(h_input, h_output, in_channels, out_channels, ksize): 47 | """ 48 | Convenience function for setting up static data required for the G-Conv. 49 | This function returns: 50 | 1) an array of indices used in the filter transformation step of gconv2d 51 | 2) shape information required by gconv2d 52 | 5) the shape of the filter tensor to be allocated and passed to gconv2d 53 | 54 | :param h_input: one of ('Z2', 'C4', 'D4'). Use 'Z2' for the first layer. Use 'C4' or 'D4' for later layers. 55 | :param h_output: one of ('C4', 'D4'). What kind of transformations to use (rotations or roto-reflections). 56 | The choice of h_output of one layer should equal h_input of the next layer. 57 | :param in_channels: the number of input channels. Note: this refers to the number of (3D) channels on the group. 58 | The number of 2D channels will be 1, 4, or 8 times larger, depending the value of h_input. 59 | :param out_channels: the number of output channels. Note: this refers to the number of (3D) channels on the group. 60 | The number of 2D channels will be 1, 4, or 8 times larger, depending on the value of h_output. 61 | :param ksize: the spatial size of the filter kernels (typically 3, 5, or 7). 62 | :return: gconv_indices 63 | """ 64 | 65 | if h_input == 'Z2' and h_output == 'C4': 66 | gconv_indices = flatten_indices(make_c4_z2_indices(ksize=ksize)) 67 | nti = 1 68 | nto = 4 69 | elif h_input == 'C4' and h_output == 'C4': 70 | gconv_indices = flatten_indices(make_c4_p4_indices(ksize=ksize)) 71 | nti = 4 72 | nto = 4 73 | elif h_input == 'Z2' and h_output == 'D4': 74 | gconv_indices = flatten_indices(make_d4_z2_indices(ksize=ksize)) 75 | nti = 1 76 | nto = 8 77 | elif h_input == 'D4' and h_output == 'D4': 78 | gconv_indices = flatten_indices(make_d4_p4m_indices(ksize=ksize)) 79 | nti = 8 80 | nto = 8 81 | else: 82 | raise ValueError('Unknown (h_input, h_output) pair:' + str((h_input, h_output))) 83 | 84 | w_shape = (ksize, ksize, in_channels * nti, out_channels) 85 | gconv_shape_info = (out_channels, nto, in_channels, nti, ksize) 86 | return gconv_indices, gconv_shape_info, w_shape 87 | 88 | 89 | def gconv2d_addbias(input, bias, nti=8): 90 | """ 91 | In a G-CNN, the feature maps are interpreted as functions on a group G instead of functions on the plane Z^2. 92 | Just like how we use a single scalar bias per 2D feature map, in a G-CNN we should use a single scalar bias per 93 | G-feature map. Failing to do this breaks the equivariance and typically hurts performance. 94 | A G-feature map usually consists of a number (e.g. 4 or 8) adjacent channels. 95 | This function will add a single bias vector to a stack of feature maps that has e.g. 4 or 8 times more 2D channels 96 | than G-channels, by replicating the bias across adjacent groups of 2D channels. 97 | 98 | :param input: tensor of shape (n, h, w, ni * nti), where n is the batch dimension, (h, w) are the height and width, 99 | ni is the number of input G-channels, and nti is the number of transformations in H. 100 | :param bias: tensor of shape (ni,) 101 | :param nti: number of transformations, e.g. 4 for C4/p4 or 8 for D4/p4m. 102 | :return: input with bias added 103 | """ 104 | # input = tf.reshape(input, ()) 105 | pass # TODO 106 | -------------------------------------------------------------------------------- /groupy/gconv/tensorflow_gconv/transform_filter.py: -------------------------------------------------------------------------------- 1 | 2 | import tensorflow as tf 3 | 4 | 5 | def transform_filter_2d_nhwc(w, flat_indices, shape_info, validate_indices=True): 6 | """ 7 | Transform a set of filters defined on a split plane group G. 8 | This is the first step of the G-Conv. The user will typically not have to call this function directly. 9 | 10 | The input filter bank w has shape (n, n, nti * ni, no), where: 11 | n: the filter width and height 12 | ni: the number of input channels (note: the input feature map is assumed to have ni * nti number of channels) 13 | nti: the number of transformations in H (the stabilizer of the origin in the input space) 14 | For example, nti == 1 for images / functions on Z2, since only the identity translation leaves the origin invariant. 15 | Similarly, nti == 4 for the group p4, because there are 4 transformations in p4 (namely, the four rotations around 16 | the origin) that leave the origin in p4 (i.e. the identity transformation) fixed. 17 | no: the number of output channels (note: the G-Conv will actually create no * nto number of channels, see below. 18 | 19 | The index array has shape (nto, nti, n, n) 20 | Index arrays for various groups can be created with functions in groupy.gconv.make_gconv_indices. 21 | For example: flat_inds = flatten_indices(make_d4_z2_indices(ksize=3)) 22 | 23 | The output filter bank transformed_w has shape (no * nto, ni * nti, n, n), 24 | (so there are nto times as many filters in the output as we had in the input w) 25 | """ 26 | 27 | # The indexing is done using tf.gather. This function can only do integer indexing along the first axis. 28 | # We want to index the spatial and transformation axes of our filter, so we must flatten them into one axis. 29 | no, nto, ni, nti, n = shape_info 30 | w_flat = tf.reshape(w, [n * n * nti, ni, no]) # shape (n * n * nti, ni, no) 31 | 32 | # Do the transformation / indexing operation. 33 | transformed_w = tf.gather(w_flat, flat_indices, 34 | validate_indices=validate_indices) # shape (nto, nti, n, n, ni, no) 35 | 36 | # Put the axes in the right order, and collapse them to get a standard shape filter bank 37 | transformed_w = tf.transpose(transformed_w, [2, 3, 4, 1, 5, 0]) # shape (n, n, ni, nti, no, nto) 38 | transformed_w = tf.reshape(transformed_w, [n, n, ni * nti, no * nto]) # shape (n, n, ni * nti, no * nto) 39 | 40 | return transformed_w 41 | 42 | 43 | def transform_filter_2d_nchw(w, flat_indices, shape_info, validate_indices=True): 44 | """ 45 | Transform a set of filters defined on a split plane group G. 46 | This is the first step of the G-Conv. The user will typically not have to call this function directly. 47 | 48 | The input filter bank w has shape (no, ni * nti, n, n), where: 49 | no: the number of output channels (note: the G-Conv will actually create no * nto number of channels, see below. 50 | ni: the number of input channels (note: the input feature map is assumed to have ni * nti number of channels) 51 | nti: the number of transformations in H (the stabilizer of the origin in the input space) 52 | For example, nti == 1 for images / functions on Z2, since only the identity translation leaves the origin invariant. 53 | Similarly, nti == 4 for the group p4, because there are 4 transformations in p4 (namely, the four rotations around 54 | the origin) that leave the origin in p4 (i.e. the identity transformation) fixed. 55 | n: the filter width and height 56 | 57 | The index array has shape (nto, nti, n, n) 58 | Index arrays for various groups can be created with functions in groupy.gconv.make_gconv_indices. 59 | For example: flat_inds = flatten_indices(make_d4_z2_indices(ksize=3)) 60 | 61 | The output filter bank transformed_w has shape (no * nto, ni * nti, n, n), 62 | (so there are nto times as many filters in the output as we had in the input w) 63 | """ 64 | 65 | # The indexing is done using tf.gather. This function can only do integer indexing along the first axis. 66 | # We want to index the spatial and transformation axes of our filter, so we must flatten them into one axis, 67 | # and bring them to the first axis 68 | no, nto, ni, nti, n = shape_info 69 | w_flat = tf.transpose(tf.reshape(w, [no, ni, nti * n * n]), [2, 0, 1]) # shape (nti * n * n, no, ni) 70 | 71 | # Do the transformation / indexing operation. 72 | transformed_w = tf.gather(w_flat, flat_indices, 73 | validate_indices=validate_indices) # shape (nto, nti, n, n, no, ni) 74 | 75 | # Put the axes in the right order, and collapse them to get a standard-shape filter bank 76 | transformed_w = tf.transpose(transformed_w, [4, 0, 5, 1, 2, 3]) # shape (no, nto, ni, nti, n, n) 77 | transformed_w = tf.reshape(transformed_w, (no * nto, ni * nti, n, n)) # shape (no * nto, ni * nti, n, n) 78 | 79 | return transformed_w 80 | -------------------------------------------------------------------------------- /groupy/gconv/theano_gconv/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscohen/GrouPy/c6f40f2c07418c940e08b5297525478e3b3a824b/groupy/gconv/theano_gconv/__init__.py -------------------------------------------------------------------------------- /groupy/gfunc/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from groupy.gfunc.p4func_array import P4FuncArray 3 | from groupy.gfunc.p4mfunc_array import P4MFuncArray 4 | from groupy.gfunc.z2func_array import Z2FuncArray 5 | -------------------------------------------------------------------------------- /groupy/gfunc/gfuncarray.py: -------------------------------------------------------------------------------- 1 | 2 | import copy 3 | import numpy as np 4 | from groupy.garray.garray import GArray 5 | 6 | 7 | class GFuncArray(object): 8 | 9 | def __init__(self, v, i2g): 10 | """ 11 | A GFunc is a discretely sampled function on a group or homogeneous space G. 12 | The GFuncArray stores an array of GFuncs, 13 | together with a map from G to an index set I (the set of sampling points) and the inverse of this map. 14 | 15 | The ndarray v can be thought of as a map 16 | v : J x I -> R 17 | from an index set J x I to real numbers. 18 | The index set J may have arbitrary shape, and each index in j identifies a GFunc. 19 | The index set I is the set of valid indices to the ndarray v. 20 | From here on, consider a single GFunc v : I -> R 21 | 22 | The GArray i2g can be thought of as a map 23 | i2g: I -> G 24 | that takes indices from I and produces a group element g in G. 25 | 26 | The map i2g is required to be invertible, and its inverse 27 | g2i : G -> I 28 | is implemented in the function g2i of a subclass. 29 | 30 | So we have the following diagram: 31 | i2g 32 | I <-----> G 33 | | g2i 34 | v | 35 | | 36 | V 37 | R 38 | 39 | So v implicitly defines a function v' on G: 40 | v'(g) = v(g2i(g)) 41 | 42 | If we have a map T: G - > G (e.g. left multiplication by g^-1), that we want to precompose with v', 43 | w'(g) = v'(T(g)) 44 | 45 | we can get the corresponding map v by composing maps like this: 46 | I ---> G ---> G ---> I ---> R 47 | i2g T g2i v 48 | to obtain the transformed function w : I -> R. 49 | This class knows how to produce such a w as an ndarray that directly maps indices to numbers, 50 | (and such that the indices correspond to group elements by the same maps i2g and g2i) 51 | 52 | :param i2g: a GArray of sample points. The sample points are elements of G or H 53 | :param v: a numpy.ndarray of values corresponding to the sample points. 54 | """ 55 | 56 | if not isinstance(i2g, GArray): 57 | raise TypeError('i2g must be of type GArray, got' + str(type(i2g)) + ' instead.') 58 | 59 | if not isinstance(v, np.ndarray): 60 | raise TypeError('v must be of type np.ndarray, got ' + str(type(v)) + ' instead.') 61 | 62 | if i2g.shape != v.shape[-i2g.ndim:]: # TODO: allow vector-valued gfunc, or leave this to Section? 63 | raise ValueError('The trailing axes of v must match the shape of i2g. Got ' + 64 | str(i2g.shape) + ' and ' + str(v.shape) + '.') 65 | 66 | self.i2g = i2g 67 | self.v = v 68 | 69 | def __call__(self, sample_points): 70 | """ 71 | Evaluate the G-func at the sample points 72 | """ 73 | if not isinstance(sample_points, type(self.i2g)): 74 | raise TypeError('Invalid type ' + str(type(sample_points)) + ' expected ' + str(type(self.i2g))) 75 | 76 | si = self.g2i(sample_points) 77 | inds = [Ellipsis] + [si[..., i] for i in range(si.shape[-1])] 78 | vi = self.v[inds] 79 | ret = copy.copy(self) 80 | ret.v = vi 81 | return ret 82 | 83 | def __getitem__(self, item): 84 | """ 85 | Get an element from the array of G-funcs 86 | """ 87 | # TODO bounds / dim checking 88 | ret = copy.copy(self) 89 | ret.v = self.v[item] 90 | return ret 91 | 92 | def __mul__(self, other): 93 | # Compute self * other 94 | if isinstance(other, GArray): 95 | gp = self.right_translation_points(other) 96 | return self(gp) 97 | else: 98 | # Python assumes we *return* NotImplemented instead of raising NotImplementedError, 99 | # when we dont know how to left multiply the given type of object by self. 100 | return NotImplemented 101 | 102 | def __rmul__(self, other): 103 | # Compute other * self 104 | if isinstance(other, GArray): 105 | gp = self.left_translation_points(other) 106 | return self(gp) 107 | else: 108 | # Python assumes we *return* NotImplemented instead of raising NotImplementedError, 109 | # when we dont know how to left multiply the given type of object by self. 110 | return NotImplemented 111 | 112 | def g2i(self, g): 113 | raise NotImplementedError() 114 | 115 | def left_translation_points(self, g): 116 | return g.inv() * self.i2g 117 | 118 | def right_translation_points(self, g): 119 | return self.i2g * g 120 | 121 | def left_translation_indices(self, g): 122 | ginv_s = self.left_translation_points(g) 123 | ginv_s_inds = self.g2i(ginv_s) 124 | return ginv_s_inds 125 | 126 | def right_translation_indices(self, g): 127 | sg = self.right_translation_points(g) 128 | sg_inds = self.g2i(sg) 129 | return sg_inds 130 | 131 | @property 132 | def ndim(self): 133 | return self.v.ndim - self.i2g.ndim 134 | 135 | @property 136 | def shape(self): 137 | return self.v.shape[:self.ndim] 138 | 139 | @property 140 | def f_shape(self): 141 | return self.i2g.shape 142 | 143 | @property 144 | def f_ndim(self): 145 | return self.i2g.ndim 146 | -------------------------------------------------------------------------------- /groupy/gfunc/p4func_array.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import groupy.garray.p4_array as p4a 4 | from groupy.gfunc.gfuncarray import GFuncArray 5 | 6 | 7 | class P4FuncArray(GFuncArray): 8 | 9 | def __init__(self, v, umin=None, umax=None, vmin=None, vmax=None): 10 | 11 | if umin is None or umax is None or vmin is None or vmax is None: 12 | if not (umin is None and umax is None and vmin is None and vmax is None): 13 | raise ValueError('Either all or none of umin, umax, vmin, vmax must equal None') 14 | 15 | # If (u, v) ranges are not given, determine them from the shape of v, 16 | # assuming the grid is centered. 17 | nu, nv = v.shape[-2:] 18 | 19 | hnu = nu // 2 20 | hnv = nv // 2 21 | 22 | umin = -hnu 23 | umax = hnu - (nu % 2 == 0) 24 | vmin = -hnv 25 | vmax = hnv - (nv % 2 == 0) 26 | 27 | self.umin = umin 28 | self.umax = umax 29 | self.vmin = vmin 30 | self.vmax = vmax 31 | 32 | i2g = p4a.meshgrid( 33 | r=p4a.r_range(0, 4), 34 | u=p4a.u_range(self.umin, self.umax + 1), 35 | v=p4a.v_range(self.vmin, self.vmax + 1) 36 | ) 37 | 38 | super(P4FuncArray, self).__init__(v=v, i2g=i2g) 39 | 40 | def g2i(self, g): 41 | # TODO: check validity of indices and wrap / clamp if necessary 42 | # (or do this in a separate function, so that this function can be more easily tested?) 43 | 44 | gint = g.reparameterize('int').data.copy() 45 | gint[..., 1] -= self.umin 46 | gint[..., 2] -= self.vmin 47 | return gint 48 | 49 | 50 | def tst(): 51 | 52 | from groupy.garray.p4_array import P4Array, meshgrid, u_range, v_range, rotation, translation 53 | 54 | x = np.random.randn(4, 3, 3) 55 | c = meshgrid(u=u_range(-1, 2), v=v_range(-1, 2)) 56 | 57 | f = P4FuncArray(v=x) 58 | 59 | g = rotation(1, center=(0, 0)) 60 | li = f.left_translation_indices(g) 61 | lp = f.left_translation_points(g) 62 | 63 | # gfi = f[li] 64 | gfp = f(lp) 65 | gf = g * f 66 | gfi = f.v[li[..., 0], li[..., 1], li[..., 2]] 67 | 68 | return x, c, f, li, gf, gfp, gfi -------------------------------------------------------------------------------- /groupy/gfunc/p4mfunc_array.py: -------------------------------------------------------------------------------- 1 | 2 | import groupy.garray.p4m_array as p4ma 3 | from groupy.gfunc.gfuncarray import GFuncArray 4 | 5 | 6 | class P4MFuncArray(GFuncArray): 7 | 8 | def __init__(self, v, umin=None, umax=None, vmin=None, vmax=None): 9 | 10 | if umin is None or umax is None or vmin is None or vmax is None: 11 | if not (umin is None and umax is None and vmin is None and vmax is None): 12 | raise ValueError('Either all or none of umin, umax, vmin, vmax must equal None') 13 | 14 | # If (u, v) ranges are not given, determine them from the shape of v, 15 | # assuming the grid is centered. 16 | nu, nv = v.shape[-2:] 17 | 18 | hnu = nu // 2 19 | hnv = nv // 2 20 | 21 | umin = -hnu 22 | umax = hnu 23 | vmin = -hnv 24 | vmax = hnv 25 | 26 | self.umin = umin 27 | self.umax = umax 28 | self.vmin = vmin 29 | self.vmax = vmax 30 | 31 | i2g = p4ma.meshgrid( 32 | m=p4ma.m_range(), 33 | r=p4ma.r_range(0, 4), 34 | u=p4ma.u_range(self.umin, self.umax + 1), 35 | v=p4ma.v_range(self.vmin, self.vmax + 1) 36 | ) 37 | 38 | if v.shape[-3] == 8: 39 | i2g = i2g.reshape(8, i2g.shape[-2], i2g.shape[-1]) 40 | self.flat_stabilizer = True 41 | else: 42 | self.flat_stabilizer = False 43 | 44 | super(P4MFuncArray, self).__init__(v=v, i2g=i2g) 45 | 46 | def g2i(self, g): 47 | # TODO: check validity of indices and wrap / clamp if necessary 48 | # (or do this in a separate function, so that this function can be more easily tested?) 49 | 50 | gint = g.reparameterize('int').data.copy() 51 | gint[..., 2] -= self.umin 52 | gint[..., 3] -= self.vmin 53 | 54 | if self.flat_stabilizer: 55 | gint[..., 1] += gint[..., 0] * 4 56 | gint = gint[..., 1:] 57 | 58 | return gint 59 | -------------------------------------------------------------------------------- /groupy/gfunc/plot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscohen/GrouPy/c6f40f2c07418c940e08b5297525478e3b3a824b/groupy/gfunc/plot/__init__.py -------------------------------------------------------------------------------- /groupy/gfunc/plot/plot_p4.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from matplotlib.patches import FancyArrowPatch 3 | 4 | from groupy.gfunc.plot import plot_z2 5 | 6 | 7 | def plot_p4(f, fignum=None, rlabels='cayley', rcolor='red', rlinestyle='-', 8 | fontsize=20, labelpad_factor_1=1.5, labelpad_factor_2=1.5, figsize=(3, 3)): 9 | """ 10 | Plot a function f : p4 -> R or f : p4 -> R^3. 11 | 12 | :param f: array of shape (4, nx, ny) or (4, nx, ny, 3) for a color plot. 13 | :param fignum: which figure the plot to. 14 | :param rlabels: the type of labels to use for the 4 patches. 15 | :param rcolor: the color of the rotation arrows. 16 | :param rlinestyle: the linestyle of the rotation arrows. 17 | :param fontsize: size of the font used to label the 4 patches. 18 | :param labelpad_factor_1: tweak the position of the label. 19 | :param labelpad_factor_2: tweak the position of the label. 20 | :param figsize: size of figure. 21 | """ 22 | 23 | assert rlabels in ['radians', 'cayley', 'indices', 'none'] 24 | assert f.shape[0] == 4 25 | assert f.ndim == 3 or f.ndim == 4 26 | ny, nx = f.shape[1:3] 27 | 28 | rlabel_names = { 29 | 'radians': ['$0$', '$\\frac{\pi}{2}$', '$\\pi$', '$\\frac{3 \pi}{2}$'], 30 | 'cayley': ['$e$', '$r$', '$r^2$', '$r^3$'], 31 | 'indices': [0, 1, 2, 3], 32 | 'none': ['', '', '', ''] 33 | } 34 | 35 | fig = plt.figure(fignum, figsize=(2 * f.shape[1], 2 * f.shape[2])) 36 | fignum = fig.number 37 | main_ax = fig.gca() 38 | 39 | figtr = fig.transFigure.inverted() # Display -> Figure 40 | 41 | ax_e = fig.add_subplot(3, 3, 2) 42 | plot_z2(f[0], fignum=fignum) 43 | ax_e.xaxis.set_label_position('bottom') 44 | ax_e.set_xlabel(rlabel_names[rlabels][0], fontsize=fontsize, labelpad=labelpad_factor_1 * fontsize) 45 | ax_e.set_xticks([]) 46 | ax_e.set_yticks([]) 47 | 48 | ax_r3 = fig.add_subplot(3, 3, 6) 49 | plot_z2(f[3], fignum=fignum) 50 | ax_r3.yaxis.set_label_position('left') 51 | ax_r3.set_ylabel(rlabel_names[rlabels][3], fontsize=fontsize, rotation='horizontal', va='center', labelpad=labelpad_factor_2 * fontsize) 52 | ax_r3.set_xticks([]) 53 | ax_r3.set_yticks([]) 54 | 55 | ax_r2 = fig.add_subplot(3, 3, 8) 56 | plot_z2(f[2], fignum=fignum) 57 | ax_r2.xaxis.set_label_position('top') 58 | ax_r2.set_xlabel(rlabel_names[rlabels][2], fontsize=fontsize, labelpad=labelpad_factor_1 * fontsize) 59 | ax_r2.set_xticks([]) 60 | ax_r2.set_yticks([]) 61 | 62 | ax_r = fig.add_subplot(3, 3, 4) 63 | plot_z2(f[1], fignum=fignum) 64 | ax_r.yaxis.set_label_position('right') 65 | ax_r.set_ylabel(rlabel_names[rlabels][1], fontsize=fontsize, rotation=0, va='center', labelpad=labelpad_factor_2 * fontsize) 66 | ax_r.set_xticks([]) 67 | ax_r.set_yticks([]) 68 | 69 | # Create pixel coordinate in the subplot coordinate systems for each beginning and enpoint of the arrows 70 | pt_right = (nx - 0.25, ny // 2) 71 | pt_top = (nx // 2, -0.75) 72 | pt_bottom = (nx // 2, ny - 0.25) 73 | pt_left = (-0.75, ny // 2) 74 | pt_center = (nx // 2, ny // 2) 75 | 76 | # Transform to figure coordinates 77 | pt_e_r = figtr.transform(ax_e.transData.transform(pt_left)) 78 | pt_r_e = figtr.transform(ax_r.transData.transform(pt_top)) 79 | 80 | pt_r_r2 = figtr.transform(ax_r.transData.transform(pt_bottom)) 81 | pt_r2_r = figtr.transform(ax_r2.transData.transform(pt_left)) 82 | 83 | pt_r2_r3 = figtr.transform(ax_r2.transData.transform(pt_right)) 84 | pt_r3_r2 = figtr.transform(ax_r3.transData.transform(pt_bottom)) 85 | 86 | pt_r3_e = figtr.transform(ax_r3.transData.transform(pt_top)) 87 | pt_e_r3 = figtr.transform(ax_e.transData.transform(pt_right)) 88 | 89 | arrow = FancyArrowPatch( 90 | pt_e_r, 91 | pt_r_e, 92 | transform=fig.transFigure, 93 | connectionstyle='angle3, angleA=10, angleB=-100', 94 | arrowstyle='->,head_length=3.5,head_width=2.5', 95 | lw='2.0', 96 | color=rcolor, 97 | linestyle=rlinestyle, 98 | ) 99 | fig.patches.append(arrow) 100 | 101 | arrow = FancyArrowPatch( 102 | pt_r_r2, 103 | pt_r2_r, 104 | transform=fig.transFigure, 105 | connectionstyle='angle3, angleA=100, angleB=170', 106 | arrowstyle='->,head_length=3.5,head_width=2.5', 107 | lw='2.0', 108 | color=rcolor, 109 | linestyle=rlinestyle, 110 | ) 111 | fig.patches.append(arrow) 112 | 113 | arrow = FancyArrowPatch( 114 | pt_r2_r3, 115 | pt_r3_r2, 116 | transform=fig.transFigure, 117 | connectionstyle='angle3, angleA=190, angleB=260', 118 | arrowstyle='->,head_length=3.5,head_width=2.5', 119 | lw='2.0', 120 | color=rcolor, 121 | linestyle=rlinestyle, 122 | ) 123 | fig.patches.append(arrow) 124 | 125 | arrow = FancyArrowPatch( 126 | pt_r3_e, 127 | pt_e_r3, 128 | transform=fig.transFigure, 129 | connectionstyle='angle3, angleA=280, angleB=-10', 130 | arrowstyle='->,head_length=3.5,head_width=2.5', 131 | lw='2.0', 132 | color=rcolor, 133 | linestyle=rlinestyle, 134 | ) 135 | fig.patches.append(arrow) 136 | 137 | main_ax.axis('off') 138 | 139 | fig.set_size_inches(figsize, forward=True) 140 | -------------------------------------------------------------------------------- /groupy/gfunc/plot/plot_p4m.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | from matplotlib.lines import Line2D 3 | from matplotlib.patches import FancyArrowPatch 4 | 5 | from groupy.gfunc.plot.plot_z2 import plot_z2 6 | 7 | 8 | # Miniature plot: 9 | # plot_p4m(imf.reshape(2, 4, 7, 7), rlabels='cayley2', fontsize=10, 10 | # labelpad_factor_1= .2, labelpad_factor_2=.8, labelpad_factor_3=0.5, labelpad_factor_4=1.2, figsize=(2.5, 2.5) 11 | 12 | 13 | def plot_p4m(f, fignum=None, rlabels='cayley_mr', rcolor='red', mcolor='blue', rlinestyle='-', mlinestyle='-', 14 | fontsize=20, labelpad_factor_1=1.5, labelpad_factor_2=1.5, labelpad_factor_3=2.5, labelpad_factor_4=2.5, 15 | figsize=(3, 3)): 16 | """ 17 | Plot a function f : p4m -> R or f : p4m -> R^3. 18 | 19 | :param f: array of shape (2, 4, nx, ny) or (2, 4, nx, ny, 3) for a color plot. 20 | :param fignum: which figure the plot to. 21 | :param rlabels: the type of labels to use for the 8 patches. 22 | :param rcolor: the color of the rotation arrows. 23 | :param mcolor: the color of the mirror lines. 24 | :param rlinestyle: the linestyle of the rotation arrows. 25 | :param mlinestyle: the linestyle of the mirror lines. 26 | :param fontsize: size of the font used to label the 8 patches. 27 | :param labelpad_factor_1: tweak the position of the label. 28 | :param figsize: size of figure. 29 | """ 30 | 31 | assert f.shape[0] == 2 32 | assert f.shape[1] == 4 33 | assert f.ndim == 4 or f.ndim == 5 34 | ny, nx = f.shape[2:4] 35 | 36 | rlabel_names = { 37 | 'cayley_rm': ['$e$', '$r$', '$r^2$', '$r^3$', '$m$', '$r^3m$', '$r^2m$', '$rm$'], 38 | 'cayley_mr': ['$e$', '$r$', '$r^2$', '$r^3$', '$m$', '$mr$', '$mr^2$', '$mr^3$'], 39 | 'cayley2': ['$e$', '$r$', '$r^2$', '$r^3$', '$m$', '$mr$\n$=$\n$r^3m$', '$r^2m = mr^2$', '$mr^3$\n$=$\n$rm$'], 40 | 'none': ['', '', '', '', '', '', '', ''] 41 | } 42 | 43 | fig = plt.figure(fignum, figsize=(2 * f.shape[1], 2 * f.shape[2])) 44 | fignum = fig.number 45 | main_ax = fig.gca() 46 | 47 | # Inner ring 48 | ax_e = fig.add_subplot(5, 5, 8) 49 | plot_z2(f[0, 0], fignum=fignum) 50 | ax_e.xaxis.set_label_position('bottom') 51 | ax_e.set_xlabel( 52 | rlabel_names[rlabels][0], 53 | fontsize=fontsize, 54 | labelpad=labelpad_factor_1 * fontsize) 55 | ax_e.set_xticks([]) 56 | ax_e.set_yticks([]) 57 | 58 | ax_r = fig.add_subplot(5, 5, 12) 59 | plot_z2(f[0, 1], fignum=fignum) 60 | ax_r.yaxis.set_label_position('right') 61 | ax_r.set_ylabel( 62 | rlabel_names[rlabels][1], 63 | fontsize=fontsize, 64 | rotation='horizontal', 65 | va='center', 66 | labelpad=labelpad_factor_2 * fontsize) 67 | ax_r.set_xticks([]) 68 | ax_r.set_yticks([]) 69 | 70 | ax_r2 = fig.add_subplot(5, 5, 18) 71 | plot_z2(f[0, 2], fignum=fignum) 72 | ax_r2.xaxis.set_label_position('top') 73 | ax_r2.set_xlabel( 74 | rlabel_names[rlabels][2], 75 | fontsize=fontsize, 76 | labelpad=labelpad_factor_1 * fontsize) 77 | ax_r2.set_xticks([]) 78 | ax_r2.set_yticks([]) 79 | 80 | ax_r3 = fig.add_subplot(5, 5, 14) 81 | plot_z2(f[0, 3], fignum=fignum) 82 | ax_r3.yaxis.set_label_position('left') 83 | ax_r3.set_ylabel( 84 | rlabel_names[rlabels][3], 85 | fontsize=fontsize, 86 | rotation=0, 87 | va='center', 88 | labelpad=labelpad_factor_2 * fontsize) 89 | ax_r3.set_xticks([]) 90 | ax_r3.set_yticks([]) 91 | 92 | # Outer ring 93 | ax_m = fig.add_subplot(5, 5, 3) 94 | plot_z2(f[1, 0], fignum=fignum) 95 | ax_m.xaxis.set_label_position('top') 96 | ax_m.set_xlabel( 97 | rlabel_names[rlabels][4], 98 | fontsize=fontsize, 99 | labelpad=labelpad_factor_3 * fontsize) 100 | ax_m.set_xticks([]) 101 | ax_m.set_yticks([]) 102 | 103 | ax_mr3 = fig.add_subplot(5, 5, 11) 104 | plot_z2(f[1, 1], fignum=fignum) 105 | ax_mr3.yaxis.set_label_position('left') 106 | ax_mr3.set_ylabel( 107 | rlabel_names[rlabels][5], 108 | fontsize=fontsize, 109 | rotation='horizontal', 110 | va='center', 111 | labelpad=labelpad_factor_4 * fontsize) 112 | ax_mr3.set_xticks([]) 113 | ax_mr3.set_yticks([]) 114 | 115 | ax_mr2 = fig.add_subplot(5, 5, 23) 116 | plot_z2(f[1, 2], fignum=fignum) 117 | ax_mr2.xaxis.set_label_position('bottom') 118 | ax_mr2.set_xlabel( 119 | rlabel_names[rlabels][6], 120 | fontsize=fontsize, 121 | labelpad=labelpad_factor_3 * fontsize) 122 | ax_mr2.set_xticks([]) 123 | ax_mr2.set_yticks([]) 124 | 125 | ax_mr = fig.add_subplot(5, 5, 15) 126 | plot_z2(f[1, 3], fignum=fignum) 127 | ax_mr.yaxis.set_label_position('right') 128 | ax_mr.set_ylabel( 129 | rlabel_names[rlabels][7], 130 | fontsize=fontsize, 131 | rotation=0, 132 | va='center', 133 | labelpad=labelpad_factor_4 * fontsize) 134 | ax_mr.set_xticks([]) 135 | ax_mr.set_yticks([]) 136 | 137 | # Create pixel coordinate in the subplot coordinate systems for each beginning and enpoint of the arrows 138 | pt_right = (nx - 0.25, ny // 2) 139 | pt_top = (nx // 2, -0.75) 140 | pt_bottom = (nx // 2, ny - 0.25) 141 | pt_left = (-0.75, ny // 2) 142 | pt_center = (nx // 2, ny // 2) 143 | 144 | figtr = fig.transFigure.inverted() # Display -> Figure 145 | 146 | # Transform to figure coordinates 147 | # Forward rotation arrows 148 | pt_e_r = figtr.transform(ax_e.transData.transform(pt_left)) 149 | pt_r_e = figtr.transform(ax_r.transData.transform(pt_top)) 150 | 151 | pt_r_r2 = figtr.transform(ax_r.transData.transform(pt_bottom)) 152 | pt_r2_r = figtr.transform(ax_r2.transData.transform(pt_left)) 153 | 154 | pt_r2_r3 = figtr.transform(ax_r2.transData.transform(pt_right)) 155 | pt_r3_r2 = figtr.transform(ax_r3.transData.transform(pt_bottom)) 156 | 157 | pt_r3_e = figtr.transform(ax_r3.transData.transform(pt_top)) 158 | pt_e_r3 = figtr.transform(ax_e.transData.transform(pt_right)) 159 | 160 | # Mirrored rotation arrows 161 | pt_m_mr = figtr.transform(ax_m.transData.transform(pt_right)) 162 | pt_mr_m = figtr.transform(ax_mr.transData.transform(pt_top)) 163 | 164 | pt_mr_mr2 = figtr.transform(ax_mr.transData.transform(pt_bottom)) 165 | pt_mr2_mr = figtr.transform(ax_mr2.transData.transform(pt_right)) 166 | 167 | pt_mr2_mr3 = figtr.transform(ax_mr2.transData.transform(pt_left)) 168 | pt_mr3_mr2 = figtr.transform(ax_mr3.transData.transform(pt_bottom)) 169 | 170 | pt_mr3_m = figtr.transform(ax_mr3.transData.transform(pt_top)) 171 | pt_m_mr3 = figtr.transform(ax_m.transData.transform(pt_left)) 172 | 173 | # Mirroring lines 174 | pt_e_m = figtr.transform(ax_e.transData.transform(pt_center)) 175 | pt_m_e = figtr.transform(ax_m.transData.transform(pt_center)) 176 | 177 | pt_r_mr3 = figtr.transform(ax_r.transData.transform(pt_center)) 178 | pt_mr3_r = figtr.transform(ax_mr3.transData.transform(pt_center)) 179 | 180 | pt_r2_mr2 = figtr.transform(ax_r2.transData.transform(pt_center)) 181 | pt_mr2_r2 = figtr.transform(ax_mr2.transData.transform(pt_center)) 182 | 183 | pt_r3_mr = figtr.transform(ax_r3.transData.transform(pt_center)) 184 | pt_mr_r3 = figtr.transform(ax_mr.transData.transform(pt_center)) 185 | 186 | # Draw rotation arrows 187 | arrow = FancyArrowPatch( 188 | pt_e_r, 189 | pt_r_e, 190 | transform=fig.transFigure, 191 | connectionstyle='angle3, angleA=10, angleB=-100', 192 | arrowstyle='->,head_length=3.5,head_width=2.5', 193 | lw='2.0', 194 | color=rcolor, 195 | linestyle=rlinestyle 196 | ) 197 | fig.patches.append(arrow) 198 | 199 | arrow = FancyArrowPatch( 200 | pt_r_r2, 201 | pt_r2_r, 202 | transform=fig.transFigure, 203 | connectionstyle='angle3, angleA=100, angleB=170', 204 | arrowstyle='->,head_length=3.5,head_width=2.5', 205 | lw='2.0', 206 | color=rcolor, 207 | linestyle=rlinestyle 208 | ) 209 | fig.patches.append(arrow) 210 | 211 | arrow = FancyArrowPatch( 212 | pt_r2_r3, 213 | pt_r3_r2, 214 | transform=fig.transFigure, 215 | connectionstyle='angle3, angleA=190, angleB=260', 216 | arrowstyle='->,head_length=3.5,head_width=2.5', 217 | lw='2.0', 218 | color=rcolor, 219 | linestyle=rlinestyle 220 | ) 221 | fig.patches.append(arrow) 222 | 223 | arrow = FancyArrowPatch( 224 | pt_r3_e, 225 | pt_e_r3, 226 | transform=fig.transFigure, 227 | connectionstyle='angle3, angleA=280, angleB=-10', 228 | arrowstyle='->,head_length=3.5,head_width=2.5', 229 | lw='2.0', 230 | color=rcolor, 231 | linestyle=rlinestyle 232 | ) 233 | fig.patches.append(arrow) 234 | 235 | arrow = FancyArrowPatch( 236 | pt_m_mr, 237 | pt_mr_m, 238 | transform=fig.transFigure, 239 | connectionstyle='angle3, angleA=170, angleB=280', 240 | arrowstyle='->,head_length=3.5,head_width=2.5', 241 | lw='2.0', 242 | color=rcolor, 243 | linestyle=rlinestyle 244 | ) 245 | fig.patches.append(arrow) 246 | 247 | arrow = FancyArrowPatch( 248 | pt_mr_mr2, 249 | pt_mr2_mr, 250 | transform=fig.transFigure, 251 | connectionstyle='angle3, angleA=260, angleB=10', 252 | arrowstyle='->,head_length=3.5,head_width=2.5', 253 | lw='2.0', 254 | color=rcolor, 255 | linestyle=rlinestyle 256 | ) 257 | fig.patches.append(arrow) 258 | 259 | arrow = FancyArrowPatch( 260 | pt_mr2_mr3, 261 | pt_mr3_mr2, 262 | transform=fig.transFigure, 263 | connectionstyle='angle3, angleA=-10, angleB=100', 264 | arrowstyle='->,head_length=3.5,head_width=2.5', 265 | lw='2.0', 266 | color=rcolor, 267 | linestyle=rlinestyle 268 | ) 269 | fig.patches.append(arrow) 270 | 271 | arrow = FancyArrowPatch( 272 | pt_mr3_m, 273 | pt_m_mr3, 274 | transform=fig.transFigure, 275 | connectionstyle='angle3, angleA=260, angleB=10', 276 | arrowstyle='->,head_length=3.5,head_width=2.5', 277 | lw='2.0', 278 | color=rcolor, 279 | linestyle=rlinestyle 280 | ) 281 | fig.patches.append(arrow) 282 | 283 | # Draw mirror lines 284 | main_ax.add_line(Line2D((pt_e_m[0], pt_m_e[0]), (pt_e_m[1], pt_m_e[1]), zorder=0, linewidth=4, color=mcolor, transform=fig.transFigure, linestyle=mlinestyle)) 285 | main_ax.add_line(Line2D((pt_r_mr3[0], pt_mr3_r[0]), (pt_r_mr3[1], pt_mr3_r[1]), zorder=0, linewidth=4, color=mcolor, transform=fig.transFigure, linestyle=mlinestyle)) 286 | main_ax.add_line(Line2D((pt_r2_mr2[0], pt_mr2_r2[0]), (pt_r2_mr2[1], pt_mr2_r2[1]), zorder=0, linewidth=4, color=mcolor, transform=fig.transFigure, linestyle=mlinestyle)) 287 | main_ax.add_line(Line2D((pt_r3_mr[0], pt_mr_r3[0]), (pt_r3_mr[1], pt_mr_r3[1]), zorder=0, linewidth=4, color=mcolor, transform=fig.transFigure, linestyle=mlinestyle)) 288 | 289 | main_ax.axis('off') 290 | 291 | fig.set_size_inches(figsize, forward=True) 292 | -------------------------------------------------------------------------------- /groupy/gfunc/plot/plot_z2.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | import matplotlib.cm as cm 5 | 6 | 7 | def plot_z2(f, fignum=None, range=None, color_map='gray'): 8 | 9 | # plt.figure(fignum) 10 | 11 | if range is None: 12 | plt.imshow(f, interpolation='nearest', cmap=cm.get_cmap(color_map)) 13 | else: 14 | plt.imshow(f, interpolation='nearest', cmap=cm.get_cmap(color_map), 15 | vmin=range[0], vmax=range[1]) 16 | 17 | plt.xticks(np.arange(f.shape[1]), [str(i) for i in np.arange(f.shape[1])]) 18 | plt.yticks(np.arange(f.shape[0]), [str(i) for i in np.arange(f.shape[0])]) 19 | -------------------------------------------------------------------------------- /groupy/gfunc/test_gfuncarray.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def test_p4_func(): 5 | from groupy.gfunc.p4func_array import P4FuncArray 6 | import groupy.garray.C4_array as c4a 7 | 8 | v = np.random.randn(2, 6, 4, 5, 5) 9 | f = P4FuncArray(v=v) 10 | 11 | g = c4a.rand(size=(1,)) 12 | h = c4a.rand(size=(1,)) 13 | 14 | check_associative(g, h, f) 15 | check_identity(c4a, f) 16 | check_invertible(g, f) 17 | check_i2g_g2i_invertible(f) 18 | 19 | 20 | def test_p4m_func(): 21 | from groupy.gfunc.p4mfunc_array import P4MFuncArray 22 | import groupy.garray.D4_array as d4a 23 | 24 | v = np.random.randn(2, 6, 8, 5, 5) 25 | f = P4MFuncArray(v=v) 26 | 27 | g = d4a.rand(size=(1,)) 28 | h = d4a.rand(size=(1,)) 29 | 30 | check_associative(g, h, f) 31 | check_identity(d4a, f) 32 | check_invertible(g, f) 33 | check_i2g_g2i_invertible(f) 34 | 35 | 36 | def test_z2_func(): 37 | from groupy.gfunc.z2func_array import Z2FuncArray 38 | import groupy.garray.C4_array as c4a 39 | import groupy.garray.C4_array as d4a 40 | 41 | v = np.random.randn(2, 6, 5, 5) 42 | f = Z2FuncArray(v=v) 43 | 44 | g = c4a.rand(size=(1,)) 45 | h = c4a.rand(size=(1,)) 46 | check_associative(g, h, f) 47 | check_identity(c4a, f) 48 | check_invertible(g, f) 49 | check_i2g_g2i_invertible(f) 50 | 51 | g = d4a.rand(size=(1,)) 52 | h = d4a.rand(size=(1,)) 53 | check_associative(g, h, f) 54 | check_identity(c4a, f) 55 | check_invertible(g, f) 56 | check_i2g_g2i_invertible(f) 57 | 58 | 59 | def check_associative(g, h, f): 60 | gh = g * h 61 | hf = h * f 62 | gh_f = gh * f 63 | g_hf = g * hf 64 | assert (gh_f.v == g_hf.v).all() 65 | 66 | 67 | def check_identity(garray_module, a): 68 | e = garray_module.identity() 69 | assert ((e * a).v == a.v).all() 70 | 71 | 72 | def check_invertible(g, f): 73 | assert ((g.inv() * (g * f)).v == f.v).all() 74 | 75 | 76 | def check_i2g_g2i_invertible(f): 77 | i2g = f.i2g 78 | i = f.g2i(i2g) 79 | inds = [i[..., j] for j in range(i.shape[-1])] 80 | assert (i2g[inds] == i2g).all() 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /groupy/gfunc/z2func_array.py: -------------------------------------------------------------------------------- 1 | 2 | import groupy.garray.Z2_array as z2a 3 | from groupy.gfunc.gfuncarray import GFuncArray 4 | 5 | 6 | class Z2FuncArray(GFuncArray): 7 | 8 | def __init__(self, v, umin=None, umax=None, vmin=None, vmax=None): 9 | 10 | if umin is None or umax is None or vmin is None or vmax is None: 11 | if not (umin is None and umax is None and vmin is None and vmax is None): 12 | raise ValueError('Either all or none of umin, umax, vmin, vmax must equal None') 13 | 14 | # If (u, v) ranges are not given, determine them from the shape of v, 15 | # assuming the grid is centered. 16 | nu, nv = v.shape[-2:] 17 | 18 | hnu = nu // 2 19 | hnv = nv // 2 20 | 21 | umin = -hnu 22 | umax = hnu - (nu % 2 == 0) 23 | vmin = -hnv 24 | vmax = hnv - (nv % 2 == 0) 25 | 26 | self.umin = umin 27 | self.umax = umax 28 | self.vmin = vmin 29 | self.vmax = vmax 30 | 31 | i2g = z2a.meshgrid( 32 | u=z2a.u_range(self.umin, self.umax + 1), 33 | v=z2a.v_range(self.vmin, self.vmax + 1) 34 | ) 35 | 36 | super(Z2FuncArray, self).__init__(v=v, i2g=i2g) 37 | 38 | def g2i(self, g): 39 | # TODO: check validity of indices and wrap / clamp if necessary 40 | # (or do this in a separate function, so that this function can be more easily tested?) 41 | 42 | gint = g.reparameterize('int').data.copy() 43 | gint[..., 0] -= self.umin 44 | gint[..., 1] -= self.vmin 45 | return gint 46 | -------------------------------------------------------------------------------- /p4_anim.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscohen/GrouPy/c6f40f2c07418c940e08b5297525478e3b3a824b/p4_anim.gif -------------------------------------------------------------------------------- /p4_fmaps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscohen/GrouPy/c6f40f2c07418c940e08b5297525478e3b3a824b/p4_fmaps.png -------------------------------------------------------------------------------- /p4m_fmaps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tscohen/GrouPy/c6f40f2c07418c940e08b5297525478e3b3a824b/p4m_fmaps.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | matplotlib -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup( 6 | name='GrouPy', 7 | version='0.1.1', 8 | description='Group equivariant convolutional neural networks', 9 | author='Taco S. Cohen', 10 | author_email='taco.cohen@gmail.com', 11 | packages=['groupy', 'groupy.garray', 'groupy.gconv', 'groupy.gconv.chainer_gconv', 'groupy.gconv.theano_gconv', 'groupy.gconv.tensorflow_gconv', 'groupy.gfunc', 'groupy.gfunc.plot'], 12 | ) 13 | --------------------------------------------------------------------------------