Browse code

Python 3 support

Of course, it still works with Python 2.x as before!

Lorenz Hüdepohl authored on08/07/2015 19:57:05 • Lorenz Hüdepohl committed on08/07/2015 20:43:21
Showing1 changed files
... ...
@@ -22,6 +22,9 @@
22 22
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 23
 # THE SOFTWARE.
24 24
 
25
+from __future__ import absolute_import
26
+from __future__ import print_function
27
+
25 28
 from .arithmetic_decorators import add_operators
26 29
 
27 30
 def projected(t, axis):
... ...
@@ -39,7 +42,14 @@ def bin_operator(func):
39 42
         return Bin(self.low, self.high, func(self.value, other_value))
40 43
     return wrapped_bin_operator
41 44
 
42
-@add_operators(bin_operator)
45
+def bin_hash(abin):
46
+    """
47
+    Two bins are equal if they are compatible and have the same value,
48
+    that is if all the attributes low, high and value are the same
49
+    """
50
+    return hash((abin.low, abin.high, abin.value))
51
+
52
+@add_operators(bin_operator, bin_hash)
43 53
 class Bin(object):
44 54
     def __init__(self, *args):
45 55
         """
... ...
@@ -67,10 +77,28 @@ class Bin(object):
67 77
         self.ndim = len(self.low)
68 78
 
69 79
     def center(self):
70
-        return tuple(map(lambda (l, h) : 0.5 * (h + l), zip(self.low, self.high)))
80
+        """
81
+        Returns the center of the bin
82
+
83
+        Example
84
+        -------
85
+        >>> b = Bin([0., 0.], [10., 10.], 1.0)
86
+        >>> b.center()
87
+        (5.0, 5.0)
88
+        """
89
+        return tuple(map(lambda p : 0.5 * (p[1] + p[0]), zip(self.low, self.high)))
71 90
 
72 91
     def half_width(self):
73
-        return tuple(map(lambda (l, h) : 0.5 * (h - l), zip(self.low, self.high)))
92
+        """
93
+        Returns the half-width of the bin
94
+
95
+        Example
96
+        -------
97
+        >>> b = Bin([0., 0.], [20., 40.], 1.0)
98
+        >>> b.half_width()
99
+        (10.0, 20.0)
100
+        """
101
+        return tuple(map(lambda p : 0.5 * (p[1] - p[0]), zip(self.low, self.high)))
74 102
 
75 103
     def __str__(self):
76 104
         ranges = ", ".join("[{0} - {1}]".format(a,b) for a,b in zip(self.low, self.high))
... ...
@@ -111,19 +139,19 @@ class Bin(object):
111 139
         Example
112 140
         -------
113 141
         >>> b = Bin([0., 0.], [1., 1.], 1.0)
114
-        >>> print b
142
+        >>> print(b)
115 143
         [0.0 - 1.0], [0.0 - 1.0] = 1.0
116 144
 
117
-        >>> print (0.5, 0.5) in b
145
+        >>> (0.5, 0.5) in b
118 146
         True
119 147
 
120
-        >>> print (0.5, 1.0) in b
148
+        >>> (0.5, 1.0) in b
121 149
         True
122 150
 
123
-        >>> print (0.5, 1.1) in b
151
+        >>> (0.5, 1.1) in b
124 152
         False
125 153
 
126
-        >>> print (0.01, 0.99) in b
154
+        >>> (0.01, 0.99) in b
127 155
         True
128 156
         """
129 157
 
... ...
@@ -138,16 +166,16 @@ class Bin(object):
138 166
         Example
139 167
         -------
140 168
         >>> b = Bin([0., 0.], [1., 1.], 1.0)
141
-        >>> print b
169
+        >>> print(b)
142 170
         [0.0 - 1.0], [0.0 - 1.0] = 1.0
143 171
 
144
-        >>> print b.point_inside((0.5, 0.5))
172
+        >>> b.point_inside((0.5, 0.5))
145 173
         True
