├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------