Skip to content

Commit cc5aa2d

Browse files
committed
Génération du dataset avec anomalies
1 parent 8445c79 commit cc5aa2d

15 files changed

+1121613
-200442
lines changed

ships_hybrid_algorithm/agents/ship.py

Lines changed: 69 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""
2-
ship.py — optimized
2+
ship.py — turbo (dataset-speed edition)
33
- Fast local-goal search (squared distances, incremental index)
4-
- Lazy STRtree index for speed-limit zones (supports both index- and geometry-returns)
5-
- Cheaper docking check (no sqrt)
6-
- Zero-allocation move_position + DEBUG-only logging
4+
- Lazy STRtree speed-zone lookup (indices or geometries)
5+
- Cheaper docking check (no sqrt) + *early* docking precheck (skips DWA when close)
6+
- Adds lightweight x/y/v properties for fast DataCollector
7+
- Queues arrived ships for removal (model cleans them up after the step)
78
"""
89
import logging
910
import math
@@ -12,7 +13,7 @@
1213
from shapely.geometry import Point, Polygon
1314
try:
1415
from shapely.strtree import STRtree
15-
except Exception: # STRtree may be unavailable on very old Shapely or missing GEOS
16+
except Exception: # STRtree may be unavailable
1617
STRtree = None
1718

1819
from a_star import astar
@@ -22,171 +23,175 @@
2223

2324

2425
def _get_rng(agent):
25-
"""Return an RNG: prefer agent.random, then model.random, else Python's random module."""
2626
try:
2727
import random as _pr
28-
except Exception: # pragma: no cover
28+
except Exception:
2929
_pr = None
3030
return getattr(agent, "random", None) or getattr(agent.model, "random", None) or _pr
3131

3232

3333
class Ship(Agent):
3434
def __init__(self, model, id, start_port, all_ports, dwa_config):
35-
# Mesa 2.x signature: Agent(unique_id, model)
3635
super().__init__(id, model)
3736
self.destination_port = self.assign_destination(all_ports, start_port)
3837
self.global_path = self.calculate_global_path(start_port.pos, self.destination_port.pos)
39-
# Per-ship copy so edits (speed limits/noise) don't mutate a shared dict
40-
self.dwa_config = dict(dwa_config)
38+
self.dwa_config = dict(dwa_config) # per-ship copy
4139

4240
self.heading_deviation = 0.0
4341
self.heading_drift_duration = 0
4442

4543
rng = _get_rng(self)
46-
# Assign a random max speed within the speed range
4744
self.dwa_config["max_speed"] = rng.uniform(self.model.max_speed_range[0], self.model.max_speed_range[1])
4845
self.original_max_speed = self.dwa_config["max_speed"]
4946
if log.isEnabledFor(logging.DEBUG):
50-
log.debug("Ship %s has a maximum speed of %.3f.", self.unique_id, self.dwa_config["max_speed"])
47+
log.debug("Ship %s max speed %.3f.", self.unique_id, self.dwa_config["max_speed"])
5148

5249
if self.global_path and len(self.global_path) > 1:
53-
first_waypoint = self.global_path[1] # Ensure it doesn't use the port position
50+
first_waypoint = self.global_path[1]
5451
dx = first_waypoint[0] - start_port.pos[0]
5552
dy = first_waypoint[1] - start_port.pos[1]
56-
# Set initial heading (theta) towards the first waypoint
5753
initial_theta = math.atan2(dy, dx)
5854
else:
5955
initial_theta = 0.0
6056

61-
# Ship's state (x, y, theta, v, w)
57+
# state = (x, y, theta, v, w)
6258
self.state = (start_port.pos[0], start_port.pos[1], initial_theta, 0.0, 0.0)
63-
6459
self.current_wp_idx = 0
60+
self.arrived = False # mark when we reach destination
61+
62+
# --- lightweight attributes for fast DataCollector ---
63+
@property
64+
def x(self):
65+
p = getattr(self, "pos", None)
66+
return float(p[0]) if p is not None else None
67+
68+
@property
69+
def y(self):
70+
p = getattr(self, "pos", None)
71+
return float(p[1]) if p is not None else None
72+
73+
@property
74+
def v(self):
75+
return float(self.state[3])
6576

6677
def step(self):
67-
# Early exit if already docked (relies on model to have set .pos initially)
78+
if self.arrived:
79+
return
80+
# early exit if position equals destination
6881
if getattr(self, "pos", None) is not None:
6982
if self.pos[0] == self.destination_port.pos[0] and self.pos[1] == self.destination_port.pos[1]:
83+
self._queue_removal()
7084
return
7185

72-
# Move the ship along the calculated global path.
7386
if self.global_path and len(self.global_path) > 1:
7487
dt = self.dwa_config["dt"]
7588

