pyxem / kikuchipy

Toolbox for analysis of electron backscatter diffraction (EBSD) patterns
https://kikuchipy.org
GNU General Public License v3.0
79 stars 30 forks source link

Integration test suite failure #614

Closed ericpre closed 1 year ago

ericpre commented 1 year ago

See https://github.com/hyperspy/hyperspy-extensions-list/actions/runs/4169659339

__________________ TestHoughIndexing.test_hough_indexing_lazy __________________

self = <pyebsdindex.opencl.band_detect_cl.BandDetect object at 0x7fd9e15a4c70>
patternsIn = dask.array<reshape, shape=(9, 60, 60), dtype=uint8, chunksize=(9, 60, 60), chunktype=numpy.ndarray>
verbose = 1, clparams = None, chunksize = 9, useCPU = False, kwargs = {}
tic0 = 1769.554711722, tic = 1769.554711922, ndim = 3
patterns = dask.array<reshape, shape=(9, 60, 60), dtype=uint8, chunksize=(9, 60, 60), chunktype=numpy.ndarray>
shape = (9, 60, 60), nPats = 9
bandData = array([[(0, 0., [0., 0.], 0., [0., 0.], 0., 0., 0., 0., 0),
        (0, 0., [0., 0.], 0., [0., 0.], 0., 0., 0., 0., 0)...<f4'), ('aveloc', '<f4', (2,)), ('pqmax', '<f4'), ('width', '<f4'), ('theta', '<f4'), ('rho', '<f4'), ('valid', 'i1')])

    def find_bands(self, patternsIn, verbose=0, clparams=None, chunksize=528, useCPU=None, **kwargs):
      if useCPU is None:
        useCPU = self.useCPU

      if useCPU == True:
        return band_detect.BandDetect.find_bands(self, patternsIn, verbose=verbose, chunksize=-1, **kwargs)
      #if clparams is None:
      #  print('noclparams')
      #else:
      #  print(type(clparams.queue))

      try:
        tic0 = timer()
        tic = timer()
        ndim = patternsIn.ndim
        if ndim == 2:
          patterns = np.expand_dims(patternsIn, axis=0)
        else:
          patterns = patternsIn

        shape = patterns.shape
        nPats = shape[0]

        bandData = np.zeros((nPats,self.nBands),dtype=self.dataType)
        if chunksize < 0:
          nchunks = 1
          chunksize = nPats
        else:
          nchunks = (np.ceil(nPats / chunksize)).astype(np.compat.long)

        chunk_start_end = [[i * chunksize,(i + 1) * chunksize] for i in range(nchunks)]
        chunk_start_end[-1][1] = nPats
        # these are timers used to gauge performance
        rdntime = 0.0
        convtime = 0.0
        lmaxtime = 0.0
        blabeltime = 0.0

        for chnk in chunk_start_end:
          tic1 = timer()
          nPatsChunk = chnk[1] - chnk[0]
          #rdnNorm, clparams, rdnNorm_gpu = self.calc_rdn(patterns[chnk[0]:chnk[1],:,:], clparams, use_gpu=self.CLOps[0])
>         rdnNorm, clparams = self.radon_fasterCL(patterns[chnk[0]:chnk[1],:,:], self.padding,
                                                                         fixArtifacts=False, background=self.backgroundsub,
                                                                         returnBuff=True, clparams=clparams)

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/band_detect_cl.py:94: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyebsdindex.opencl.band_detect_cl.BandDetect object at 0x7fd9e15a4c70>
image = dask.array<reshape, shape=(9, 60, 60), dtype=uint8, chunksize=(9, 60, 60), chunktype=numpy.ndarray>
padding = array([15, 21]), fixArtifacts = False, background = None
returnBuff = True
clparams = <pyebsdindex.opencl.openclparam.OpenClParam object at 0x7fd9e1349c[90](https://github.com/hyperspy/hyperspy-extensions-list/actions/runs/4169659339/jobs/7217866326#step:17:91)>

    def radon_fasterCL(self,image,padding = np.array([0,0]), fixArtifacts = False, background = None, returnBuff = True, clparams=None ):
      # this function executes the radon sumations on the GPU
      tic = timer()
      # make sure we have an OpenCL environment
      if clparams is not None:
        if clparams.queue is None:
          clparams.get_queue()
        gpu = clparams.gpu
        gpu_id = clparams.gpu_id
        ctx = clparams.ctx
        prg = clparams.prg
        queue = clparams.queue
        mf = clparams.memflags
      else:
        clparams = openclparam.OpenClParam()
>       clparams.get_queue()

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/band_detect_cl.py:210: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyebsdindex.opencl.openclparam.OpenClParam object at 0x7fd9e1349c90>
gpu_id = None, random_gpu = False

    def get_queue(self, gpu_id=None, random_gpu=False):

      if self.ctx is None:
>       self.get_context()

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/openclparam.py:78: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyebsdindex.opencl.openclparam.OpenClParam object at 0x7fd9e1349c90>

    def get_context(self):
      if self.gpu is None:
>       self.get_gpu()

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/openclparam.py:69: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyebsdindex.opencl.openclparam.OpenClParam object at 0x7fd9e1349c90>

    def get_gpu(self):

      if self.platform is None:
>       self.get_platform()

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/openclparam.py:60: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyebsdindex.opencl.openclparam.OpenClParam object at 0x7fd9e1349c90>

    def get_platform(self):
>     self.platform = cl.get_platforms()[0]
E     pyopencl._cl.LogicError: clGetPlatformIDs failed: PLATFORM_NOT_FOUND_KHR

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/openclparam.py:56: LogicError

During handling of the above exception, another exception occurred:

self = <kikuchipy.signals.tests.test_ebsd_hough_indexing.TestHoughIndexing object at 0x7fd9ed09e230>

    def test_hough_indexing_lazy(self):  # pragma: no cover
        s = self.signal.as_lazy()

        from pyebsdindex import _pyopencl_installed

        if not _pyopencl_installed:
            with pytest.raises(ValueError, match="Hough indexing of lazy signals must"):
                _ = s.hough_indexing(self.phase_list, self.indexer, verbose=2)
        else:
>           xmap1 = s.hough_indexing(self.phase_list, self.indexer)

/usr/share/miniconda3/lib/python3.10/site-packages/kikuchipy/signals/tests/test_ebsd_hough_indexing.py:99: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/share/miniconda3/lib/python3.10/site-packages/kikuchipy/signals/ebsd.py:1698: in hough_indexing
    xmap, index_data, band_data = _hough_indexing(
/usr/share/miniconda3/lib/python3.10/site-packages/kikuchipy/indexing/_hough_indexing.py:228: in _hough_indexing
    index_data, band_data, _, _ = indexer.index_pats(
/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/_ebsd_index_single.py:475: in index_pats
    bandData = self.bandDetectPlan.find_bands(
/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/band_detect_cl.py:190: in find_bands
    bandData = band_detect.BandDetect.find_bands(self, patternsIn, verbose=verbose, chunksize=-1, **kwargs)
/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/band_detect.py:322: in find_bands
    rdnNorm = self.radonPlan.radon_faster(patterns[chnk[0]:chnk[1],:,:], self.padding, fixArtifacts=False, background=self.backgroundsub)
/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/radon_fast.py:178: in radon_faster
    counter = self.rdn_loops(image,self.indexPlan,nIm,nPx,indxDim,radon, np.asarray(padding))
/usr/share/miniconda3/lib/python3.10/site-packages/numba/core/dispatcher.py:468: in _compile_for_args
    error_rewrite(e, 'typing')
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

e = TypingError('Failed in nopython mode pipeline (step: nopython frontend)\n\x1b[1m\x1b[1mnon-precise type pyobject\x1b[0...ollowing argument(s):\n- argument 0: \x1b[1mCannot determine Numba type of <class \'dask.array.core.Array\'>\x1b[0m\n')
issue_type = 'typing'

    def error_rewrite(e, issue_type):
        """
        Rewrite and raise Exception `e` with help supplied based on the
        specified issue_type.
        """
        if config.SHOW_HELP:
            help_msg = errors.error_extras[issue_type]
            e.patch_message('\n'.join((str(e).rstrip(), help_msg)))
        if config.FULL_TRACEBACKS:
            raise e
        else:
>           raise e.with_traceback(None)
E           numba.core.errors.TypingError: Failed in nopython mode pipeline (step: nopython frontend)
E           non-precise type pyobject
E           During: typing of argument at /usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/radon_fast.py (193)
E           
E           File "../../usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/radon_fast.py", line 193:
E             def rdn_loops(images,index,nIm,nPx,indxdim,radon, padding):
E               nRho = indxdim[0]
E               ^ 
E           
E           This error may have been caused by the following argument(s):
E           - argument 0: Cannot determine Numba type of <class 'dask.array.core.Array'>

/usr/share/miniconda3/lib/python3.10/site-packages/numba/core/dispatcher.py:409: TypingError
----------------------------- Captured stdout call -----------------------------
Hough indexing with PyEBSDIndex information:
  GPU: True
  Projection center (mean): (0.4251, 0.2134, 0.5007)
  Indexing 9 pattern(s) in 1 chunk(s)
clGetPlatformIDs failed: PLATFORM_NOT_FOUND_KHR
clGetPlatformIDs failed: PLATFORM_NOT_FOUND_KHR
___________________ TestHoughIndexing.test_optimize_pc_lazy ____________________

self = <pyebsdindex.opencl.band_detect_cl.BandDetect object at 0x7fd9e127f790>
patternsIn = dask.array<reshape, shape=(9, 60, 60), dtype=uint8, chunksize=(9, 60, 60), chunktype=numpy.ndarray>
verbose = 0, clparams = None, chunksize = 528, useCPU = False, kwargs = {}
tic0 = 1785.949406329, tic = 1785.949406629, ndim = 3
patterns = dask.array<reshape, shape=(9, 60, 60), dtype=uint8, chunksize=(9, 60, 60), chunktype=numpy.ndarray>
shape = (9, 60, 60), nPats = 9
bandData = array([[(0, 0., [0., 0.], 0., [0., 0.], 0., 0., 0., 0., 0),
        (0, 0., [0., 0.], 0., [0., 0.], 0., 0., 0., 0., 0)...<f4'), ('aveloc', '<f4', (2,)), ('pqmax', '<f4'), ('width', '<f4'), ('theta', '<f4'), ('rho', '<f4'), ('valid', 'i1')])

    def find_bands(self, patternsIn, verbose=0, clparams=None, chunksize=528, useCPU=None, **kwargs):
      if useCPU is None:
        useCPU = self.useCPU

      if useCPU == True:
        return band_detect.BandDetect.find_bands(self, patternsIn, verbose=verbose, chunksize=-1, **kwargs)
      #if clparams is None:
      #  print('noclparams')
      #else:
      #  print(type(clparams.queue))

      try:
        tic0 = timer()
        tic = timer()
        ndim = patternsIn.ndim
        if ndim == 2:
          patterns = np.expand_dims(patternsIn, axis=0)
        else:
          patterns = patternsIn

        shape = patterns.shape
        nPats = shape[0]

        bandData = np.zeros((nPats,self.nBands),dtype=self.dataType)
        if chunksize < 0:
          nchunks = 1
          chunksize = nPats
        else:
          nchunks = (np.ceil(nPats / chunksize)).astype(np.compat.long)

        chunk_start_end = [[i * chunksize,(i + 1) * chunksize] for i in range(nchunks)]
        chunk_start_end[-1][1] = nPats
        # these are timers used to gauge performance
        rdntime = 0.0
        convtime = 0.0
        lmaxtime = 0.0
        blabeltime = 0.0

        for chnk in chunk_start_end:
          tic1 = timer()
          nPatsChunk = chnk[1] - chnk[0]
          #rdnNorm, clparams, rdnNorm_gpu = self.calc_rdn(patterns[chnk[0]:chnk[1],:,:], clparams, use_gpu=self.CLOps[0])
>         rdnNorm, clparams = self.radon_fasterCL(patterns[chnk[0]:chnk[1],:,:], self.padding,
                                                                         fixArtifacts=False, background=self.backgroundsub,
                                                                         returnBuff=True, clparams=clparams)

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/band_detect_cl.py:94: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyebsdindex.opencl.band_detect_cl.BandDetect object at 0x7fd9e127f790>
image = dask.array<reshape, shape=(9, 60, 60), dtype=uint8, chunksize=(9, 60, 60), chunktype=numpy.ndarray>
padding = array([15, 21]), fixArtifacts = False, background = None
returnBuff = True
clparams = <pyebsdindex.opencl.openclparam.OpenClParam object at 0x7fd9e[92](https://github.com/hyperspy/hyperspy-extensions-list/actions/runs/4169659339/jobs/7217866326#step:17:93)dbb20>

    def radon_fasterCL(self,image,padding = np.array([0,0]), fixArtifacts = False, background = None, returnBuff = True, clparams=None ):
      # this function executes the radon sumations on the GPU
      tic = timer()
      # make sure we have an OpenCL environment
      if clparams is not None:
        if clparams.queue is None:
          clparams.get_queue()
        gpu = clparams.gpu
        gpu_id = clparams.gpu_id
        ctx = clparams.ctx
        prg = clparams.prg
        queue = clparams.queue
        mf = clparams.memflags
      else:
        clparams = openclparam.OpenClParam()
>       clparams.get_queue()

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/band_detect_cl.py:210: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyebsdindex.opencl.openclparam.OpenClParam object at 0x7fd9e92dbb20>
gpu_id = None, random_gpu = False

    def get_queue(self, gpu_id=None, random_gpu=False):

      if self.ctx is None:
>       self.get_context()

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/openclparam.py:78: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyebsdindex.opencl.openclparam.OpenClParam object at 0x7fd9e92dbb20>

    def get_context(self):
      if self.gpu is None:
>       self.get_gpu()

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/openclparam.py:69: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyebsdindex.opencl.openclparam.OpenClParam object at 0x7fd9e92dbb20>

    def get_gpu(self):

      if self.platform is None:
>       self.get_platform()

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/openclparam.py:60: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <pyebsdindex.opencl.openclparam.OpenClParam object at 0x7fd9e92dbb20>

    def get_platform(self):
>     self.platform = cl.get_platforms()[0]
E     pyopencl._cl.LogicError: clGetPlatformIDs failed: PLATFORM_NOT_FOUND_KHR

/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/openclparam.py:56: LogicError

During handling of the above exception, another exception occurred:

self = <kikuchipy.signals.tests.test_ebsd_hough_indexing.TestHoughIndexing object at 0x7fd9ed09ceb0>

    def test_optimize_pc_lazy(self):
        s = self.signal.as_lazy()

        from pyebsdindex import _pyopencl_installed

        if not _pyopencl_installed:
            with pytest.raises(ValueError, match="Hough indexing of lazy signals must"):
                _ = s.hough_indexing_optimize_pc(self.detector.pc_average, self.indexer)
        else:  # pragma: no cover
>           det = s.hough_indexing_optimize_pc(self.detector.pc_average, self.indexer)

/usr/share/miniconda3/lib/python3.10/site-packages/kikuchipy/signals/tests/test_ebsd_hough_indexing.py:279: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/share/miniconda3/lib/python3.10/site-packages/kikuchipy/signals/ebsd.py:1811: in hough_indexing_optimize_pc
    pc = _optimize_pc(
/usr/share/miniconda3/lib/python3.10/site-packages/kikuchipy/indexing/_hough_indexing.py:411: in _optimize_pc
    return optimize_func(pats=patterns, indexer=indexer, PC0=pc0, batch=batch)
/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/pcopt.py:100: in optimize
    banddat = indexer.bandDetectPlan.find_bands(pats)
/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/opencl/band_detect_cl.py:190: in find_bands
    bandData = band_detect.BandDetect.find_bands(self, patternsIn, verbose=verbose, chunksize=-1, **kwargs)
/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/band_detect.py:322: in find_bands
    rdnNorm = self.radonPlan.radon_faster(patterns[chnk[0]:chnk[1],:,:], self.padding, fixArtifacts=False, background=self.backgroundsub)
/usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/radon_fast.py:178: in radon_faster
    counter = self.rdn_loops(image,self.indexPlan,nIm,nPx,indxDim,radon, np.asarray(padding))
/usr/share/miniconda3/lib/python3.10/site-packages/numba/core/dispatcher.py:468: in _compile_for_args
    error_rewrite(e, 'typing')
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

e = TypingError('Failed in nopython mode pipeline (step: nopython frontend)\n\x1b[1m\x1b[1mnon-precise type pyobject\x1b[0...ollowing argument(s):\n- argument 0: \x1b[1mCannot determine Numba type of <class \'dask.array.core.Array\'>\x1b[0m\n')
issue_type = 'typing'

    def error_rewrite(e, issue_type):
        """
        Rewrite and raise Exception `e` with help supplied based on the
        specified issue_type.
        """
        if config.SHOW_HELP:
            help_msg = errors.error_extras[issue_type]
            e.patch_message('\n'.join((str(e).rstrip(), help_msg)))
        if config.FULL_TRACEBACKS:
            raise e
        else:
>           raise e.with_traceback(None)
E           numba.core.errors.TypingError: Failed in nopython mode pipeline (step: nopython frontend)
E           non-precise type pyobject
E           During: typing of argument at /usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/radon_fast.py (1[93](https://github.com/hyperspy/hyperspy-extensions-list/actions/runs/4169659339/jobs/7217866326#step:17:94))
E           
E           File "../../usr/share/miniconda3/lib/python3.10/site-packages/pyebsdindex/radon_fast.py", line [193](https://github.com/hyperspy/hyperspy-extensions-list/actions/runs/4169659339/jobs/7217866326#step:17:194):
E             def rdn_loops(images,index,nIm,nPx,indxdim,radon, padding):
E               nRho = indxdim[0]
E               ^ 
E           
E           This error may have been caused by the following argument(s):
E           - argument 0: Cannot determine Numba type of <class 'dask.array.core.Array'> 
E           
E           This error may have been caused by the following argument(s):
E           - argument 0: Cannot determine Numba type of <class 'dask.array.core.Array'>

/usr/share/miniconda3/lib/python3.10/site-packages/numba/core/dispatcher.py:[409](https://github.com/hyperspy/hyperspy-extensions-list/actions/runs/4169659339/jobs/7217866326#step:17:410): TypingError
----------------------------- Captured stdout call -----------------------------
clGetPlatformIDs failed: PLATFORM_NOT_FOUND_KHR
clGetPlatformIDs failed: PLATFORM_NOT_FOUND_KHR
hakonanes commented 1 year ago

Thank you for this, Eric.

The failure is a result of trying to pass a Dask array to Numba. PyEBSDIndex does not support Dask arrays, but its GPU implementation (using PyOpenCL) handles them well. kikuchipy therefore throws an error whenever a Dask array is attempted indexed without PyOpenCL available, as PyEBSDIndex falls back to the CPU/Numba implementation. However, having PyOpenCL does not guarantee that a GPU is available, I realize.

A better implementation would be to check whether a GPU is available, in addition to PyOpenCL being installed. Will do this change. I consider this a bug, and will see if we can release a v0.8.1 patch release when I fix this.

As a sidenote: The test passes whenever you have PyOpenCL installed and access to a GPU, which the GitHub action runners in kikuchipy's test suite have. Must be some other package which PyOpenCL requires to access the GPU that's not available in the integration tests.

ericpre commented 1 year ago

As a sidenote: The test passes whenever you have PyOpenCL installed and access to a GPU, which the GitHub action runners in kikuchipy's test suite have. Must be some other package which PyOpenCL requires to access the GPU that's not available in the integration tests.

How do you know that they have GPUs? This is not mentioned in the github documentation.

I suspect that the issue is that the pyebsdindex conda package install pyopencl, while pypi packages doesn't. pyopencl can also run on cpu, so maybe it could be possible to get it to run on CPU of the github runners.

hakonanes commented 1 year ago

so maybe it could be possible to get it to run on CPU of the github runners.

I think you're right, sorry for my misleading comments. I realize that I have to take another look at kikuchipy's PyEBSDIndex tests on GitHub's runners.

ericpre commented 1 year ago

The same issue will occur when installing from conda packages on system without gpu, because there is no guaranty that pyopencl will work.

hakonanes commented 1 year ago

This issue should be fixed in kikuchipy 0.8.1 available on conda-forge and PyPI.