Multithreading in python with matplotlib

Question:

Alright, I think it’s finally time to call on every python user’s best friend: Stack Overflow.

Bear in mind that I am at a bit of a beginner level in python, so obvious solutions and optimisations might not have occurred to me.

My Error:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow drag regions should only be invalidated on the Main Thread!'
abort() called
terminating with uncaught exception of type NSException

There is a stack overflow question on this error as well but under a different context but my attempts to fix the error using backend "Agg" with matplotlib didn’t work. Their were no longer any trading errors but matplotlib errors which didn’t make any sense (as in they shouldn’t have been there) appeared. This error was described in the link above in the apple developer support page and I couldn’t implement those solutions either (prob cuz im a bad programmer).

Note: I’m using macOS, and this error only seems to happen on macOS with matplotlib.
Also the error shouldn’t happen in my case because I’m trying to allow only the first thread to access the display function (which might be the part which is going wrong?)

I’ve been making this little evolution simulator (a little similar to this one) which I’m still on the starting stage of. Here is the code:

import random
import math
from matplotlib import pyplot as plt
import threading



class Element:
    default_attr = {
        "colour": "#000000",
        "survival": 75,
        "reproduction": 50,
        "energy": 150,
        "sensory_range": 100,
        "genetic_deviation": 5,
        "socialization": 20,
        "position": (0, 0,),
        "objective_attained": False,
        "socialization_attained": False
    }

    __slots__ = (
        "colour",
        "survival",
        "reproduction",
        "energy",
        "sensory_range",
        "aggression",
        "self_aggression",
        "genetic_deviation",
        "socialization",
        "position",
        "objective_attained",
        "socialization_attained",
    )


    def __init__(self, **attributes):
        Element.__slots__ = tuple((i + "s" for i in self.__slots__))
        self.default_attr.update(attributes)
        for key, value in self.default_attr.items():
            setattr(self, key, value)
        for key, value in self.default_attr.items():
            try:
                setattr(Element, key + "s", getattr(Element, key + "s") + [value])
            except AttributeError:
                setattr(Element, key + "s", [value])
                

    def move(self, objective_colour, delay, height, width, energy=None):
        if energy is None:
            energy = self.energy

        lock = threading.RLock()
        event = threading.Event()

        objective_positions = tuple((p for i, p in enumerate(Element.positions) if Element.colours[i] == objective_colour))
        positions = tuple((p for i, p in enumerate(Element.positions) if Element.colours[i] == self.colour and p != self.position))

        objectives_in_range = []
        for objective in objective_positions:
            if ((objective[0] - self.position[0])**2 + (objective[1] - self.position[1])**2)**0.5 <= self.sensory_range:
                objectives_in_range.append([objective[0] - self.position[0], objective[1] - self.position[1]])
        objectives = tuple(sorted(objectives_in_range, key=lambda x: (x[0]**2 + x[1]**2)**0.5))

        positions_in_range = []
        for pos in positions:
            if ((pos[0] - self.position[0])**2 + (pos[1] - self.position[1])**2)**0.5 <= self.sensory_range:
                positions_in_range.append([pos[0] - self.position[0], pos[1] - self.position[1]])
        positions = tuple(sorted(positions_in_range, key=lambda x: (x[0]**2 + x[1]**2)**0.5))

        if positions:
            cluster = [0, 0]
            for pos in positions:
                cluster[0] += pos[0] + self.position[0]
                cluster[1] += pos[1] + self.position[0]
            midpoint = (cluster[0] / len(positions) - self.position[0], cluster[1] / len(positions) - self.position[1],)
            try:
                distance = 100 / (midpoint[0] ** 2 + midpoint[1] ** 2) ** 0.5 * (height if height > width else width) / 100
            except ArithmeticError:
                distance = 100
            if self.socialization <= distance:
                self.socialization_attained = True

        if self.objective_attained is False and not objectives and self.socialization_attained is False and not positions and energy > self.energy*0.5:
            direction = math.radians(random.uniform(0.0, 360.0))
            old_position = self.position
            self.position = (self.position[0] + math.sin(direction), self.position[1] + math.cos(direction),)
            if 90 <= direction <= 270:
                self.position = (self.position[0] * -1, self.position[1] * -1,)

            for i, position in enumerate(Element.positions):
                if position == old_position and Element.colours[i] == self.colour:
                    Element.positions[i] = self.position
                    break

            with lock:
                if not event.is_set():
                    display(delay, height, width)
                    event.set()
            event.clear()

            self.move(objective_colour, delay, height, width, energy - 1)

        elif self.objective_attained is False and energy > 0 and objectives:
            try:
                x, y = math.sin(math.atan(objectives[0][0] / objectives[0][1])), math.cos(math.atan(objectives[0][0] / objectives[0][1]))
                if objectives[0][1] < 0:
                    x *= -1
                    y *= -1
            except ArithmeticError:
                x, y = 1 if objectives[0][0] > 0 else -1, 0
            old_position = self.position
            self.position = tuple(map(lambda x, y: x + y, self.position, (x, y,)))

            for i, position in enumerate(Element.positions):
                if position == old_position and Element.colours[i] == self.colour:
                    Element.positions[i] = self.position
                    break

            if (self.position[0] - old_position[0] - objectives[0][0])**2 + (self.position[1] - old_position[1] - objectives[0][1])**2 <= 1:
                self.objective_attained = True
                with lock:
                    for i, position in enumerate(Element.positions):
                        if [int(position[0]), int(position[1])] == [objectives[0][0] + old_position[0], objectives[0][1] + old_position[1]] and Element.colours[i] == objective_colour:
                            Element.positions.pop(i)
                            Element.colours.pop(i)
                            break

            with lock:
                if not event.is_set():
                    display(delay, height, width)
                    event.set()
            # a little confusion here, do threads pause over here until all threads have exited the with lock statement or not? If not I need to change the line below.
            event.clear()

            if self.objective_attained is True:
                self.move(objective_colour, delay, height, width, (energy - 1) * 1.5)
            else:
                self.move(objective_colour, delay, height, width, energy - 1)

        elif self.socialization_attained is False and energy > 0 and positions and self.socialization > distance:
            try:
                x, y = math.sin(math.atan(midpoint[0] / midpoint[1])), math.cos(math.atan(midpoint[0] / midpoint[1]))
                if midpoint[1] < 0:
                    x *= -1
                    y *= -1
            except ArithmeticError:
                x, y = 1 if midpoint[0] > 0 else -1, 0
            old_position = self.position
            self.position = tuple(map(lambda x, y: x + y, self.position, (x, y,)))

            for i, position in enumerate(Element.positions):
                if position == old_position and Element.colours[i] == self.colour:
                    Element.positions[i] = self.position
                    break

            with lock:
                if not event.is_set():
                    display(delay, height, width)
                    event.set()
            event.clear()

            self.move(objective_colour, delay, height, width, energy - 1)

        else:
            for thread in globals() ["threads"]:
                thread.join()
            # a little confusion here too on whether this would wait till all threads reach this statement before joining them


