simweave 0.4.0__py3-none-any.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.
- simweave/__init__.py +198 -0
- simweave/agents/__init__.py +23 -0
- simweave/agents/agent.py +161 -0
- simweave/agents/compass.py +48 -0
- simweave/agents/routing.py +113 -0
- simweave/continuous/__init__.py +31 -0
- simweave/continuous/solver.py +237 -0
- simweave/continuous/systems/__init__.py +14 -0
- simweave/continuous/systems/mass_spring_damper.py +43 -0
- simweave/continuous/systems/pendulum.py +49 -0
- simweave/continuous/systems/quarter_car.py +59 -0
- simweave/continuous/systems/rlc.py +44 -0
- simweave/continuous/systems/thermal.py +127 -0
- simweave/core/__init__.py +18 -0
- simweave/core/clock.py +64 -0
- simweave/core/entity.py +74 -0
- simweave/core/environment.py +121 -0
- simweave/core/logging.py +41 -0
- simweave/core/scheduler.py +85 -0
- simweave/currency/__init__.py +64 -0
- simweave/currency/codes.py +257 -0
- simweave/currency/format.py +92 -0
- simweave/currency/fx.py +126 -0
- simweave/currency/money.py +328 -0
- simweave/discrete/__init__.py +28 -0
- simweave/discrete/properties.py +113 -0
- simweave/discrete/queues.py +317 -0
- simweave/discrete/resources.py +89 -0
- simweave/discrete/services.py +228 -0
- simweave/mc/__init__.py +5 -0
- simweave/mc/runner.py +151 -0
- simweave/spatial/__init__.py +5 -0
- simweave/spatial/graph.py +147 -0
- simweave/supplychain/__init__.py +9 -0
- simweave/supplychain/inventory.py +77 -0
- simweave/supplychain/optimization.py +197 -0
- simweave/supplychain/warehouse.py +153 -0
- simweave/units/__init__.py +25 -0
- simweave/units/si.py +213 -0
- simweave/viz/__init__.py +77 -0
- simweave/viz/_plotly.py +35 -0
- simweave/viz/plots.py +618 -0
- simweave/viz/recorders.py +172 -0
- simweave/viz/themes.py +187 -0
- simweave-0.4.0.dist-info/METADATA +304 -0
- simweave-0.4.0.dist-info/RECORD +48 -0
- simweave-0.4.0.dist-info/WHEEL +5 -0
- simweave-0.4.0.dist-info/top_level.txt +1 -0
simweave/__init__.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""simweave: atomic-clock hybrid simulation engine.
|
|
2
|
+
|
|
3
|
+
Public surface re-exports the primitives most users will need. For anything
|
|
4
|
+
else, import the submodule directly (``simweave.continuous``, ``simweave.agents``,
|
|
5
|
+
etc).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from simweave.core import (
|
|
9
|
+
Clock,
|
|
10
|
+
EventQueue,
|
|
11
|
+
ScheduledEvent,
|
|
12
|
+
Entity,
|
|
13
|
+
SimEnvironment,
|
|
14
|
+
configure as configure_logging,
|
|
15
|
+
get_logger,
|
|
16
|
+
)
|
|
17
|
+
from simweave.discrete import (
|
|
18
|
+
EntityProperties,
|
|
19
|
+
exponential,
|
|
20
|
+
uniform,
|
|
21
|
+
normal,
|
|
22
|
+
deterministic,
|
|
23
|
+
set_default_seed,
|
|
24
|
+
Queue,
|
|
25
|
+
PriorityQueue,
|
|
26
|
+
Resource,
|
|
27
|
+
ResourcePool,
|
|
28
|
+
Service,
|
|
29
|
+
ArrivalGenerator,
|
|
30
|
+
)
|
|
31
|
+
from simweave.continuous import (
|
|
32
|
+
DynamicSystem,
|
|
33
|
+
SupportsDynamics,
|
|
34
|
+
SimulationResult,
|
|
35
|
+
simulate,
|
|
36
|
+
ContinuousProcess,
|
|
37
|
+
MassSpringDamper,
|
|
38
|
+
SimplePendulum,
|
|
39
|
+
QuarterCarModel,
|
|
40
|
+
SeriesRLC,
|
|
41
|
+
ThermalRC,
|
|
42
|
+
TwoMassThermal,
|
|
43
|
+
)
|
|
44
|
+
from simweave.spatial import Graph, grid_graph
|
|
45
|
+
from simweave.agents import (
|
|
46
|
+
Compass,
|
|
47
|
+
Agent,
|
|
48
|
+
a_star,
|
|
49
|
+
dijkstra,
|
|
50
|
+
NoPathError,
|
|
51
|
+
manhattan,
|
|
52
|
+
euclidean,
|
|
53
|
+
chebyshev,
|
|
54
|
+
)
|
|
55
|
+
from simweave.supplychain import InventoryItems, Warehouse
|
|
56
|
+
from simweave.mc import MCResult, run_monte_carlo, run_batched_mc
|
|
57
|
+
from simweave.units import (
|
|
58
|
+
SIUnit,
|
|
59
|
+
Distance,
|
|
60
|
+
Velocity,
|
|
61
|
+
Acceleration,
|
|
62
|
+
Mass,
|
|
63
|
+
Force,
|
|
64
|
+
Area,
|
|
65
|
+
Volume,
|
|
66
|
+
TimeUnit,
|
|
67
|
+
)
|
|
68
|
+
from simweave.currency import (
|
|
69
|
+
Money,
|
|
70
|
+
CurrencyMismatchError,
|
|
71
|
+
FXConverter,
|
|
72
|
+
StaticFXConverter,
|
|
73
|
+
CallableFXConverter,
|
|
74
|
+
format_money,
|
|
75
|
+
register_custom,
|
|
76
|
+
unregister_custom,
|
|
77
|
+
is_valid_currency,
|
|
78
|
+
get_decimals,
|
|
79
|
+
list_codes,
|
|
80
|
+
)
|
|
81
|
+
# simweave.viz is import-cheap: plotly is only required when a plot helper
|
|
82
|
+
# is actually called. The names below are always importable; calling one
|
|
83
|
+
# without the `simweave[viz]` extra raises a clear ImportError.
|
|
84
|
+
from simweave.viz import (
|
|
85
|
+
QueueLengthRecorder,
|
|
86
|
+
ServiceUtilisationRecorder,
|
|
87
|
+
WarehouseStockRecorder,
|
|
88
|
+
apply_theme,
|
|
89
|
+
available_themes,
|
|
90
|
+
get_default_theme,
|
|
91
|
+
have_plotly,
|
|
92
|
+
plot_agent_path,
|
|
93
|
+
plot_mc_fan,
|
|
94
|
+
plot_phase_portrait,
|
|
95
|
+
plot_queue_length,
|
|
96
|
+
plot_service_utilisation,
|
|
97
|
+
plot_state_trajectories,
|
|
98
|
+
plot_warehouse_stock,
|
|
99
|
+
register_theme,
|
|
100
|
+
set_default_theme,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
__version__ = "0.4.0"
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
# Core
|
|
107
|
+
"Clock",
|
|
108
|
+
"EventQueue",
|
|
109
|
+
"ScheduledEvent",
|
|
110
|
+
"Entity",
|
|
111
|
+
"SimEnvironment",
|
|
112
|
+
"configure_logging",
|
|
113
|
+
"get_logger",
|
|
114
|
+
# Discrete
|
|
115
|
+
"EntityProperties",
|
|
116
|
+
"exponential",
|
|
117
|
+
"uniform",
|
|
118
|
+
"normal",
|
|
119
|
+
"deterministic",
|
|
120
|
+
"set_default_seed",
|
|
121
|
+
"Queue",
|
|
122
|
+
"PriorityQueue",
|
|
123
|
+
"Resource",
|
|
124
|
+
"ResourcePool",
|
|
125
|
+
"Service",
|
|
126
|
+
"ArrivalGenerator",
|
|
127
|
+
# Continuous
|
|
128
|
+
"DynamicSystem",
|
|
129
|
+
"SupportsDynamics",
|
|
130
|
+
"SimulationResult",
|
|
131
|
+
"simulate",
|
|
132
|
+
"ContinuousProcess",
|
|
133
|
+
"MassSpringDamper",
|
|
134
|
+
"SimplePendulum",
|
|
135
|
+
"QuarterCarModel",
|
|
136
|
+
"SeriesRLC",
|
|
137
|
+
"ThermalRC",
|
|
138
|
+
"TwoMassThermal",
|
|
139
|
+
# Spatial / Agents
|
|
140
|
+
"Graph",
|
|
141
|
+
"grid_graph",
|
|
142
|
+
"Compass",
|
|
143
|
+
"Agent",
|
|
144
|
+
"a_star",
|
|
145
|
+
"dijkstra",
|
|
146
|
+
"NoPathError",
|
|
147
|
+
"manhattan",
|
|
148
|
+
"euclidean",
|
|
149
|
+
"chebyshev",
|
|
150
|
+
# Supply chain
|
|
151
|
+
"InventoryItems",
|
|
152
|
+
"Warehouse",
|
|
153
|
+
# Monte Carlo
|
|
154
|
+
"MCResult",
|
|
155
|
+
"run_monte_carlo",
|
|
156
|
+
"run_batched_mc",
|
|
157
|
+
# Units
|
|
158
|
+
"SIUnit",
|
|
159
|
+
"Distance",
|
|
160
|
+
"Velocity",
|
|
161
|
+
"Acceleration",
|
|
162
|
+
"Mass",
|
|
163
|
+
"Force",
|
|
164
|
+
"Area",
|
|
165
|
+
"Volume",
|
|
166
|
+
"TimeUnit",
|
|
167
|
+
# Currency
|
|
168
|
+
"Money",
|
|
169
|
+
"CurrencyMismatchError",
|
|
170
|
+
"FXConverter",
|
|
171
|
+
"StaticFXConverter",
|
|
172
|
+
"CallableFXConverter",
|
|
173
|
+
"format_money",
|
|
174
|
+
"register_custom",
|
|
175
|
+
"unregister_custom",
|
|
176
|
+
"is_valid_currency",
|
|
177
|
+
"get_decimals",
|
|
178
|
+
"list_codes",
|
|
179
|
+
# Viz
|
|
180
|
+
"QueueLengthRecorder",
|
|
181
|
+
"ServiceUtilisationRecorder",
|
|
182
|
+
"WarehouseStockRecorder",
|
|
183
|
+
"apply_theme",
|
|
184
|
+
"available_themes",
|
|
185
|
+
"get_default_theme",
|
|
186
|
+
"have_plotly",
|
|
187
|
+
"plot_agent_path",
|
|
188
|
+
"plot_mc_fan",
|
|
189
|
+
"plot_phase_portrait",
|
|
190
|
+
"plot_queue_length",
|
|
191
|
+
"plot_service_utilisation",
|
|
192
|
+
"plot_state_trajectories",
|
|
193
|
+
"plot_warehouse_stock",
|
|
194
|
+
"register_theme",
|
|
195
|
+
"set_default_theme",
|
|
196
|
+
# Meta
|
|
197
|
+
"__version__",
|
|
198
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Agents, routing, and orientation primitives."""
|
|
2
|
+
|
|
3
|
+
from simweave.agents.compass import Compass
|
|
4
|
+
from simweave.agents.routing import (
|
|
5
|
+
a_star,
|
|
6
|
+
dijkstra,
|
|
7
|
+
NoPathError,
|
|
8
|
+
manhattan,
|
|
9
|
+
euclidean,
|
|
10
|
+
chebyshev,
|
|
11
|
+
)
|
|
12
|
+
from simweave.agents.agent import Agent
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Compass",
|
|
16
|
+
"a_star",
|
|
17
|
+
"dijkstra",
|
|
18
|
+
"NoPathError",
|
|
19
|
+
"manhattan",
|
|
20
|
+
"euclidean",
|
|
21
|
+
"chebyshev",
|
|
22
|
+
"Agent",
|
|
23
|
+
]
|
simweave/agents/agent.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Routable agent that inherits from :class:`~simweave.core.entity.Entity`.
|
|
2
|
+
|
|
3
|
+
An Agent traverses a graph under its own steam. On each tick:
|
|
4
|
+
|
|
5
|
+
* If it has no plan and has tasks queued, it replans to the next task via A*.
|
|
6
|
+
* If it is mid-edge, it consumes distance according to ``speed * dt``.
|
|
7
|
+
* On arrival at a node, it pops it from the path, records history, and may
|
|
8
|
+
fire ``on_arrive(node)``.
|
|
9
|
+
|
|
10
|
+
The Agent holds only a reference to the graph; it does not own it. Several
|
|
11
|
+
agents may share a graph.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any, Callable, Hashable, Sequence
|
|
17
|
+
|
|
18
|
+
from simweave.core.entity import Entity
|
|
19
|
+
from simweave.core.logging import get_logger
|
|
20
|
+
from simweave.agents.compass import Compass
|
|
21
|
+
from simweave.agents.routing import a_star, NoPathError
|
|
22
|
+
from simweave.spatial.graph import adj_view, edge_weight
|
|
23
|
+
|
|
24
|
+
log = get_logger("agents.agent")
|
|
25
|
+
|
|
26
|
+
Node = Hashable
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Agent(Entity):
|
|
30
|
+
"""Graph-routing agent.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
----------
|
|
34
|
+
graph:
|
|
35
|
+
A simweave Graph, networkx graph, or dict-of-dict adjacency.
|
|
36
|
+
start_node:
|
|
37
|
+
Node at which the agent spawns.
|
|
38
|
+
tasks:
|
|
39
|
+
Ordered list of target nodes to visit.
|
|
40
|
+
speed:
|
|
41
|
+
Distance per unit time.
|
|
42
|
+
heuristic:
|
|
43
|
+
Optional heuristic for A*. If None, A* degenerates to Dijkstra.
|
|
44
|
+
on_arrive:
|
|
45
|
+
Optional callback ``on_arrive(agent, node, env)`` fired when the
|
|
46
|
+
agent arrives at any node (including intermediate path nodes).
|
|
47
|
+
name:
|
|
48
|
+
Optional agent name.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
graph: Any,
|
|
54
|
+
start_node: Node,
|
|
55
|
+
tasks: Sequence[Node] | None = None,
|
|
56
|
+
speed: float = 1.0,
|
|
57
|
+
heuristic: Callable[[Node, Node], float] | None = None,
|
|
58
|
+
on_arrive: Callable[["Agent", Node, Any], None] | None = None,
|
|
59
|
+
weight_attr: str = "weight",
|
|
60
|
+
name: str | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
super().__init__(name=name)
|
|
63
|
+
self.graph = graph
|
|
64
|
+
self.position: Node = start_node
|
|
65
|
+
self.tasks: list[Node] = list(tasks or [])
|
|
66
|
+
self.path: list[Node] = []
|
|
67
|
+
self.speed = float(speed)
|
|
68
|
+
self.heuristic = heuristic
|
|
69
|
+
self.compass = Compass(points=8)
|
|
70
|
+
self.weight_attr = weight_attr
|
|
71
|
+
self.on_arrive = on_arrive
|
|
72
|
+
|
|
73
|
+
self._edge_remaining: float = 0.0
|
|
74
|
+
self._edge_weight: float = 0.0
|
|
75
|
+
self.history: list[tuple[float, Node]] = []
|
|
76
|
+
self.completed_tasks: int = 0
|
|
77
|
+
|
|
78
|
+
# ------------------------------------------------------------------
|
|
79
|
+
# Routing
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
def plan_to(self, target: Node) -> None:
|
|
82
|
+
"""Replan to ``target`` from the current position using A*."""
|
|
83
|
+
try:
|
|
84
|
+
path = a_star(
|
|
85
|
+
self.graph,
|
|
86
|
+
self.position,
|
|
87
|
+
target,
|
|
88
|
+
heuristic=self.heuristic,
|
|
89
|
+
weight_attr=self.weight_attr,
|
|
90
|
+
)
|
|
91
|
+
except NoPathError:
|
|
92
|
+
log.warning("%s: no path from %s to %s", self.name, self.position, target)
|
|
93
|
+
self.path = []
|
|
94
|
+
return
|
|
95
|
+
# path[0] is the current position; drop it.
|
|
96
|
+
self.path = path[1:] if path and path[0] == self.position else path
|
|
97
|
+
|
|
98
|
+
def _take_next_task(self) -> None:
|
|
99
|
+
while self.tasks and not self.path:
|
|
100
|
+
target = self.tasks.pop(0)
|
|
101
|
+
if target == self.position:
|
|
102
|
+
# Already here; count as arrival.
|
|
103
|
+
self.completed_tasks += 1
|
|
104
|
+
if self.on_arrive is not None:
|
|
105
|
+
self.on_arrive(self, target, None)
|
|
106
|
+
continue
|
|
107
|
+
self.plan_to(target)
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Tick loop
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
def tick(self, dt: float, env) -> None:
|
|
113
|
+
super().tick(dt, env)
|
|
114
|
+
if not self.path:
|
|
115
|
+
self._take_next_task()
|
|
116
|
+
if not self.path:
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
remaining_step = self.speed * dt
|
|
120
|
+
while remaining_step > 0 and self.path:
|
|
121
|
+
if self._edge_remaining <= 0:
|
|
122
|
+
# Begin traversing next edge.
|
|
123
|
+
next_node = self.path[0]
|
|
124
|
+
nbrs = adj_view(self.graph, self.position)
|
|
125
|
+
if next_node not in nbrs:
|
|
126
|
+
log.warning(
|
|
127
|
+
"%s: edge %s->%s missing; replanning.",
|
|
128
|
+
self.name,
|
|
129
|
+
self.position,
|
|
130
|
+
next_node,
|
|
131
|
+
)
|
|
132
|
+
if self.path:
|
|
133
|
+
target = self.path[-1]
|
|
134
|
+
self.path = []
|
|
135
|
+
self.plan_to(target)
|
|
136
|
+
return
|
|
137
|
+
self._edge_weight = edge_weight(nbrs[next_node], self.weight_attr)
|
|
138
|
+
self._edge_remaining = self._edge_weight
|
|
139
|
+
|
|
140
|
+
step = min(remaining_step, self._edge_remaining)
|
|
141
|
+
self._edge_remaining -= step
|
|
142
|
+
remaining_step -= step
|
|
143
|
+
|
|
144
|
+
if self._edge_remaining <= 1e-12:
|
|
145
|
+
self.position = self.path.pop(0)
|
|
146
|
+
self.history.append((env.clock.t + dt, self.position))
|
|
147
|
+
if self.on_arrive is not None:
|
|
148
|
+
self.on_arrive(self, self.position, env)
|
|
149
|
+
# If we've exhausted the current task plan, count the
|
|
150
|
+
# arrival as a completed task regardless of whether more
|
|
151
|
+
# tasks are queued behind it.
|
|
152
|
+
if not self.path:
|
|
153
|
+
self.completed_tasks += 1
|
|
154
|
+
|
|
155
|
+
# If the path finished and more tasks queued, plan immediately so the
|
|
156
|
+
# next tick doesn't waste a step standing still.
|
|
157
|
+
if not self.path and self.tasks:
|
|
158
|
+
self._take_next_task()
|
|
159
|
+
|
|
160
|
+
def has_work(self, env) -> bool:
|
|
161
|
+
return bool(self.path) or bool(self.tasks)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Discrete compass (4, 6, 8 or 16 point).
|
|
2
|
+
|
|
3
|
+
Used by :class:`~simweave.agents.agent.Agent` for cheap orientation tracking
|
|
4
|
+
independent of any graph topology.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Compass:
|
|
11
|
+
"""Discrete compass quantised to a fixed number of points."""
|
|
12
|
+
|
|
13
|
+
_LABELS_8 = ("N", "NE", "E", "SE", "S", "SW", "W", "NW")
|
|
14
|
+
_LABELS_4 = ("N", "E", "S", "W")
|
|
15
|
+
_ALLOWED = (4, 6, 8, 16)
|
|
16
|
+
|
|
17
|
+
def __init__(self, points: int = 8, angle: float = 0.0) -> None:
|
|
18
|
+
if points not in self._ALLOWED:
|
|
19
|
+
raise ValueError(f"Compass points must be one of {self._ALLOWED}")
|
|
20
|
+
self.points = points
|
|
21
|
+
self.atomic_angle = 360.0 / points
|
|
22
|
+
self.angle = self._quantize(float(angle))
|
|
23
|
+
|
|
24
|
+
def _quantize(self, angle: float) -> float:
|
|
25
|
+
angle %= 360.0
|
|
26
|
+
return (round(angle / self.atomic_angle) % self.points) * self.atomic_angle
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def direction(self) -> str:
|
|
30
|
+
if self.points == 8:
|
|
31
|
+
idx = int(round(self.angle / 45.0)) % 8
|
|
32
|
+
return self._LABELS_8[idx]
|
|
33
|
+
if self.points == 4:
|
|
34
|
+
idx = int(round(self.angle / 90.0)) % 4
|
|
35
|
+
return self._LABELS_4[idx]
|
|
36
|
+
return f"CW_{self.angle:g}"
|
|
37
|
+
|
|
38
|
+
def set_absolute(self, angle: float) -> str:
|
|
39
|
+
self.angle = self._quantize(angle)
|
|
40
|
+
return self.direction
|
|
41
|
+
|
|
42
|
+
def clockwise(self, delta: float) -> str:
|
|
43
|
+
self.angle = self._quantize(self.angle + delta)
|
|
44
|
+
return self.direction
|
|
45
|
+
|
|
46
|
+
def anti_clockwise(self, delta: float) -> str:
|
|
47
|
+
self.angle = self._quantize(self.angle - delta)
|
|
48
|
+
return self.direction
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""A* (and Dijkstra) shortest path over a simweave / networkx / dict graph.
|
|
2
|
+
|
|
3
|
+
Pure stdlib; networkx is not required. If ``heuristic`` is ``None`` the
|
|
4
|
+
algorithm degrades to Dijkstra (heuristic = 0 is admissible).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import heapq
|
|
10
|
+
from math import inf, sqrt
|
|
11
|
+
from typing import Any, Callable, Hashable
|
|
12
|
+
|
|
13
|
+
from simweave.spatial.graph import adj_view, edge_weight
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Node = Hashable
|
|
17
|
+
Heuristic = Callable[[Node, Node], float]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class NoPathError(ValueError):
|
|
21
|
+
"""Raised when A* cannot reach the goal from the start node."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def a_star(
|
|
25
|
+
graph: Any,
|
|
26
|
+
start: Node,
|
|
27
|
+
goal: Node,
|
|
28
|
+
heuristic: Heuristic | None = None,
|
|
29
|
+
weight_attr: str = "weight",
|
|
30
|
+
) -> list[Node]:
|
|
31
|
+
"""Compute shortest path from ``start`` to ``goal``.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
graph:
|
|
36
|
+
Any object understood by :func:`simweave.spatial.graph.adj_view`.
|
|
37
|
+
heuristic:
|
|
38
|
+
Admissible heuristic ``h(node, goal)``. Defaults to zero (Dijkstra).
|
|
39
|
+
weight_attr:
|
|
40
|
+
Edge attribute to treat as weight. Defaults to ``"weight"``.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
list[Node]
|
|
45
|
+
Path from start to goal inclusive.
|
|
46
|
+
"""
|
|
47
|
+
if heuristic is None:
|
|
48
|
+
|
|
49
|
+
def heuristic(a: Node, b: Node) -> float:
|
|
50
|
+
return 0.0
|
|
51
|
+
|
|
52
|
+
open_heap: list[tuple[float, int, Node]] = [(0.0, 0, start)]
|
|
53
|
+
came_from: dict[Node, Node] = {}
|
|
54
|
+
g_score: dict[Node, float] = {start: 0.0}
|
|
55
|
+
counter = 1
|
|
56
|
+
closed: set[Node] = set()
|
|
57
|
+
|
|
58
|
+
while open_heap:
|
|
59
|
+
_, _, current = heapq.heappop(open_heap)
|
|
60
|
+
if current in closed:
|
|
61
|
+
continue
|
|
62
|
+
if current == goal:
|
|
63
|
+
return _reconstruct(came_from, current)
|
|
64
|
+
closed.add(current)
|
|
65
|
+
|
|
66
|
+
for neighbor, edge_data in adj_view(graph, current).items():
|
|
67
|
+
if neighbor in closed:
|
|
68
|
+
continue
|
|
69
|
+
w = edge_weight(edge_data, weight_attr)
|
|
70
|
+
tentative = g_score[current] + w
|
|
71
|
+
if tentative < g_score.get(neighbor, inf):
|
|
72
|
+
g_score[neighbor] = tentative
|
|
73
|
+
came_from[neighbor] = current
|
|
74
|
+
f = tentative + heuristic(neighbor, goal)
|
|
75
|
+
heapq.heappush(open_heap, (f, counter, neighbor))
|
|
76
|
+
counter += 1
|
|
77
|
+
|
|
78
|
+
raise NoPathError(f"No path from {start!r} to {goal!r}.")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def dijkstra(
|
|
82
|
+
graph: Any, start: Node, goal: Node, weight_attr: str = "weight"
|
|
83
|
+
) -> list[Node]:
|
|
84
|
+
"""Convenience wrapper -- A* with zero heuristic."""
|
|
85
|
+
return a_star(graph, start, goal, heuristic=None, weight_attr=weight_attr)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _reconstruct(came_from: dict[Node, Node], current: Node) -> list[Node]:
|
|
89
|
+
path = [current]
|
|
90
|
+
while current in came_from:
|
|
91
|
+
current = came_from[current]
|
|
92
|
+
path.append(current)
|
|
93
|
+
path.reverse()
|
|
94
|
+
return path
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Heuristics for 2D grid-like nodes (tuples of (row, col) or (x, y)).
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def manhattan(a: Node, b: Node) -> float:
|
|
103
|
+
return abs(a[0] - b[0]) + abs(a[1] - b[1]) # type: ignore[index]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def euclidean(a: Node, b: Node) -> float:
|
|
107
|
+
dx = a[0] - b[0] # type: ignore[index]
|
|
108
|
+
dy = a[1] - b[1] # type: ignore[index]
|
|
109
|
+
return sqrt(dx * dx + dy * dy)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def chebyshev(a: Node, b: Node) -> float:
|
|
113
|
+
return max(abs(a[0] - b[0]), abs(a[1] - b[1])) # type: ignore[index]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Continuous-time dynamics: state-space solver, plug-in systems, hybrid wrapper."""
|
|
2
|
+
|
|
3
|
+
from simweave.continuous.solver import (
|
|
4
|
+
DynamicSystem,
|
|
5
|
+
SupportsDynamics,
|
|
6
|
+
SimulationResult,
|
|
7
|
+
simulate,
|
|
8
|
+
ContinuousProcess,
|
|
9
|
+
)
|
|
10
|
+
from simweave.continuous.systems import (
|
|
11
|
+
MassSpringDamper,
|
|
12
|
+
SimplePendulum,
|
|
13
|
+
QuarterCarModel,
|
|
14
|
+
SeriesRLC,
|
|
15
|
+
ThermalRC,
|
|
16
|
+
TwoMassThermal,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"DynamicSystem",
|
|
21
|
+
"SupportsDynamics",
|
|
22
|
+
"SimulationResult",
|
|
23
|
+
"simulate",
|
|
24
|
+
"ContinuousProcess",
|
|
25
|
+
"MassSpringDamper",
|
|
26
|
+
"SimplePendulum",
|
|
27
|
+
"QuarterCarModel",
|
|
28
|
+
"SeriesRLC",
|
|
29
|
+
"ThermalRC",
|
|
30
|
+
"TwoMassThermal",
|
|
31
|
+
]
|