simweave 0.8.0__tar.gz → 0.8.2__tar.gz
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-0.8.0/src/simweave.egg-info → simweave-0.8.2}/PKG-INFO +1 -1
- {simweave-0.8.0 → simweave-0.8.2}/pyproject.toml +1 -1
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/__init__.py +3 -1
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/control/suspension.py +4 -4
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/mass_spring_damper.py +2 -2
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/pendulum.py +2 -2
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/discrete/queues.py +10 -2
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/discrete/services.py +21 -2
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/supplychain/optimization.py +51 -20
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/__init__.py +2 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/plots.py +90 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/vehicle_dynamics.py +2 -3
- {simweave-0.8.0 → simweave-0.8.2/src/simweave.egg-info}/PKG-INFO +1 -1
- {simweave-0.8.0 → simweave-0.8.2}/MANIFEST.in +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/README.md +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/requirements-dev.txt +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/requirements.txt +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/setup.cfg +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/agents/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/agents/agent.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/agents/compass.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/agents/routing.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/analysis/vehicle.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/control/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/control/pid.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/solver.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/full_car.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/half_car.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/quarter_car.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/rlc.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/roll_car.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/thermal.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/clock.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/entity.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/environment.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/logging.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/scheduler.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/time_axis.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/currency/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/currency/codes.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/currency/format.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/currency/fx.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/currency/money.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/discrete/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/discrete/properties.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/discrete/resources.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/faults/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/faults/dataset.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/faults/fault.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/faults/injector.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/faults/recorder.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/mc/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/mc/runner.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/entity.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/fleet.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/repair.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/sensitivity.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/subsystem.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/intersection.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/network.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/recorder.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/road.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/roundabout.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/signal.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/vehicle.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/spatial/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/spatial/graph.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/supplychain/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/supplychain/inventory.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/supplychain/warehouse.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/units/__init__.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/units/constants.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/units/si.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/_plotly.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/recorders.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/themes.py +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave.egg-info/SOURCES.txt +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave.egg-info/dependency_links.txt +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave.egg-info/requires.txt +0 -0
- {simweave-0.8.0 → simweave-0.8.2}/src/simweave.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simweave"
|
|
7
|
-
version = "0.8.
|
|
7
|
+
version = "0.8.2"
|
|
8
8
|
description = "SimWeave: a hybrid discrete/continuous simulation engine with fault injection, currency, units, agents, supply chains, and Monte Carlo."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -133,6 +133,7 @@ from simweave.viz import (
|
|
|
133
133
|
plot_service_utilisation,
|
|
134
134
|
plot_state_trajectories,
|
|
135
135
|
plot_warehouse_stock,
|
|
136
|
+
plot_pareto_sweep,
|
|
136
137
|
plot_fleet_availability,
|
|
137
138
|
plot_sensitivity_surface,
|
|
138
139
|
plot_road_occupancy,
|
|
@@ -144,7 +145,7 @@ from simweave.viz import (
|
|
|
144
145
|
plot_health_index,
|
|
145
146
|
)
|
|
146
147
|
|
|
147
|
-
__version__ = "0.8.
|
|
148
|
+
__version__ = "0.8.2"
|
|
148
149
|
|
|
149
150
|
__all__ = [
|
|
150
151
|
# Core
|
|
@@ -269,6 +270,7 @@ __all__ = [
|
|
|
269
270
|
"plot_service_utilisation",
|
|
270
271
|
"plot_state_trajectories",
|
|
271
272
|
"plot_warehouse_stock",
|
|
273
|
+
"plot_pareto_sweep",
|
|
272
274
|
"plot_fleet_availability",
|
|
273
275
|
"plot_sensitivity_surface",
|
|
274
276
|
"plot_road_occupancy",
|
|
@@ -21,7 +21,7 @@ class GroundhookDamper:
|
|
|
21
21
|
|
|
22
22
|
def force(self, body_velocity: float, wheel_velocity: float) -> float:
|
|
23
23
|
# For groundhook uses wheel velocity only, but signature makes modular/easy to toggle controller
|
|
24
|
-
return
|
|
24
|
+
return self.c * wheel_velocity
|
|
25
25
|
|
|
26
26
|
class HybridActiveDamper:
|
|
27
27
|
"Hybrid Skyhook & Groundhook."
|
|
@@ -30,10 +30,10 @@ class HybridActiveDamper:
|
|
|
30
30
|
self.ground = GroundhookDamper(c_ground)
|
|
31
31
|
self.alpha = float(alpha)
|
|
32
32
|
|
|
33
|
-
def force(self,
|
|
33
|
+
def force(self, body_velocity: float, wheel_velocity: float) -> float:
|
|
34
34
|
return (
|
|
35
|
-
self.alpha * self.sky.force(
|
|
36
|
-
+ (1 - self.alpha) * self.ground.force(
|
|
35
|
+
self.alpha * self.sky.force(body_velocity, 0)
|
|
36
|
+
+ (1 - self.alpha) * self.ground.force(0, wheel_velocity)
|
|
37
37
|
)
|
|
38
38
|
|
|
39
39
|
class SemiActiveWrapper:
|
|
@@ -24,9 +24,9 @@ class MassSpringDamper(DynamicSystem):
|
|
|
24
24
|
stiffness: float | SpringStiffness,
|
|
25
25
|
x0: tuple[float, float] = (0.0, 0.0),
|
|
26
26
|
):
|
|
27
|
-
if mass <= 0:
|
|
28
|
-
raise ValueError("mass must be positive")
|
|
29
27
|
self.mass = self._val(mass)
|
|
28
|
+
if self.mass <= 0:
|
|
29
|
+
raise ValueError("mass must be positive")
|
|
30
30
|
self.damping = self._val(damping)
|
|
31
31
|
self.stiffness = self._val(stiffness)
|
|
32
32
|
self._x0 = np.asarray([self._val(v) for v in x0], dtype=float)
|
|
@@ -26,10 +26,10 @@ class SimplePendulum(DynamicSystem):
|
|
|
26
26
|
damping: float | Damping = 0.0,
|
|
27
27
|
x0: tuple[float, float] = (0.0, 0.0),
|
|
28
28
|
):
|
|
29
|
-
if length <= 0 or mass <= 0:
|
|
30
|
-
raise ValueError("length and mass must be positive")
|
|
31
29
|
self.length = self._val(length)
|
|
32
30
|
self.mass = self._val(mass)
|
|
31
|
+
if self.length <= 0 or self.mass <= 0:
|
|
32
|
+
raise ValueError("length and mass must be positive")
|
|
33
33
|
self.gravity = self._val(gravity)
|
|
34
34
|
self.damping = self._val(damping)
|
|
35
35
|
self._x0 = np.asarray([self._val(v) for v in x0], dtype=float)
|
|
@@ -212,13 +212,21 @@ class Queue(Entity):
|
|
|
212
212
|
# Metrics
|
|
213
213
|
# ------------------------------------------------------------------
|
|
214
214
|
def average_length(self, elapsed: float) -> float:
|
|
215
|
-
"""
|
|
215
|
+
"""Time-averaged number waiting in the buffer (``Lq``), excluding items in service.
|
|
216
|
+
|
|
217
|
+
For M/M/1, ``Lq = rho**2 / (1 - rho)``; Little's law gives ``Lq = lambda * Wq``.
|
|
218
|
+
"""
|
|
216
219
|
if elapsed <= 0:
|
|
217
220
|
return 0.0
|
|
218
221
|
return self.cumulative_length_time / elapsed
|
|
219
222
|
|
|
220
223
|
def average_wait(self) -> float:
|
|
221
|
-
"""Mean
|
|
224
|
+
"""Mean queue-only wait (``Wq``) of items that have left the buffer.
|
|
225
|
+
|
|
226
|
+
Only time spent sitting in the buffer counts; service time does not. For
|
|
227
|
+
M/M/1, ``Wq = rho / (mu - lambda)`` and the total time in system is
|
|
228
|
+
``W = Wq + 1/mu`` (see :meth:`Service.average_residence`).
|
|
229
|
+
"""
|
|
222
230
|
if self.departures == 0:
|
|
223
231
|
return 0.0
|
|
224
232
|
return self.cumulative_wait_time / self.departures
|
|
@@ -8,6 +8,7 @@ next queue is full) it sits in a "blocked-completion" slot until it clears.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import weakref
|
|
11
12
|
from typing import TYPE_CHECKING, Callable
|
|
12
13
|
|
|
13
14
|
import numpy as np
|
|
@@ -142,6 +143,9 @@ class Service(Queue):
|
|
|
142
143
|
self.completed_total_time: float = (
|
|
143
144
|
0.0 # sum of (wait + service) across completed
|
|
144
145
|
)
|
|
146
|
+
self._entry_age: "weakref.WeakKeyDictionary[Entity, float]" = (
|
|
147
|
+
weakref.WeakKeyDictionary()
|
|
148
|
+
)
|
|
145
149
|
|
|
146
150
|
# ------------------------------------------------------------------
|
|
147
151
|
# next_q propagation to channels is implicit: they read parent.next_q.
|
|
@@ -168,14 +172,29 @@ class Service(Queue):
|
|
|
168
172
|
return busy / (self.capacity * elapsed)
|
|
169
173
|
|
|
170
174
|
def average_residence(self) -> float:
|
|
171
|
-
"""Mean
|
|
175
|
+
"""Mean time in this service (buffer wait + service) per completed item.
|
|
176
|
+
|
|
177
|
+
This is ``W`` in Little's law (``L = lambda * W``); for M/M/1 it is
|
|
178
|
+
``Wq + 1/mu = 1/(mu - lambda)``. Compare :meth:`Queue.average_wait`,
|
|
179
|
+
which is the buffer-only wait ``Wq``.
|
|
180
|
+
"""
|
|
172
181
|
if self.completed_count == 0:
|
|
173
182
|
return 0.0
|
|
174
183
|
return self.completed_total_time / self.completed_count
|
|
175
184
|
|
|
185
|
+
def enqueue(self, item: Entity) -> bool:
|
|
186
|
+
# ``age`` accrues both in the buffer and in service, so residence at
|
|
187
|
+
# this stage is the age delta between entry and completion. Recording
|
|
188
|
+
# the entry age keeps this correct for chained services too.
|
|
189
|
+
age_in = item.age
|
|
190
|
+
accepted = super().enqueue(item)
|
|
191
|
+
if accepted:
|
|
192
|
+
self._entry_age[item] = age_in
|
|
193
|
+
return accepted
|
|
194
|
+
|
|
176
195
|
def _record_completion(self, item: Entity) -> None:
|
|
177
196
|
self.completed_count += 1
|
|
178
|
-
self.completed_total_time += item.
|
|
197
|
+
self.completed_total_time += item.age - self._entry_age.pop(item, 0.0)
|
|
179
198
|
|
|
180
199
|
|
|
181
200
|
# ---------------------------------------------------------------------------
|
|
@@ -81,6 +81,12 @@ def poisson_reorder_points(
|
|
|
81
81
|
|
|
82
82
|
return k, total_cost
|
|
83
83
|
|
|
84
|
+
def _objective(x: np.ndarray, inv, quant) -> float:
|
|
85
|
+
return float(np.sum(inv.unit_cost * np.round(x) * quant))
|
|
86
|
+
|
|
87
|
+
def _availability_con(x: np.ndarray, lam) -> float:
|
|
88
|
+
import scipy.stats as stats
|
|
89
|
+
return float(np.prod(stats.poisson.cdf(np.round(x), lam)))
|
|
84
90
|
|
|
85
91
|
def cost_optimise_stock(
|
|
86
92
|
warehouse: Warehouse,
|
|
@@ -90,14 +96,19 @@ def cost_optimise_stock(
|
|
|
90
96
|
ub: float = 100.0,
|
|
91
97
|
seed: int | None = 1,
|
|
92
98
|
maxiter: int = 200,
|
|
99
|
+
workers: int = -1,
|
|
93
100
|
) -> tuple[np.ndarray, float]:
|
|
94
101
|
"""Differential-evolution cost minimisation with availability constraint.
|
|
95
102
|
|
|
96
103
|
This balances cost vs availability on a whole-warehouse basis rather than
|
|
97
104
|
per item -- cheap items should cover more risk, expensive items less.
|
|
105
|
+
|
|
106
|
+
workers defaults to -1 which is using all available cores (fastest).
|
|
107
|
+
For reproducibility, use 1.
|
|
108
|
+
Specify workers as desired on shared devices.
|
|
98
109
|
"""
|
|
99
110
|
_ensure_scipy()
|
|
100
|
-
|
|
111
|
+
from functools import partial
|
|
101
112
|
from scipy.optimize import differential_evolution, NonlinearConstraint, Bounds
|
|
102
113
|
|
|
103
114
|
if not 0.0 < target_availability < 0.999:
|
|
@@ -117,19 +128,26 @@ def cost_optimise_stock(
|
|
|
117
128
|
lam = demand * logistics_delay
|
|
118
129
|
quant = inv.batchsize if quantize_by_batchsize else np.ones(inv.n_items)
|
|
119
130
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return float(np.prod(stats.poisson.cdf(np.round(x), lam)))
|
|
131
|
+
## v0.8.2 we moved _objective and _availability_con outside this function, to be pickable, required for workers>1
|
|
132
|
+
## partial is required, as scipy multiprocessing won't pickle lambda either (or any local-defined callable)
|
|
133
|
+
obj = partial(_objective, inv=inv, quant=quant)
|
|
134
|
+
con = partial(_availability_con, lam=lam)
|
|
125
135
|
|
|
126
136
|
bounds = Bounds(lb=np.zeros(inv.n_items), ub=ub * np.ones(inv.n_items))
|
|
127
|
-
nlc = NonlinearConstraint(
|
|
137
|
+
nlc = NonlinearConstraint(con, target_availability, 1.0)
|
|
128
138
|
|
|
129
139
|
result = differential_evolution(
|
|
130
|
-
|
|
140
|
+
obj,
|
|
141
|
+
bounds,
|
|
142
|
+
constraints=nlc,
|
|
143
|
+
seed=seed,
|
|
144
|
+
maxiter=maxiter,
|
|
145
|
+
polish=False,
|
|
146
|
+
updating="deferred",
|
|
147
|
+
workers=workers
|
|
131
148
|
)
|
|
132
149
|
|
|
150
|
+
|
|
133
151
|
solution = np.round(result.x * quant)
|
|
134
152
|
total_cost = float(np.sum(inv.unit_cost * (inv.batchsize + solution)))
|
|
135
153
|
|
|
@@ -141,25 +159,38 @@ def cost_optimise_stock(
|
|
|
141
159
|
|
|
142
160
|
|
|
143
161
|
def pareto_sweep(
|
|
144
|
-
warehouse: Warehouse,
|
|
162
|
+
warehouse: Warehouse,
|
|
163
|
+
availability_range: np.ndarray | None = None,
|
|
164
|
+
*,
|
|
165
|
+
method: str = "both",
|
|
145
166
|
) -> dict[str, np.ndarray]:
|
|
146
|
-
"""Sweep availability targets and return costs for
|
|
167
|
+
"""Sweep availability targets and return costs for Poisson and/or DE."""
|
|
168
|
+
|
|
147
169
|
if availability_range is None:
|
|
148
170
|
availability_range = np.clip(np.arange(0.1, 1.0, 0.05), 0.0, 0.99)
|
|
149
171
|
|
|
172
|
+
do_de = method in ("both", "de")
|
|
173
|
+
do_poisson = method in ("both", "poisson")
|
|
174
|
+
|
|
150
175
|
costs_de = []
|
|
151
176
|
costs_poisson = []
|
|
177
|
+
|
|
152
178
|
for a in availability_range:
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
179
|
+
if do_de:
|
|
180
|
+
_, c_de = cost_optimise_stock(warehouse, target_availability=float(a))
|
|
181
|
+
costs_de.append(c_de)
|
|
182
|
+
if do_poisson:
|
|
183
|
+
_, c_p = poisson_reorder_points(warehouse, target_availability=float(a))
|
|
184
|
+
costs_poisson.append(c_p)
|
|
185
|
+
|
|
186
|
+
out = {"availability": np.asarray(availability_range)}
|
|
187
|
+
|
|
188
|
+
if do_de:
|
|
189
|
+
out["cost_cost_optimal"] = np.asarray(costs_de)
|
|
190
|
+
if do_poisson:
|
|
191
|
+
out["cost_poisson"] = np.asarray(costs_poisson)
|
|
192
|
+
|
|
193
|
+
return out
|
|
163
194
|
|
|
164
195
|
|
|
165
196
|
# ---------------------------------------------------------------------------
|
|
@@ -48,6 +48,7 @@ from simweave.viz.plots import (
|
|
|
48
48
|
plot_service_utilisation,
|
|
49
49
|
plot_state_trajectories,
|
|
50
50
|
plot_warehouse_stock,
|
|
51
|
+
plot_pareto_sweep,
|
|
51
52
|
plot_fleet_availability,
|
|
52
53
|
plot_sensitivity_surface,
|
|
53
54
|
plot_road_occupancy,
|
|
@@ -81,6 +82,7 @@ __all__ = [
|
|
|
81
82
|
"plot_service_utilisation",
|
|
82
83
|
"plot_state_trajectories",
|
|
83
84
|
"plot_warehouse_stock",
|
|
85
|
+
"plot_pareto_sweep",
|
|
84
86
|
"plot_fleet_availability",
|
|
85
87
|
"plot_sensitivity_surface",
|
|
86
88
|
"plot_road_occupancy",
|
|
@@ -41,6 +41,8 @@ import numpy as np
|
|
|
41
41
|
|
|
42
42
|
from simweave.viz import _plotly
|
|
43
43
|
from simweave.viz.themes import apply_theme
|
|
44
|
+
from simweave.supplychain.warehouse import Warehouse
|
|
45
|
+
from simweave.supplychain.optimization import pareto_sweep
|
|
44
46
|
|
|
45
47
|
if TYPE_CHECKING:
|
|
46
48
|
from simweave.core.time_axis import SimTimeAxis
|
|
@@ -349,6 +351,93 @@ def plot_warehouse_stock(
|
|
|
349
351
|
)
|
|
350
352
|
return apply_theme(_apply_time_axis(fig, time_axis), theme)
|
|
351
353
|
|
|
354
|
+
# --------------------------------------------------------------------------- #
|
|
355
|
+
# Inventory optimisation: pareto sweep #
|
|
356
|
+
# --------------------------------------------------------------------------- #
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def plot_pareto_sweep(
|
|
360
|
+
warehouse: Warehouse,
|
|
361
|
+
availability_range: np.ndarray | None = None,
|
|
362
|
+
*,
|
|
363
|
+
method: str = "both",
|
|
364
|
+
theme: str | None = None,
|
|
365
|
+
title: str | None = None,
|
|
366
|
+
) -> Any:
|
|
367
|
+
"""Plot cost vs availability with optional DE and Poisson curves.
|
|
368
|
+
|
|
369
|
+
Parameters
|
|
370
|
+
----------
|
|
371
|
+
warehouse:
|
|
372
|
+
Warehouse instance to optimise.
|
|
373
|
+
availability_range:
|
|
374
|
+
Optional array of availability targets.
|
|
375
|
+
method:
|
|
376
|
+
"both" (default), "poisson", or "de".
|
|
377
|
+
theme:
|
|
378
|
+
Optional Plotly theme.
|
|
379
|
+
title:
|
|
380
|
+
Optional plot title.
|
|
381
|
+
"""
|
|
382
|
+
go = _plotly.require_go()
|
|
383
|
+
|
|
384
|
+
sweep = pareto_sweep(warehouse, availability_range, method=method)
|
|
385
|
+
a = sweep["availability"]
|
|
386
|
+
|
|
387
|
+
fig = go.Figure()
|
|
388
|
+
|
|
389
|
+
# Poisson heuristic
|
|
390
|
+
if "cost_poisson" in sweep:
|
|
391
|
+
c_p = sweep["cost_poisson"]
|
|
392
|
+
fig.add_trace(
|
|
393
|
+
go.Scatter(
|
|
394
|
+
x=c_p,
|
|
395
|
+
y=a,
|
|
396
|
+
mode="lines+markers",
|
|
397
|
+
name="Poisson heuristic",
|
|
398
|
+
line={"dash": "dash", "width": 2},
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
# DE cost-optimal
|
|
403
|
+
if "cost_cost_optimal" in sweep:
|
|
404
|
+
c_de = sweep["cost_cost_optimal"]
|
|
405
|
+
fig.add_trace(
|
|
406
|
+
go.Scatter(
|
|
407
|
+
x=c_de,
|
|
408
|
+
y=a,
|
|
409
|
+
mode="lines+markers",
|
|
410
|
+
name="cost-optimal (DE)",
|
|
411
|
+
line={"width": 2},
|
|
412
|
+
)
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Shaded savings region (only if Poisson also present)
|
|
416
|
+
if "cost_poisson" in sweep:
|
|
417
|
+
fig.add_trace(
|
|
418
|
+
go.Scatter(
|
|
419
|
+
x=np.concatenate([c_p, c_de[::-1]]),
|
|
420
|
+
y=np.concatenate([a, a[::-1]]),
|
|
421
|
+
fill="toself",
|
|
422
|
+
fillcolor="rgba(0, 150, 0, 0.1)",
|
|
423
|
+
line={"width": 0},
|
|
424
|
+
name="savings region",
|
|
425
|
+
hoverinfo="skip",
|
|
426
|
+
showlegend=True,
|
|
427
|
+
)
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
wname = getattr(warehouse, "name", "warehouse")
|
|
431
|
+
|
|
432
|
+
fig.update_layout(
|
|
433
|
+
title=title or f"cost vs availability: {wname}",
|
|
434
|
+
xaxis_title="total cost",
|
|
435
|
+
yaxis_title="target availability",
|
|
436
|
+
legend_title_text="method",
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
return apply_theme(fig, theme)
|
|
440
|
+
|
|
352
441
|
|
|
353
442
|
# --------------------------------------------------------------------------- #
|
|
354
443
|
# Monte Carlo: percentile fan #
|
|
@@ -1126,6 +1215,7 @@ __all__ = [
|
|
|
1126
1215
|
"plot_queue_length",
|
|
1127
1216
|
"plot_service_utilisation",
|
|
1128
1217
|
"plot_warehouse_stock",
|
|
1218
|
+
"plot_pareto_sweep",
|
|
1129
1219
|
"plot_mc_fan",
|
|
1130
1220
|
"plot_agent_path",
|
|
1131
1221
|
"plot_fleet_availability",
|
|
@@ -13,8 +13,6 @@ from simweave.units.si import (
|
|
|
13
13
|
Force,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
from plotly.subplots import make_subplots
|
|
17
|
-
|
|
18
16
|
|
|
19
17
|
def plot_vehicle_metrics(
|
|
20
18
|
result,
|
|
@@ -25,7 +23,8 @@ def plot_vehicle_metrics(
|
|
|
25
23
|
):
|
|
26
24
|
"""Plot vehicle metrics with adaptive layout and automatic units."""
|
|
27
25
|
|
|
28
|
-
go = _plotly.require_go()
|
|
26
|
+
go = _plotly.require_go() # friendly ImportError if plotly is missing
|
|
27
|
+
from plotly.subplots import make_subplots
|
|
29
28
|
|
|
30
29
|
metrics = compute_vehicle_metrics(result, model)
|
|
31
30
|
t = np.asarray(result.time)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|