146 174
 
147
-        >>> print b.point_inside((0.5, 1.0))
175
+        >>> b.point_inside((0.5, 1.0))
148 176
         False
149 177
 
150
-        >>> print b.point_inside((1e-5, 0.9999))
178
+        >>> b.point_inside((1e-5, 0.9999))
151 179
         True
152 180
         """
153 181
 
... ...
@@ -164,25 +192,25 @@ class Bin(object):
164 192
         Example
165 193
         -------
166 194
         >>> b = Bin([0.], [1.], 1.0)
167
-        >>> print b
195
+        >>> print(b)
168 196
         [0.0 - 1.0] = 1.0
169 197
 
170 198
         >>> b1, b2 = b.split_on_axis(0, 0.25)
171
-        >>> print b1
199
+        >>> print(b1)
172 200
         [0.0 - 0.25] = 0.25
173 201
 
174
-        >>> print b2
202
+        >>> print(b2)
175 203
         [0.25 - 1.0] = 0.75
176 204
 
177 205
         >>> b = Bin([0., 0.], [1., 1.], 1.0)
178
-        >>> print b
206
+        >>> print(b)
179 207
         [0.0 - 1.0], [0.0 - 1.0] = 1.0
180 208
 
181 209
         >>> b1, b2 = b.split_on_axis(1, 0.25)
182
-        >>> print b1
210
+        >>> print(b1)
183 211
         [0.0 - 1.0], [0.0 - 0.25] = 0.25
184 212
 
185
-        >>> print b2
213
+        >>> print(b2)
186 214
         [0.0 - 1.0], [0.25 - 1.0] = 0.75
187 215
 
188 216
         """
... ...
@@ -233,20 +261,20 @@ class Bin(object):
233 261
         Example
234 262
         -------
235 263
         >>> b = Bin([0., 0.], [1., 1.], 1.0)
236
-        >>> print b
264
+        >>> print(b)
237 265
         [0.0 - 1.0], [0.0 - 1.0] = 1.0
238 266
 
239 267
         >>> b1, b2, b3, b4 = b.split([0.25, 0.25])
240
-        >>> print b1
268
+        >>> print(b1)
241 269
         [0.0 - 0.25], [0.0 - 0.25] = 0.0625
242 270
 
243
-        >>> print b2
271
+        >>> print(b2)
244 272
         [0.0 - 0.25], [0.25 - 1.0] = 0.1875
245 273
 
246
-        >>> print b3
274
+        >>> print(b3)
247 275
         [0.25 - 1.0], [0.0 - 0.25] = 0.1875
248 276
 
249
-        >>> print b4
277
+        >>> print(b4)
250 278
         [0.25 - 1.0], [0.25 - 1.0] = 0.5625
251 279
 
252 280
         >>> b.split([0.25, 1.5])
Browse code

Slight rewrite to never allow overlapping bins

Also, comparsion functions for Bins and BinnedObjects

Lorenz Hüdepohl authored on03/12/2014 07:13:40
Showing1 changed files
... ...
@@ -86,6 +86,21 @@ class Bin(object):
86 86
             return False
87 87
         return True
88 88
 
89
+    def __eq__(self, other):
90
+        if not self.ndim == other.ndim:
91
+            raise TypeError("Cannot compare bins with different dimensions!")
92
+        if not self.compatible(other):
93
+            return False
94
+        return self.value == other.value
95
+
96
+    def __bool__(self):
97
+        # Python 3
98
+        return bool(self.value)
99
+
100
+    def __nonzero__(self):
101
+        # Python 2
102
+        return self.__bool__()
103
+
89 104
     def projected(self, axis):
90 105
         return Bin(projected(self.low, axis), projected(self.high, axis), self.value)
91 106
 
... ...
@@ -280,6 +295,43 @@ class Bin(object):
280 295
                 return False
281 296
         return True
282 297
 
