flocking 0.1.0__cp39-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flocking/__init__.py +22 -0
- flocking/_native.pyd +0 -0
- flocking/_native.pyi +340 -0
- flocking/py.typed +0 -0
- flocking-0.1.0.dist-info/METADATA +108 -0
- flocking-0.1.0.dist-info/RECORD +10 -0
- flocking-0.1.0.dist-info/WHEEL +4 -0
- flocking-0.1.0.dist-info/licenses/LICENSE-APACHE +201 -0
- flocking-0.1.0.dist-info/licenses/LICENSE-MIT +21 -0
- flocking-0.1.0.dist-info/sboms/flocking-python.cyclonedx.json +1003 -0
flocking/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Rust-owned vehicle simulation with coherent snapshots and queued commands.
|
|
2
|
+
|
|
3
|
+
Command IDs acknowledge queue admission. State changes become visible at tick
|
|
4
|
+
commit. Stop worlds explicitly (or use a context manager) to join their worker.
|
|
5
|
+
NumPy is optional: install ``flocking[numpy]`` for heightmaps and array snapshots.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ._native import (
|
|
9
|
+
CapacityError, CollisionError, Deadlock, DynamicObstacle, FlockingError,
|
|
10
|
+
FormationConfig, FormationMemberStatus, FormationStatus, InvalidArgumentError,
|
|
11
|
+
LifecycleError, NavigationStatus, QueueFullError, RecoveryConfig, RecoveryStatus, SafetyStatus, Snapshot, StateFrame,
|
|
12
|
+
StoppedError, TimeoutError, UnknownIdError, UnsupportedError, VehicleControl,
|
|
13
|
+
VehicleProfile, WorkerError, World,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"CapacityError", "CollisionError", "Deadlock", "DynamicObstacle", "FlockingError",
|
|
18
|
+
"FormationConfig", "FormationMemberStatus", "FormationStatus", "InvalidArgumentError",
|
|
19
|
+
"LifecycleError", "NavigationStatus", "QueueFullError", "RecoveryConfig", "RecoveryStatus", "SafetyStatus", "Snapshot", "StateFrame",
|
|
20
|
+
"StoppedError", "TimeoutError", "UnknownIdError", "UnsupportedError", "VehicleControl",
|
|
21
|
+
"VehicleProfile", "WorkerError", "World",
|
|
22
|
+
]
|
flocking/_native.pyd
ADDED
|
Binary file
|
flocking/_native.pyi
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""Public native API. Coordinates are meters, angles radians, times seconds."""
|
|
2
|
+
from collections.abc import Iterable, Sequence
|
|
3
|
+
from types import TracebackType
|
|
4
|
+
from typing import Any, Literal, Optional, TypedDict, Union
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from numpy.typing import ArrayLike, NDArray
|
|
8
|
+
|
|
9
|
+
class FlockingError(RuntimeError): ...
|
|
10
|
+
class LifecycleError(FlockingError): ...
|
|
11
|
+
class QueueFullError(FlockingError): ...
|
|
12
|
+
class StoppedError(FlockingError): ...
|
|
13
|
+
class WorkerError(FlockingError): ...
|
|
14
|
+
class UnknownIdError(FlockingError): ...
|
|
15
|
+
class InvalidArgumentError(FlockingError): ...
|
|
16
|
+
class CapacityError(FlockingError): ...
|
|
17
|
+
class CollisionError(FlockingError): ...
|
|
18
|
+
class UnsupportedError(FlockingError): ...
|
|
19
|
+
class TimeoutError(FlockingError): ...
|
|
20
|
+
|
|
21
|
+
class VehicleProfile:
|
|
22
|
+
def __init__(self, *, wheelbase: float = 2.7, length: float = 4.5,
|
|
23
|
+
width: float = 1.8, max_steering_angle: float = 0.6,
|
|
24
|
+
max_forward_speed: float = 20.0, max_reverse_speed: float = 5.0,
|
|
25
|
+
max_acceleration: float = 3.0, max_braking: float = 8.0,
|
|
26
|
+
max_slope_radians: float = 1.5707963267948966) -> None: ...
|
|
27
|
+
@property
|
|
28
|
+
def wheelbase(self) -> float: ...
|
|
29
|
+
@property
|
|
30
|
+
def length(self) -> float: ...
|
|
31
|
+
@property
|
|
32
|
+
def width(self) -> float: ...
|
|
33
|
+
@property
|
|
34
|
+
def max_steering_angle(self) -> float: ...
|
|
35
|
+
@property
|
|
36
|
+
def max_forward_speed(self) -> float: ...
|
|
37
|
+
@property
|
|
38
|
+
def max_reverse_speed(self) -> float: ...
|
|
39
|
+
@property
|
|
40
|
+
def max_acceleration(self) -> float: ...
|
|
41
|
+
@property
|
|
42
|
+
def max_braking(self) -> float: ...
|
|
43
|
+
@property
|
|
44
|
+
def max_slope_radians(self) -> float: ...
|
|
45
|
+
|
|
46
|
+
class VehicleControl:
|
|
47
|
+
def __init__(self, throttle: float = 0.0, brake: float = 0.0,
|
|
48
|
+
steering: float = 0.0, reverse: bool = False) -> None: ...
|
|
49
|
+
@property
|
|
50
|
+
def throttle(self) -> float: ...
|
|
51
|
+
@property
|
|
52
|
+
def brake(self) -> float: ...
|
|
53
|
+
@property
|
|
54
|
+
def steering(self) -> float: ...
|
|
55
|
+
@property
|
|
56
|
+
def reverse(self) -> bool: ...
|
|
57
|
+
|
|
58
|
+
class FormationConfig:
|
|
59
|
+
def __init__(self, kind: str = "convoy", *, spacing: float = 3.0,
|
|
60
|
+
max_speed: float = 8.0, target_weight: float = 1.0,
|
|
61
|
+
separation_weight: float = 1.5, alignment_weight: float = 0.25,
|
|
62
|
+
cohesion_weight: float = 0.1, neighbor_radius: float = 25.0,
|
|
63
|
+
separation_radius: float = 8.0, arrival_radius: float = 1.0,
|
|
64
|
+
reassignment_interval: int = 30, reassignment_hysteresis: float = 2.0,
|
|
65
|
+
bottleneck_lookahead: float = 12.0, widening_ticks: int = 30,
|
|
66
|
+
overtaking: bool = False, narrowing: bool = True, max_members: int = 256,
|
|
67
|
+
max_neighbors: int = 64, work_per_tick: int = 65536) -> None: ...
|
|
68
|
+
@property
|
|
69
|
+
def kind(self) -> str: ...
|
|
70
|
+
@property
|
|
71
|
+
def spacing(self) -> float: ...
|
|
72
|
+
@property
|
|
73
|
+
def max_speed(self) -> float: ...
|
|
74
|
+
@property
|
|
75
|
+
def target_weight(self) -> float: ...
|
|
76
|
+
@property
|
|
77
|
+
def separation_weight(self) -> float: ...
|
|
78
|
+
@property
|
|
79
|
+
def alignment_weight(self) -> float: ...
|
|
80
|
+
@property
|
|
81
|
+
def cohesion_weight(self) -> float: ...
|
|
82
|
+
@property
|
|
83
|
+
def neighbor_radius(self) -> float: ...
|
|
84
|
+
@property
|
|
85
|
+
def separation_radius(self) -> float: ...
|
|
86
|
+
@property
|
|
87
|
+
def arrival_radius(self) -> float: ...
|
|
88
|
+
@property
|
|
89
|
+
def reassignment_interval(self) -> int: ...
|
|
90
|
+
@property
|
|
91
|
+
def reassignment_hysteresis(self) -> float: ...
|
|
92
|
+
@property
|
|
93
|
+
def bottleneck_lookahead(self) -> float: ...
|
|
94
|
+
@property
|
|
95
|
+
def widening_ticks(self) -> int: ...
|
|
96
|
+
@property
|
|
97
|
+
def overtaking(self) -> bool: ...
|
|
98
|
+
@property
|
|
99
|
+
def narrowing(self) -> bool: ...
|
|
100
|
+
@property
|
|
101
|
+
def max_members(self) -> int: ...
|
|
102
|
+
@property
|
|
103
|
+
def max_neighbors(self) -> int: ...
|
|
104
|
+
@property
|
|
105
|
+
def work_per_tick(self) -> int: ...
|
|
106
|
+
|
|
107
|
+
class FormationMemberStatus:
|
|
108
|
+
@property
|
|
109
|
+
def vehicle_id(self) -> int: ...
|
|
110
|
+
@property
|
|
111
|
+
def slot(self) -> int: ...
|
|
112
|
+
@property
|
|
113
|
+
def target(self) -> tuple[float, float]: ...
|
|
114
|
+
@property
|
|
115
|
+
def state(self) -> str: ...
|
|
116
|
+
|
|
117
|
+
class FormationStatus:
|
|
118
|
+
@property
|
|
119
|
+
def id(self) -> int: ...
|
|
120
|
+
@property
|
|
121
|
+
def leader_id(self) -> int: ...
|
|
122
|
+
@property
|
|
123
|
+
def kind(self) -> str: ...
|
|
124
|
+
@property
|
|
125
|
+
def narrowed(self) -> bool: ...
|
|
126
|
+
@property
|
|
127
|
+
def resource_limited(self) -> bool: ...
|
|
128
|
+
@property
|
|
129
|
+
def work_used(self) -> int: ...
|
|
130
|
+
@property
|
|
131
|
+
def members(self) -> list[FormationMemberStatus]: ...
|
|
132
|
+
|
|
133
|
+
class SafetyStatus:
|
|
134
|
+
@property
|
|
135
|
+
def vehicle_id(self) -> int: ...
|
|
136
|
+
@property
|
|
137
|
+
def state(self) -> str: ...
|
|
138
|
+
@property
|
|
139
|
+
def blocked_by(self) -> list[str]: ...
|
|
140
|
+
@property
|
|
141
|
+
def blocked_ticks(self) -> int: ...
|
|
142
|
+
|
|
143
|
+
class DynamicObstacle:
|
|
144
|
+
@property
|
|
145
|
+
def id(self) -> int: ...
|
|
146
|
+
@property
|
|
147
|
+
def bounds(self) -> tuple[float, float, float, float]: ...
|
|
148
|
+
@property
|
|
149
|
+
def velocity(self) -> tuple[float, float]: ...
|
|
150
|
+
@property
|
|
151
|
+
def stopped(self) -> bool: ...
|
|
152
|
+
|
|
153
|
+
class Deadlock:
|
|
154
|
+
@property
|
|
155
|
+
def vehicle_ids(self) -> list[int]: ...
|
|
156
|
+
@property
|
|
157
|
+
def dynamic_obstacle_ids(self) -> list[int]: ...
|
|
158
|
+
@property
|
|
159
|
+
def blocked_ticks(self) -> int: ...
|
|
160
|
+
|
|
161
|
+
class NavigationStatus:
|
|
162
|
+
@property
|
|
163
|
+
def vehicle_id(self) -> int: ...
|
|
164
|
+
@property
|
|
165
|
+
def request_id(self) -> Optional[int]: ...
|
|
166
|
+
@property
|
|
167
|
+
def goal(self) -> Optional[tuple[float, float, Optional[float]]]: ...
|
|
168
|
+
@property
|
|
169
|
+
def state(self) -> str: ...
|
|
170
|
+
@property
|
|
171
|
+
def failure(self) -> Optional[str]: ...
|
|
172
|
+
@property
|
|
173
|
+
def route_id(self) -> Optional[int]: ...
|
|
174
|
+
@property
|
|
175
|
+
def progress(self) -> float: ...
|
|
176
|
+
@property
|
|
177
|
+
def attempt(self) -> int: ...
|
|
178
|
+
@property
|
|
179
|
+
def work_units(self) -> int: ...
|
|
180
|
+
@property
|
|
181
|
+
def guard_deferred(self) -> bool: ...
|
|
182
|
+
@property
|
|
183
|
+
def deferred_ticks(self) -> int: ...
|
|
184
|
+
@property
|
|
185
|
+
def cache_hits(self) -> int: ...
|
|
186
|
+
|
|
187
|
+
class SnapshotArrays(TypedDict):
|
|
188
|
+
tick: int
|
|
189
|
+
version: int
|
|
190
|
+
ids: NDArray[np.uint64]
|
|
191
|
+
states: NDArray[np.float32]
|
|
192
|
+
corners: NDArray[np.float32]
|
|
193
|
+
|
|
194
|
+
class RecoveryConfig:
|
|
195
|
+
def __init__(self, *, enabled: bool = False, blocked_ticks: int = 60,
|
|
196
|
+
reverse_ticks: int = 120, cooldown_ticks: int = 60, max_distance: float = 6.0,
|
|
197
|
+
reverse_speed: float = 2.0, max_attempts: int = 3, max_active: int = 16,
|
|
198
|
+
work_per_tick: int = 65536) -> None: ...
|
|
199
|
+
@property
|
|
200
|
+
def enabled(self) -> bool: ...
|
|
201
|
+
@property
|
|
202
|
+
def blocked_ticks(self) -> int: ...
|
|
203
|
+
@property
|
|
204
|
+
def reverse_ticks(self) -> int: ...
|
|
205
|
+
@property
|
|
206
|
+
def cooldown_ticks(self) -> int: ...
|
|
207
|
+
@property
|
|
208
|
+
def max_distance(self) -> float: ...
|
|
209
|
+
@property
|
|
210
|
+
def reverse_speed(self) -> float: ...
|
|
211
|
+
@property
|
|
212
|
+
def max_attempts(self) -> int: ...
|
|
213
|
+
@property
|
|
214
|
+
def max_active(self) -> int: ...
|
|
215
|
+
@property
|
|
216
|
+
def work_per_tick(self) -> int: ...
|
|
217
|
+
|
|
218
|
+
class RecoveryStatus:
|
|
219
|
+
@property
|
|
220
|
+
def vehicle_id(self) -> int: ...
|
|
221
|
+
@property
|
|
222
|
+
def state(self) -> str: ...
|
|
223
|
+
@property
|
|
224
|
+
def attempt(self) -> int: ...
|
|
225
|
+
@property
|
|
226
|
+
def started_tick(self) -> int: ...
|
|
227
|
+
@property
|
|
228
|
+
def distance(self) -> float: ...
|
|
229
|
+
|
|
230
|
+
class Snapshot:
|
|
231
|
+
@property
|
|
232
|
+
def tick(self) -> int: ...
|
|
233
|
+
@property
|
|
234
|
+
def version(self) -> int: ...
|
|
235
|
+
@property
|
|
236
|
+
def terrain_revision(self) -> int: ...
|
|
237
|
+
@property
|
|
238
|
+
def simulated_seconds(self) -> float: ...
|
|
239
|
+
@property
|
|
240
|
+
def vehicles(self) -> list[tuple[int, float, float, float]]: ...
|
|
241
|
+
@property
|
|
242
|
+
def formations(self) -> list[FormationStatus]: ...
|
|
243
|
+
@property
|
|
244
|
+
def recovery(self) -> list[RecoveryStatus]: ...
|
|
245
|
+
@property
|
|
246
|
+
def safety(self) -> list[SafetyStatus]: ...
|
|
247
|
+
@property
|
|
248
|
+
def dynamic_obstacles(self) -> list[DynamicObstacle]: ...
|
|
249
|
+
@property
|
|
250
|
+
def deadlocks(self) -> list[Deadlock]: ...
|
|
251
|
+
@property
|
|
252
|
+
def navigation(self) -> list[NavigationStatus]: ...
|
|
253
|
+
def to_bytes(self) -> bytes: ...
|
|
254
|
+
def numpy(self) -> SnapshotArrays:
|
|
255
|
+
"""Immutable owned arrays: ids[N], states[N,8], corners[N,4,2]."""
|
|
256
|
+
...
|
|
257
|
+
|
|
258
|
+
class StateFrame:
|
|
259
|
+
@staticmethod
|
|
260
|
+
def from_bytes(data: bytes) -> StateFrame: ...
|
|
261
|
+
@property
|
|
262
|
+
def tick(self) -> int: ...
|
|
263
|
+
@property
|
|
264
|
+
def version(self) -> int: ...
|
|
265
|
+
@property
|
|
266
|
+
def simulated_seconds(self) -> float: ...
|
|
267
|
+
@property
|
|
268
|
+
def vehicles(self) -> list[tuple[int, float, float, float]]: ...
|
|
269
|
+
@property
|
|
270
|
+
def dynamic_obstacles(self) -> list[DynamicObstacle]: ...
|
|
271
|
+
def numpy(self) -> SnapshotArrays: ...
|
|
272
|
+
def to_bytes(self) -> bytes: ...
|
|
273
|
+
|
|
274
|
+
Profile = Union[VehicleProfile, Literal["default"], None]
|
|
275
|
+
GoalRow = Union[tuple[int, float, float], tuple[int, float, float, Optional[float]]]
|
|
276
|
+
|
|
277
|
+
class World:
|
|
278
|
+
def __init__(self, width: int = 10000, height: int = 10000,
|
|
279
|
+
tick_hz: int = 30, queue_capacity: int = 1024, *, threads: Optional[int] = None,
|
|
280
|
+
parallel_threshold: int = 128,
|
|
281
|
+
max_slope_radians: float = 0.7853981633974483, terrain_cost_weight: float = 0.0) -> None: ...
|
|
282
|
+
@staticmethod
|
|
283
|
+
def from_heightmap(heights: ArrayLike, meters_per_cell: float = 1.0,
|
|
284
|
+
tick_hz: int = 30, queue_capacity: int = 1024, *, threads: Optional[int] = None,
|
|
285
|
+
parallel_threshold: int = 128,
|
|
286
|
+
max_slope_radians: float = 0.7853981633974483, terrain_cost_weight: float = 0.0, minimum_height: Optional[float] = None) -> World: ...
|
|
287
|
+
def update_heightmap(self, x: int, y: int, heights: ArrayLike) -> int:
|
|
288
|
+
"""Admit a bounded absolute sample patch; snapshot.terrain_revision confirms commit."""
|
|
289
|
+
...
|
|
290
|
+
def set_static_obstacles(self, obstacles: Iterable[tuple[int, float, float, float, float]]) -> int:
|
|
291
|
+
"""Replace at most 128 (id,min_x,min_y,max_x,max_y) footprints; snapshot.terrain_revision confirms commit."""
|
|
292
|
+
...
|
|
293
|
+
def __enter__(self) -> World: ...
|
|
294
|
+
def __exit__(self, exc_type: Optional[type[BaseException]],
|
|
295
|
+
exc_value: Optional[BaseException], traceback: Optional[TracebackType]) -> bool: ...
|
|
296
|
+
@property
|
|
297
|
+
def lifecycle(self) -> str: ...
|
|
298
|
+
@property
|
|
299
|
+
def threads(self) -> int: ...
|
|
300
|
+
@property
|
|
301
|
+
def tick_hz(self) -> int: ...
|
|
302
|
+
@property
|
|
303
|
+
def max_slope_radians(self) -> float: ...
|
|
304
|
+
@property
|
|
305
|
+
def terrain_cost_weight(self) -> float: ...
|
|
306
|
+
def metrics(self) -> dict[str, Any]: ...
|
|
307
|
+
def queue_metrics(self) -> dict[str, Any]: ...
|
|
308
|
+
def start(self) -> None: ...
|
|
309
|
+
def pause(self) -> None: ...
|
|
310
|
+
def resume(self) -> None: ...
|
|
311
|
+
def stop(self) -> None: ...
|
|
312
|
+
def step_once(self) -> None: ...
|
|
313
|
+
def snapshot(self) -> Snapshot: ...
|
|
314
|
+
def wait_for_tick(self, minimum_tick: int, timeout: float = 1.0) -> Snapshot: ...
|
|
315
|
+
def spawn_vehicle(self, x: float, y: float, heading: float = 0.0,
|
|
316
|
+
profile: Profile = None, *, control: Optional[VehicleControl] = None) -> int:
|
|
317
|
+
"""Admit spawn and optional initial control together; control uses a second queue slot."""
|
|
318
|
+
...
|
|
319
|
+
def spawn_vehicles(self, poses: Union[Iterable[tuple[float, float, float]], NDArray[Any]],
|
|
320
|
+
profile: Profile = None) -> list[int]: ...
|
|
321
|
+
def set_vehicle_control(self, id: int, control: VehicleControl) -> None: ...
|
|
322
|
+
def set_vehicle_controls(self, controls: Iterable[tuple[int, VehicleControl]]) -> None: ...
|
|
323
|
+
def go_to(self, id: int, x: float, y: float) -> None: ...
|
|
324
|
+
def request_go_to(self, id: int, x: float, y: float, heading: Optional[float] = None) -> int: ...
|
|
325
|
+
def request_go_to_many(self, goals: Iterable[GoalRow]) -> list[int]: ...
|
|
326
|
+
def cancel_navigation(self, id: int) -> None: ...
|
|
327
|
+
def despawn_vehicle(self, id: int) -> None: ...
|
|
328
|
+
def despawn_vehicles(self, ids: Iterable[int]) -> None: ...
|
|
329
|
+
def set_vehicle_priority(self, id: int, priority: int) -> None: ...
|
|
330
|
+
def configure_recovery(self, config: RecoveryConfig) -> None: ...
|
|
331
|
+
def create_formation(self, leader_id: int, follower_ids: Sequence[int],
|
|
332
|
+
config: Optional[FormationConfig] = None) -> int: ...
|
|
333
|
+
def configure_formation(self, id: int, config: FormationConfig) -> None: ...
|
|
334
|
+
def set_formation_members(self, id: int, follower_ids: Sequence[int]) -> None: ...
|
|
335
|
+
def disband_formation(self, id: int) -> None: ...
|
|
336
|
+
def spawn_dynamic_obstacle(self, min_x: float, min_y: float, max_x: float,
|
|
337
|
+
max_y: float, velocity_x: float = 0.0, velocity_y: float = 0.0) -> int: ...
|
|
338
|
+
def set_dynamic_obstacle_velocity(self, id: int, velocity_x: float,
|
|
339
|
+
velocity_y: float) -> None: ...
|
|
340
|
+
def despawn_dynamic_obstacle(self, id: int) -> None: ...
|
flocking/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flocking
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: Intended Audience :: Science/Research
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Rust
|
|
11
|
+
Classifier: Topic :: Games/Entertainment :: Simulation
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Dist: numpy>=1.26 ; extra == 'numpy'
|
|
15
|
+
Provides-Extra: numpy
|
|
16
|
+
License-File: LICENSE-MIT
|
|
17
|
+
License-File: LICENSE-APACHE
|
|
18
|
+
Summary: Multithreaded off-road vehicle navigation, collision safety and formations in Rust, driven from Python
|
|
19
|
+
Keywords: simulation,navigation,pathfinding,flocking,formations,vehicles,game-server
|
|
20
|
+
Home-Page: https://github.com/benahedron/flocking
|
|
21
|
+
Author: Flocking contributors
|
|
22
|
+
License-Expression: MIT OR Apache-2.0
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
25
|
+
Project-URL: Changelog, https://github.com/benahedron/flocking/blob/main/CHANGELOG.md
|
|
26
|
+
Project-URL: Documentation, https://github.com/benahedron/flocking/blob/main/docs/python-api.md
|
|
27
|
+
Project-URL: Homepage, https://github.com/benahedron/flocking
|
|
28
|
+
Project-URL: Issues, https://github.com/benahedron/flocking/issues
|
|
29
|
+
|
|
30
|
+
# Flocking
|
|
31
|
+
|
|
32
|
+
Flocking moves large numbers of vehicles across open terrain for game servers
|
|
33
|
+
and simulations. A multithreaded Rust core handles off-road route planning,
|
|
34
|
+
vehicle dynamics, collision safety and formations (convoys, rows, wedges and
|
|
35
|
+
flocks). Python drives it through a small queued-command API.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install flocking # or: pip install "flocking[numpy]"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import time
|
|
43
|
+
import flocking
|
|
44
|
+
|
|
45
|
+
with flocking.World(width=500, height=500) as world: # flat 500 m map
|
|
46
|
+
world.start() # Rust worker ticks at 30 Hz
|
|
47
|
+
car = world.spawn_vehicle(x=50.0, y=50.0)
|
|
48
|
+
world.go_to(car, x=120.0, y=90.0) # queued; planned off-road
|
|
49
|
+
|
|
50
|
+
while True:
|
|
51
|
+
snap = world.snapshot() # coherent, non-blocking
|
|
52
|
+
status = next((s for s in snap.navigation if s.vehicle_id == car), None)
|
|
53
|
+
if status and status.state in ("Arrived", "Failed"):
|
|
54
|
+
break
|
|
55
|
+
time.sleep(0.1)
|
|
56
|
+
|
|
57
|
+
vid, x, y, heading = snap.vehicles[0]
|
|
58
|
+
print(f"tick {snap.tick}: vehicle {vid} {status.state} at ({x:.1f}, {y:.1f})")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The simulation runs on its own thread. Commands enter a bounded queue and take
|
|
62
|
+
effect at the next tick, and every `snapshot()` is a consistent, tick-stamped
|
|
63
|
+
view of the world. Python callbacks are never invoked from the simulation, and
|
|
64
|
+
blocking calls release the GIL.
|
|
65
|
+
|
|
66
|
+
## Heightmaps and bulk control
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import numpy as np
|
|
70
|
+
import flocking
|
|
71
|
+
|
|
72
|
+
xs = np.linspace(0, 6 * np.pi, 1001, dtype=np.float32)
|
|
73
|
+
heights = 20 + 8 * np.sin(xs)[None, :] * np.cos(xs)[:, None] # 1 km, [y, x]
|
|
74
|
+
|
|
75
|
+
with flocking.World.from_heightmap(heights, meters_per_cell=1.0) as world:
|
|
76
|
+
world.pause() # deterministic manual stepping
|
|
77
|
+
ids = world.spawn_vehicles([(100.0, 100.0 + 8 * i, 0.0) for i in range(10)])
|
|
78
|
+
world.request_go_to_many([(i, 600.0, 140.0 + 8 * n) for n, i in enumerate(ids)])
|
|
79
|
+
for _ in range(900): # 30 simulated seconds
|
|
80
|
+
world.step_once()
|
|
81
|
+
arrays = world.snapshot().numpy()
|
|
82
|
+
print(arrays["ids"], arrays["states"][:, :2])
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Terrain can be edited at runtime with `update_heightmap()` and
|
|
86
|
+
`set_static_obstacles()`. Each committed revision is an immutable snapshot.
|
|
87
|
+
|
|
88
|
+
## Features
|
|
89
|
+
|
|
90
|
+
- Worlds up to 10 km × 10 km at 1 m resolution, chunked terrain with slope queries
|
|
91
|
+
- Kinematic bicycle dynamics with footprints, reversing and slope-aware climbing
|
|
92
|
+
- Hierarchical A*, Hybrid A* and Reeds–Shepp route planning with caching
|
|
93
|
+
- Swept collision safety, emergency stops, priorities, deadlock detection and recovery
|
|
94
|
+
- Convoy, row, wedge and flock formations with slot reassignment and bottleneck narrowing
|
|
95
|
+
- Fixed-step worker lifecycle, bulk commands, deterministic replay, NumPy snapshots
|
|
96
|
+
|
|
97
|
+
Wheels are abi3 (CPython 3.9+) for Linux, macOS and Windows. NumPy is optional
|
|
98
|
+
and is needed only for heightmap arrays and `snapshot.numpy()`.
|
|
99
|
+
|
|
100
|
+
## Links
|
|
101
|
+
|
|
102
|
+
- [Source and 3D debug viewer](https://github.com/benahedron/flocking)
|
|
103
|
+
- [Python API reference](https://github.com/benahedron/flocking/blob/main/docs/python-api.md)
|
|
104
|
+
- [Example scripts](https://github.com/benahedron/flocking/blob/main/docs/examples.md)
|
|
105
|
+
- [Changelog](https://github.com/benahedron/flocking/blob/main/CHANGELOG.md)
|
|
106
|
+
|
|
107
|
+
Licensed under MIT or Apache-2.0, at your option.
|
|
108
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
flocking/__init__.py,sha256=lgC5uI0uM66cz9PQP_rJVdK2P59DnGXc4VbNkIbfRz8,1245
|
|
2
|
+
flocking/_native.pyd,sha256=d6UkVCCXuBnBW7Xl0mplQC7ZJ9N1mXr8oJn1iaWsLG8,1656832
|
|
3
|
+
flocking/_native.pyi,sha256=PPx0yI04lmFZ8bRuc9-LJ3-5n8I8Kcn-Rj0kc6ic_Ac,12541
|
|
4
|
+
flocking/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
flocking-0.1.0.dist-info/METADATA,sha256=CdUtO3DeAFc4aPI1p9-JWEt25oBaCE-yW_hBeR1308c,4745
|
|
6
|
+
flocking-0.1.0.dist-info/WHEEL,sha256=xw8F9xBWUOgm5ym2S9tihj_bLJOb723bjhA0kdANo2I,95
|
|
7
|
+
flocking-0.1.0.dist-info/licenses/LICENSE-APACHE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
8
|
+
flocking-0.1.0.dist-info/licenses/LICENSE-MIT,sha256=jk_Ux4ZorHf_chJsGq_8RmKwfGfyF7h3BRvfQc-iVPs,1099
|
|
9
|
+
flocking-0.1.0.dist-info/sboms/flocking-python.cyclonedx.json,sha256=Rr1zgsUxVrEgJz0O2t0ahUE5SnkYhA7ryF-REyhALFk,30299
|
|
10
|
+
flocking-0.1.0.dist-info/RECORD,,
|