Lukas König EASy Agent Simulation – Tutorial Aufgabe0(Installation,fallsnichtvorhanden) a) Installiere Java 8 (JDK, neueste Version). http://www.oracle.com/technetwork/java/javase/downloads/index‐jsp‐138363.html#javasejdk b) Installiere Eclipse Luna (oder neuer „for Java Developers“) durch Herunterladen und Entpacken. http://www.eclipse.org/downloads c) Einen Benutzernamen *username* bei Sourceforge (https://sourceforge.net) anlegen und diesen an [email protected] schicken. d) Checke das EAS‐Framework in Eclipse aus (s.a. Eclipse‐Abbildung am Ende des Tutorials): Die GIT‐Perspektive öffnen (Window ‐> Open Perspective ‐> Other ‐> Git). Auf den Button "Clone a Git Repository... usw." klicken. Als URI das hier eingeben: ssh://*username*@git.code.sf.net/p/easyagentsimulation/code Sourceforge Passwort eingeben. Auf Next klicken. Unter "Projects" das Häkchen bei "Import all existing projects after clone finishes" anklicken. Finish anklicken und warten... Nach dieser Prozedur ist ein EAS‐Projekt vorhanden (in Java‐Perspektive wechseln). In dieses können eigene Implementierungen „commitet“, also an den Server geschickt werden: Rechte Maustaste auf das Projekt im „package explorer“ Team Commit… Eine sinnvolle Beschreibung der Änderungen als „message“ eingeben. Commit and Push (danach überprüfen, ob „Pushed to code – origin” erscheint). Ebenso können die neuesten Änderungen anderer Nutzer vom Server abgerufen werden: Rechte Maustaste auf das Projekt im „package explorer“ Team Pull e) Eigenes Package im Projekt EasyAgentSimulation erstellen: Zuerst in die hierarchische Ansicht wechseln: Im Package‐Explorer auf den weißen Pfeil klicken, der nach unten zeigt Package Representation Hierarchical Wechseln zu EasyAgentSimulation eas users courseOC ss15workingDir Rechte Maustaste auf ss15workingDir New Package „Name“: eas.users.courseOC.ss15workingDir.*MeinName*. (Sterne nicht mit eingeben!) Finish. 2 EAS – Easy Agent Simulation Tutorial f) Zum Test eine Simulation starten (ein OC‐Traffic‐Szenario). Im Package Explorer: Rechte Maustaste auf eas.startSetup.Starter Run as Java Application. Auf Tabellenzeile „MasterScheduler“ klicken und „defaultmaster‐traffic“ auswählen. Auf Tabellenzeile „Plugin“ klicken, sicherstellen, dass das Plugin „ALLROUND‐videoplugin“ ausgewählt ist. Außerdem können zum Herumspielen auch die folgenden Plugins ausgewählt werden (STRG drücken, um mehrere zu selektieren): cars‐lights (fügt Ampeln an den Kreuzungen ein), ALLROUND‐chartplugin (zeigt Diagramme an; s.a. ALLROUND‐observerAndController), ALLROUND‐liveParameterSetter (passt Parameter zur Laufzeit an – Anzahl Autos etc.), ALLROUND‐observerAndController (ruft beliebige Java‐Methoden zur Laufzeit auf; kann kombiniert mit ALLROUND‐chartplugin genutzt werden, um zur Laufzeit Diagramme zu generieren), Start Simulation (vorher können die Parameter gespeichert werden, damit sie nicht jedes Mal neu eingegeben werden müssen: „Generate and store Parameters“). Es sollten sich folgende Fenster auftun (ALLROUND‐chartplugin erst, nachdem man auf „Plot return value“ in ALLROUND‐observerAndController geklickt hat). 3 EAS – Easy Agent Simulation Tutorial Im Folgenden wollen wir als Beispiel eine Grid‐Welt erzeugen, in der Pacman‐Agenten herumfahren und Schmutz aufsammeln. Der Schmutz wird als grüne Färbung der Felder angezeigt und wird durch das Putzen der Agenten immer weniger, bis nach 500 Zyklen (etwa weil ein großes Festival stattfindet) wieder alles verschmutzt wird (dafür werden wir ein Plugin programmieren). Zum Schluss sehen wir, wie man verschiedene Simulationsergebnisse in Diagrammen darstellen kann. Bevor Sie fortfahren, empfiehlt es sich, die Beschreibung der EAS‐Architektur gelesen zu haben. Falls Sie das getan haben, bedenken Sie im Folgenden, dass die Begriffe „Plugin“ und „Scheduler“ synonym verwendet werden und dass der „Master Scheduler“, der bestimmt, wie der Startzustand der Simulation aussieht, selber ein (spezielles) Plugin ist. 4 EAS – Easy Agent Simulation Tutorial Aufgabe1(ErsteSchritte:ErzeugeeineGrid‐Welt–„EnvironmentAgentScheduler“) a) Erstelle eine Environment‐Klasse (in deinem Package), die von der Grid‐Umgebung ableitet: Rechte Maustaste auf das Package New Class. „Name“: CleaningEnvironment. „Superclass“: AbstractGridEnvironment (auswählen über “Browse”). Finish Im Editor das Generic <AgentType> ersetzen durch <AbstractAgent2D<?>> Im Editor: Durch Klick auf roten Fehlermarker Konstruktor generieren lassen („add constructor…“; es werden verschiedene angeboten, der mit den wenigsten Parametern ist in Ordnung). Im Editor: Durch Klick auf das gelbe Warndreieck Serial Version ID für die Klasse erstellen („add default serial version ID“). Diese Aktion sollte auch bei allen folgenden Klassen durchgeführt werden. b) Erstelle eine Agent‐Klasse, die vom schönen Pacman‐Agenten ableitet: Rechte Maustaste auf das Package New Class. „Name“: CleaningAgent. „Superclass“: PacmanAgent2D (auswählen über “Browse”). Finish. Im Editor: Durch Klick auf roten Fehlermarker Konstruktor generieren lassen (der einfachste ist wieder ok). c) Erstelle eine Scheduler‐Klasse (diese verbindet den Taktgeber der Simulation mit der Umgebung): Rechte Maustaste auf das Package New Class. „Name“: CleaningScheduler. „Superclass“: AbstractDefaultMaster (auswählen über “Browse”). Finish. Im Editor, in Text AbstractDefaultMaster<Env>: Ersetze <Env> durch <CleaningEnvironment>. Im Editor: Durch Klick auf roten Fehlermarker Methoden generieren („add unimplemented methods…“). Der Scheduler wird über eine ID angesprochen und gibt eine Liste von zu simulierenden Umgebungen zurück. Im Editor, in der Methode id() folgende Scheduler‐ID zurückgeben (*MeinName* ersetzen): return "*MeinName*.cleaning"; Die Methode generateRunnables(…) muss eine Liste von Umgebungen zurückgeben. o Hier erzeugen wir nur eine einzige Umgebung für diese Liste: CleaningEnvironment env = new CleaningEnvironment(29, 29, false); (Die Umgebung hat die ID 0, besteht aus einer Matrix von 29 x 29 Feldern und ist KEINE Torus‐Welt (dies bestimmt der letzte Parameter „false“)). o Füge 29 Agenten an zufälligen Positionen hinzu: Random rand = new Random(params.getSeed()); for (int i = 0; i < 29; i++) { env.addCollidingAgent(new CleaningAgent(i, env, params), new Vector2D( rand.nextInt(env.getWidth()), rand.nextInt(env.getHeight())), 0, new Vector2D(0.5, 0.5)); } o Die Umgebung wird folgendermaßen als Liste zurückgegeben: return new CleaningEnvironment[] {env}; o Durch STRG‐SHIFT‐O müssen eventuell die Importe organisiert werden, auch im Folgenden. d) Starte zum Test den erzeugten Scheduler: Zum Programmstart sollte jetzt Drücken auf grüne PLAY‐Taste genügen (sonst wie in Aufgabe 0‐f). „Find new plugins…“ anklicken und ohne „hidden“ plugins suchen (jetzt wird das neue Plugin angezeigt). Auf Tabellenzeile „MasterScheduler“ klicken und „*MeinName*‐cleaning“ auswählen. Auf Tabellenzeile „Plugin“ klicken, sicherstellen, dass das Plugin „ALLROUND‐videoplugin“ ausgewählt ist. Start Simulation (vorher evtl. wieder Parameter speichern: „Generate and store Parameters“). 5 EAS – Easy Agent Simulation Tutorial Aufgabe2(Putzszenario:ErzeugeeineSchmutzklasse,gibdenAgentenReinigungsfähigkeiten) a) Erzeuge eine Schmutz‐Klasse, die das Interface „GridObject“ implementiert. (Schmutz ist KEIN Agent!) Im Package Explorer: Rechte Maustaste auf eigenes Package New Class. „Name“: Dirt. „Interfaces“: GridObject (auswählen über “Add”). Finish. Schmutzwert speichern: Erzeuge eine reelle Variable schmutzwert und zugehörige Getter und Setter: private double schmutzWert = 0; public double getSchmutzWert() { return this.schmutzWert; } public void setSchmutzWert(double schmutz) { this.schmutzWert = schmutz; } Visualisierung: Ersetze Methode getAgentShape() durch folgenden Code („Schmutz“ erhält quadratische Form): private Polygon2D shape; @Override public Polygon2D getAgentShape() { if (shape == null) { shape = new Polygon2D(); shape.add(new Vector2D(0.5, 0.5)); shape.add(new Vector2D(-0.5, 0.5)); shape.add(new Vector2D(-0.5, -0.5)); shape.add(new Vector2D(0.5, -0.5)); } return shape; } Visualisierung: Gib in Methode getAgentColor() folgende Farbe zurück (je schmutziger, desto grüner): return new Color( 255 - (int) schmutzWert, 255, 255 - (int) schmutzWert, (int) (schmutzWert / 2.5)); Erzeuge Methoden zum Abfragen der Schmutzwerte der Umgebung von „außerhalb“: o Wechsle zur Klasse CleaningEnvironment o Füge folgende Methode ein zum Abrufen des Schmutzes auf einem Feld (x, y): public Dirt getDirt(double x, double y) { List<GridObject> liste = this.getFieldPosition(x, y); for (GridObject go : liste) { if (go.getClass().isInstance(new Dirt())) { return (Dirt) go; } } return null; } 6 EAS – Easy Agent Simulation Tutorial o Füge folgende Methode ein zum Abrufen der Schmutzsumme der ganzen Umgebung. public double getSumDirt() { double sum = 0; for (int x = 0; x < this.getGridWidth(); x++) { for (int y = 0; y < this.getGridWidth(); y++) { sum += this.getDirt(x, y).getSchmutzWert(); } } return sum; } Hinweis: Statt der Dirt‐Klasse kann auch eas.simulation.spatial.sim2D.gridSimulation.standardGridObjects.GridDoubleValue verwendet werden. Dort ist die gesamte o.a. Funktionalität (und noch etwas mehr) bereits implementiert. b) Füge Schmutzflächen zu Umgebung hinzu <Danach kann gestartet werden, um die Funktionalität zu testen, es müsste die Umgebung mit Schmutz bedeckt sein und einige Pacmen enthalten>: Folgende Methode muss im CleaningScheduler hinzugefügt werden: @Override public void runBeforeSimulation(CleaningEnvironment umg, ParCollection params) { super.runBeforeSimulation(umg, params); Random rand = new Random(params.getSeed()); CleaningEnvironment env = umg; for (int i = 0; i < env.getWidth(); i++) { for (int j = 0; j < env.getHeight(); j++) { Dirt dirt = new Dirt(); dirt.setSchmutzWert(rand.nextDouble() * 250); env.addGridObject(dirt, i, j); } } } c) Erzeuge Schmutzsensor, der Schmutz aus der Von‐Neumann‐Nachbarschaft des Agenten als Liste zurückgibt: Im Package Explorer: Rechte Maustaste auf eigenes Package New Class. „Name“: CleaningSensor. „Superclass“: GenericSensor (auswählen über “Browse”). Finish. Im Editor: Ersetze die Generic‐Parameter in spitzen Klammern <…, …, …> o ReturnType durch LinkedList<Double>, o Environment durch CleaningEnvironment, o Agent durch AbstractAgent2D<?>. Durch Klick auf roten Fehlermarker Methoden generieren lassen („add unimplemented methods…“). Methode id(), über die der Sensor aufgerufen wird, gibt folgende ID zurück: return "Dirty"; Methode sense(…) gibt Liste der Schmutzwerte aus Von‐Neumann‐Nachbarschaft zurück: Vector2D pos = agent.getAgentPosition(); LinkedList<Double> liste = new LinkedList<Double>(); liste.add(env.getDirt(pos.x, pos.y).getSchmutzWert()); liste.add(env.getDirt(pos.x, pos.y ‐ 1).getSchmutzWert()); liste.add(env.getDirt(pos.x ‐ 1, pos.y).getSchmutzWert()); liste.add(env.getDirt(pos.x, pos.y + 1).getSchmutzWert()); liste.add(env.getDirt(pos.x + 1, pos.y).getSchmutzWert()); return liste; Durch Klicken auf roten Fehlermarker bei „Vector2D“ Vektor importieren („Import Vector2D“) <Wenn der erste Teil von Schritt e) vorgezogen wird, kann danach gestartet werden, um die Funktionalität zu testen> 7 EAS – Easy Agent Simulation Tutorial d) Erzeuge einen Aktuator, der auf dem aktuellen Feld reinigt und dann zum verschmutztesten Feld aus der Von‐Neumann‐Nachbarschaft (erkannt durch den Sensor) wechselt: Im Package Explorer: Rechte Maustaste auf eigenes Package New Class. „Name“: CleanAndMove. „Superclass“: GenericActuator (auswählen über “Browse”). Finish. Im Editor: Ersetze die Generic‐Parameter in spitzen Klammern <…, …> o Environment durch CleaningEnvironment, o Agent durch AbstractAgent2D<?>. Durch Klick auf roten Fehlermarker, Methoden generieren lassen („add unimplemented methods…“). Die Methode id(), über die der Aktuator aufgerufen wird gibt folgende ID zurück: return "Clean+Move"; Die Methode actuate(…) soll zuerst reinigen, dann über den Sensor das am meisten verschmutzte Feld finden und dorthin gehen. o Reinigen: Vector2D pos = agent.getAgentPosition(); Dirt dirt = env.getDirt(pos.x, pos.y); if (dirt.getSchmutzWert() > 10) { double toClean = dirt.getSchmutzWert() / 1.2; dirt.setSchmutzWert(toClean); } o Zu nächstem Feld gehen: Vector2D position = agent.getAgentPosition(); @SuppressWarnings("unchecked") LinkedList<Double> dirty = (LinkedList<Double>) agent.sense("Dirty"); double max = dirty.get(0); double maxIndex = 0; for (int i = 1; i < dirty.size(); i++) { if (dirty.get(i) > max) { max = dirty.get(i); maxIndex = i; } } if (maxIndex == 1) { // hoch. agent.setAgentPosition(new Vector2D(position.x, position.y ‐ 1)); agent.setAgentAngle(180); } if (maxIndex == 2) { // links. agent.setAgentPosition(new Vector2D(position.x ‐ 1, position.y)); agent.setAgentAngle(90); } if (maxIndex == 3) { // runter. agent.setAgentPosition(new Vector2D(position.x, position.y + 1)); agent.setAgentAngle(0); } if (maxIndex == 4) { // rechts. agent.setAgentPosition(new Vector2D(position.x + 1, position.y)); agent.setAgentAngle(270); } if (maxIndex == 0) {} // Keine Bewegung. 8 EAS – Easy Agent Simulation Tutorial e) Füge den neu erzeugten Sensor und den neu erzeugten Aktuator zu dem Cleaning‐Agenten hinzu: Im Konstruktor von CleaningAgent müssen folgende Zeilen hinzugefügt werden: this.addSensor(new CleaningSensor()); this.addActuator(new CleanAndMove()); f) Steuere in jedem Simulationsschritt den Aktuator an Füge im Agenten folgende Methode ein: @Override public void step(Wink simTime) super.step(simTime); this.actuate("Clean+Move"); } { Starte wie in Aufgabe 1. <Die Pacmen sollten jetzt herumfahren und Schmutz aufsammeln.> Aufgabe3(PluginzumVerstreuenvonSchmutzalle500Zyklen) a) Erstelle eine Plugin‐Klasse in deinem Package, die von der Klasse AbstractDefaultPlugin ableitet und auf der Grid‐Umgebung aufbaut: Rechte Maustaste auf das Package New Class. „Name“: CleaningPlugin. „Superclass“: AbstractDefaultPlugin (auswählen über “Browse”). Finish. Im Editor: Ersetze <T> durch <CleaningEnvironment>. Durch Klick auf roten Fehlermarker Methoden generieren lassen („add unimplemented methods…“). b) Es müssen im Plugin nur zwei Methoden implementiert werden: Die Methode id() kann bspw. folgendes zurückgeben (ersetze *MeinName*): return "*MeinName*.MakeItDirty"; Die Methode runDuringSim(…) wird in jedem Simulationszyklus aufgerufen und enthält folgendes: Random rand = new Random(); if (currentSimTime.getLastTick() % 500 == 499) { for (int i = 0; i < env.getWidth(); i++) { for (int j = 0; j < env.getHeight(); j++) { env.getDirt(i, j).setSchmutzWert(rand.nextDouble() * 250); } } } c) Starten wie in Aufgabe 1, nur dass zusätzlich zu ALLROUND‐videoplugin das Plugin *MeinName*.MakeItDirty ausgewählt wird (Strg drücken während der Auswahl; vorher neue Plugins finden). <Alle 500 Zyklen neuer Schmutz.> Aufgabe4(DarstellenvonDiagrammenzurLaufzeit) Diagramme erstellen ist (inzwischen) sehr einfach und kann auf zwei Arten erfolgen. Für beide muss das Plugin „ALLROUND‐chartplugin“ eingebunden werden. 1) Diagramme Darstellen zur Laufzeit: Durch Einbinden des Plugins „ALLROUND‐observerAndController“ kann während der Simulation jede Methode des Environments oder jeweils der Agenten (je nachdem, was selektiert ist) aufgerufen oder, wenn der Rückgabewert als Zahl interpretiert werden kann, als Diagramm geplottet werden (Klick auf „Plut return value“). Das geht allerdings bisher nur mit Liniendiagrammen. Testen kann man das zum Beispiel mit der Methode getSumDirt() des Environments. 2) Diagramme aus dem Programm heraus erzeugen: Für diese Methode muss wieder programmiert werden, aber nicht viel. Im Wesentlichen müssen wiederholt Events erzeugt werden, die Informationen zu einem Datenpunkt enthalten. Durch Angabe einer Diagramm‐ID und einer Serien‐ID wird der Datenpunkt entweder 9 EAS – Easy Agent Simulation Tutorial einem schon vorhandenen Diagramm zugeordnet oder es wird ein neues Diagramm erzeugt. Beispielsweise können folgende Zeilen dem Programm hinzugefügt werden: EASEvent event = new ChartEvent("MyChart", "MySeries", env.getSumDirt()); env.getSimTime().broadcastEvent(event); Eine gute Stelle dafür ist bspw. die Methode runDuringSimulation, die der CleaningScheduler erbt: @Override public synchronized void runDuringSimulation(CleaningEnvironment env, Wink currentTime, ParCollection params) { super.runDuringSimulation(env, currentTime, params); EASEvent event = new ChartEvent("MyChart", "MySeries", env.getSumDirt()); env.getSimTime().broadcastEvent(event); } Standardmäßig zeigt ALLROUND‐chartplugin die Diagramme zur Laufzeit an, man kann sie aber auch „im Stillen“ erzeugen und durch folgende Zeilen in eine PDF‐Datei speichern. EASEvent event2 = new ChartEventStoreAsPDF("MyChart", new File(*filename*)); env.getSimTime().broadcastEvent(event2); Allerdings sollte das nicht in jedem Simulationstick passieren, sondern beispielsweise am Ende der Simulation, was durch Einsetzen in die vom CleaningScheduler geerbte Methode runAfterSimulation erreicht werden kann. d) Starten wie in Aufgabe 1, nur dass zusätzlich zu ALLROUND‐videoplugin und *MeinName*.MakeItDirty das Plugin ALLROUND‐chartplugin ausgewählt wird (Strg drücken während der Auswahl). <Von Beginn an sollte nun ein Fenster mit einem Diagramm sichtbar sein, in dem die Schmutzsumme der Umgebung angezeigt wird.> Aufgabe5(ErstellenvonProgrammparametern) Wir wollen nun die Anzahl der Reinigungs‐Pacmen variabel machen, sodass sie vor Programmstart durch einen Benutzer eingegeben werden können. EAS hat dafür eine Klasse ParCollection, die einen Push‐Service anbietet, bei dem man einen Parameter anmelden kann, damit immer, wenn er von außen durch einen Benutzer verändert wird, auch eine Veränderung der entsprechenden Variable im Programm initiiert wird. Wir erstellen eine Klasse ParsCleaning, die von keiner anderen Klasse erben muss (es muss im Allgemeinen auch keine eigene Klasse sein, sondern könnte bspw. auch im CleaningScheduler implementiert werden). Darin implementieren wir folgenden Code: Wir erzeugen eine Liste von Parametern (Typ SingleParameter; dabei melden wir die Klasse ParsCleaning durch den Wert ParsCleaning.class im Konstruktor des Parameters für den Push‐Service an): public static LinkedList<SingleParameter> getParameters() { LinkedList<SingleParameter> list = new LinkedList<>(); list.add(new SingleParameter( "numAgents", // Name des Parameters. Datatypes.integerRange(10, 100), // Datentyp: Int‐Werte zwischen 10 und 100. 10, // Standardwert. ParsCleaning.class)); // Listener‐Klasse für den Push‐Service. return list; } S 10 EAS – Easy Agent Simulation Tutorial Nun erstellen wir eine Variable mit genau dem gleichen Namen, wie oben der des Parameters und die entsprechenden Getter und Setter (zu beachten ist, dass in Java bei Booleschen Variablen der Getter mit is… gebildet wird, was hier aber keine Rolle spielt): private static int numAgents; public static int getNumAgents() { return numAgents; } public static void setNumAgents(int numAgents) { ParsCleaning.numAgents = numAgents; } Der Push‐Service wird später den Setter nutzen, um den Parameter immer dann zu aktualisieren, wenn der Benutzer ihn verändert, vor allem aber beim Programmstart. Beachten Sie, dass alle Methoden und Variablen in der Klasse ParsCleaning statisch sind, denn ein Programmparameter hat zu jedem Zeitpunkt des Programmlaufs nur einen Wert, muss also nicht in verschiedenen Objekten verschiedene Werte annehmen. Nun bauen wir den neuen Parameter in unseren vorhandenen Code ein. Dazu muss die Klasse ParCollection zunächst einmal wissen, dass es einen neuen Parameter überhaupt gibt. Wir implementieren dafür die folgende geerbte Methode im CleaningScheduler, die dafür sorgt, dass der Parameter (bzw. die ganze Liste, falls noch weitere eingefügt werden) weitergeleitet wird: @Override public List<SingleParameter> getParameters() { List<SingleParameter> list = super.getParameters(); list.addAll(ParsCleaning.getParameters()); return list; } Nun müssen wir noch die neue Variable beim Initialisieren der Simulation verwenden, um die Anzahl der Agenten festzulegen. Dazu ersetzen wir in der Methode generateRunnables den Wert 29 durch ParsCleaning.getNumAgents() Bzw. entsteht folgender Code für die ganze Schleife: for (int i = 0; i < ParsCleaning.getNumAgents(); i++) { env.addCollidingAgent(new CleaningAgent(i, env, params), new Vector2D( rand.nextInt(env.getWidth()), rand.nextInt(env.getHeight())), 0, new Vector2D(0.5, 0.5)); } <Nun sollte der Parameter „numAgents“ im Starter‐Menü auftauchen und seine Einstellung die Anzahl der Agenten beeinflussen.> (Beachten Sie, dass in diesem Fall der Parameter nur zu Beginn der Simulation in der o.a. Schleife verwendet wird. Das heißt, dass er zwar durch den Push‐Service immer aktuell gehalten wird, etwa, wenn der Benutzer ihn durch den ALLROUND‐liveParameterSetter verändert, dass das aber später keine Auswirkung mehr hat, weil er nicht mehr abgefragt wird. Sie können sich selbst überlegen, wie eine „reset“‐Methode aussehen könnte, und wie sie durch den Aufruf des Setters in ParsCleaning getriggert werden könnte. Siehe auch nachfolgende Abbildung.) 11 EAS – Easy Agent Simulation Tutorial 12 EAS – Easy Agent Simulation Tutorial Eclipse Standardansicht 13 EAS – Easy Agent Simulation Tutorial Hierarchie der wichtigsten Agenten‐ und Environment‐Klassen in EAS 14 EAS – Easy Agent Simulation Tutorial EAS‐Arbeitsweise:
© Copyright 2024