diff --git a/SimulationAnalyser/main.py b/SimulationAnalyser/main.py index d6239c8241ebd6dc560a7967bb6286913879db5a..5c1a51010028ef96cf6b3e896a4eb0710b024d85 100644 --- a/SimulationAnalyser/main.py +++ b/SimulationAnalyser/main.py @@ -17,10 +17,17 @@ LEARNING_STR = "LEARNING" FOLDER_PATH_LOGS_TO_ANALYSE = "../impulse/Assets/SimulationLogs" FOLDER_PATH_SAVE_GRAPH_ANALYSIS = "./Analysis" +FEATURES = [ + "scoreDelta", "distanceDelta", "obstacleDelta", "explorationEfficiency", + "variationInDirection", "velocityMagnitude", "collectibleCollectEfficiency", + "obstacleAvoidanceEfficiency", "timeSinceLastCollectible", "timeSinceLastHitObstacle" + ] + def compute_velocity_magnitude(velocity): return np.sqrt(velocity.get("x", 0)**2 + velocity.get("y", 0)**2 + velocity.get("z", 0)**2) def auto_label(entry): + # TO REWORK TO BE BETTER or WHAT KIND OF LEARNING ML USE ? is_stationary = entry["distanceDelta"] < 0.01 and entry["velocityMagnitude"] < 0.01 no_progress = entry["scoreDelta"] < 0.5 and entry["collectibleCollectEfficiency"] < 0.1 if is_stationary or no_progress: @@ -71,18 +78,19 @@ def run_unsupervised_clustering(df, output_folder): centers = pd.DataFrame(kmeans.cluster_centers_, columns=features) plateau_cluster = centers["scoreDelta"].idxmin() df["inferred_label"] = df["cluster"].apply(lambda c: PLATEAU_STR if c == plateau_cluster else LEARNING_STR) - plt.figure(figsize=(10, 6)) - for label, color in zip([PLATEAU_STR, LEARNING_STR], ["red", "green"]): - subset = df[df["inferred_label"] == label] - plt.plot(subset["timestamp"], subset["scoreDelta"], label=label, color=color, alpha=0.6) - plt.title("Unsupervised Learning: Inferred Labels Over Time") - plt.xlabel("Timestamp (s)") - plt.ylabel("Score Delta") - plt.legend() - plt.grid(True) - plt.tight_layout() - plt.savefig(os.path.join(output_folder, "unsupervised_inferred_labels_over_time.png")) - plt.close() + # plt.figure(figsize=(10, 6)) + # for label, color in zip([PLATEAU_STR, LEARNING_STR], ["red", "green"]): + # subset = df[df["inferred_label"] == label] + # plt.plot(subset["timestamp"], subset["scoreDelta"], label=label, color=color, alpha=0.6) + # plt.title("Unsupervised Learning: Inferred Labels Over Time") + # plt.xlabel("Timestamp (s)") + # plt.ylabel("Score Delta") + # plt.legend() + # plt.grid(True) + # plt.tight_layout() + # plt.savefig(os.path.join(output_folder, "unsupervised_inferred_labels_over_time.png")) + # plt.close() + def compare_manual_vs_kmeans(df, output_folder): cm = confusion_matrix(df["label"], df["inferred_label"], labels=[PLATEAU_STR, LEARNING_STR]) @@ -101,11 +109,7 @@ def plot_decision_tree(X, y, feature_names, output_folder): plt.close() def plot_feature_evolution(df, output_folder): - features = [ - "scoreDelta", "distanceDelta", "obstacleDelta", "explorationEfficiency", - "variationInDirection", "velocityMagnitude", "collectibleCollectEfficiency", - "obstacleAvoidanceEfficiency", "timeSinceLastCollectible", "timeSinceLastHitObstacle" - ] + features = FEATURES num_features = len(features) cols = 2 rows = (num_features + 1) // cols @@ -130,11 +134,7 @@ def main(): df = extract_features_labels_from_logs(FOLDER_PATH_LOGS_TO_ANALYSE) run_unsupervised_clustering(df, FOLDER_PATH_SAVE_GRAPH_ANALYSIS) compare_manual_vs_kmeans(df, FOLDER_PATH_SAVE_GRAPH_ANALYSIS) - features = [ - "scoreDelta", "distanceDelta", "obstacleDelta", "explorationEfficiency", - "variationInDirection", "velocityMagnitude", "collectibleCollectEfficiency", - "obstacleAvoidanceEfficiency", "timeSinceLastCollectible", "timeSinceLastHitObstacle" - ] + features = FEATURES X = df[features] y = df["inferred_label"] plot_decision_tree(X, y, features, FOLDER_PATH_SAVE_GRAPH_ANALYSIS) diff --git a/SimulationGraph/Simulation-14-04-2025-10-14-58.json_full_analysis.png b/SimulationGraph/Simulation-14-04-2025-10-14-58.json_full_analysis.png new file mode 100644 index 0000000000000000000000000000000000000000..f36204fc4d2e8fd0f674668ec576b71b5d7a2482 Binary files /dev/null and b/SimulationGraph/Simulation-14-04-2025-10-14-58.json_full_analysis.png differ diff --git a/SimulationGraph/Simulation-14-04-2025-10-14-58.json_report.md b/SimulationGraph/Simulation-14-04-2025-10-14-58.json_report.md new file mode 100644 index 0000000000000000000000000000000000000000..e8dedf7528c9d86b0a92998c8a59fa48977c059f --- /dev/null +++ b/SimulationGraph/Simulation-14-04-2025-10-14-58.json_report.md @@ -0,0 +1,180 @@ +# Simulation report +**File**: `Simulation-14-04-2025-10-14-58.json` +**Report generation date**: 2025-04-14 10:47:48 +**Seed**: `723505677` + +## Resume +- Parcour length : **624.22** units +- Collectibles collected : **41 / 41** +- Obstacles hit : **8 / 43** +- Distance made : **670.97** units (107.5%) + +## Performance over time +- Duration : **249.98 s** +- Speed average : **2.70 units/s** +- Time average between collectibles : **5.84 s** +- Time average between collisions : **38.14 s** + +## Efficacity +- Collectibles : **100.00% (raw)** | **100.00% (smoothed)** +- Obstacles : **81.40% (raw)** | **81.40% (smoothed)** +- Exploration : **107.49% (raw)** | **107.43% (smoothed)** + +## Behaviour analysis +- Directionnal variation (moving average) : **0.66 changes** + - Smooth and controlled exploration. + +## Parameters of the simulation +- `Left` = **3.0** +- `Large Left` = **5.0** +- `Up` = **2.0** +- `Right` = **5.0** +- `Large Right` = **7.0** +- `Down` = **3.0** +- `Speed` = **0.0** +- `Movement Sensibility` = **3.0** +- `Rotation Sensibility` = **1.0** +- `Obstacle Percentage` = **50.0** +- `Collectible Preferences` = **2.0** +- `Collectible Height` = **0.0** + +## Spawned elements +### Road Parts +- **Start 0** @ (-0.2, 0.0, 2.9) +- **Straight 1** @ (-0.2, 0.0, 14.9) +- **Straight 2** @ (-0.2, 0.0, 22.9) +- **Straight 3** @ (-0.2, 0.0, 30.9) +- **Straight 4** @ (-0.2, 0.0, 38.9) +- **Right 5** @ (0.8, 0.0, 46.4) +- **Up 6** @ (21.8, 0.0, 46.9) +- **Right 7** @ (41.3, 8.0, 47.9) +- **Up 8** @ (41.8, 8.0, 26.9) +- **Straight 9** @ (43.8, 16.0, 6.9) +- **Large Left 10** @ (45.8, 16.0, -5.1) +- **Straight 11** @ (57.8, 16.0, -7.1) +- **Large Left 12** @ (69.8, 16.0, -5.1) +- **Straight 13** @ (71.8, 16.0, 6.9) +- **Large Right 14** @ (73.8, 16.0, 18.9) +- **Large Left 15** @ (89.8, 16.0, 22.9) +- **Right 16** @ (92.8, 16.0, 34.4) +- **Straight 17** @ (101.8, 16.0, 36.9) +- **Straight 18** @ (109.8, 16.0, 36.9) +- **Large Right 19** @ (121.8, 16.0, 34.9) +- **Left 20** @ (125.0, 16.0, 22.6) +- **Straight 21** @ (133.8, 16.0, 20.9) +- **Large Right 22** @ (145.8, 16.0, 18.9) +- **Straight 23** @ (147.8, 16.0, 6.9) +- **Straight 24** @ (147.8, 16.0, -1.1) +- **Large Right 25** @ (145.8, 16.0, -13.1) +- **Left 26** @ (133.5, 16.0, -16.4) +- **Straight 27** @ (131.8, 16.0, -25.1) +- **Straight 28** @ (131.8, 16.0, -33.1) +- **Straight 29** @ (131.8, 16.0, -41.1) +- **Straight 30** @ (131.8, 16.0, -49.1) +- **Down 31** @ (129.8, 7.8, -69.1) +- **Large Left 32** @ (133.8, 8.0, -93.1) +- **Straight 33** @ (145.8, 8.0, -95.1) +- **Right 34** @ (153.3, 8.0, -96.1) +- **Large Right 35** @ (153.8, 8.0, -109.1) +- **Down 36** @ (129.8, -0.2, -109.1) +- **Large Right 37** @ (105.8, 0.0, -109.1) +- **Straight 38** @ (103.8, 0.0, -97.1) +- **Straight 39** @ (103.8, 0.0, -89.1) +- **Straight 40** @ (103.8, 0.0, -81.1) +- **Right 41** @ (104.8, 0.0, -73.6) +- **Large Right 42** @ (117.8, 0.0, -73.1) +- **Large Left 43** @ (121.8, 0.0, -89.1) +- **Straight 44** @ (133.8, 0.0, -91.1) +- **Straight 45** @ (141.8, 0.0, -91.1) +- **Straight 46** @ (149.8, 0.0, -91.1) +- **Straight 47** @ (157.8, 0.0, -91.1) +- **Straight 48** @ (165.8, 0.0, -91.1) +- **Left 49** @ (174.0, 0.0, -89.9) +- **Down 50** @ (177.8, -8.2, -69.1) +- **End 51** @ (175.5, -8.0, -47.3) +### Collectibles +- **Cogwheel 0** @ (1.2, 0.7, 22.9) +- **Cogwheel 1** @ (-1.3, 0.7, 30.9) +- **Cogwheel 2** @ (1.2, 0.7, 38.9) +- **Cogwheel 3** @ (1.7, 0.7, 47.3) +- **Cogwheel 4** @ (31.7, 8.4, 50.2) +- **Cogwheel 5** @ (41.2, 8.7, 46.1) +- **Cogwheel 6** @ (42.5, 16.4, 17.0) +- **Cogwheel 7** @ (45.1, 9.3, 36.8) +- **Cogwheel 8** @ (45.1, 12.7, 26.9) +- **Cogwheel 9** @ (43.6, 16.7, 6.9) +- **Cogwheel 10** @ (57.8, 16.7, -7.3) +- **Cogwheel 11** @ (70.7, 16.7, 6.9) +- **Cogwheel 12** @ (75.3, 16.7, 16.7) +- **Cogwheel 13** @ (78.7, 16.7, 19.2) +- **Cogwheel 14** @ (90.2, 16.7, 28.9) +- **Cogwheel 15** @ (84.9, 16.7, 21.4) +- **Cogwheel 16** @ (94.5, 16.7, 34.3) +- **Cogwheel 17** @ (119.6, 16.7, 33.4) +- **Cogwheel 18** @ (115.3, 16.7, 35.3) +- **Cogwheel 19** @ (124.3, 16.7, 22.1) +- **Cogwheel 20** @ (147.6, 16.7, 6.9) +- **Cogwheel 21** @ (148.9, 16.7, -1.1) +- **Cogwheel 22** @ (147.4, 16.7, -7.0) +- **Cogwheel 23** @ (135.6, 8.7, -91.3) +- **Cogwheel 24** @ (138.7, 8.7, -95.8) +- **Cogwheel 25** @ (145.8, 8.7, -96.5) +- **Cogwheel 26** @ (152.3, 8.7, -106.9) +- **Cogwheel 27** @ (150.2, 8.7, -111.6) +- **Cogwheel 28** @ (119.8, 1.1, -109.8) +- **Cogwheel 29** @ (107.2, 0.7, -108.6) +- **Cogwheel 30** @ (106.5, 0.7, -73.7) +- **Cogwheel 31** @ (116.4, 0.7, -73.6) +- **Cogwheel 32** @ (112.2, 0.7, -70.3) +- **Cogwheel 33** @ (118.1, 0.7, -78.0) +- **Cogwheel 34** @ (126.7, 0.7, -91.8) +- **Cogwheel 35** @ (120.2, 0.7, -84.2) +- **Cogwheel 36** @ (133.8, 0.7, -90.0) +- **Cogwheel 37** @ (141.8, 0.7, -90.0) +- **Cogwheel 38** @ (157.8, 0.7, -91.3) +- **Cogwheel 39** @ (174.5, -6.9, -59.1) +- **Cogwheel 40** @ (177.0, 0.1, -79.0) +### Obstacles +- **Meteor 0** @ (-0.1, 0.0, 14.9) +- **Meteor 1** @ (11.9, 0.7, 47.6) +- **Meteor 2** @ (21.8, 4.1, 50.2) +- **Meteor 3** @ (45.8, 16.0, -5.1) +- **Meteor 4** @ (51.2, 16.0, -6.7) +- **Meteor 5** @ (44.2, 16.0, -0.2) +- **Meteor 6** @ (68.8, 16.0, -4.2) +- **Meteor 7** @ (71.3, 16.0, 0.3) +- **Meteor 8** @ (65.4, 16.0, -7.8) +- **Meteor 9** @ (73.4, 16.0, 12.4) +- **Meteor 10** @ (87.9, 16.0, 24.7) +- **Meteor 11** @ (101.8, 16.0, 36.7) +- **Meteor 12** @ (109.8, 16.0, 36.7) +- **Meteor 13** @ (123.2, 16.0, 30.6) +- **Meteor 14** @ (133.8, 16.0, 22.0) +- **Meteor 15** @ (145.2, 16.0, 19.4) +- **Meteor 16** @ (139.3, 16.0, 19.3) +- **Meteor 17** @ (148.3, 16.0, 15.3) +- **Meteor 18** @ (144.3, 16.0, -10.9) +- **Meteor 19** @ (142.2, 16.0, -15.6) +- **Meteor 20** @ (133.0, 16.0, -15.6) +- **Meteor 21** @ (132.9, 16.0, -25.1) +- **Meteor 22** @ (130.4, 16.0, -33.1) +- **Meteor 23** @ (131.6, 16.0, -41.1) +- **Meteor 24** @ (132.9, 16.0, -49.1) +- **Meteor 25** @ (133.1, 8.4, -79.1) +- **Meteor 26** @ (133.1, 15.4, -59.2) +- **Meteor 27** @ (130.5, 11.8, -69.1) +- **Meteor 28** @ (132.2, 8.0, -88.2) +- **Meteor 29** @ (155.1, 8.0, -96.2) +- **Meteor 30** @ (155.4, 8.0, -103.0) +- **Meteor 31** @ (139.7, 7.4, -112.4) +- **Meteor 32** @ (129.8, 3.8, -112.4) +- **Meteor 33** @ (111.9, 0.0, -110.7) +- **Meteor 34** @ (105.5, 0.0, -104.2) +- **Meteor 35** @ (105.2, 0.0, -97.1) +- **Meteor 36** @ (103.9, 0.0, -89.1) +- **Meteor 37** @ (103.9, 0.0, -81.1) +- **Meteor 38** @ (122.7, 0.0, -88.2) +- **Meteor 39** @ (149.8, 0.0, -90.0) +- **Meteor 40** @ (165.8, 0.0, -91.3) +- **Meteor 41** @ (172.8, 0.0, -88.8) +- **Meteor 42** @ (177.0, -4.2, -69.1) \ No newline at end of file diff --git a/SimulationGraph/graphMaker.py b/SimulationGraph/graphMaker.py new file mode 100644 index 0000000000000000000000000000000000000000..0369d7899b2b26ef1650f14ba621cefd635fcbc0 --- /dev/null +++ b/SimulationGraph/graphMaker.py @@ -0,0 +1,76 @@ +import json +import matplotlib.pyplot as plt +import pandas as pd +import sys +import os + +SIMULATIONS_FOLDER_PATH = "impulse/Assets/SimulationLogs/" +OUTPUT_FOLDER_PATH = "./SimulationGraph/" + +def plot_all_metrics(file_name): + with open(f"{SIMULATIONS_FOLDER_PATH}{file_name}", "r") as f: + data = json.load(f) + + logs = data["logs"] + metadata = data.get("metadata", {}) + + df = pd.DataFrame(logs) + df["speed"] = df["velocity"].apply(lambda v: (v['x']**2 + v['y']**2 + v['z']**2)**0.5) + + metrics = { + "Distance Since Last Frame": "distanceSinceLastFrame", + "Speed": "speed", + "Score Over Time": "currentScore", + "Average Time Between Collectibles": "averageTimeBetweenCollectibles", + "Collectible Efficiency (Raw)": "rawCollectibleEfficiency", + "Collectible Efficiency (Smoothed)": "collectibleCollectEfficiency", + "Obstacle Avoidance Efficiency (Raw)": "rawObstacleAvoidanceEfficiency", + "Obstacle Avoidance Efficiency (Smoothed)": "obstacleAvoidanceEfficiency", + "Exploration Efficiency (Raw)": "rawExplorationEfficiency", + "Exploration Efficiency (Smoothed)": "explorationEfficiency", + "Obstacles Hit Count": "obstaclesHitCount", + "Time Since Last Collectible": "timeSinceLastCollectible", + "Time Since Last Hit Obstacle": "timeSinceLastHitObstacle", + "Direction Variation": "variationInDirection", + } + + num_plots = len(metrics) + cols = 2 + rows = (num_plots + cols - 1) // cols + + fig, axs = plt.subplots(rows, cols, figsize=(16, 5 * rows)) + fig.suptitle(f"Simulation Analysis: {file_name} | Seed: {metadata.get('seed', 'N/A')}", fontsize=18) + axs = axs.flatten() + + for i, (title, column) in enumerate(metrics.items()): + if column in df.columns: + axs[i].plot(df["timestamp"], df[column], label=title) + axs[i].set_title(title) + axs[i].set_xlabel("Time (s)") + axs[i].set_ylabel(column) + axs[i].legend() + axs[i].grid(True) + else: + axs[i].text(0.5, 0.5, f"{column} not found", ha='center', va='center') + axs[i].axis("off") + + for j in range(i + 1, len(axs)): + axs[j].axis("off") + + plt.tight_layout(rect=[0, 0.03, 1, 0.95]) + output_path = os.path.join(OUTPUT_FOLDER_PATH, f"{file_name}_full_analysis.png") + plt.savefig(output_path) + print(f"Graph saved to: {output_path}") + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python graphMaker.py <simulation_file.json>") + print(f"Note: The file should be in {SIMULATIONS_FOLDER_PATH} !") + sys.exit(1) + + simulation_file = sys.argv[1] + if not os.path.exists(f"{SIMULATIONS_FOLDER_PATH}{simulation_file}"): + print(f"File not found: {SIMULATIONS_FOLDER_PATH}{simulation_file}") + sys.exit(1) + + plot_all_metrics(simulation_file) diff --git a/SimulationGraph/main.py b/SimulationGraph/main.py deleted file mode 100644 index 8e7e64b53a185f1d58c070b412489f33f2d8893c..0000000000000000000000000000000000000000 --- a/SimulationGraph/main.py +++ /dev/null @@ -1,74 +0,0 @@ -import json -import matplotlib.pyplot as plt -import pandas as pd -import sys -import os - -SIMULATIONS_FOLDER_PATH = "impulse/Assets/SimulationLogs/" -OUTPUT_FOLDER_PATH = "./" - -def plot_simulation_analysis(file_name): - with open(f"{SIMULATIONS_FOLDER_PATH}{file_name}", "r") as f: - data = json.load(f) - - logs = data["logs"] - - df = pd.DataFrame(logs) - - fig, axs = plt.subplots(3, 2, figsize=(15, 12)) - fig.suptitle("Simulation Session Analysis", fontsize=16) - - axs[0, 0].plot(df["timestamp"], df["totalDistance"], label="Total Distance", color="blue") - axs[0, 0].set_title("Total Distance Travelled") - axs[0, 0].set_xlabel("Time (s)") - axs[0, 0].set_ylabel("Distance (units)") - axs[0, 0].legend() - - axs[0, 1].plot(df["timestamp"], df["currentScore"], label="Score", color="green") - axs[0, 1].set_title("Score Over Time") - axs[0, 1].set_xlabel("Time (s)") - axs[0, 1].set_ylabel("Score") - axs[0, 1].legend() - - df["speed"] = df["velocity"].apply(lambda v: (v['x']**2 + v['y']**2 + v['z']**2)**0.5) - axs[1, 0].plot(df["timestamp"], df["speed"], label="Speed", color="orange") - axs[1, 0].set_title("Instantaneous Speed") - axs[1, 0].set_xlabel("Time (s)") - axs[1, 0].set_ylabel("Speed (units/s)") - axs[1, 0].legend() - - axs[1, 1].plot(df["timestamp"], df["variationInDirection"], label="Direction Changes", color="purple") - axs[1, 1].set_title("Direction Variation (sliding window)") - axs[1, 1].set_xlabel("Time (s)") - axs[1, 1].set_ylabel("Changes") - axs[1, 1].legend() - - axs[2, 0].plot(df["timestamp"], df["explorationEfficiency"], label="Exploration Efficiency", color="red") - axs[2, 0].set_title("Exploration Efficiency") - axs[2, 0].set_xlabel("Time (s)") - axs[2, 0].set_ylabel("Ratio (%)") - axs[2, 0].legend() - - axs[2, 1].plot(df["timestamp"], df["obstaclesHitCount"], label="Obstacles Hit", color="brown") - axs[2, 1].set_title("Obstacles Hit Over Time") - axs[2, 1].set_xlabel("Time (s)") - axs[2, 1].set_ylabel("Count") - axs[2, 1].legend() - - plt.tight_layout(rect=[0, 0.03, 1, 0.95]) - output_path = f"{OUTPUT_FOLDER_PATH}{file_name}_simulation_analysis.png" - plt.savefig(output_path) - print(f"Graph saved to {output_path}") - -if __name__ == "__main__": - if len(sys.argv) != 2: - print("Usage: python main.py <simulation_file.json>") - print(f"Note: The file should be in {SIMULATIONS_FOLDER_PATH} !") - sys.exit(1) - - simulation_file = sys.argv[1] - if not os.path.exists(f"{SIMULATIONS_FOLDER_PATH}{simulation_file}"): - print(f"File not found: {SIMULATIONS_FOLDER_PATH}{simulation_file}") - sys.exit(1) - - plot_simulation_analysis(simulation_file) diff --git a/SimulationGraph/reportMaker.py b/SimulationGraph/reportMaker.py new file mode 100644 index 0000000000000000000000000000000000000000..9c607417ba41a904dd2eb938937d733fb664fc80 --- /dev/null +++ b/SimulationGraph/reportMaker.py @@ -0,0 +1,128 @@ +import json +from datetime import datetime +import os +import sys +import pandas as pd + +SIMULATIONS_FOLDER_PATH = "impulse/Assets/SimulationLogs/" +OUTPUT_FOLDER_PATH = "./SimulationGraph/" + +def interpret_direction_variation(value): + if value < 0.3: + return "Very stable direction — almost no changes." + elif value < 0.7: + return "Smooth and controlled exploration." + elif value < 1.2: + return "Moderately active — some directional adjustments." + else: + return "High variation — chaotic or exploratory movement." + +def generate_md_report(file_name): + with open(f"{SIMULATIONS_FOLDER_PATH}{file_name}", "r") as f: + data = json.load(f) + + logs = data["logs"] + metadata = data.get("metadata", {}) + df = pd.DataFrame(logs) + + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + seed = metadata.get("seed", "N/A") + parcour_length = metadata.get("totalParcourLength", 0.0) + total_collectibles = metadata.get("totalCollectibles", 0) + total_obstacles = metadata.get("totalObstacles", 0) + + if not df.empty: + final_score = int(df["currentScore"].iloc[-1]) + total_distance = df["distanceSinceLastFrame"].sum() + simulation_duration = df["timestamp"].iloc[-1] + mean_speed = df["velocity"].apply(lambda v: (v['x']**2 + v['y']**2 + v['z']**2)**0.5).mean() + avg_collectible_time = df["averageTimeBetweenCollectibles"].iloc[-1] + avg_time_between_hits = df["timeSinceLastHitObstacle"].mean() + variation_direction = df["variationInDirection"].mean() + + raw_collectible_eff = df["rawCollectibleEfficiency"].iloc[-1] + smooth_collectible_eff = df["collectibleCollectEfficiency"].iloc[-1] + raw_avoid_eff = df["rawObstacleAvoidanceEfficiency"].iloc[-1] + smooth_avoid_eff = df["obstacleAvoidanceEfficiency"].iloc[-1] + raw_exploration_eff = df["rawExplorationEfficiency"].iloc[-1] + smooth_exploration_eff = df["explorationEfficiency"].iloc[-1] + + hits = int(df["obstaclesHitCount"].iloc[-1]) + else: + final_score = total_distance = simulation_duration = mean_speed = avg_collectible_time = 0 + avg_time_between_hits = variation_direction = 0 + raw_collectible_eff = smooth_collectible_eff = 0 + raw_avoid_eff = smooth_avoid_eff = 0 + raw_exploration_eff = smooth_exploration_eff = 0 + hits = 0 + + direction_comment = interpret_direction_variation(variation_direction) + + report_lines = [ + f"# Simulation report", + f"**File**: `{file_name}` ", + f"**Report generation date**: {now} ", + f"**Seed**: `{seed}` ", + "", + "## Resume", + f"- Parcour length : **{parcour_length:.2f}** units", + f"- Collectibles collected : **{final_score} / {total_collectibles}**", + f"- Obstacles hit : **{hits} / {total_obstacles}**", + f"- Distance made : **{total_distance:.2f}** units ({(total_distance/parcour_length)*100 if parcour_length else 0:.1f}%)", + "", + "## Performance over time", + f"- Duration : **{simulation_duration:.2f} s**", + f"- Speed average : **{mean_speed:.2f} units/s**", + f"- Time average between collectibles : **{avg_collectible_time:.2f} s**", + f"- Time average between collisions : **{avg_time_between_hits:.2f} s**", + "", + "## Efficacity", + f"- Collectibles : **{raw_collectible_eff:.2%} (raw)** | **{smooth_collectible_eff:.2%} (smoothed)**", + f"- Obstacles : **{raw_avoid_eff:.2%} (raw)** | **{smooth_avoid_eff:.2%} (smoothed)**", + f"- Exploration : **{raw_exploration_eff:.2%} (raw)** | **{smooth_exploration_eff:.2%} (smoothed)**", + "", + "## Behaviour analysis", + f"- Directionnal variation (moving average) : **{variation_direction:.2f} changes**", + f" - {direction_comment}", + "", + "## Parameters of the simulation" + ] + + for param in metadata.get("parameters", []): + report_lines.append(f"- `{param['name']}` = **{param['value']}**") + + report_lines.append("") + report_lines.append("## Spawned elements") + + def format_spawn_list(title, items): + section = [f"### {title}"] + if items: + for obj in items: + pos = obj["position"] + section.append(f"- **{obj['name']}** @ ({pos['x']:.1f}, {pos['y']:.1f}, {pos['z']:.1f})") + else: + section.append("_Nothing recorded._") + return section + + report_lines.extend(format_spawn_list("Road Parts", metadata.get("roadParts", []))) + report_lines.extend(format_spawn_list("Collectibles", metadata.get("collectibles", []))) + report_lines.extend(format_spawn_list("Obstacles", metadata.get("obstacles", []))) + + output_path = os.path.join(OUTPUT_FOLDER_PATH, f"{file_name}_report.md") + with open(output_path, "w", encoding="utf-8") as f: + f.write("\n".join(report_lines)) + + output_path + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python reportMaker.py <simulation_file.json>") + print(f"Note: The file should be in {SIMULATIONS_FOLDER_PATH} !") + sys.exit(1) + + simulation_file = sys.argv[1] + if not os.path.exists(f"{SIMULATIONS_FOLDER_PATH}{simulation_file}"): + print(f"File not found: {SIMULATIONS_FOLDER_PATH}{simulation_file}") + sys.exit(1) + + generate_md_report(simulation_file)