prtools 2.0.1__tar.gz → 2.1.0__tar.gz

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 (35) hide show
  1. {prtools-2.0.1/prtools.egg-info → prtools-2.1.0}/PKG-INFO +2 -2
  2. {prtools-2.0.1 → prtools-2.1.0}/prtools/__init__.py +6 -2
  3. {prtools-2.0.1 → prtools-2.1.0}/prtools/array.py +78 -2
  4. {prtools-2.0.1 → prtools-2.1.0}/prtools/convolve.py +26 -10
  5. prtools-2.1.0/prtools/cost.py +120 -0
  6. {prtools-2.0.1 → prtools-2.1.0}/prtools/jax.py +1 -1
  7. prtools-2.1.0/prtools/mesh.py +40 -0
  8. {prtools-2.0.1 → prtools-2.1.0}/prtools/segmented.py +18 -13
  9. {prtools-2.0.1 → prtools-2.1.0}/prtools/shape.py +91 -97
  10. {prtools-2.0.1 → prtools-2.1.0}/prtools/zernike.py +259 -43
  11. {prtools-2.0.1 → prtools-2.1.0/prtools.egg-info}/PKG-INFO +2 -2
  12. {prtools-2.0.1 → prtools-2.1.0}/prtools.egg-info/SOURCES.txt +5 -1
  13. {prtools-2.0.1 → prtools-2.1.0}/prtools.egg-info/requires.txt +1 -1
  14. {prtools-2.0.1 → prtools-2.1.0}/pyproject.toml +1 -1
  15. prtools-2.1.0/tests/test_array.py +207 -0
  16. prtools-2.1.0/tests/test_cost.py +103 -0
  17. prtools-2.1.0/tests/test_shape.py +109 -0
  18. prtools-2.1.0/tests/test_zernike.py +246 -0
  19. prtools-2.0.1/prtools/cost.py +0 -115
  20. prtools-2.0.1/tests/test_array.py +0 -112
  21. {prtools-2.0.1 → prtools-2.1.0}/LICENSE +0 -0
  22. {prtools-2.0.1 → prtools-2.1.0}/NOTICE +0 -0
  23. {prtools-2.0.1 → prtools-2.1.0}/README.rst +0 -0
  24. {prtools-2.0.1 → prtools-2.1.0}/prtools/_array_api.py +0 -0
  25. {prtools-2.0.1 → prtools-2.1.0}/prtools/fourier.py +0 -0
  26. {prtools-2.0.1 → prtools-2.1.0}/prtools/misc.py +0 -0
  27. {prtools-2.0.1 → prtools-2.1.0}/prtools/morphology.py +0 -0
  28. {prtools-2.0.1 → prtools-2.1.0}/prtools/sparse.py +0 -0
  29. {prtools-2.0.1 → prtools-2.1.0}/prtools/stats.py +0 -0
  30. {prtools-2.0.1 → prtools-2.1.0}/prtools.egg-info/dependency_links.txt +0 -0
  31. {prtools-2.0.1 → prtools-2.1.0}/prtools.egg-info/top_level.txt +0 -0
  32. {prtools-2.0.1 → prtools-2.1.0}/setup.cfg +0 -0
  33. {prtools-2.0.1 → prtools-2.1.0}/tests/test_convolve.py +0 -0
  34. {prtools-2.0.1 → prtools-2.1.0}/tests/test_fourier.py +0 -0
  35. {prtools-2.0.1 → prtools-2.1.0}/tests/test_jax.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: prtools
3
- Version: 2.0.1
3
+ Version: 2.1.0
4
4
  Summary: Utility functions for image-based phase retrieval
5
5
  Author: Andy Kee
6
6
  License: MIT License
@@ -45,7 +45,7 @@ Requires-Dist: scipy
45
45
  Requires-Dist: array_api_compat
46
46
  Provides-Extra: jax
47
47
  Requires-Dist: jax; extra == "jax"
48
- Requires-Dist: optax; extra == "jax"
48
+ Requires-Dist: optax>=0.2.5; extra == "jax"
49
49
  Dynamic: license-file
50
50
 
51
51
  prtools
@@ -1,4 +1,4 @@
1
- __version__ = '2.0.1'
1
+ __version__ = '2.1.0'
2
2
 
3
3
 
4
4
  from prtools._array_api import use
