1

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. There were no longer any threading 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",
        "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!

user17301834
  • 443
  • 1
  • 8
  • The comments (in my code) point out my main suspects for where the problem lies, feel free to check them out (there are only 2 comments) – user17301834 Aug 04 '22 at 14:04
  • 2
    This will happen on any OS because you are performing GUI operations (Matplotlib calls) from non-GUI threads, which is a no-no. Good starting point is on line 176: `UserWarning: Starting a Matplotlib GUI outside of the main thread will likely fail.` – Basil Aug 04 '22 at 15:52
  • Oooh, thx, that really clears things up – user17301834 Aug 04 '22 at 16:07

1 Answers1

1

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.

Basil
  • 659
  • 4
  • 11
  • This solved the problem, thx! The problem I'm having now is that the main thread needs to run in concurrence with all others and despite making all other threads daemon threads (which I'm not sure will help), the main thread waits till all others complete their tasks. – user17301834 Aug 04 '22 at 17:42
  • 1
    Well, how would you expect it to run? The main thread is typically where the GUI is created and managed. Without it, you don't have a responsive UI. Each worker threads' main task should be to perform calculations and then to send state updates to the UI. This is typically done via thread-safe messages, which is beyond the scope of this question. You're pretty deep in multithreading for a beginner; if you're willing to go deeper, I suggest learning GUI frameworks like PyQt/PySide. They provide proven and robust mechanisms for handling multithreaded UIs, though it's a steep learning curve. – Basil Aug 04 '22 at 17:55
  • 1
    Now that clears up many things. This is actually my first time using multithreading (in any language) and it was a complete brainf**k. I'll make sure to check out PyQt/PySide since I've got a feeling I'm gonna need them, but thx for the help – user17301834 Aug 04 '22 at 18:17