Skip to content

Commit 5933939

Browse files
authored
Merge pull request #5 from movingpandas/ship-sim-improvements
Ship sim improvements
2 parents 119c5d1 + d11c891 commit 5933939

File tree

7 files changed

+1060
-61
lines changed

7 files changed

+1060
-61
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,5 @@ dmypy.json
9292
# JS dependencies
9393
mesa/visualization/templates/external/
9494
mesa/visualization/templates/js/external/
95+
96+
ships_hybrid_algorithm/ship-venv/

ships_hybrid_algorithm/agents/ship.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ def __init__(self, model, id, start_port, all_ports, dwa_config):
1414
self.destination_port = self.assign_destination(all_ports, start_port)
1515
self.global_path = self.calculate_global_path(start_port.pos, self.destination_port.pos)
1616
self.dwa_config = dwa_config
17-
17+
18+
self.heading_deviation = 0.0
19+
self.heading_drift_duration = 0
20+
1821
# Assign a random max speed within the speed range
1922
dwa_config["max_speed"] = self.random.uniform(self.model.max_speed_range[0], self.model.max_speed_range[1])
2023
self.original_max_speed = self.dwa_config["max_speed"]
@@ -43,8 +46,40 @@ def step(self):
4346
if self.global_path and len(self.global_path) > 1:
4447
local_goal = self.get_local_goal(self.state, self.global_path, lookahead=self.model.lookahead)
4548
self.dwa_config["max_speed"] = self.get_speed_limit()
49+
50+
if self.model.speed_variation["enabled"]:
51+
self.dwa_config["max_speed"] = self.get_noisy_speed()
52+
53+
# if self.model.directional_variation["enabled"]:
54+
# noisy_state = self.get_noisy_state()
55+
# else:
56+
# noisy_state = self.state
57+
58+
if self.model.directional_variation["enabled"]:
59+
# Randomly trigger heading deviation
60+
if self.heading_drift_duration > 0:
61+
# Continue existing deviation
62+
noisy_theta = self.state[2] + self.heading_deviation
63+
self.heading_drift_duration -= 1
64+
logging.info(f"Continue deviation. Ship {self.unique_id}, Theta = {self.heading_deviation}")
65+
else:
66+
# Random chance to start a new deviation
67+
if self.random.random() < self.model.deviation_chance:
68+
self.heading_deviation = self.random.uniform(-self.model.max_heading_deviation, self.model.max_heading_deviation)
69+
self.heading_drift_duration = self.model.deviation_duration
70+
noisy_theta = self.state[2] + self.heading_deviation
71+
logging.info(f"Starting directional deviation. Ship {self.unique_id}, Theta = {self.heading_deviation}")
72+
else:
73+
noisy_theta = self.state[2]
74+
75+
# Normalize heading
76+
noisy_theta = (noisy_theta + math.pi) % (2 * math.pi) - math.pi
77+
noisy_state = (self.state[0], self.state[1], noisy_theta, self.state[3], self.state[4])
78+
else:
79+
noisy_state = self.state
80+
4681
control, predicted_trajectory, cost_info = dwa_control(
47-
self.state, self.dwa_config, self.model.obstacle_tree,
82+
noisy_state, self.dwa_config, self.model.obstacle_tree,
4883
self.model.buffered_obstacles, local_goal
4984
)
5085

@@ -54,6 +89,19 @@ def step(self):
5489
else:
5590
self.state = motion(self.state, control[0], control[1], self.dwa_config["dt"])
5691
self.move_position()
92+
93+
# After motion, restore true max speed
94+
self.dwa_config["max_speed"] = self.original_max_speed
95+
96+
def get_noisy_speed(self):
97+
# Assign ± speed variation as a fraction of max_speed
98+
max_speed_variation = self.model.max_speed_variation
99+
variation_amount = self.dwa_config["max_speed"] * max_speed_variation
100+
noisy_speed = self.dwa_config["max_speed"] + self.random.uniform(-variation_amount, variation_amount)
101+
102+
# Clamp to valid range
103+
noisy_speed = max(self.dwa_config["min_speed"], min(noisy_speed, self.dwa_config["max_speed"]))
104+
return noisy_speed
57105