89+
# Early docking precheck: skip DWA when we're within one max step of destination
90+
max_spd_here = self.get_speed_limit()
91+
if self.model.speed_variation.get("enabled", False):
92+
var = self.model.max_speed_variation
93+
rng = _get_rng(self)
94+
max_spd_here = max(self.dwa_config["min_speed"],
95+
min(max_spd_here + rng.uniform(-max_spd_here*var, max_spd_here*var),
96+
self.dwa_config["max_speed"]))
97+
if self._close_enough_to_dock(max_spd_here, dt):
98+
self.move_to_destination()
99+
self._queue_removal()
100+
return
101+
76102
local_goal = self.get_local_goal(self.state, self.global_path, lookahead=self.model.lookahead)
77-
# Speed limit via spatial index (built lazily & cached on model)
78-
self.dwa_config["max_speed"] = self.get_speed_limit()
79103

80-
if self.model.speed_variation["enabled"]:
81-
self.dwa_config["max_speed"] = self.get_noisy_speed()
104+
# Speed limit via spatial index (built lazily & cached on model)
105+
self.dwa_config["max_speed"] = max_spd_here
82106

83107
rng = _get_rng(self)
84108
# Directional (heading) variation
85109
if self.model.directional_variation["enabled"]:
86-
# Randomly trigger heading deviation
87110
if self.heading_drift_duration > 0:
88-
# Continue existing deviation
89111
noisy_theta = self.state[2] + self.heading_deviation
90112
self.heading_drift_duration -= 1
91-
if log.isEnabledFor(logging.DEBUG):
92-
log.debug("Continue deviation. Ship %s, Theta=%.4f", self.unique_id, self.heading_deviation)
93113
else:
94-
# Random chance to start a new deviation
95114
if rng.random() < self.model.deviation_chance:
96115
self.heading_deviation = rng.uniform(-self.model.max_heading_deviation, self.model.max_heading_deviation)
97116
self.heading_drift_duration = self.model.deviation_duration
98117
noisy_theta = self.state[2] + self.heading_deviation
99-
if log.isEnabledFor(logging.DEBUG):
100-
log.debug("Starting directional deviation. Ship %s, Theta=%.4f", self.unique_id, self.heading_deviation)
101118
else:
102119
noisy_theta = self.state[2]
103-
104-
# Normalize heading
105120
noisy_theta = (noisy_theta + math.pi) % (2 * math.pi) - math.pi
106121
noisy_state = (self.state[0], self.state[1], noisy_theta, self.state[3], self.state[4])
107122
else:
108123
noisy_state = self.state
109124

