polytope-python 1.0.31__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. polytope_feature/__init__.py +1 -0
  2. polytope_feature/datacube/__init__.py +1 -0
  3. polytope_feature/datacube/backends/__init__.py +1 -0
  4. polytope_feature/datacube/backends/datacube.py +171 -0
  5. polytope_feature/datacube/backends/fdb.py +399 -0
  6. polytope_feature/datacube/backends/mock.py +71 -0
  7. polytope_feature/datacube/backends/xarray.py +142 -0
  8. polytope_feature/datacube/datacube_axis.py +332 -0
  9. polytope_feature/datacube/index_tree_pb2.py +27 -0
  10. polytope_feature/datacube/tensor_index_tree.py +228 -0
  11. polytope_feature/datacube/transformations/__init__.py +1 -0
  12. polytope_feature/datacube/transformations/datacube_cyclic/__init__.py +1 -0
  13. polytope_feature/datacube/transformations/datacube_cyclic/datacube_cyclic.py +171 -0
  14. polytope_feature/datacube/transformations/datacube_mappers/__init__.py +1 -0
  15. polytope_feature/datacube/transformations/datacube_mappers/datacube_mappers.py +141 -0
  16. polytope_feature/datacube/transformations/datacube_mappers/mapper_types/__init__.py +5 -0
  17. polytope_feature/datacube/transformations/datacube_mappers/mapper_types/healpix.py +147 -0
  18. polytope_feature/datacube/transformations/datacube_mappers/mapper_types/healpix_nested.py +229 -0
  19. polytope_feature/datacube/transformations/datacube_mappers/mapper_types/local_regular.py +95 -0
  20. polytope_feature/datacube/transformations/datacube_mappers/mapper_types/octahedral.py +7896 -0
  21. polytope_feature/datacube/transformations/datacube_mappers/mapper_types/reduced_gaussian.py +1459 -0
  22. polytope_feature/datacube/transformations/datacube_mappers/mapper_types/reduced_ll.py +5128 -0
  23. polytope_feature/datacube/transformations/datacube_mappers/mapper_types/regular.py +75 -0
  24. polytope_feature/datacube/transformations/datacube_merger/__init__.py +1 -0
  25. polytope_feature/datacube/transformations/datacube_merger/datacube_merger.py +95 -0
  26. polytope_feature/datacube/transformations/datacube_reverse/__init__.py +1 -0
  27. polytope_feature/datacube/transformations/datacube_reverse/datacube_reverse.py +65 -0
  28. polytope_feature/datacube/transformations/datacube_transformations.py +96 -0
  29. polytope_feature/datacube/transformations/datacube_type_change/__init__.py +1 -0
  30. polytope_feature/datacube/transformations/datacube_type_change/datacube_type_change.py +124 -0
  31. polytope_feature/datacube/tree_encoding.py +132 -0
  32. polytope_feature/engine/__init__.py +1 -0
  33. polytope_feature/engine/engine.py +19 -0
  34. polytope_feature/engine/hullslicer.py +316 -0
  35. polytope_feature/options.py +77 -0
  36. polytope_feature/polytope.py +71 -0
  37. polytope_feature/shapes.py +405 -0
  38. polytope_feature/utility/__init__.py +0 -0
  39. polytope_feature/utility/combinatorics.py +48 -0
  40. polytope_feature/utility/exceptions.py +45 -0
  41. polytope_feature/utility/geometry.py +26 -0
  42. polytope_feature/utility/list_tools.py +41 -0
  43. polytope_feature/utility/profiling.py +14 -0
  44. polytope_feature/version.py +1 -0
  45. polytope_python-1.0.31.dist-info/LICENSE +201 -0
  46. polytope_python-1.0.31.dist-info/METADATA +21 -0
  47. polytope_python-1.0.31.dist-info/RECORD +49 -0
  48. polytope_python-1.0.31.dist-info/WHEEL +5 -0
  49. polytope_python-1.0.31.dist-info/top_level.txt +1 -0
