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.
Files changed (48) hide show
  1. simweave/__init__.py +198 -0
  2. simweave/agents/__init__.py +23 -0
  3. simweave/agents/agent.py +161 -0
  4. simweave/agents/compass.py +48 -0
  5. simweave/agents/routing.py +113 -0
  6. simweave/continuous/__init__.py +31 -0
  7. simweave/continuous/solver.py +237 -0
  8. simweave/continuous/systems/__init__.py +14 -0
  9. simweave/continuous/systems/mass_spring_damper.py +43 -0
  10. simweave/continuous/systems/pendulum.py +49 -0
  11. simweave/continuous/systems/quarter_car.py +59 -0
  12. simweave/continuous/systems/rlc.py +44 -0
  13. simweave/continuous/systems/thermal.py +127 -0
  14. simweave/core/__init__.py +18 -0
  15. simweave/core/clock.py +64 -0
  16. simweave/core/entity.py +74 -0
  17. simweave/core/environment.py +121 -0
  18. simweave/core/logging.py +41 -0
  19. simweave/core/scheduler.py +85 -0
  20. simweave/currency/__init__.py +64 -0
  21. simweave/currency/codes.py +257 -0
  22. simweave/currency/format.py +92 -0
  23. simweave/currency/fx.py +126 -0
  24. simweave/currency/money.py +328 -0
  25. simweave/discrete/__init__.py +28 -0
  26. simweave/discrete/properties.py +113 -0
  27. simweave/discrete/queues.py +317 -0
  28. simweave/discrete/resources.py +89 -0
  29. simweave/discrete/services.py +228 -0
  30. simweave/mc/__init__.py +5 -0
  31. simweave/mc/runner.py +151 -0
  32. simweave/spatial/__init__.py +5 -0
  33. simweave/spatial/graph.py +147 -0
  34. simweave/supplychain/__init__.py +9 -0
  35. simweave/supplychain/inventory.py +77 -0
  36. simweave/supplychain/optimization.py +197 -0
  37. simweave/supplychain/warehouse.py +153 -0
  38. simweave/units/__init__.py +25 -0
  39. simweave/units/si.py +213 -0
  40. simweave/viz/__init__.py +77 -0
  41. simweave/viz/_plotly.py +35 -0
  42. simweave/viz/plots.py +618 -0
  43. simweave/viz/recorders.py +172 -0
  44. simweave/viz/themes.py +187 -0
  45. simweave-0.4.0.dist-info/METADATA +304 -0
  46. simweave-0.4.0.dist-info/RECORD +48 -0
  47. simweave-0.4.0.dist-info/WHEEL +5 -0
  48. 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
+ ]
@@ -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
+ ]