110-
control, predicted_trajectory, cost_info = dwa_control(
125+
control, _, _ = dwa_control(
111126
noisy_state, self.dwa_config, self.model.obstacle_tree,
112127
self.model.buffered_obstacles, local_goal
113128
)
114129

115-
# Check if we should dock
116130
if self.should_dock(control[0]):
117131
self.move_to_destination()
132+
self._queue_removal()
118133
else:
119134
self.state = motion(self.state, control[0], control[1], dt)
120135
self.move_position()
121136

122-
# After motion, restore true max speed
137+
# restore true max speed
123138
self.dwa_config["max_speed"] = self.original_max_speed
124139

140+
def _close_enough_to_dock(self, speed, dt):
141+
dx = self.state[0] - self.destination_port.pos[0]
142+
dy = self.state[1] - self.destination_port.pos[1]
143+
return (dx*dx + dy*dy) <= (speed*dt)*(speed*dt)
144+
145+
def _queue_removal(self):
146+
"""Mark ship for removal by the model at end-of-step (safe w.r.t. scheduler iteration)."""
147+
self.arrived = True
148+
q = getattr(self.model, "_arrived_to_remove", None)
149+
if q is None:
150+
self.model._arrived_to_remove = [self]
151+
else:
152+
q.append(self)
153+
125154
def get_noisy_speed(self):
126-
# Assign ± speed variation as a fraction of max_speed
127155
max_speed_variation = self.model.max_speed_variation
128156
variation_amount = self.dwa_config["max_speed"] * max_speed_variation
129157
rng = _get_rng(self)
130158
noisy_speed = self.dwa_config["max_speed"] + rng.uniform(-variation_amount, variation_amount)
131-
132-
# Clamp to valid range
133-
noisy_speed = max(self.dwa_config["min_speed"], min(noisy_speed, self.dwa_config["max_speed"]))
134-
return noisy_speed
159+
return max(self.dwa_config["min_speed"], min(noisy_speed, self.dwa_config["max_speed"]))
135160

136161
def should_dock(self, current_speed):
137-
"""Determine if the ship should dock at its destination (squared-distance, no sqrt)."""
138162
dx = self.state[0] - self.destination_port.pos[0]
139163
dy = self.state[1] - self.destination_port.pos[1]
140164
dist2 = dx * dx + dy * dy
141165
step_move = current_speed * self.dwa_config['dt']
142166
return dist2 <= step_move * step_move
143167

144168
def move_position(self):
145-
"""Update the ship's position in the simulation space (avoids numpy allocation)."""
146169
self.pos = (float(self.state[0]), float(self.state[1]))
147170
self.model.space.move_agent(self, self.pos)
148-
if log.isEnabledFor(logging.DEBUG):
149-
log.debug(
150-
"Ship %s moving towards %s. Current position: (%.3f, %.3f). Speed: %.3f.",
151-
self.unique_id, self.destination_port.pos, self.pos[0], self.pos[1], self.state[3]
152-
)
153171

154172
def move_to_destination(self):
155-
"""Move the ship directly to its destination."""
156173
self.pos = self.destination_port.pos
157174
self.model.space.move_agent(self, self.destination_port.pos)
158-
if log.isEnabledFor(logging.DEBUG):
159-
log.debug("Ship %s arrived at port %s.", self.unique_id, self.destination_port.pos)
160175

161176
def assign_destination(self, all_ports, start_port):
162-
"""Select a destination port different from the starting port."""
163177
possible_destinations = [port for port in all_ports if port != start_port]
164178
rng = _get_rng(self)
165179
return rng.choice(possible_destinations) if possible_destinations else start_port
166180

167181
def calculate_global_path(self, start, destination):
168-
"""Calculate a path using A* algorithm."""
169182
grid_start = (int(start[0] / self.model.resolution), int(start[1] / self.model.resolution))
170183
grid_goal = (int(destination[0] / self.model.resolution), int(destination[1] / self.model.resolution))
171184
global_path_indices = astar(self.model.occupancy_grid, grid_start, grid_goal)
172-
173185
if global_path_indices is None:
174-
if log.isEnabledFor(logging.DEBUG):
175-
log.debug("No global path for ship %s.", self.unique_id)
176186
return
177-
178-
global_path = [((i) * self.model.resolution, (j) * self.model.resolution)
179-
for (i, j) in global_path_indices]
180-
return global_path
187+
return [((i) * self.model.resolution, (j) * self.model.resolution) for (i, j) in global_path_indices]
181188

182189
# --- Speed-limit zones: lazy-built STRtree on the model ---
183190
def _build_speed_zone_index_if_needed(self):
184-
"""Build and cache a spatial index for speed-limit zones on the model (lazy)."""
185191
if getattr(self.model, "_speed_zone_tree", None) is not None:
186-
return # already built
192+
return
187193
zones = getattr(self.model, "speed_limit_zones", None)
188194
if not zones:
189-
# no zones configured
190195
self.model._speed_zone_tree = None
191196
self.model._speed_zone_polys = []
192197
self.model._speed_zone_speed_by_id = {}
@@ -195,7 +200,6 @@ def _build_speed_zone_index_if_needed(self):
195200

196201
polys = [Polygon(z["zone"]) for z in zones]
197202
self.model._speed_zone_polys = polys
198-
# store speeds in parallel list for index-based lookups
199203
self.model._speed_zone_speeds = [z["max_speed"] for z in zones]
200204

201205
if STRtree is not None and polys:
@@ -204,54 +208,40 @@ def _build_speed_zone_index_if_needed(self):
204208
except Exception:
205209
self.model._speed_zone_tree = None
206210
else:
207-
self.model._speed_zone_tree = None # fallback path will be used
208-
# map polygon id -> speed for geometry-based lookups
211+
self.model._speed_zone_tree = None
209212
self.model._speed_zone_speed_by_id = {id(p): z["max_speed"] for p, z in zip(polys, zones)}
210213

211214
def get_speed_limit(self):
212-
"""Return max speed at current position.
213-
Works with STRtree returning indices or geometries.
214-
"""
215215
zones = getattr(self.model, "speed_limit_zones", None)
216216
if not zones:
217217
return self.original_max_speed
218-
219218
self._build_speed_zone_index_if_needed()
220219

221220
pt = Point(self.state[0], self.state[1])
222221
tree = getattr(self.model, "_speed_zone_tree", None)
223-
224222
if tree is not None:
225-
# Try to use a predicate if supported (Shapely 2.x). It may return indices.
226223
try:
227224
candidates = tree.query(pt, predicate="contains")
228225
except TypeError:
229-
# Older Shapely: no predicate argument
230226
candidates = tree.query(pt)
231-
232-
# `candidates` can be a list of indices OR a list of Polygon objects.
233227
for cand in candidates:
234-
if isinstance(cand, Integral): # index path (e.g., numpy.int64)
228+
if isinstance(cand, Integral):
235229
idx = int(cand)
236230
poly = self.model._speed_zone_polys[idx]
237231
if poly.contains(pt):
238232
return self.model._speed_zone_speeds[idx]
239-
else: # geometry path
233+
else:
240234
poly = cand
241235
if poly.contains(pt):
242236
return self.model._speed_zone_speed_by_id.get(id(poly), self.original_max_speed)
243-
244237
return self.original_max_speed
245238

246-
# Fallback: no STRtree available; scan the prebuilt polygons once
247239
for idx, poly in enumerate(self.model._speed_zone_polys):
248240
if poly.contains(pt):
249241
return self.model._speed_zone_speeds[idx]
250-
251242
return self.original_max_speed
252243

253244
def get_local_goal(self, state, global_path, lookahead=3.0):
254-
"""Return the first waypoint at least 'lookahead' away (squared distance, O(1–few))."""
255245
x, y = state[0], state[1]
256246
la2 = lookahead * lookahead
257247
i = self.current_wp_idx

0 commit comments

Comments
 (0)