@@ -0,0 +1,405 @@
1
+ import copy
2
+ import math
3
+ import warnings
4
+ from abc import ABC, abstractmethod
5
+ from typing import List
6
+
7
+ import tripy
8
+
9
+ from .utility.list_tools import unique
10
+
11
+ """
12
+ Shapes used for the constructive geometry API of Polytope
13
+ """
14
+
15
+
16
+ class Shape(ABC):
17
+ """Represents a multi-axis shape to be expanded"""
18
+
19
+ @abstractmethod
20
+ def polytope(self):
21
+ raise NotImplementedError()
22
+
23
+ @abstractmethod
24
+ def axes(self) -> List[str]:
25
+ raise NotImplementedError()
26
+
27
+
28
+ class ConvexPolytope(Shape):
29
+ def __init__(self, axes, points, method=None, is_orthogonal=False):
30
+ self._axes = list(axes)
31
+ self.is_flat = False
32
+ if len(self._axes) == 1 and len(points) == 1:
33
+ self.is_flat = True
34
+ self.points = points
35
+ self.method = method
36
+ self.is_orthogonal = is_orthogonal
37
+ self.is_in_union = False
38
+
39
+ def add_to_union(self):
40
+ self.is_in_union = True
41
+
42
+ def extents(self, axis):
43
+ if self.is_flat:
44
+ slice_axis_idx = 0
45
+ lower = min(self.points)[0]
46
+ upper = max(self.points)[0]
47
+ else:
48
+ slice_axis_idx = self.axes().index(axis)
49
+ axis_values = [point[slice_axis_idx] for point in self.points]
50
+ lower = min(axis_values)
51
+ upper = max(axis_values)
52
+ return (lower, upper, slice_axis_idx)
53
+
54
+ def __str__(self):
55
+ return f"Polytope in {self.axes()} with points {self.points}"
56
+
57
+ def axes(self):
58
+ return self._axes
59
+
60
+ def polytope(self):
61
+ return [self]
62
+
63
+
64
+ # This is the only shape which can slice on axes without a discretizer or interpolator
65
+ class Select(Shape):
66
+ """Matches several discrete values"""
67
+
68
+ def __init__(self, axis, values, method=None):
69
+ self.axis = axis
70
+ self.values = unique(values)
71
+ if len(self.values) != len(values):
72
+ warnings.warn("Duplicate request values were removed")
73
+ self.method = method
74
+
75
+ def axes(self):
76
+ return [self.axis]
77
+
78
+ def polytope(self):
79
+ return [ConvexPolytope([self.axis], [[v]], self.method, is_orthogonal=True) for v in self.values]
80
+
81
+ def __repr__(self):
82
+ return f"Select in {self.axis} with points {self.values}"
83
+
84
+
85
+ class Point(Shape):
86
+ """Matches several discrete value"""
87
+
88
+ def __init__(self, axes, values, method=None):
89
+ self._axes = axes
90
+ self.values = values
91
+ self.method = method
92
+ self.polytopes = []
93
+ if method == "nearest":
94
+ assert len(self.values) == 1
95
+ for i in range(len(axes)):
96
+ polytope_points = [v[i] for v in self.values]
97
+ self.polytopes.extend(
98
+ [ConvexPolytope([axes[i]], [[point]], self.method, is_orthogonal=True) for point in polytope_points]
99
+ )
100
+
101
+ def axes(self):
102
+ return self._axes
103
+
104
+ def polytope(self):
105
+ return self.polytopes
106
+
107
+ def __repr__(self):
108
+ return f"Point in {self._axes} with points {self.values}"
109
+
110
+
111
+ class Span(Shape):
112
+ """1-D range along a single axis"""
113
+
114
+ def __init__(self, axis, lower=-math.inf, upper=math.inf):
115
+ assert not isinstance(lower, list)
116
+ assert not isinstance(upper, list)
117
+ self.axis = axis
118
+ self.lower = lower
119
+ self.upper = upper
120
+
121
+ def axes(self):
122
+ return [self.axis]
123
+
124
+ def polytope(self):
125
+ return [ConvexPolytope([self.axis], [[self.lower], [self.upper]], is_orthogonal=True)]
126
+
127
+ def __repr__(self):
128
+ return f"Span in {self.axis} with range from {self.lower} to {self.upper}"
129
+
130
+
131
+ class All(Span):
132
+ """Matches all indices in an axis"""
133
+
134
+ def __init__(self, axis):
135
+ super().__init__(axis)
136
+
137
+ def __repr__(self):
138
+ return f"All in {self.axis}"
139
+
140
+
141
+ class Box(Shape):
142
+ """N-D axis-aligned bounding box (AABB), specified by two opposite corners"""
143
+
144
+ def __init__(self, axes, lower_corner=None, upper_corner=None):
145
+ dimension = len(axes)
146
+ self._lower_corner = lower_corner
147
+ self._upper_corner = upper_corner
148
+ self._axes = axes
149
+ assert len(lower_corner) == dimension
150
+ assert len(upper_corner) == dimension
151
+
152
+ if lower_corner is None:
153
+ lower_corner = [-math.inf] * dimension
154
+ if upper_corner is None:
155
+ upper_corner = [math.inf] * dimension
156
+
157
+ # Build every vertex of the box from the two extremes
158
+ # ... take a binary representation of hypercube vertices
159
+ # ... [00, 01, 10, 11] in 2D
160
+ # ... take lower/upper corner for each 0/1
161
+ self.vertices = []
162
+ for i in range(0, 2**dimension):
163
+ vertex = copy.copy(lower_corner)
164
+ for d in range(0, dimension):
165
+ if i >> d & 1:
166
+ vertex[d] = upper_corner[d]
167
+ self.vertices.append(vertex)
168
+ assert lower_corner in self.vertices
169
+ assert upper_corner in self.vertices
170
+ assert len(self.vertices) == 2**dimension
171
+
172
+ def axes(self):
173
+ return self._axes
174
+
175
+ def polytope(self):
176
+ return [ConvexPolytope(self.axes(), self.vertices, is_orthogonal=True)]
177
+
178
+ def __repr__(self):
179
+ return f"Box in {self._axes} with with lower corner {self._lower_corner} and upper corner{self._upper_corner}"
180
+
181
+
182
+ class Disk(Shape):
183
+ """2-D shape bounded by an ellipse"""
184
+
185
+ # NB radius is two dimensional
186
+ # NB number of segments is hard-coded, not exposed to user
187
+
188
+ def __init__(self, axes, centre=[0, 0], radius=[1, 1]):
189
+ self._axes = axes
190
+ self.centre = centre
191
+ self.radius = radius
192
+ self.segments = 12
193
+
194
+ assert len(axes) == 2
195
+ assert len(centre) == 2
196
+ assert len(radius) == 2
197
+
198
+ expanded_radius = self._expansion_to_circumscribe_circle(self.segments)
199
+ self.points = self._points_on_circle(self.segments, expanded_radius)
200
+
201
+ for i in range(0, len(self.points)):
202
+ x = centre[0] + self.points[i][0] * radius[0]
203
+ y = centre[1] + self.points[i][1] * radius[1]
204
+ self.points[i] = [x, y]
205
+
206
+ def _points_on_circle(self, n, r):
207
+ return [[math.cos(2 * math.pi / n * x) * r, math.sin(2 * math.pi / n * x) * r] for x in range(0, n)]
208
+
209
+ def _expansion_to_circumscribe_circle(self, n):
210
+ half_angle_between_segments = math.pi / n
211
+ return 1 / math.cos(half_angle_between_segments)
212
+
213
+ def axes(self):
214
+ return self._axes
215
+
216
+ def polytope(self):
217
+ return [ConvexPolytope(self.axes(), self.points)]
218
+
219
+ def __repr__(self):
220
+ return f"Disk in {self._axes} with centred at {self.centre} and with radius {self.radius}"
221
+
222
+
223
+ class Ellipsoid(Shape):
224
+ # Here we use the formula for the inscribed circle in an icosahedron
225
+ # See https://en.wikipedia.org/wiki/Platonic_solid
226
+
227
+ def __init__(self, axes, centre=[0, 0, 0], radius=[1, 1, 1]):
228
+ self._axes = axes
229
+ self.centre = centre
230
+ self.radius = radius
231
+
232
+ assert len(axes) == 3
233
+ assert len(centre) == 3
234
+ assert len(radius) == 3
235
+
236
+ expanded_radius = self._icosahedron_edge_length_coeff()
237
+ self.points = self._points_on_icosahedron(expanded_radius)
238
+
239
+ for i in range(0, len(self.points)):
240
+ x = centre[0] + self.points[i][0] * radius[0]
241
+ y = centre[1] + self.points[i][1] * radius[1]
242
+ z = centre[2] + self.points[i][2] * radius[2]
243
+ self.points[i] = [x, y, z]
244
+
245
+ def axes(self):
246
+ return self._axes
247
+
248
+ def _points_on_icosahedron(self, coeff):
249
+ golden_ratio = (1 + math.sqrt(5)) / 2
250
+ return [
251
+ [0, coeff / 2, coeff * golden_ratio / 2],
252
+ [0, coeff / 2, -coeff * golden_ratio / 2],
253
+ [0, -coeff / 2, coeff * golden_ratio / 2],
254
+ [0, -coeff / 2, -coeff * golden_ratio / 2],
255
+ [coeff / 2, coeff * golden_ratio / 2, 0],
256
+ [coeff / 2, -coeff * golden_ratio / 2, 0],
257
+ [-coeff / 2, coeff * golden_ratio / 2, 0],
258
+ [-coeff / 2, -coeff * golden_ratio / 2, 0],
259
+ [coeff * golden_ratio / 2, 0, coeff / 2],
260
+ [coeff * golden_ratio / 2, 0, -coeff / 2],
261
+ [-coeff * golden_ratio / 2, 0, coeff / 2],
262
+ [-coeff * golden_ratio / 2, 0, -coeff / 2],
263
+ ]
264
+
265
+ def _icosahedron_edge_length_coeff(self):
266
+ # theta is the dihedral angle for an icosahedron here
267
+ theta = (138.19 / 180) * math.pi
268
+ edge_length = (2 / math.tan(theta / 2)) * math.tan(math.pi / 3)
269
+ return edge_length
270
+
271
+ def polytope(self):
272
+ return [ConvexPolytope(self.axes(), self.points)]
273
+
274
+ def __repr__(self):
275
+ return f"Ellipsoid in {self._axes} with centred at {self.centre} and with radius {self.radius}"
276
+
277
+
278
+ class PathSegment(Shape):
279
+ """N-D polytope defined by a shape which is swept along a straight line between two points"""
280
+
281
+ def __init__(self, axes, shape: Shape, start: List, end: List):
282
+ self._axes = axes
283
+ self._start = start
284
+ self._end = end
285
+ self._shape = shape
286
+
287
+ assert shape.axes() == self.axes()
288
+ assert len(start) == len(self.axes())
289
+ assert len(end) == len(self.axes())
290
+
291
+ self.polytopes = []
292
+ for polytope in shape.polytope():
293
+ # We can generate a polytope by taking all the start points and all the end points and passing them
294
+ # to the polytope constructor as-is. This is not a convex hull, there will inevitably be interior points.
295
+ # This is currently OK, because we first determine the min/max on each axis before slicing.
296
+
297
+ points = []
298
+ for p in polytope.points:
299
+ points.append([a + b for a, b in zip(p, start)])
300
+ points.append([a + b for a, b in zip(p, end)])
301
+ poly = ConvexPolytope(self.axes(), points)
302
+ poly.add_to_union()
303
+ self.polytopes.append(poly)
304
+
305
+ def axes(self):
306
+ return self._axes
307
+
308
+ def polytope(self):
309
+ return self.polytopes
310
+
311
+ def __repr__(self):
312
+ return f"PathSegment in {self._axes} obtained by sweeping a {self._shape.__repr__()} \
313
+ between the points {self._start} and {self._end}"
314
+
315
+
316
+ class Path(Shape):
317
+ """N-D polytope defined by a shape which is swept along a polyline defined by multiple points"""
318
+
319
+ def __init__(self, axes, shape, *points, closed=False):
320
+ self._axes = axes
321
+ self._shape = shape
322
+ self._points = points
323
+
324
+ assert shape.axes() == self.axes()
325
+ for p in points:
326
+ assert len(p) == len(self.axes())
327
+
328
+ path_segments = []
329
+
330
+ for i in range(0, len(points) - 1):
331
+ path_segments.append(PathSegment(axes, shape, points[i], points[i + 1]))
332
+
333
+ if closed:
334
+ path_segments.append(PathSegment(axes, shape, points[-1], points[0]))
335
+
336
+ self.union = Union(self.axes(), *path_segments)
337
+
338
+ def axes(self):
339
+ return self._axes
340
+
341
+ def polytope(self):
342
+ return self.union.polytope()
343
+
344
+ def __repr__(self):
345
+ return f"Path in {self._axes} obtained by sweeping a {self._shape.__repr__()} \
346
+ between the points {self._points}"
347
+
348
+
349
+ class Union(Shape):
350
+ """N-D union of two shapes with the same axes"""
351
+
352
+ def __init__(self, axes, *shapes):
353
+ self._axes = axes
354
+ for s in shapes:
355
+ assert s.axes() == self.axes()
356
+
357
+ self.polytopes = []
358
+ self._shapes = shapes
359
+
360
+ for s in shapes:
361
+ for poly in s.polytope():
362
+ poly.add_to_union()
363
+ self.polytopes.append(poly)
364
+
365
+ def axes(self):
366
+ return self._axes
367
+
368
+ def polytope(self):
369
+ return self.polytopes
370
+
371
+ def __repr__(self):
372
+ return f"Union in {self._axes} of the shapes {self._shapes}"
373
+
374
+
375
+ class Polygon(Shape):
376
+ """2-D polygon defined by a set of exterior points"""
377
+
378
+ def __init__(self, axes, points):
379
+ self._axes = axes
380
+ assert len(axes) == 2
381
+ for p in points:
382
+ assert len(p) == 2
383
+
384
+ self._points = points
385
+ triangles = tripy.earclip(points)
386
+ self.polytopes = []
387
+
388
+ if len(points) > 0 and len(triangles) == 0:
389
+ self.polytopes = [ConvexPolytope(self.axes(), points)]
390
+
391
+ else:
392
+ for t in triangles:
393
+ tri_points = [list(point) for point in t]
394
+ poly = ConvexPolytope(self.axes(), tri_points)
395
+ poly.add_to_union()
396
+ self.polytopes.append(poly)
397
+
398
+ def axes(self):
399
+ return self._axes
400
+
401
+ def polytope(self):
402
+ return self.polytopes
403
+
404
+ def __repr__(self):
405
+ return f"Polygon in {self._axes} with points {self._points}"
File without changes
@@ -0,0 +1,48 @@
1
+ import itertools
2
+ from collections import Counter
3
+ from typing import List
4
+
5
+ from ..shapes import ConvexPolytope
6
+ from .exceptions import AxisNotFoundError, AxisOverdefinedError, AxisUnderdefinedError
7
+
8
+
9
+ def group(polytopes: List[ConvexPolytope]):
10
+ # Group polytopes into polytopes which share the same axes
11
+ # If the polytopes are orthogonal and not in a union, we first group them together into an additional list
12
+ # so we can treat them together as a single object
13
+ groups = {}
14
+ for p in polytopes:
15
+ if p.is_orthogonal and not p.is_in_union:
16
+ groups.setdefault(tuple(sorted(p.axes())), [[]])[0].append(p)
17
+ else:
18
+ groups.setdefault(tuple(sorted(p.axes())), []).append(p)
19
+ concatenation = []
20
+ for other_group in list(groups.keys()):
21
+ for key in other_group:
22
+ concatenation.append(key)
23
+ return groups, concatenation
24
+
25
+
26
+ def tensor_product(groups):
27
+ # Compute the tensor product of polytope groups
28
+ return list(itertools.product(*groups.values()))
29
+
30
+
31
+ def validate_axes(actual_axes, test_axes):
32
+ # Each axis should be defined only once
33
+ count = Counter(test_axes)
34
+ axes = list(count.keys())
35
+ counts = [count[key] for key in axes]
36
+ for c in zip(axes, counts):
37
+ if c[1] > 1:
38
+ raise AxisOverdefinedError(c[0])
39
+
40
+ # Check for missing axes
41
+ for ax in set(actual_axes).difference(set(test_axes)):
42
+ raise AxisUnderdefinedError(ax)
43
+
44
+ # Check for too many axes
45
+ for ax in set(test_axes).difference(set(actual_axes)):
46
+ raise AxisNotFoundError(ax)
47
+
48
+ return True
@@ -0,0 +1,45 @@
1
+ class PolytopeError(Exception):
2
+ pass
3
+
4
+
5
+ class BadRequestError(PolytopeError):
6
+ def __init__(self, pre_path):
7
+ self.pre_path = pre_path
8
+ self.message = f"No data for {pre_path} is available on the FDB."
9
+
10
+
11
+ class AxisOverdefinedError(PolytopeError, KeyError):
12
+ def __init__(self, axis):
13
+ self.axis = axis
14
+ self.message = (
15
+ f"Axis {axis} is overdefined. You have used it in two or more input polytopes which"
16
+ f" cannot form a union (because they span different axes)."
17
+ )
18
+
19
+
20
+ class AxisUnderdefinedError(PolytopeError, KeyError):
21
+ def __init__(self, axis):
22
+ self.axis = axis
23
+ self.message = f"Axis {axis} is underdefined. It does not appear in any input polytope."
24
+
25
+
26
+ class AxisNotFoundError(PolytopeError, KeyError):
27
+ def __init__(self, axis):
28
+ self.axis = axis
29
+ self.message = f"Axis {axis} does not exist in the datacube."
30
+
31
+
32
+ class UnsliceableShapeError(PolytopeError, KeyError):
33
+ def __init__(self, axis):
34
+ self.axis = axis
35
+ self.message = f"Higher-dimensional shape does not support unsliceable axis {axis.name}."
36
+
37
+
38
+ class BadGridError(PolytopeError, ValueError):
39
+ def __init__(self):
40
+ self.message = "Data on this grid is not supported by Polytope."
41
+
42
+
43
+ class GribJumpNoIndexError(PolytopeError, ValueError):
44
+ def __init__(self):
45
+ self.message = "GribJump index for this data was not yet written."
@@ -0,0 +1,26 @@
1
+ import math
2
+
3
+
4
+ def lerp(a, b, value):
5
+ intersect = [b + (a - b) * value for a, b in zip(a, b)]
6
+ return intersect
7
+
8
+
9
+ def nearest_pt(pts_list, pt):
10
+ new_pts_list = []
11
+ for potential_pt in pts_list:
12
+ for first_val in potential_pt[0]:
13
+ for second_val in potential_pt[1]:
14
+ new_pts_list.append((first_val, second_val))
15
+ nearest_pt = new_pts_list[0]
16
+ distance = l2_norm(new_pts_list[0], pt)
17
+ for new_pt in new_pts_list[1:]:
18
+ new_distance = l2_norm(new_pt, pt)
19
+ if new_distance < distance:
20
+ distance = new_distance
21
+ nearest_pt = new_pt
22
+ return nearest_pt
23
+
24
+
25
+ def l2_norm(pt1, pt2):
26
+ return math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1]))
@@ -0,0 +1,41 @@
1
+ import itertools
2
+
3
+
4
+ def bisect_left_cmp(arr, val, cmp):
5
+ left = -1
6
+ r = len(arr)
7
+ while r - left > 1:
8
+ e = (left + r) >> 1
9
+ if cmp(arr[e], val):
10
+ left = e
11
+ else:
12
+ r = e
13
+ return left
14
+
15
+
16
+ def bisect_right_cmp(arr, val, cmp):
17
+ left = -1
18
+ r = len(arr)
19
+ while r - left > 1:
20
+ e = (left + r) >> 1
21
+ if cmp(arr[e], val):
22
+ left = e
23
+ else:
24
+ r = e
25
+ return r
26
+
27
+
28
+ def unique(points):
29
+ points.sort()
30
+ points = [k for k, _ in itertools.groupby(points)]
31
+ return points
32
+
33
+
34
+ def argmin(points):
35
+ amin = min(range(len(points)), key=points.__getitem__)
36
+ return amin
37
+
38
+
39
+ def argmax(points):
40
+ amax = max(range(len(points)), key=points.__getitem__)
41
+ return amax
@@ -0,0 +1,14 @@
1
+ import time
2
+
3
+
4
+ class benchmark(object):
5
+ def __init__(self, name):
6
+ self.name = name
7
+
8
+ def __enter__(self):
9
+ self.start = time.perf_counter()
10
+
11
+ def __exit__(self, ty, val, tb):
12
+ end = time.perf_counter()
13
+ print("%s : %0.7f seconds" % (self.name, end - self.start))
14
+ return False
@@ -0,0 +1 @@
1
+ __version__ = "1.0.31"