def display(delay, height, width):
    x = tuple((i[0] for i in Element.positions)) + (0, width,)
    y = tuple((i[1] for i in Element.positions)) + (0, height,)
    c = tuple(Element.colours) + ("#FFFFFF",) * 2
    plt.scatter(x, y, c=c)
    plt.show()
    plt.pause(delay)
    plt.close()



r = lambda x: random.randint(0, x)
elements = tuple((Element(position=(r(200), r(200),)) for i in range(10))) + tuple((Element(position=(r(200), r(200),), colour="#FF0000") for i in range(10)))
[Element(colour="#00FF00", position=(r(200), r(200),), energy=0, reproduction=0) for i in range(20)]
globals() ["threads"] = []
for organism in elements:
    globals() ["threads"].append(threading.Thread(target=organism.move, args=("#00FF00", 0.02, 200, 200,)))
    globals() ["threads"][-1].start()

This is a big chunk of code but this is my first time using multithreading so I don’t know where the error could pop up, though I have narrowed it down to this section fs.

Sry for the eyesore, ik this is a really long question, but I would be really grateful
if u could help!

Asked By: user17301834

||

Answers:

This issue goes by a few names, the most common of which is "cross-threading". This occurs when you perform GUI operations (in your case, matplotlib calls) from non-GUI threads. This is a no-no regardless of OS.

To solve the problem, ensure that you’re making matplotlib calls from the main thread. A good starting point is on line 176: UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail.

Answered By: Basil