@@ -8,6 +8,7 @@ from prtools.array import (
8
8
  pad,
9
9
  crop,
10
10
  boundary,
11
+ sample,
11
12
  rebin,
12
13
  rescale,
13
14
  normpow,
@@ -32,6 +33,8 @@ from prtools.fourier import dft2, idft2
32
33
 
33
34
  import prtools.jax
34
35
 
36
+ from prtools.mesh import mesh
37
+
35
38
  from prtools.misc import (
36
39
  calcpsf,
37
40
  translation_defocus,
@@ -85,5 +88,6 @@ from prtools.zernike import (
85
88
  zernike_basis,
86
89
  zernike_fit,
87
90
  zernike_remove,
88
- zernike_coordinates
91
+ zernike_coordinates,
92
+ zernike_coeff_legacy
89
93
  )
@@ -75,7 +75,7 @@ def centroid(x, where=None, mode='absolute', indexing='ij'):
75
75
  c = xp.dot(cc[where].ravel(), anorm.ravel())
76
76
 
77
77
  if mode == 'center':
78
- rc, cc = np.array(x.shape)/2
78
+ rc, cc = np.floor(np.array(x.shape)/2)
79
79
  r = r - rc
80
80
  c = c - cc
81
81
 
@@ -261,7 +261,7 @@ def crop(x, shape, shift=None, mode='absolute', indexing='ij'):
261
261
  if shift is not None:
262
262
  shift = np.asarray(shift, dtype=int)
263
263
  if indexing == 'xy':
264
- shift = (-shift[1], shift[0])
264
+ shift = (shift[1], shift[0])
265
265
  if mode == 'absolute':
266
266
  shift = np.array(shift - np.array(x.shape)//2)
267
267
  else:
@@ -326,6 +326,82 @@ def boundary(x, threshold=0):
326
326
  return rmin, rmax, cmin, cmax
327
327
 
328
328
 
329
+ def sample(x, oversample, offset=0, axes=(-2, -1), indexing='ij'):
330
+ """Extract samples from an oversampled array.
331
+
332
+ The sampled axes are tiled into contiguous blocks of ``oversample``
333
+ samples, with one sample being taken from each block. No averaging or
334
+ filtering is performed.
335
+
336
+ Parameters
337
+ ----------
338
+ x : array_like
339
+ Array to sample
340
+ oversample : int or tuple of ints
341
+ Decimation factor. Providing two values defines asymmetric
342
+ oversampling with ordering interpreted according to ``indexing``.
343
+ offset : int or tuple of ints, optional
344
+ Position of the sample with each block, measured from
345
+ ``oversample//2``. The sub-sampled position reachable by ``offset`` is
346
+ quantized to ``1/oversample``. Default is 0. If two values are
347
+ provided, their ordering is interpreted according to ``indexing``.
348
+ axes : (2,) sequence of ints, optional
349
+ Axes over which to sample. If not provided, the last two axes are
350
+ used.
351
+ indexing : {'ij', 'xy'}, optional
352
+ Matrix (‘ij’, default) or cartesian (‘xy’) indexing of ``x`` grid.
353
+
354
+ Returns
355
+ -------
356
+ x : ndarray
357
+ Each sampled axis has length ``x.shape[axis]//oversample``. If axis
358
+ length is not a multiple of ``oversample``, samples taken beyond the
359
+ last whole block are ignored.
360
+
361
+ Examples
362
+ --------
363
+ .. code-block:: python
364
+
365
+ >>> psf = prtools.calcpsf(amp, opd, wave, sampling, (32, 32),
366
+ ... oversample=8)
367
+ >>> k = prtools.pixel_kernel(psf.shape, oversample=8)
368
+ >>> native = prtools.sample(prtools.fftconv(psf, k), 8)
369
+ >>> native.shape
370
+ (32, 32)
371
+
372
+ """
373
+ xp = array_namespace(x)
374
+ x = xp.asarray(x)
375
+
376
+ if indexing not in ('ij', 'xy'):
377
+ raise ValueError("Valid values for indexing are 'xy' and 'ij'.")
378
+
379
+ oversample = tuple(int(v) for v in np.broadcast_to(oversample, (2,)))
380
+ offset = tuple(int(v) for v in np.broadcast_to(offset, (2,)))
381
+ axes = tuple(int(a) % x.ndim for a in axes)
382
+
383
+ if indexing == 'xy':
384
+ offset = (offset[1], offset[0])
385
+ oversample = (oversample[1], oversample[0])
386
+
387
+ idx = [slice(None)] * x.ndim
388
+
389
+ for ax, m, o in zip(axes, oversample, offset):
390
+
391
+ lo, hi = -(m // 2), m - 1 - m // 2
392
+ if not lo <= o <= hi:
393
+ raise ValueError(f'offset {o} lies outside one pixel for oversample '
394
+ f'{m} on axis {ax}; require {lo} <= offset <= {hi}')
395
+
396
+ n = x.shape[ax] // m
397
+ start = m // 2 + o
398
+ stop = start + m * n
399
+
400
+ idx[ax] = slice(start, stop, m)
401
+
402
+ return x[tuple(idx)]
403
+
404
+
329
405
  def rebin(x, factor):
330
406
  """Rebin an array by an integer factor.
331
407
 
@@ -7,7 +7,7 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
7
7
  fft_array=True, fft_kernel=False, fftshift_kernel=False):
8
8
  r"""Convolve an array with a kernel using the FFT.
9
9
 
10
- The colvolution is computed as
10
+ The convolution is computed as
11
11
 
12
12
  .. math::
13
13
 
@@ -18,12 +18,13 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
18
18
  array : array_like
19
19
  Array to be convolved with ``kernel``.
20
20
  kernel : array_like
21
- Convolution kernel. Should have the same shape as ``array``.
21
+ Convolution kernel. Should have the same shape as ``array``. May be
22
+ given in either the frequency or spatial domain, as indicated by
23
+ ``fft_kernel``. The kernel is applied exactly as supplied. See Notes
24
+ for normalization details.
22
25
  axes : sequence of ints, optional
23
26
  Axes over which to apply the convolution. If not given, the last two
24
27
  axes are used.
25
- normalize_kernel : bool, optional
26
- If True (default), kernel is normalized so that ``sum(kernel) == 1``.
27
28
  fft_array : bool, optional
28
29
  If True (default), the array is assumed to be provided in the spatial
29
30
  domain and its FFT will be computed by this function. If False, the
@@ -46,6 +47,24 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
46
47
  --------
47
48
  :func:`~prtools.gauss_blur`
48
49
  :func:`~prtools.pixelate`
50
+
51
+ Notes
52
+ -----
53
+ **Kernel normalization** This function does not normalize or scale the
54
+ supplied kernel in any way.
55
+
56
+ The sum of ``array`` is conserved when the kernel's transfer function is
57
+ unity at zero frequency. Depending on which domain the kernel is supplied
58
+ in, the kernel normaliztion approach is different:
59
+
60
+ * A frequency-domain kernel (``fft_kernel=False``) should be scaled such
61
+ that ``kernel[0,0] == 1``. Note that ``kernel[0,0]`` is the
62
+ zero-frequency term only for a kernel in standard (unshifted) FFT layout.
63
+ If the kernel origin lies at the center of the array
64
+ (``fftshift_kernel=True``), the DC term is at ``kernel[nr//2, nc//2]``.
65
+
66
+ * A spatial-domain kernal(``fft_kernel=True``) should have
67
+ ``sum(kernel) == 1``.
49
68
  """
50
69
  xp = array_namespace(array)
51
70
 
@@ -53,9 +72,6 @@ def fftconv(array, kernel, axes=(-2, -1), normalize_kernel=True,
53
72
  k = xp.fft.fftshift(kernel) if fftshift_kernel else kernel
54
73
  k = xp.fft.fft2(k) if fft_kernel else k
55
74
 
56
- if normalize_kernel:
57
- k = k / xp.sum(k)
58
-
59
75
  return xp.fft.ifft2(a*k, axes=axes).real
60
76
 
61
77
 
@@ -312,10 +328,10 @@ def gauss_kernel(shape, sigma, oversample=1, pixelscale=1.0, fftshift=False,
312
328
  return k
313
329
 
314
330
 
315
- def pixel_kernel(shape, oversample=1, pixelscale=1.0, fftshift=False,
331
+ def pixel_kernel(shape, oversample=1, pixelscale=1.0, fftshift=False,
316
332
  xp=None):
317
333
  r"""2D pixel MTF filter kernel
318
-
334
+
319
335
  This function returns a normalized 2D sinc function sized to represent
320
336
  the transfer function of an idealized square pixel.
321
337
 
@@ -361,7 +377,7 @@ def pixel_kernel(shape, oversample=1, pixelscale=1.0, fftshift=False,
361
377
  x1 = xp.fft.fftfreq(shape[0], d=pixelscale[0]) * oversample
362
378
  x2 = xp.fft.fftfreq(shape[1], d=pixelscale[1]) * oversample
363
379
 
364
- k = xp.abs(sinc(x1, x2, indexing='ij', xp=xp))
380
+ k = sinc(x1, x2, indexing='ij', xp=xp)
365
381
 
366
382
  if fftshift:
367
383
  return xp.fft.fftshift(k)
@@ -0,0 +1,120 @@
1
+ from prtools._array_api import array_namespace
2
+
3
+
4
+ def sserror(data, est, mask=None, gain_bias_invariant=False, ghat=None):
5
+ r"""Compute the normalized sum squared error between two arrays.
6
+
7
+ The error between measured data :math:`\tilde{s}` and estimated data
8
+ :math:`\hat{s}` is computed as the gain invariant error metric
9
+
10
+ .. math::
11
+
12
+ \Phi = \frac{1}{K}\sum_k
13
+ \frac{\sum_{x,y} w(x,y,k)\left[\alpha_k\hat{s}(x,y,k) -
14
+ \tilde{s}(x,y,k)\right]^2}{\sum_{x,y} w(x,y,k)\tilde{s}^2(x,y,k)}
15
+
16
+ for :math:`k = 1, 2, ... K` independent measurements, where
17
+ :math:`w` is a weighting function given by `mask` and each
18
+ :math:`\alpha_k` is chosen to minimize the error. The result is
19
+ independent of any relative scaling (gain) difference between the inputs.
20
+
21
+ If `gain_bias_invariant` is ``True``, the gain and bias invariant error
22
+ metric is computed instead
23
+
24
+ .. math::
25
+
26
+ \Phi = \frac{1}{K}\sum_k
27
+ \frac{\sum_{x,y} w(x,y,k)\left[\alpha_k\hat{g}(x,y,k) -
28
+ \tilde{g}(x,y,k)\right]^2}{\sum_{x,y} w(x,y,k)\tilde{s}^2(x,y,k)}
29
+
30
+ where :math:`\hat{g}` and :math:`\tilde{g}` are :math:`\hat{s}` and
31
+ :math:`\tilde{s}` with their weighted means subtracted and each
32
+ :math:`\alpha_k` is chosen to minimize the error. The result is
33
+ independent of any relative scaling (gain) and offset (bias) differences
34
+ between the inputs.
35
+
36
+ Parameters
37
+ ----------
38
+ data : array_like
39
+ Ideal or measured data with shape (M, N) or (K, M, N)
40
+ est : array_like
41
+ Estimated or modeled data with the same shape as `data`
42
+ mask : array_like, optional
43
+ Weighting function :math:`w` applied to the inputs. Entries equal to
44
+ 1 (or True) are fully included in the error computation and entries
45
+ equal to 0 (or False) are ignored. Intermediate values partially
46
+ weight the corresponding elements. Mask must either have the same
47
+ shape as the inputs or be broadcastable to the same shape. If None
48
+ (default), all elements are equally weighted.
49
+ gain_bias_invariant : bool, optional
50
+ If True, the error is computed to be independent of any gain or bias
51
+ differences between the inputs. If False (default), the error is
52
+ computed to be independent of any gain differences between the inputs.
53
+ ghat : array_like, optional
54
+ Precomputed bias-subtracted estimate :math:`\hat{g}`. Only used if
55
+ `gain_bias_invariant` is True. If None (default), :math:`\hat{g}`
56
+ is computed from `est`.
57
+
58
+ Returns
59
+ -------
60
+ error : float
61
+ Normalized sum squared error
62
+
63
+ References
64
+ ----------
65
+ [1] S. Thurman and J. Fienup, "Phase retrieval with signal bias", J. Opt. Soc. Am. A/Vol. 26, No. 4 (2009)
66
+
67
+ [2] A. Jurling and J. Fienup, "Applications of algorithmic differentiation to phase retrieval algorithms", J. Opt. Soc. Am. A/Vol. 31, No. 7 (2014)
68
+
69
+ """
70
+ xp = array_namespace(data, est)
71
+
72
+ if gain_bias_invariant:
73
+ if ghat is None:
74
+ ghat = g(est, mask, xp) # Eq. 12
75
+ mtil = g(data, mask, xp) # Eq. 13
76
+ mhat = ghat
77
+ else:
78
+ mtil = data
79
+ mhat = est
80
+
81
+ alpha = _a(mtil, mhat, mask, xp) # Eq. 4 or Eq. 14
82
+ alpha = alpha[..., xp.newaxis, xp.newaxis]
83
+
84
+ num = _wsum(xp.square(alpha * mhat - mtil), mask, xp)
85
+ den = _wsum(xp.square(data), mask, xp)
86
+
87
+ # Eq. 3 or Eq. 11
88
+ return xp.mean(num/den)
89
+
90
+
91
+ def _wsum(x, mask, xp):
92
+ # Weighted sum over the last two axes
93
+ if mask is not None:
94
+ x = mask * x
95
+ return xp.sum(x, axis=(-2, -1))
96
+
97
+
98
+ def g(data, mask, xp):
99
+ # This function implements Eqs. 12 and 13 in [1]
100
+ if mask is None:
101
+ numer = xp.sum(data, axis=(-2, -1))
102
+ denom = xp.prod(xp.asarray(data.shape[-2:]))
103
+ else:
104
+ numer = xp.sum(mask * data, axis=(-2, -1))
105
+ denom = xp.sum(mask, axis=(-2, -1))
106
+ x = numer/denom
107
+ if data.ndim == 3:
108
+ x = x[:, xp.newaxis, xp.newaxis]
109
+ return data - x
110
+
111
+
112
+ def _a(gtil, ghat, mask, xp):
113
+ # Eq. 4 or Eq. 14 in [1]
114
+ try:
115
+ return _wsum(gtil * ghat, mask, xp)/_wsum(xp.square(ghat), mask, xp)
116
+ except FloatingPointError as e:
117
+ if xp.all(ghat == 0):
118
+ raise ZeroDivisionError('ghat must be nonzero')
119
+ else:
120
+ raise e
@@ -87,7 +87,7 @@ def lbfgs(fn, x0, gtol=None, maxiter=None, callback=None, fn_kwargs=None):
87
87
  _, state = carry
88
88
  iter_num = otu.tree_get(state, 'count')
89
89
  grad = otu.tree_get(state, 'grad')
90
- grad_norm = otu.tree_l2_norm(grad)
90
+ grad_norm = otu.tree_norm(grad)
91
91
  return (iter_num == 0) | ((iter_num < maxiter) & (grad_norm >= gtol))
92
92
 
93
93
  init_carry = (x0, opt.init(x0))
@@ -0,0 +1,40 @@
1
+ import numpy as np
2
+
3
+
4
+ def mesh(shape, shift=(0, 0), angle=0, indexing='ij'):
5
+ """Generate a standard mesh.
6
+
7
+ Parameters
8
+ ----------
9
+ shape : array_like
10
+ Size of output in pixels (nrows, ncols)
11
+ shift : (2,) array_like, optional
12
+ Coordinate translation from center of containing array.
13
+ Default is (0, 0). Shift is applied according to the specified
14
+ ``indexing`` scheme.
15
+ angle : float, optional
16
+ Mesh rotation in degrees. When ``indexing = 'xy'`` the rotation is
17
+ measured from the +x axis toward the +y axis. When ``indexing = 'ij'``
18
+ the rotation is measured from the +i axis toward the +j axis. Note
19
+ that rotations occur about the shifted center if ``shift`` is nonzero.
20
+ Default is 0.
21
+ indexing : {'ij', 'xy'}, optional
22
+ Matrix ('ij', default) or Cartesian ('xy') indexing of mesh.
23
+
24
+ Returns
25
+ -------
26
+ list of ndarrays
27
+
28
+ """
29
+ shape = np.broadcast_to(np.asarray(shape), (2,))
30
+ if indexing == 'xy':
31
+ shape = np.flip(shape)
32
+ angle = np.radians(angle)
33
+
34
+ x1, x2 = np.meshgrid(np.arange(shape[0]) - np.floor(shape[0]/2) - shift[0],
35
+ np.arange(shape[1]) - np.floor(shape[1]/2) - shift[1],
36
+ indexing=indexing)
37
+
38
+ X1 = x1 * np.cos(angle) + x2 * np.sin(angle)
39
+ X2 = x1 * -np.sin(angle) + x2 * np.cos(angle)
40
+ return X1, X2
@@ -5,7 +5,7 @@ import numpy as np
5
5
  import prtools
6
6
 
7
7
 
8
- def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
8
+ def hex_segments(rings, seg_radius, seg_gap, rotate=True, antialias=True,
9
9
  flatten=False, pad=2, drop=(0,)):
10
10
  """Draw a segmented aperture made up of hexagonal segments
11
11
 
@@ -18,9 +18,8 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
18
18
  seg_gap : float
19
19
  Spacing between adjacent segments in pixels
20
20
  rotate : bool, optional
21
- Rotate segments so that flat sides are aligned with the Y direction
22
- instead of the default orientation which is aligned with the X
23
- direction.
21
+ If True (default), the segment mask is rotated so that flat sides are
22
+ aligned with the X direction instead of the Y direction.
24
23
  antialias : bool, optional
25
24
  If True (default), the segment edges are antialiased.
26
25
  flatten : bool, optional
@@ -29,8 +28,8 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
29
28
  pad : int, optional
30
29
  Number of additional pixels to zero-pad output by. Default is 2.
31
30
  drop : list_like, optional
32
- Segments to exclude from output. 0 represents the central segment.
33
-
31
+ Segments to exclude from output. 0 represents the central segment.
32
+
34
33
  Returns
35
34
  -------
36
35
  ndarray
@@ -63,20 +62,24 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
63
62
  mask = []
64
63
 
65
64
  if 0 not in drop:
66
- mask.append(prtools.hexagon(shape, seg_radius, shift=(0, 0), antialias=antialias, rotate=rotate))
65
+ mask.append(prtools.hexagon(shape, seg_radius, shift=(0, 0),
66
+ antialias=antialias, rotate=rotate,
67
+ indexing='ij'))
67
68
 
68
69
  seg = 1
69
70
  for ring in range(1, rings+1):
70
71
  for h in hex_ring(ring):
71
72
  r, c = hex_to_rc(h, seg_radius + seg_gap/2, rotate)
72
73
  if seg not in drop:
73
- mask.append(prtools.hexagon(shape, seg_radius, shift=(r, c), antialias=antialias, rotate=rotate))
74
+ mask.append(prtools.hexagon(shape, seg_radius, shift=(r, c),
75
+ antialias=antialias, rotate=rotate,
76
+ indexing='ij'))
74
77
  seg += 1
75
-
78
+
76
79
  mask = np.asarray(mask)
77
80
  if flatten:
78
81
  mask = np.sum(mask, axis=0)
79
-
82
+
80
83
  return mask
81
84
 
82
85
 
@@ -84,6 +87,8 @@ def hex_segments(rings, seg_radius, seg_gap, rotate=False, antialias=True,
84
87
  # exactly from http://www.redblobgames.com/grids/hexagons/
85
88
 
86
89
  _Hex = collections.namedtuple("Hex", ["q", "r", "s"])
90
+
91
+
87
92
  def Hex(q, r, s):
88
93
  assert not (round(q + r + s) != 0), "q + r + s must be 0"
89
94
  return _Hex(q, r, s)
@@ -93,7 +98,7 @@ def hex_add(a, b):
93
98
  return Hex(a.q + b.q, a.r + b.r, a.s + b.s)
94
99
 
95
100
 
96
- hex_directions = [Hex(1, 0, -1), Hex(1, -1, 0), Hex(0, -1, 1),
101
+ hex_directions = [Hex(1, 0, -1), Hex(1, -1, 0), Hex(0, -1, 1),
97
102
  Hex(-1, 0, 1), Hex(-1, 1, 0), Hex(0, 1, -1)]
98
103
 
99
104
 
@@ -117,13 +122,13 @@ def hex_ring(radius):
117
122
 
118
123
 
119
124
  def hex_to_xy(hex, radius, rotate=False):
120
- if rotate:
125
+ if not rotate:
121
126
  x = radius * (np.sqrt(3) * hex.q + np.sqrt(3)/2 * hex.r)
122
127
  y = radius * (3/2 * hex.r)
123
128
  else:
124
129
  x = radius * (3/2 * hex.q)
125
130
  y = radius * (np.sqrt(3)/2 * hex.q + np.sqrt(3) * hex.r)
126
-
131
+
127
132
  return x, y
128
133
 
129
134