298
+    def overlap_fraction(self, other):
299
+        """
300
+        Return the volume ratio between the overlap of self and other and self itself.
301
+
302
+        Examples
303
+        --------
304
+
305
+        >>> bin1 = Bin([0.], [1.0], 0.5)
306
+        >>> bin2 = Bin([0.5], [4.5], 1.)
307
+        >>> bin3 = Bin([1.0], [2.0], 1.)
308
+        >>> bin1.overlap_fraction(bin2)
309
+        0.5
310
+        >>> bin2.overlap_fraction(bin1)
311
+        0.125
312
+        >>> bin3.overlap_fraction(bin1)
313
+        0.0
314
+        """
315
+        if not self.ndim == other.ndim:
316
+            raise Exception("Cannot operate on Bins with different dimension!")
317
+
318
+        def clip(a):
319
+            if a < self.low[n]:
320
+                return self.low[n]
321
+            elif self.low[n] <= a <= self.high[n]:
322
+                return a
323
+            elif self.high[n] < a:
324
+                return self.high[n]
325
+            else:
326
+                raise Exception("Tertium datur")
327
+
328
+        r = 1.0
329
+
330
+        for n in range(self.ndim):
331
+            r *= (clip(other.high[n]) - clip(other.low[n])) / (self.high[n] - self.low[n])
332
+
333
+        return r
334
+
283 335
     def overlap_points(self, other):
284 336
         """
285 337
         Return the edge points of the intersection hypercube
Browse code

Eliminate O(n^2) brute-force loops

Lorenz Hüdepohl authored on22/10/2014 16:57:45
Showing1 changed files
... ...
@@ -41,7 +41,19 @@ def bin_operator(func):
41 41
 
42 42
 @add_operators(bin_operator)
43 43
 class Bin(object):
44
-    def __init__(self, low, high, value):
44
+    def __init__(self, *args):
45
+        """
46
+        Construct a new bin
47
+
48
+          bin = Bin(low, high, value)
49
+          bin = Bin(otherbin)
50
+
51
+        """
52
+        if len(args) == 1:
53
+            otherbin, = args
54
+            low, high, value = otherbin.low, otherbin.high, otherbin.value
55
+        elif len(args) == 3:
56
+            low, high, value = args
45 57
 
46 58
         if not len(low) == len(high):
47 59
             raise Exception("len(low) != len(high)")
... ...
@@ -168,9 +180,12 @@ class Bin(object):
168 180
 
169 181
         if 0 < l1 < 1e-7 or 0 < l2 < 1e-7:
170 182
             import warnings
171
-            warnings.warn(("Split into extremely degenerate bins along dimension {0}, check your bin boundaries:\n" +
172
-                          "(low, split, end)\n  {1!r}, {2!r}, {3!r})\n").format(
173
-                                axis, self.low[axis], coord, self.high[axis]),
183
+            warnings.warn(("Split into extremely degenerate bins along dimension {0}, "
184
+                           "check your bin boundaries:\n"
185
+                           "  {1:20} {2:20} {3:20}\n  {4!r:20} {5!r:20} {6!r:20}\n").format(
186
+                                axis,
187
+                                "left", "split", "right",
188
+                                self.low[axis], coord, self.high[axis]),
174 189
                           stacklevel=2)
175 190
 
176 191
         if not abs(l1 + l2 - 1.0) < 1e-11:
Browse code

Store bins in SortedLists

The bins are stored in various automatically sorted lists,
once sorted lexicographically over their central point, and
for each dimension along that coordinate for both the low and
high value

Lorenz Hüdepohl authored on09/10/2014 23:16:50
Showing1 changed files
... ...
@@ -55,12 +55,10 @@ class Bin(object):
55 55
         self.ndim = len(self.low)
56 56
 
57 57
     def center(self):
58
-        from numpy import asarray
59
-        return tuple(0.5 * (asarray(self.low) + asarray(self.high)))
58
+        return tuple(map(lambda (l, h) : 0.5 * (h + l), zip(self.low, self.high)))
60 59
 
61 60
     def half_width(self):
62
-        from numpy import asarray
63
-        return tuple(0.5 * (asarray(self.high) - asarray(self.low)))
61
+        return tuple(map(lambda (l, h) : 0.5 * (h - l), zip(self.low, self.high)))
64 62
 
65 63
     def __str__(self):
66 64
         ranges = ", ".join("[{0} - {1}]".format(a,b) for a,b in zip(self.low, self.high))
... ...
@@ -106,6 +104,30 @@ class Bin(object):
106 104
             raise Exception("Must specify a {0}-dimensional point for this bin!".format(self.ndim))
107 105
         return all(l <= point[n] <= h for (n, (l,h)) in enumerate(zip(self.low, self.high)))
108 106
 
107
+    def point_inside(self, point):
108
+        """
109
+        Returns True if point is strictly inside of the bin's domain
110
+
111
+        Example
112
+        -------
113
+        >>> b = Bin([0., 0.], [1., 1.], 1.0)
114
+        >>> print b
115
+        [0.0 - 1.0], [0.0 - 1.0] = 1.0
116
+
117
+        >>> print b.point_inside((0.5, 0.5))
118
+        True
119
+
120
+        >>> print b.point_inside((0.5, 1.0))
121
+        False
122
+
123
+        >>> print b.point_inside((1e-5, 0.9999))
124
+        True
125
+        """
126
+
127
+        if not len(point) == self.ndim:
128
+            raise Exception("Must specify a {0}-dimensional point for this bin!".format(self.ndim))
129
+        return all(l < point[n] < h for (n, (l,h)) in enumerate(zip(self.low, self.high)))
130
+
109 131
     def split_on_axis(self, axis, coord):
110 132
         """