58106
def should_dock(self, current_speed):
59107
"""Determine if the ship should dock at its destination."""

ships_hybrid_algorithm/config/config.json

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
{
2-
"simulation_steps": 10000,
2+
"simulation_steps": 1500,
33
"width": 100,
44
"height": 100,
5-
"num_ships": 1000,
5+
"num_ships": 35,
66
"max_speed_range": [1.0, 1.5],
7+
"speed_variation": {
8+
"enabled": true,
9+
"max_speed_variation": 0.05
10+
},
11+
"directional_variation": {
12+
"enabled": true,
13+
"max_deviation_deg": 45.0,
14+
"activation_chance": 0.02,
15+
"duration_steps": 15
16+
},
717
"resolution": 1,
818
"obstacle_threshold": 2,
919
"lookahead": 5,
@@ -47,6 +57,13 @@
4757
[[75, 75], [78, 73], [82, 76], [80, 80], [76, 79]],
4858
[[27, 28], [35, 27], [38, 29], [39, 32], [37, 36], [32, 39], [28, 37], [26, 33]]
4959
],
60+
"geospatial_bounds": {
61+
"min_lat": 20.0,
62+
"max_lat": 40.0,
63+
"min_lon": 0.0,
64+
"max_lon": 20.0
65+
},
66+
"time_step_seconds": 0.002,
5067
"dwa_config": {
5168
"max_speed": 1.0,
5269
"min_speed": 0.1,

ships_hybrid_algorithm/main.py

Lines changed: 68 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import logging
22
from datetime import datetime
33
import json
4+
from mesa.batchrunner import batch_run, _collect_data
5+
from multiprocessing import freeze_support
6+
import pandas as pd
47

58
from model import ShipModel
69
from visualization import plot_simulation
@@ -13,38 +16,43 @@
1316
format="%(asctime)s - %(levelname)s - %(message)s"
1417
)
1518

16-
def run_simulation(config_file="config/config.json"):
19+
if __name__ == "__main__":
20+
freeze_support()
21+
22+
config_file="config/config.json"
1723
with open(config_file) as f:
1824
config = json.load(f)
1925

2026
steps = config["simulation_steps"]
2127

22-
model = ShipModel(
23-
width=config["width"],
24-
height=config["height"],
25-
num_ships=config["num_ships"],
26-
max_speed_range=config["max_speed_range"],
27-
ports=config["ports"],
28-
speed_limit_zones=config.get("speed_limit_zones", []),
29-
obstacles=config["obstacles"],
30-
dwa_config=config["dwa_config"],
31-
resolution=config["resolution"],
32-
obstacle_threshold=config["obstacle_threshold"],
33-
lookahead=config["lookahead"]
34-
)
35-
36-
# Print agent counts by type
37-
for agent_type, agents in model.agents_by_type.items():
38-
logging.info(f'{agent_type}: {len(agents)}')
28+
model_params = {
29+
"width": [config["width"]],
30+
"height": [config["height"]],
31+
"num_ships": [config["num_ships"]],
32+
"max_speed_range": [config["max_speed_range"]],
33+
"speed_variation": [config["speed_variation"]],
34+
"directional_variation": [config["directional_variation"]],
35+
"ports": [config["ports"]],
36+
"speed_limit_zones": [config.get("speed_limit_zones", [])],
37+
"obstacles": [config["obstacles"]],
38+
"dwa_config": [config["dwa_config"]],
39+
"resolution": [config["resolution"]],
40+
"obstacle_threshold": [config["obstacle_threshold"]],
41+
"lookahead": [config["lookahead"]]
42+
}
3943

40-
# Run the simulation
41-
logging.info(f"{datetime.now()} Starting ...")
42-
for t in range(steps):
43-
logging.info(f"Step {t}")
44-
model.step()
45-
logging.info(f"{datetime.now()} Finished.")
44+
results = batch_run(
45+
ShipModel,
46+
parameters=model_params,
47+
iterations=1,
48+
max_steps=steps,
49+
number_processes=None,
50+
data_collection_period=1,
51+
display_progress=True,
52+
)
4653

