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.
Files changed (85) hide show
  1. {simweave-0.8.0/src/simweave.egg-info → simweave-0.8.2}/PKG-INFO +1 -1
  2. {simweave-0.8.0 → simweave-0.8.2}/pyproject.toml +1 -1
  3. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/__init__.py +3 -1
  4. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/control/suspension.py +4 -4
  5. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/mass_spring_damper.py +2 -2
  6. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/pendulum.py +2 -2
  7. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/discrete/queues.py +10 -2
  8. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/discrete/services.py +21 -2
  9. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/supplychain/optimization.py +51 -20
  10. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/__init__.py +2 -0
  11. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/plots.py +90 -0
  12. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/vehicle_dynamics.py +2 -3
  13. {simweave-0.8.0 → simweave-0.8.2/src/simweave.egg-info}/PKG-INFO +1 -1
  14. {simweave-0.8.0 → simweave-0.8.2}/MANIFEST.in +0 -0
  15. {simweave-0.8.0 → simweave-0.8.2}/README.md +0 -0
  16. {simweave-0.8.0 → simweave-0.8.2}/requirements-dev.txt +0 -0
  17. {simweave-0.8.0 → simweave-0.8.2}/requirements.txt +0 -0
  18. {simweave-0.8.0 → simweave-0.8.2}/setup.cfg +0 -0
  19. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/agents/__init__.py +0 -0
  20. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/agents/agent.py +0 -0
  21. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/agents/compass.py +0 -0
  22. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/agents/routing.py +0 -0
  23. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/analysis/vehicle.py +0 -0
  24. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/__init__.py +0 -0
  25. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/control/__init__.py +0 -0
  26. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/control/pid.py +0 -0
  27. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/solver.py +0 -0
  28. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/__init__.py +0 -0
  29. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/full_car.py +0 -0
  30. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/half_car.py +0 -0
  31. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/quarter_car.py +0 -0
  32. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/rlc.py +0 -0
  33. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/roll_car.py +0 -0
  34. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/continuous/systems/thermal.py +0 -0
  35. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/__init__.py +0 -0
  36. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/clock.py +0 -0
  37. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/entity.py +0 -0
  38. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/environment.py +0 -0
  39. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/logging.py +0 -0
  40. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/scheduler.py +0 -0
  41. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/core/time_axis.py +0 -0
  42. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/currency/__init__.py +0 -0
  43. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/currency/codes.py +0 -0
  44. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/currency/format.py +0 -0
  45. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/currency/fx.py +0 -0
  46. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/currency/money.py +0 -0
  47. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/discrete/__init__.py +0 -0
  48. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/discrete/properties.py +0 -0
  49. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/discrete/resources.py +0 -0
  50. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/faults/__init__.py +0 -0
  51. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/faults/dataset.py +0 -0
  52. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/faults/fault.py +0 -0
  53. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/faults/injector.py +0 -0
  54. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/faults/recorder.py +0 -0
  55. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/mc/__init__.py +0 -0
  56. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/mc/runner.py +0 -0
  57. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/__init__.py +0 -0
  58. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/entity.py +0 -0
  59. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/fleet.py +0 -0
  60. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/repair.py +0 -0
  61. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/sensitivity.py +0 -0
  62. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/reliability/subsystem.py +0 -0
  63. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/__init__.py +0 -0
  64. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/intersection.py +0 -0
  65. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/network.py +0 -0
  66. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/recorder.py +0 -0
  67. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/road.py +0 -0
  68. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/roundabout.py +0 -0
  69. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/signal.py +0 -0
  70. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/roads/vehicle.py +0 -0
  71. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/spatial/__init__.py +0 -0
  72. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/spatial/graph.py +0 -0
  73. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/supplychain/__init__.py +0 -0
  74. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/supplychain/inventory.py +0 -0
  75. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/supplychain/warehouse.py +0 -0
  76. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/units/__init__.py +0 -0
  77. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/units/constants.py +0 -0
  78. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/units/si.py +0 -0
  79. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/_plotly.py +0 -0
  80. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/recorders.py +0 -0
  81. {simweave-0.8.0 → simweave-0.8.2}/src/simweave/viz/themes.py +0 -0
  82. {simweave-0.8.0 → simweave-0.8.2}/src/simweave.egg-info/SOURCES.txt +0 -0
  83. {simweave-0.8.0 → simweave-0.8.2}/src/simweave.egg-info/dependency_links.txt +0 -0
  84. {simweave-0.8.0 → simweave-0.8.2}/src/simweave.egg-info/requires.txt +0 -0
  85. {simweave-0.8.0 → simweave-0.8.2}/src/simweave.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simweave
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: SimWeave: a hybrid discrete/continuous simulation engine with fault injection, currency, units, agents, supply chains, and Monte Carlo.
5
5
  Author: Stuart
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simweave"
7
- version = "0.8.0"
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.0"
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 -self.c * wheel_velocity
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, body_v: float, wheel_v: float) -> float:
33
+ def force(self, body_velocity: float, wheel_velocity: float) -> float:
34
34
  return (
35
- self.alpha * self.sky.force(body_v)
36
- + (1 - self.alpha) * self.ground.force(wheel_v)
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
- """Mean queue length over the elapsed simulation time (L in Little's law)."""
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 residence time of completed items (W in Little's law)."""
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 total time (waiting + service) for completed items."""
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.total_wait_time + item.age # rough summary
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
- import scipy.stats as stats
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
- def objective(x: np.ndarray) -> float:
121
- return float(np.sum(inv.unit_cost * np.round(x) * quant))
122
-
123
- def availability_con(x: np.ndarray) -> float:
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(availability_con, target_availability, 1.0)
137
+ nlc = NonlinearConstraint(con, target_availability, 1.0)
128
138
 
129
139
  result = differential_evolution(
130
- objective, bounds, constraints=nlc, seed=seed, maxiter=maxiter, polish=True
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, availability_range: np.ndarray | None = None
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 both heuristic and DE."""
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
- _, c_de = cost_optimise_stock(warehouse, target_availability=float(a))
154
- _, c_p = poisson_reorder_points(warehouse, target_availability=float(a))
155
- costs_de.append(c_de)
156
- costs_poisson.append(c_p)
157
-
158
- return {
159
- "availability": np.asarray(availability_range),
160
- "cost_cost_optimal": np.asarray(costs_de),
161
- "cost_poisson": np.asarray(costs_poisson),
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simweave
3
- Version: 0.8.0
3
+ Version: 0.8.2
4
4
  Summary: SimWeave: a hybrid discrete/continuous simulation engine with fault injection, currency, units, agents, supply chains, and Monte Carlo.
5
5
  Author: Stuart
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes