Python-Framework mesa - 3 Fragen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
hcshm
User
Beiträge: 48
Registriert: Dienstag 11. Februar 2020, 08:23

Zunächst erneut vielen Dank für die heutige super Fragenbeantwortung von deets. Ich habe 3 Fragen zum aktuellen Stand meines Codes. Es geht um die traverse_grid-Methode:
- Lässt sich die einmalige Ausführung eines Methoden-Kommandos nur über ein Klassenattribut realisieren (wie in meinem Code). Oder gibt es dafür "schlankere" Möglichkeiten, die die Klasse nicht unnötig "belasten"?
- Auch die coords-Liste (Liste der Koordinaten, die durchlaufen werden sollen) habe ich als Klassenattribut definiert, um sicherzustellen, dass pop den jeweils nächsten Wert aufruft. Lässt sich das auch schlanker machen, oder muss ich damit auch die Klasse "belasten"?
- Zur step-Iteration in der main-Funktion: Bislang habe ich die Anzahl der Durchläufe so gesetzt, dass ich "hinkomme". Wie bewerkstellige ich es, dass gestept wird, bis die Kommandos "abgearbeitet" sind (und nicht weiter)?
Vielen Dank im Voraus.
Hier der Code:

Code: Alles auswählen

import mesa

class Model(mesa.Model):
    """Modell mit 2 Agenten, die sich in einem Grid bewegen sollen"""
    def __init__(self, number_of_agents, width, height):
        super().__init__(number_of_agents, width, height)
        self.num_agents = number_of_agents
        self.grid = mesa.space.MultiGrid(width, height, True)
        self.schedule = mesa.time.BaseScheduler(self)

        """Nachfolgend werden 2 Agenten produziert, als Modell-Attribute definiert
         (damit  z.B. ihre Position aus dem Modell aufgerufen werden kann) und auf dem Grid platziert"""

        self.alter = Agent("alter", self)
        self.schedule.add(self.alter)
        coord = (self.random.randrange(self.grid.width), self.random.randrange(self.grid.height))
        print(f"Anfangsposition von alter: {coord}")
        self.grid.place_agent(self.alter, coord)

        self.ego = Agent("ego", self)
        self.schedule.add(self.ego)
        coord = (self.random.randrange(self.grid.width), self.random.randrange(self.grid.height))
        print(f"Anfangsposition von ego: {coord}")
        self.grid.place_agent(self.ego, coord)

    def step(self):
        """Ruft die step-Methode des Agenten auf"""
        self.schedule.step()

class Agent(mesa.Agent):
    def __init__(self, name, model):
        super().__init__(name, model)
        self.name = name
        self._state = self.move_to_corner
        self.coords = []
        self.traverse_grid_first_run = True  # Erster Durchlauf traverse_grid-Methode

    def move_to_corner(self):
        """Bewegt die beiden Agenten von ihrer Ausgangsposition in die
        linke untere Ecke"""
        x, y = self.pos
        if x > 0 or y > 0:
            new_position = (max(x - 1, 0), max(y - 1, 0))
            print(self.name, " position: ", new_position)
            self.model.grid.move_agent(self, new_position)
        else:
            self._state = self.traverse_grid

    def traverse_grid(self):
        """Agenten sollen das gesamte Grid in Schleifen durchlaufen, von links unten
        (0,0) bis in die rechte obere Ecke, zunächst Prüfung der Ausgangsposition
         für diesen Durchlauf"""
        if self.traverse_grid_first_run:
            print(self.name, ": Position zu Beginn traverse_grid: ", self.pos)
            self.traverse_grid_first_run = False  # Erster Durchlauf abgeschlossen
        "Ermittlung der Koordinaten für den Durchlauf"
        if not self.coords:
            self.coords = [(i, j) if i % 2 == 0 else (i, self.model.grid.height - 1 - j)
                  for i in range(self.model.grid.width) for j in range(self.model.grid.height)][1:]
        if self.pos != (self.model.grid.width - 1, self.model.grid.height - 1):
            new_pos = self.coords.pop(0)
            print(self.name, " position: ", new_pos)
            self.model.grid.move_agent(self, new_pos)
        else:
            self._state = self.next_method

    def next_method(self):
        print(self.name, ": Position zu Beginn next_method: ", self.pos)

    def step(self):
        """Wird von self.schedule.step aus dem Modell aufgerufen"""
        self._state()