111 133
         Split-up a bin into two, along given value "coord" on axis "axis".
Browse code

Big cleanup, new names, new layout, remove cruft

Lorenz Hüdepohl authored on17/09/2014 21:18:38
Showing1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,257 @@
1
+# coding=utf-8
2
+#
3
+# The MIT License (MIT)
4
+#
5
+# Copyright (c) 2014 Lorenz Hüdepohl, Emmanuel Stamou
6
+#
7
+# Permission is hereby granted, free of charge, to any person obtaining a copy
8
+# of this software and associated documentation files (the "Software"), to deal
9
+# in the Software without restriction, including without limitation the rights
10
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+# copies of the Software, and to permit persons to whom the Software is
12
+# furnished to do so, subject to the following conditions:
13
+#
14
+# The above copyright notice and this permission notice shall be included in
15
+# all copies or substantial portions of the Software.
16
+#
17
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+# THE SOFTWARE.
24
+
25
+from .arithmetic_decorators import add_operators
26
+
27
+def projected(t, axis):
28
+    t = tuple(t)
29
+    return t[:axis] + t[axis+1:]
30
+
31
+def bin_operator(func):
32
+    def wrapped_bin_operator(self, other):
33
+        if hasattr(other, "low"):
34
+            if not self.compatible(other):
35
+                raise Exception("Bins not compatible")
36
+            other_value = other.value
37
+        else:
38
+            other_value = other
39
+        return Bin(self.low, self.high, func(self.value, other_value))
40
+    return wrapped_bin_operator
41
+
42
+@add_operators(bin_operator)
43
+class Bin(object):
44
+    def __init__(self, low, high, value):
45
+
46
+        if not len(low) == len(high):
47
+            raise Exception("len(low) != len(high)")
48
+
49
+        if not all(l < h for l, h in zip(low, high)):
50
+            raise Exception("low < high not fullfilled")
51
+
52
+        self.low = tuple(low)
53
+        self.high = tuple(high)
54
+        self.value = value
55
+        self.ndim = len(self.low)
56
+
57
+    def center(self):
58
+        from numpy import asarray
59
+        return tuple(0.5 * (asarray(self.low) + asarray(self.high)))
60
+
61
+    def half_width(self):
62
+        from numpy import asarray
63
+        return tuple(0.5 * (asarray(self.high) - asarray(self.low)))
64
+
65
+    def __str__(self):
66
+        ranges = ", ".join("[{0} - {1}]".format(a,b) for a,b in zip(self.low, self.high))
67
+        return "{0} = {1}".format(ranges, self.value)
68
+
69
+    def __repr__(self):
70
+        return "Bin({0.low}, {0.high}, {0.value})".format(self)
71
+
72
+    def compatible(self, other):
73
+        if self.low != other.low:
74
+            return False
75
+        if self.high != other.high:
76
+            return False
77
+        return True
78
+
79
+    def projected(self, axis):
80
+        return Bin(projected(self.low, axis), projected(self.high, axis), self.value)
81
+
82
+    def __contains__(self, point):
83
+        """
84
+        Returns True if point is inside or on the surface of the bin's domain
85
+
86
+        Example
87
+        -------
88
+        >>> b = Bin([0., 0.], [1., 1.], 1.0)
89
+        >>> print b
90
+        [0.0 - 1.0], [0.0 - 1.0] = 1.0
91
+
92
+        >>> print (0.5, 0.5) in b
93
+        True
94
+
95
+        >>> print (0.5, 1.0) in b
96
+        True
97
+
98
+        >>> print (0.5, 1.1) in b
99
+        False
100
+
101
+        >>> print (0.01, 0.99) in b
102
+        True
103
+        """
104
+
105
+        if not len(point) == self.ndim:
106
+            raise Exception("Must specify a {0}-dimensional point for this bin!".format(self.ndim))
107
+        return all(l <= point[n] <= h for (n, (l,h)) in enumerate(zip(self.low, self.high)))
108
+
109
+    def split_on_axis(self, axis, coord):
110
+        """
111
+        Split-up a bin into two, along given value "coord" on axis "axis".
112
+
113
+        New bin values are obtained as volume-weighted fractions of the original value
114
+
115
+        Example
116
+        -------
117
+        >>> b = Bin([0.], [1.], 1.0)
118
+        >>> print b
119
+        [0.0 - 1.0] = 1.0
120
+
121
+        >>> b1, b2 = b.split_on_axis(0, 0.25)
122
+        >>> print b1
123
+        [0.0 - 0.25] = 0.25
124
+
125
+        >>> print b2
126
+        [0.25 - 1.0] = 0.75
127
+
128
+        >>> b = Bin([0., 0.], [1., 1.], 1.0)
129
+        >>> print b
130
+        [0.0 - 1.0], [0.0 - 1.0] = 1.0
131
+
132
+        >>> b1, b2 = b.split_on_axis(1, 0.25)
133
+        >>> print b1
134
+        [0.0 - 1.0], [0.0 - 0.25] = 0.25
135
+
136
+        >>> print b2
137
+        [0.0 - 1.0], [0.25 - 1.0] = 0.75
138
+
139
+        """
140
+        if not self.low[axis] <= coord <= self.high[axis]:
141
+            raise Exception("Value {0} is outside bin in axis {1}".format(coord, axis))
142
+
143
+        w = self.high[axis] - self.low[axis]
144
+        l1 = (coord -  self.low[axis]) / w
145
+        l2 = (self.high[axis] - coord) / w
146
+
147
+        if 0 < l1 < 1e-7 or 0 < l2 < 1e-7:
148
+            import warnings
149
+            warnings.warn(("Split into extremely degenerate bins along dimension {0}, check your bin boundaries:\n" +
150
+                          "(low, split, end)\n  {1!r}, {2!r}, {3!r})\n").format(
151
+                                axis, self.low[axis], coord, self.high[axis]),
152
+                          stacklevel=2)
153
+
154
+        if not abs(l1 + l2 - 1.0) < 1e-11:
155
+            raise Exception("Something wrong here")
156
+
157
+        v1 = float(l1) * self.value
158
+        v2 = float(l2) * self.value
159
+
160
+        if l1 > 0:
161
+            b1 = Bin(self.low,
162
+                     self.high[:axis] + (coord,) + self.high[axis+1:],
163
+                     v1)
164
+        if l2 > 0:
165
+            b2 = Bin(self.low[:axis] + (coord,) + self.low[axis+1:],
166
+                     self.high,
167
+                     v2)
168
+        if l1 > 0 and l2 > 0:
169
+            return b1, b2
170
+        elif l1 == 1.0 or l2 == 1.0:
171
+            return self,
172
+        else:
173
+            raise Exception("Something wrong in split_in_axis")
174
+
175
+    def split(self, point):
176
+        """
177
+        Divide a bin into 2^self.ndim bins around the given point within the bin
178
+
179
+        New bin values are obtained as volume-weighted fractions of the original value
180
+
181
+        Example
182
+        -------
183
+        >>> b = Bin([0., 0.], [1., 1.], 1.0)
184
+        >>> print b
185
+        [0.0 - 1.0], [0.0 - 1.0] = 1.0
186
+
187
+        >>> b1, b2, b3, b4 = b.split([0.25, 0.25])
188
+        >>> print b1
189
+        [0.0 - 0.25], [0.0 - 0.25] = 0.0625
190
+
191
+        >>> print b2
192
+        [0.0 - 0.25], [0.25 - 1.0] = 0.1875
193
+
194
+        >>> print b3
195
+        [0.25 - 1.0], [0.0 - 0.25] = 0.1875
196
+
197
+        >>> print b4
198
+        [0.25 - 1.0], [0.25 - 1.0] = 0.5625
199
+
200
+        >>> b.split([0.25, 1.5])
201
+        Traceback (most recent call last):
202
+          ...
203
+        Exception: Point not contained in bin!
204
+        """
205
+        if not point in self:
206
+            raise Exception("Point not contained in bin!".format(self.ndim))
207
+
208
+        subbins = [self]
209
+        for (axis, coord) in enumerate(point):
210
+            new_subbins = []
211
+            for bin in subbins:
212
+                new_subbins.extend(bin.split_on_axis(axis, coord))
213
+            subbins = new_subbins
214
+
215
+        return tuple(subbins)
216
+
217
+    def vertices(self):
218
+        from itertools import product
219
+        return tuple(product(*zip(self.low, self.high)))
220
+
221
+    def volume(self):
222
+        from operator import mul
223
+        return Bin(self.low, self.high, reduce(mul, (h - l for (l,h) in zip(self.low, self.high))))
224
+
225
+    def overlaps(self, other):
226
+        """
227
+        Return true if the domain of self overlaps with the domain of other
228
+
229
+        Example
230
+        -------
231
+        >>> bin1 = Bin([0.], [0.25], 0.25)
232
+        >>> bin2 = Bin([0.25], [1.0], 0.75)
233
+        >>> bin3 = Bin([0.0], [1.0], 1.0)
234
+        >>> bin1.overlaps(bin2)
235
+        False
236
+        >>> bin1.overlaps(bin3)
237
+        True
238
+        """
239
+        if not self.ndim == other.ndim:
240
+            raise Exception("Cannot operate on Bins with different dimension!")
241
+        for n in range(self.ndim):
242
+            if not(self.low[n] <= other.low[n] < self.high[n] or other.low[n] <= self.low[n] < other.high[n]):
243
+                return False
244
+        return True
245
+
246
+    def overlap_points(self, other):
247
+        """
248
+        Return the edge points of the intersection hypercube
249
+        of self and other -- provided self and other overlap --
250
+        that are not coincident with both an edge point of self
251
+        and other at the same time
252
+        """
253
+        from itertools import product
254
+        vertices = set(self.vertices()).intersection(set(other.vertices()))
255
+        low  = map(max, zip(self.low, other.low))
256
+        high = map(min, zip(self.high, other.high))
257
+        return filter(lambda p : p not in vertices, product(*zip(low, high)))