Tag: mandelbrot

  • Parallel MicroPython Benchmarking

    On the left, a Raspberry Pi Pico 2W. On the right, a Raspberry Pi Pico. Each is connected to its own small OLED screen. When a button is pressed, both boards calculate and display the Mandelbrot set, along with its completion time. Needless to say, the Pico 2 W is quite a bit quicker.
    two small OLED screens side by side on a breadboard. They're the type that are surplus from pulse oximeter machines, so the top 16 pixels are yellow, and the rest of the rows are blue.

The left screen displays: "micropython 1.25.0.preview RP2350 150 MHz 128*64; 120", while the screen on the right shows "micropython 1.24.1 RP2040 125 MHz 128*64; 120"
    the before screens …
    The same two OLED screens, this time showing a complete Mandelbrot set and an elapsed time for each microcontroller. Pico 2 comes in at 10.3 seconds, original Pico at 19.8 seconds
    Pico 2 comes in at 10.3 seconds, original Pico at 19.8 seconds

    Stuff I found out setting this up:

    • some old OLEDs, like these surplus pulse oximeter ones, don’t have pull-up resistors on their data lines. These I’ve carefully hidden behind the displays, but they’re there.
    • Some MicroPython ports don’t include the complex type, so I had to lose the elegant z→z²+C mapping to some ugly code.
    • Some MicroPython ports don’t have os.uname(), but sys.implementation seems to cover most of the data I need.
    • On some boards, machine.freq() is an integer value representing the CPU frequency. On others, it’s a list. Aargh.

    These displays came from the collection of the late Tom Luff, a Toronto maker who passed away late 2024 after a long illness. Tom had a huge component collection, and my way of remembering him is to show off his stuff being used.

    Source:

    # benchmark Mandelbrot set (aka Brooks-Matelski set) on OLED
    # scruss, 2025-01
    # MicroPython
    # -*- coding: utf-8 -*-
    
    from machine import Pin, I2C, idle, reset, freq
    
    # from os import uname
    from sys import implementation
    from ssd1306 import SSD1306_I2C
    from time import ticks_ms, ticks_diff
    
    # %%% These are the only things you should edit %%%
    startpin = 16  # pin for trigger configured with external pulldown
    # I2C connection for display
    i2c = machine.I2C(1, freq=400000, scl=19, sda=18, timeout=50000)
    # %%% Stop editing here - I mean it!!!1! %%%
    
    
    # maps value between istart..istop to range ostart..ostop
    def valmap(value, istart, istop, ostart, ostop):
        return ostart + (ostop - ostart) * (
            (value - istart) / (istop - istart)
        )
    
    
    WIDTH = 128
    HEIGHT = 64
    TEXTSIZE = 8  # 16x8 text chars
    maxit = 120  # DO NOT CHANGE!
    # value of 120 gives roughly 10 second run time for Pico 2W
    
    # get some information about the board
    # thanks to projectgus for the sys.implementation tip
    if type(freq()) is int:
        f_mhz = freq() // 1_000_000
    else:
        # STM32 has freq return a tuple
        f_mhz = freq()[0] // 1_000_000
    sys_id = (
        implementation.name,
        ".".join([str(x) for x in implementation.version]).rstrip(
            "."
        ),  # version
        implementation._machine.split()[-1],  # processor
        "%d MHz" % (f_mhz),  # frequency
        "%d*%d; %d" % (WIDTH, HEIGHT, maxit),  # run parameters
    )
    
    p = Pin(startpin, Pin.IN)
    
    # displays I have are yellow/blue, have no pull-up resistors
    #  and have a confusing I2C address on the silkscreen
    oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)
    oled.contrast(31)
    oled.fill(0)
    # display system info
    ypos = (HEIGHT - TEXTSIZE * len(sys_id)) // 2
    for s in sys_id:
        ts = s[: WIDTH // TEXTSIZE]
        xpos = (WIDTH - TEXTSIZE * len(ts)) // 2
        oled.text(ts, xpos, ypos)
        ypos = ypos + TEXTSIZE
    
    oled.show()
    
    while p.value() == 0:
        # wait for button press
        idle()
    
    oled.fill(0)
    oled.show()
    start = ticks_ms()
    # NB: oled.pixel() is *slow*, so only refresh once per row
    for y in range(HEIGHT):
        # complex range reversed because display axes wrong way up
        cc = valmap(float(y + 1), 1.0, float(HEIGHT), 1.2, -1.2)
        for x in range(WIDTH):
            cr = valmap(float(x + 1), 1.0, float(WIDTH), -2.8, 2.0)
            # can't use complex type as small boards don't have it dammit)
            zr = 0.0
            zc = 0.0
            for k in range(maxit):
                t = zr
                zr = zr * zr - zc * zc + cr
                zc = 2 * t * zc + cc
                if zr * zr + zc * zc > 4.0:
                    oled.pixel(x, y, k % 2)  # set pixel if escaped
                    break
        oled.show()
    elapsed = ticks_diff(ticks_ms(), start) / 1000
    elapsed_str = "%.1f s" % elapsed
    # oled.text(" " * len(elapsed_str), 0, HEIGHT - TEXTSIZE)
    oled.rect(
        0, HEIGHT - TEXTSIZE, TEXTSIZE * len(elapsed_str), TEXTSIZE, 0, True
    )
    
    oled.text(elapsed_str, 0, HEIGHT - TEXTSIZE)
    oled.show()
    
    # we're done, so clear screen and reset after the button is pressed
    while p.value() == 0:
        idle()
    oled.fill(0)
    oled.show()
    reset()
    
    

    (also here: benchmark Mandelbrot set (aka Brooks-Matelski set) on OLED – MicroPython)

    I will add more tests as I get to wiring up the boards. I have so many (too many?) MicroPython boards!

    Results are here: MicroPython Benchmarks