def main():
    """Produziert ein Grid"""
    model = Model(2, 5, 5)
    for _ in range(100):
        model.step()

    print("Endposition von alter:", model.alter.pos)
    print("Endposition von ego:", model.ego.pos)

if __name__ == "__main__":
    main()

__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Was heisst denn fuer dich "belasten"? Einfach nur, dass die ein Instanz-Attribut haben? Warum ist das eine Last?

Es sind uebrigens KEINE Klassen-Attribute. Klassenattribute leben auf der Ebene der Klassen. Also zB

Code: Alles auswählen

class Foo:
    bar = "klassenattribut"
Die will man aber meistens *nicht*, weil die dann von allen Instanzen - also allen Agenten in deinem Fall - *wertgleich* geteilt werden. Attribute, die pro Instanz vorgehalten werden, heissen eben Instanz-Attribute.

Zur letzten Frage: du bestimmst, wann Ende ist. ZB ein steady state, wenn sich niemand mehr bewegt.
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@hcshm: Hier gilt natürlich auch wieder was ich zuletzt in dem anderen Thema geschrieben habe.

Die `traverse_grid()` macht zu viel und das ist teilweise komisch formuliert. Zum Beispiel wenn man sich ``if not self.coords`` anschaut, wird das ja irgendwann wieder „wahr“ wenn man mit `pop()` da immer Elemente entfernt. Nur das das im Code dann wieder nicht wahr werden kann, weil der Code beim letzten `pop()` die Abbruchbedingung für diesen Zustand trifft. Also ist der Test ob `self.coords` leer ist schon im Test ob es sich um den ersten Aufruf dieses Zustands handelt mit enthalten. *Das* würde ich hier nicht als Flag speichern, sondern einfach als zusätzlichen Zustand. Falls man das nicht möchte, braucht man kein zusätzliches Flag, sondern kann einfach `self.coords` dafür verwenden.

Das mit der Liste und ``pop(0)`` ist ineffizient. Letztlich wird hier ja bis auf den ersten Wert, jeder aus der Liste der Reihe nach genommen. Statt der „list comprehension“ könnte man also einen Generatorausdruck verwenden, und bei jedem Aufruf den nächsten Wert aus dem Generator nehmen.

Zu den Fragen:

Ad 1: Den Zustand direkt über die Methoden auszudrücken ist bereits die schlanke Variante. Man sieht auch öfter eine zusätzliche Indirektion über einen Aufzählungstyp und eine `step()`-Implementierung die über diesen Wert entscheidet welche Methode aufgerufen wird, statt direkt eine Methode aufzurufen.

Ad 2: Hier könnte man das mit einem Closure (oder einer zusätzlichen Klasse) für den betroffenen Zustand lösen.

Ad 3: Man könnte Agenten die fertig sind, sich aus dem Scheduler nehmen lassen und die Simulation solange laufen lassen bis kein Agent mehr im Scheduler ist.

Code: Alles auswählen

#!/usr/bin/env python3
from functools import partial

import mesa


class Agent(mesa.Agent):
    def __init__(self, unique_id, model, name):
        super().__init__(unique_id, model)
        self.name = name
        self._state = self.move_to_corner

    def move_to_corner(self):
        """
        Bewegt den Agenten von der Ausgangsposition in die linke untere Ecke.
        """
        x, y = self.pos
        if x > 0 or y > 0:
            new_position = (max(x - 1, 0), max(y - 1, 0))
            print(self.name, "position:", new_position)
            self.model.grid.move_agent(self, new_position)
        else:
            self._state = self.start_traverse_grid

    def start_traverse_grid(self):
        print(f"{self.name}: Position zu Beginn traverse_grid: {self.pos}")
        coords = (
            (i, j) if i % 2 == 0 else (i, self.model.grid.height - 1 - j)
            for i in range(self.model.grid.width)
            for j in range(self.model.grid.height)
        )
        first_position = next(coords)
        assert first_position == self.pos
        self._state = partial(self.traverse_grid, coords)
        self.step()

    def traverse_grid(self, coords):
        """
        Agenten sollen das gesamte Grid in Schleifen durchlaufen, von links
        unten (0,0) bis in die rechte obere Ecke, zunächst Prüfung der
        Ausgangsposition für diesen Durchlauf.
        """
        try:
            position = next(coords)
        except StopIteration:
            self.model.schedule.remove(self)
        else:
            print(f"{self.name} position: {position}")
            self.model.grid.move_agent(self, position)

    def step(self):
        self._state()


