pgzero icon indicating copy to clipboard operation
pgzero copied to clipboard

Is `numpy` a necessary dependency?

Open lgautier opened this issue 3 years ago • 4 comments

numpy is a required dependency but it only appears to be used in one place in the code: https://github.com/lordmauve/pgzero/blob/38cb6021496bd2f9dae0b37ef3ee6304dee71e2f/pgzero/ptext.py#L348

numpy is a rather large package. Is the dependency really necessary? The same could be implemented in pure Python is there is no significant loss of performance as a result, or a minimal C-extension would just do the job (and could eventually be pushed upstream to pygame if this is something general for writing games in Python).

lgautier avatar Jan 01 '22 21:01 lgautier

It's also used in screen.py.

ptext is vendored from elsewhere, and needs this for the outline feature (and others?) so this isn't easy to fix. We could submit a patch, or we could actually go ahead and fork it.

It is a huge dependency and I was reluctant to add it in the first place, but the situation we had where it was optional was not healthy because some stuff only worked when it was installed. Requiring it was a simpler solution.

Note that the amount of numpy usage has gone down a lot now we use pyfxr to generate tones in tone.py.

lordmauve avatar Jan 01 '22 22:01 lordmauve

Looks like the current version of ptext doesn't need numpy. But it has changed significantly, so merging that would need some care.

lordmauve avatar Jan 01 '22 22:01 lordmauve

Thanks. The dependency on numpy in screen.py is for gradients, and it uses an optional pygame capability that wraps memory regions in numpy arrays (getting rid of numpy in pygame zero would not suppress the need for numpy in the end). Interestingly the current ptext also has an implementation of vertical gradients in rectangles: https://github.com/cosmologicon/pygame-text/blob/master/ptext.py#L487

Overall it seems that gradients are generally useful, and arguably should not require numpy and be part of pygame (and in a C-extension module).

lgautier avatar Jan 08 '22 23:01 lgautier

It seems that there is no need to go for a C-extension and get decent performances without numpy. Running the test below shows a candidate alternative implementation without numpy that is 4.7 times faster:

import timeit
import pygame

# colors.
gradient_start = (0, 50, 100)
gradient_stop = (255, 250, 200)

# modest screen size.
WIDTH = 800
HEIGHT = 600


def blit_gradient(start, stop, dest_surface):
    """ New proposed implementation."
    surface_compact = pygame.Surface((2, 2))
    pixelarray = pygame.PixelArray(surface_compact)
    pixelarray[0][0] = start
    pixelarray[0][1] = stop
    pixelarray[1][0] = start
    pixelarray[1][1] = stop
    pygame.transform.smoothscale(surface_compact,
                                 pygame.PixelArray(dest_surface).shape,
                                 dest_surface=dest_surface)


def old_blit_gradient(start, stop, dest_surface):
    """The current implementation requiring numpy."""
    import numpy as np
    pixs = pygame.surfarray.pixels3d(dest_surface)

    gradient = np.dstack(
        [np.linspace(a, b, HEIGHT) for a, b in zip(start, stop)][:3]
    )

    pixs[...] = gradient

# Speed benchmark.

# Proposed new implementation.
setup_test = """
import pygame
dest_surface = pygame.Surface((WIDTH, HEIGHT))
"""
test = 'blit_gradient(gradient_start, gradient_stop, dest_surface)'

t_new = min(
    timeit.Timer(
        test, setup_test,
        globals=locals()).repeat(
            repeat=5,
            number=1000)
)

# Current implementation requiring numpy.
setup_test_old = """
import pygame
import numpy as np

dest_surface = pygame.Surface((WIDTH, HEIGHT))"""
test_old = 'old_blit_gradient(gradient_start, gradient_stop, dest_surface)'

t_current = min(
    timeit.Timer(
        test_old, setup_test_old,
    globals=locals()).repeat(
        repeat=5,
        number=1000)
)

print(f'Speedup: {t_current/t_new:.2f}x')

lgautier avatar Jan 09 '22 02:01 lgautier