47-
agent_df = model.datacollector.get_agent_vars_dataframe().dropna()
54+
results_df = pd.DataFrame(results)
55+
agent_df = results_df[["Step", "AgentID", "x", "y", "AStarPath"]].dropna()
4856
df = agent_df.reset_index()
4957

5058
# Save to CSV
@@ -54,6 +62,39 @@ def run_simulation(config_file="config/config.json"):
5462
# Run visualization
5563
plot_simulation(df, config)
5664

65+
# model = ShipModel(
66+
# width=config["width"],
67+
# height=config["height"],
68+
# num_ships=config["num_ships"],
69+
# max_speed_range=config["max_speed_range"],
70+
# speed_variation=config["speed_variation"],
71+
# directional_variation=config["directional_variation"],
72+
# ports=config["ports"],
73+
# speed_limit_zones=config.get("speed_limit_zones", []),
74+
# obstacles=config["obstacles"],
75+
# dwa_config=config["dwa_config"],
76+
# resolution=config["resolution"],
77+
# obstacle_threshold=config["obstacle_threshold"],
78+
# lookahead=config["lookahead"]
79+
# )
5780

58-
if __name__ == "__main__":
59-
run_simulation()
81+
# # Print agent counts by type
82+
# for agent_type, agents in model.agents_by_type.items():
83+
# logging.info(f'{agent_type}: {len(agents)}')
84+
85+
# # Run the simulation
86+
# logging.info(f"{datetime.now()} Starting ...")
87+
# for t in range(steps):
88+
# logging.info(f"Step {t}")
89+
# model.step()
90+
# logging.info(f"{datetime.now()} Finished.")
91+
92+
# agent_df = model.datacollector.get_agent_vars_dataframe().dropna()
93+
# df = agent_df.reset_index()
94+
95+
# # Save to CSV
96+
# df.to_csv("ship_simulation_output.csv", index=False)
97+
# logging.info("Saved simulation data to ship_simulation_output.csv")
98+
99+
# # Run visualization
100+
# plot_simulation(df, config)

ships_hybrid_algorithm/model.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,39 @@
1+
import logging
12
from mesa import Model
23
from mesa.space import ContinuousSpace
34
from mesa.datacollection import DataCollector
45
from mesa.time import RandomActivation
56
from shapely.geometry import Polygon
67
import random
78
from shapely.strtree import STRtree
9+
import math
810

911
from agents.ship import Ship
1012
from agents.port import Port
1113
from agents.obstacle import Obstacle
1214
from a_star import create_occupancy_grid
1315

1416
class ShipModel(Model):
15-
def __init__(self, width, height, num_ships, max_speed_range, ports, speed_limit_zones, obstacles, dwa_config, resolution=1, obstacle_threshold=0, lookahead=3.0):
17+
def __init__(self, width, height, num_ships, max_speed_range, speed_variation, directional_variation, ports, speed_limit_zones, obstacles, dwa_config, resolution=1, obstacle_threshold=0, lookahead=3.0):
1618
super().__init__()
19+
# self.step = 0
1720
self.width = width
1821
self.height = height
1922
self.max_speed_range = max_speed_range
23+
self.speed_variation = speed_variation
24+
self.directional_variation = directional_variation
2025
self.dwa_config = dwa_config
2126
self.resolution = resolution
2227
self.obstacle_threshold = obstacle_threshold
2328
self.lookahead = lookahead
2429

30+
if speed_variation["enabled"]:
31+
self.max_speed_variation = speed_variation["max_speed_variation"]
32+
if directional_variation["enabled"]:
33+
self.max_heading_deviation = math.radians(directional_variation["max_deviation_deg"])
34+
self.deviation_duration = directional_variation["duration_steps"]
35+
self.deviation_chance = directional_variation["activation_chance"]
36+
2537
self.space = ContinuousSpace(self.width, self.height, torus=False)
2638
self.schedule = RandomActivation(self)
2739

@@ -100,5 +112,7 @@ def step(self):
100112
"""Run one step of the model.
101113
102114
All agents are activated in random order."""
115+
# logging.info(f"Step {self.step}")
103116
self.schedule.step()
104-
self.datacollector.collect(self)
117+
self.datacollector.collect(self)
118+
# self.step += 1

0 commit comments

Comments
 (0)