class Model(mesa.Model):
    """
    Modell mit 2 Agenten, die sich in einem Grid bewegen sollen.
    """

    def __init__(self, width, height):
        super().__init__(width, height)
        self.grid = mesa.space.MultiGrid(width, height, True)
        self.schedule = mesa.time.BaseScheduler(self)
        self.agents = []
        for name in ["alter", "ego"]:
            agent = Agent(self.next_id(), self, name)
            self.schedule.add(agent)
            coord = (
                self.random.randrange(self.grid.width),
                self.random.randrange(self.grid.height),
            )
            print(f"Anfangsposition von alter: {coord}")
            self.grid.place_agent(agent, coord)
            self.agents.append(agent)

    def step(self):
        if self.schedule.get_agent_count():
            self.schedule.step()
        else:
            self.running = False


def main():
    model = Model(5, 5)
    model.run_model()
    for agent in model.agents:
        print(f"Endposition von {agent.name}: {agent.pos}")


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
hcshm
User
Beiträge: 48
Registriert: Dienstag 11. Februar 2020, 08:23

Vielen Dank, blackjack! Ich sitze weiterhin ungläubig vor Deinem Code und staune, was Ausnahme-Experten in Python bewerkstelligen können. Besonders eindrucksvoll fand ich den Code für den traverse_grid-Part: partielle Methodenimplementierung, Iterierung/Ausstieg via try/except/else. Kurzum: Dein Code ist für mich ein Lehrstück, das mir zahlreiche wichtige neue Einsichten vermittelt hat.
An Rande: im Modell - also ganz am Anfang des Durchlaufs Deines Codes - musste das Instanzen-Attribut agents in eine Instanzen-Variable umdefiniert werden, damit der Code läuft (Attribut agents "not settable"). Bedeutet das etwa, dass Du diesen - wie ich finde - super Code runtergeschrieben hast, ohne ihn durchlaufen zu lassen? Selbst bei einem Ausnahme-Experten wäre es für mich einfach unvorstellbar, dass so etwas möglich ist ... Erneut vielen Dank!!
Benutzeravatar
Dennis89
User
Beiträge: 1157
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

@hcshm das Problem ist/war das `mesa.Model` bereits eine Eigenschaft `agents` hat und da wurde jetzt versucht dieser Eigenschaft eine leere Liste zuzuweisen. Hier der Code zu der erwähnten Eigenschaft:
https://github.com/projectmesa/mesa/blo ... del.py#L74

Ich weis nicht was du geändert hast, aber es genügt wenn man sich für `agents` aus __blackjack__'s Code einen anderen Namen überlegt.

Grüße
Dennis

P.S. Das ungläubige sitzen vor Codes die hier gepostet werden, kenne ich zu gut :D
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Grrr, bei mir funktioniert's mit Mesa 2.1.5 noch, da hatte `Model` noch keine `agents`. Die gibt's seit vier Tagen in Version 2.2.0.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
hcshm
User
Beiträge: 48
Registriert: Dienstag 11. Februar 2020, 08:23

Vielen Dank, Dennis, vielen Dank, blackjack, für die Erläuterung!
hcshm
User
Beiträge: 48
Registriert: Dienstag 11. Februar 2020, 08:23

Vielen Dank! Meine Frage ist geklärt und kann entsprechend "abgehakt" werden.
Antworten