simweave 0.5.0__tar.gz → 0.7.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 (82) hide show
  1. {simweave-0.5.0/src/simweave.egg-info → simweave-0.7.2}/PKG-INFO +15 -4
  2. {simweave-0.5.0 → simweave-0.7.2}/README.md +14 -3
  3. {simweave-0.5.0 → simweave-0.7.2}/pyproject.toml +1 -1
  4. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/__init__.py +69 -1
  5. simweave-0.7.2/src/simweave/analysis/vehicle.py +207 -0
  6. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/__init__.py +18 -0
  7. simweave-0.7.2/src/simweave/continuous/control/__init__.py +17 -0
  8. simweave-0.7.2/src/simweave/continuous/control/pid.py +48 -0
  9. simweave-0.7.2/src/simweave/continuous/control/suspension.py +54 -0
  10. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/solver.py +66 -2
  11. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/systems/__init__.py +6 -0
  12. simweave-0.7.2/src/simweave/continuous/systems/full_car.py +155 -0
  13. simweave-0.7.2/src/simweave/continuous/systems/half_car.py +143 -0
  14. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/systems/mass_spring_damper.py +13 -8
  15. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/systems/pendulum.py +15 -10
  16. simweave-0.7.2/src/simweave/continuous/systems/quarter_car.py +82 -0
  17. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/systems/rlc.py +14 -9
  18. simweave-0.7.2/src/simweave/continuous/systems/roll_car.py +110 -0
  19. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/systems/thermal.py +40 -24
  20. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/__init__.py +2 -0
  21. simweave-0.7.2/src/simweave/core/time_axis.py +236 -0
  22. simweave-0.7.2/src/simweave/reliability/__init__.py +34 -0
  23. simweave-0.7.2/src/simweave/reliability/entity.py +256 -0
  24. simweave-0.7.2/src/simweave/reliability/fleet.py +199 -0
  25. simweave-0.7.2/src/simweave/reliability/repair.py +160 -0
  26. simweave-0.7.2/src/simweave/reliability/sensitivity.py +211 -0
  27. simweave-0.7.2/src/simweave/reliability/subsystem.py +109 -0
  28. simweave-0.7.2/src/simweave/roads/__init__.py +52 -0
  29. simweave-0.7.2/src/simweave/roads/intersection.py +191 -0
  30. simweave-0.7.2/src/simweave/roads/network.py +149 -0
  31. simweave-0.7.2/src/simweave/roads/recorder.py +80 -0
  32. simweave-0.7.2/src/simweave/roads/road.py +203 -0
  33. simweave-0.7.2/src/simweave/roads/roundabout.py +220 -0
  34. simweave-0.7.2/src/simweave/roads/signal.py +107 -0
  35. simweave-0.7.2/src/simweave/roads/vehicle.py +116 -0
  36. simweave-0.7.2/src/simweave/units/__init__.py +71 -0
  37. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/units/si.py +378 -28
  38. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/viz/__init__.py +12 -0
  39. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/viz/plots.py +375 -5
  40. simweave-0.7.2/src/simweave/viz/vehicle_dynamics.py +148 -0
  41. {simweave-0.5.0 → simweave-0.7.2/src/simweave.egg-info}/PKG-INFO +15 -4
  42. {simweave-0.5.0 → simweave-0.7.2}/src/simweave.egg-info/SOURCES.txt +24 -1
  43. simweave-0.5.0/src/simweave/continuous/systems/quarter_car.py +0 -59
  44. simweave-0.5.0/src/simweave/units/__init__.py +0 -25
  45. {simweave-0.5.0 → simweave-0.7.2}/MANIFEST.in +0 -0
  46. {simweave-0.5.0 → simweave-0.7.2}/requirements-dev.txt +0 -0
  47. {simweave-0.5.0 → simweave-0.7.2}/requirements.txt +0 -0
  48. {simweave-0.5.0 → simweave-0.7.2}/setup.cfg +0 -0
  49. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/agents/__init__.py +0 -0
  50. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/agents/agent.py +0 -0
  51. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/agents/compass.py +0 -0
  52. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/agents/routing.py +0 -0
  53. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/clock.py +0 -0
  54. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/entity.py +0 -0
  55. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/environment.py +0 -0
  56. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/logging.py +0 -0
  57. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/scheduler.py +0 -0
  58. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/currency/__init__.py +0 -0
  59. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/currency/codes.py +0 -0
  60. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/currency/format.py +0 -0
  61. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/currency/fx.py +0 -0
  62. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/currency/money.py +0 -0
  63. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/discrete/__init__.py +0 -0
  64. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/discrete/properties.py +0 -0
  65. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/discrete/queues.py +0 -0
  66. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/discrete/resources.py +0 -0
  67. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/discrete/services.py +0 -0
  68. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/mc/__init__.py +0 -0
  69. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/mc/runner.py +0 -0
  70. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/spatial/__init__.py +0 -0
  71. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/spatial/graph.py +0 -0
  72. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/supplychain/__init__.py +0 -0
  73. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/supplychain/inventory.py +0 -0
  74. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/supplychain/optimization.py +0 -0
  75. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/supplychain/warehouse.py +0 -0
  76. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/units/constants.py +0 -0
  77. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/viz/_plotly.py +0 -0
  78. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/viz/recorders.py +0 -0
  79. {simweave-0.5.0 → simweave-0.7.2}/src/simweave/viz/themes.py +0 -0
  80. {simweave-0.5.0 → simweave-0.7.2}/src/simweave.egg-info/dependency_links.txt +0 -0
  81. {simweave-0.5.0 → simweave-0.7.2}/src/simweave.egg-info/requires.txt +0 -0
  82. {simweave-0.5.0 → simweave-0.7.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.5.0
3
+ Version: 0.7.2
4
4
  Summary: SimWeave: a hybrid discrete/continuous simulation engine with currency, units, agents, supply chains, and Monte Carlo.
5
5
  Author: Stuart
6
6
  License: MIT
@@ -115,6 +115,20 @@ t2 = Temperature(20, "C")
115
115
 
116
116
  delta = t1 - t2
117
117
  ```
118
+ Finally, we can also use fractional powers when they produce valid physical dimensions
119
+ (e.g. √area → distance) using either:
120
+ ```python
121
+ area = sw.Area(np.array([1, 4, 9]))
122
+ length = area ** 0.5 # Distance([1.0, 2.0, 3.0]) [m]
123
+ length = area.sqrt() # Distance([1.0, 2.0, 3.0]) [m]
124
+ ```
125
+ or indeed:
126
+ ```python
127
+ volume = sw.Volume(np.array([1, 8, 27]))
128
+ length = volume ** (1/3) # Distance([1.0, 2.0, 3.0]) [m]
129
+ length = volume.cbrt() # Distance([1.0, 2.0, 3.0]) [m]
130
+ ```
131
+
118
132
  ✔ Prevents invalid operations (e.g. distance + time)
119
133
 
120
134
  ✔ Automatically derives units (e.g. m/s → Velocity)
@@ -328,9 +342,6 @@ sim_engine/
328
342
 
329
343
  ## Next steps on the roadmap
330
344
 
331
- - **Numpy aware SIUnits.** Ensure our improved system works on arrays
332
- not just scalars, with no regression. Add new classes such as
333
- Voltage, Current, Resistance.
334
345
  - **PID Controllers.** To extend utility of dynamic simulations, allow
335
346
  controllers which are easily exposed to EdgeWeave.
336
347
  - **Fault injection.** This should be suitable for ML datasets with use
@@ -66,6 +66,20 @@ t2 = Temperature(20, "C")
66
66
 
67
67
  delta = t1 - t2
68
68
  ```
69
+ Finally, we can also use fractional powers when they produce valid physical dimensions
70
+ (e.g. √area → distance) using either:
71
+ ```python
72
+ area = sw.Area(np.array([1, 4, 9]))
73
+ length = area ** 0.5 # Distance([1.0, 2.0, 3.0]) [m]
74
+ length = area.sqrt() # Distance([1.0, 2.0, 3.0]) [m]
75
+ ```
76
+ or indeed:
77
+ ```python
78
+ volume = sw.Volume(np.array([1, 8, 27]))
79
+ length = volume ** (1/3) # Distance([1.0, 2.0, 3.0]) [m]
80
+ length = volume.cbrt() # Distance([1.0, 2.0, 3.0]) [m]
81
+ ```
82
+
69
83
  ✔ Prevents invalid operations (e.g. distance + time)
70
84
 
71
85
  ✔ Automatically derives units (e.g. m/s → Velocity)
@@ -279,9 +293,6 @@ sim_engine/
279
293
 
280
294
  ## Next steps on the roadmap
281
295
 
282
- - **Numpy aware SIUnits.** Ensure our improved system works on arrays
283
- not just scalars, with no regression. Add new classes such as
284
- Voltage, Current, Resistance.
285
296
  - **PID Controllers.** To extend utility of dynamic simulations, allow
286
297
  controllers which are easily exposed to EdgeWeave.
287
298
  - **Fault injection.** This should be suitable for ML datasets with use
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simweave"
7
- version = "0.5.0"
7
+ version = "0.7.2"
8
8
  description = "SimWeave: a hybrid discrete/continuous simulation engine with currency, units, agents, supply chains, and Monte Carlo."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -13,6 +13,7 @@ from simweave.core import (
13
13
  SimEnvironment,
14
14
  configure as configure_logging,
15
15
  get_logger,
16
+ SimTimeAxis,
16
17
  )
17
18
  from simweave.discrete import (
18
19
  EntityProperties,
@@ -37,6 +38,9 @@ from simweave.continuous import (
37
38
  MassSpringDamper,
38
39
  SimplePendulum,
39
40
  QuarterCarModel,
41
+ HalfCarModel,
42
+ RollCarModel,
43
+ FullCarModel,
40
44
  SeriesRLC,
41
45
  ThermalRC,
42
46
  TwoMassThermal,
@@ -53,6 +57,32 @@ from simweave.agents import (
53
57
  chebyshev,
54
58
  )
55
59
  from simweave.supplychain import InventoryItems, Warehouse
60
+ from simweave.reliability import (
61
+ SubsystemSpec,
62
+ SubsystemState,
63
+ SubsystemStatus,
64
+ RepairJob,
65
+ RepairCentre,
66
+ ReliableEntity,
67
+ Fleet,
68
+ FleetAvailabilityRecorder,
69
+ SweepResult,
70
+ sensitivity_sweep,
71
+ )
72
+ from simweave.roads import (
73
+ Vehicle,
74
+ VehicleArrivalProcess,
75
+ Road,
76
+ DualCarriageway,
77
+ SignalPhase,
78
+ TrafficSignal,
79
+ Intersection,
80
+ Handedness,
81
+ Roundabout,
82
+ RoadNetwork,
83
+ RoadOccupancyRecorder,
84
+ IntersectionQueueRecorder,
85
+ )
56
86
  from simweave.mc import MCResult, run_monte_carlo, run_batched_mc
57
87
  from simweave.units import (
58
88
  SIUnit,
@@ -96,11 +126,16 @@ from simweave.viz import (
96
126
  plot_service_utilisation,
97
127
  plot_state_trajectories,
98
128
  plot_warehouse_stock,
129
+ plot_fleet_availability,
130
+ plot_sensitivity_surface,
131
+ plot_road_occupancy,
132
+ plot_intersection_queues,
99
133
  register_theme,
100
134
  set_default_theme,
135
+ plot_vehicle_metrics,
101
136
  )
102
137
 
103
- __version__ = "0.5.0"
138
+ __version__ = "0.7.2"
104
139
 
105
140
  __all__ = [
106
141
  # Core
@@ -109,6 +144,7 @@ __all__ = [
109
144
  "ScheduledEvent",
110
145
  "Entity",
111
146
  "SimEnvironment",
147
+ "SimTimeAxis",
112
148
  "configure_logging",
113
149
  "get_logger",
114
150
  # Discrete
@@ -133,6 +169,9 @@ __all__ = [
133
169
  "MassSpringDamper",
134
170
  "SimplePendulum",
135
171
  "QuarterCarModel",
172
+ "HalfCarModel",
173
+ "RollCarModel",
174
+ "FullCarModel",
136
175
  "SeriesRLC",
137
176
  "ThermalRC",
138
177
  "TwoMassThermal",
@@ -150,6 +189,30 @@ __all__ = [
150
189
  # Supply chain
151
190
  "InventoryItems",
152
191
  "Warehouse",
192
+ # Roads
193
+ "Vehicle",
194
+ "VehicleArrivalProcess",
195
+ "Road",
196
+ "DualCarriageway",
197
+ "SignalPhase",
198
+ "TrafficSignal",
199
+ "Intersection",
200
+ "Handedness",
201
+ "Roundabout",
202
+ "RoadNetwork",
203
+ "RoadOccupancyRecorder",
204
+ "IntersectionQueueRecorder",
205
+ # Reliability
206
+ "SubsystemSpec",
207
+ "SubsystemState",
208
+ "SubsystemStatus",
209
+ "RepairJob",
210
+ "RepairCentre",
211
+ "ReliableEntity",
212
+ "Fleet",
213
+ "FleetAvailabilityRecorder",
214
+ "SweepResult",
215
+ "sensitivity_sweep",
153
216
  # Monte Carlo
154
217
  "MCResult",
155
218
  "run_monte_carlo",
@@ -191,6 +254,11 @@ __all__ = [
191
254
  "plot_service_utilisation",
192
255
  "plot_state_trajectories",
193
256
  "plot_warehouse_stock",
257
+ "plot_fleet_availability",
258
+ "plot_sensitivity_surface",
259
+ "plot_road_occupancy",
260
+ "plot_intersection_queues",
261
+ "plot_vehicle_metrics",
194
262
  "register_theme",
195
263
  "set_default_theme",
196
264
  # Meta
@@ -0,0 +1,207 @@
1
+ import numpy as np
2
+ from simweave.continuous.systems.full_car import FullCarModel
3
+
4
+ def compute_quarter_car_metrics(result, model=None):
5
+ state = np.asarray(result.state)
6
+
7
+ z_s = state[:, 0]
8
+ z_s_dot = state[:, 1]
9
+ z_u = state[:, 2]
10
+
11
+ dt = np.mean(np.diff(result.time))
12
+ z_s_ddot = np.gradient(z_s_dot, dt)
13
+
14
+ rms = np.sqrt(np.mean(z_s_ddot**2))
15
+
16
+ travel = {
17
+ "single": z_s - z_u,
18
+ }
19
+
20
+ if model is not None:
21
+ k_t = model.k_t
22
+ z_r = result.inputs[:, 0] if result.inputs is not None else 0.0
23
+
24
+ tyre = {
25
+ "single": k_t * (z_u - z_r),
26
+ }
27
+ tyre_metric = {"name": "Tyre Load", "unit": "N"}
28
+ else:
29
+ tyre = {"single": z_u}
30
+ tyre_metric = {"name": "Tyre deflection", "unit": "m"}
31
+
32
+ return {
33
+ "body_accel": z_s_ddot,
34
+ "body_accel_RMS": rms,
35
+ "pitch": None,
36
+ "roll": None,
37
+ "suspension_travel": travel,
38
+ "tyre": tyre,
39
+ "tyre_metric": tyre_metric,
40
+ }
41
+
42
+ def compute_half_car_pitch_metrics(result, model=None):
43
+ state = np.asarray(result.state)
44
+
45
+ z_s = state[:, 0]
46
+ z_s_dot = state[:, 1]
47
+ theta = state[:, 2]
48
+
49
+ z_uf = state[:, 4]
50
+ z_ur = state[:, 6]
51
+
52
+ dt = np.mean(np.diff(result.time))
53
+ z_s_ddot = np.gradient(z_s_dot, dt)
54
+ rms = np.sqrt(np.mean(z_s_ddot**2))
55
+
56
+ travel = {
57
+ "front": z_s - z_uf,
58
+ "rear": z_s - z_ur,
59
+ }
60
+
61
+ tyre = {
62
+ "front": z_uf,
63
+ "rear": z_ur,
64
+ }
65
+
66
+ return {
67
+ "body_accel": z_s_ddot,
68
+ "body_accel_RMS": rms,
69
+ "pitch": theta,
70
+ "roll": None,
71
+ "suspension_travel": travel,
72
+ "tyre": tyre,
73
+ "tyre_metric": {"name": "Tyre deflection", "unit": "m"},
74
+ }
75
+
76
+ def compute_half_car_roll_metrics(result, model=None):
77
+ state = np.asarray(result.state)
78
+
79
+ z_s = state[:, 0]
80
+ z_s_dot = state[:, 1]
81
+ phi = state[:, 2]
82
+
83
+ z_ul = state[:, 4]
84
+ z_ur = state[:, 6]
85
+
86
+ dt = np.mean(np.diff(result.time))
87
+ z_s_ddot = np.gradient(z_s_dot, dt)
88
+ rms = np.sqrt(np.mean(z_s_ddot**2))
89
+
90
+ travel = {
91
+ "left": z_s - z_ul,
92
+ "right": z_s - z_ur,
93
+ }
94
+
95
+ tyre = {
96
+ "left": z_ul,
97
+ "right": z_ur,
98
+ }
99
+
100
+ return {
101
+ "body_accel": z_s_ddot,
102
+ "body_accel_RMS": rms,
103
+ "pitch": None,
104
+ "roll": phi,
105
+ "suspension_travel": travel,
106
+ "tyre": tyre,
107
+ "tyre_metric": {"name": "Tyre deflection", "unit": "m"},
108
+ }
109
+
110
+
111
+ def compute_full_car_metrics(result, model: FullCarModel | None = None):
112
+ """
113
+ Compute relevant engineering metrics.
114
+ Use optional 'model' input for tyre forces or default to displacement only.
115
+ """
116
+ state = np.asarray(result.state)
117
+
118
+ z_s = state[:, 0]
119
+ theta = state[:, 2]
120
+ phi = state[:, 4]
121
+
122
+ # velocities
123
+ z_s_dot = state[:, 1]
124
+
125
+ # numerical acceleration
126
+ dt = np.mean(np.diff(result.time))
127
+ z_s_ddot = np.gradient(z_s_dot, dt)
128
+
129
+ # rms of acceleration
130
+ body_accel = z_s_ddot
131
+ rms = np.sqrt(np.mean(body_accel**2))
132
+
133
+ # suspension travel
134
+ z_ufl = state[:, 6]
135
+ z_ufr = state[:, 8]
136
+ z_url = state[:, 10]
137
+ z_urr = state[:, 12]
138
+
139
+ travel = {
140
+ "fl": z_s - z_ufl,
141
+ "fr": z_s - z_ufr,
142
+ "rl": z_s - z_url,
143
+ "rr": z_s - z_urr,
144
+ }
145
+
146
+
147
+ if model is not None and result.inputs is not None:
148
+ # tyre stiffness
149
+ k_t = model.k_t
150
+
151
+ # gather road positions
152
+ z_rfl = result.inputs[:, 0]
153
+ z_rfr = result.inputs[:, 1]
154
+ z_rrl = result.inputs[:, 2]
155
+ z_rrr = result.inputs[:, 3]
156
+
157
+ # tyre force loads
158
+ F_fl = k_t * (z_ufl - z_rfl)
159
+ F_fr = k_t * (z_ufr - z_rfr)
160
+ F_rl = k_t * (z_url - z_rrl)
161
+ F_rr = k_t * (z_urr - z_rrr)
162
+
163
+ tyre = {
164
+ "fl": F_fl,
165
+ "fr": F_fr,
166
+ "rl": F_rl,
167
+ "rr": F_rr,
168
+ }
169
+ tyre_metric = {
170
+ "name": "Tyre Loads",
171
+ "unit": "N"
172
+ }
173
+ else:
174
+ # tyre deflection (proxy for load)
175
+ tyre = {
176
+ "fl": z_ufl,
177
+ "fr": z_ufr,
178
+ "rl": z_url,
179
+ "rr": z_urr,
180
+ }
181
+ tyre_metric = {
182
+ "name": "Tyre deflection",
183
+ "unit": "m"
184
+ }
185
+
186
+ return {
187
+ "body_accel": body_accel,
188
+ "body_accel_RMS": rms,
189
+ "pitch": theta,
190
+ "roll": phi,
191
+ "suspension_travel": travel,
192
+ "tyre": tyre,
193
+ "tyre_metric": tyre_metric
194
+ }
195
+
196
+ def compute_vehicle_metrics(result, model=None):
197
+ n = result.state.shape[1]
198
+
199
+ if n == 4:
200
+ return compute_quarter_car_metrics(result, model)
201
+ elif n == 8:
202
+ # ambiguous → assume pitch (or detect via labels)
203
+ return compute_half_car_pitch_metrics(result, model)
204
+ elif n == 14:
205
+ return compute_full_car_metrics(result, model)
206
+ else:
207
+ raise ValueError(f"Unsupported system with {n} states")
@@ -7,10 +7,20 @@ from simweave.continuous.solver import (
7
7
  simulate,
8
8
  ContinuousProcess,
9
9
  )
10
+ from simweave.continuous.control import (
11
+ PIDController,
12
+ SkyhookDamper,
13
+ GroundhookDamper,
14
+ HybridActiveDamper,
15
+ SemiActiveWrapper,
16
+ )
10
17
  from simweave.continuous.systems import (
11
18
  MassSpringDamper,
12
19
  SimplePendulum,
13
20
  QuarterCarModel,
21
+ HalfCarModel,
22
+ RollCarModel,
23
+ FullCarModel,
14
24
  SeriesRLC,
15
25
  ThermalRC,
16
26
  TwoMassThermal,
@@ -22,9 +32,17 @@ __all__ = [
22
32
  "SimulationResult",
23
33
  "simulate",
24
34
  "ContinuousProcess",
35
+ "PIDController",
36
+ "SkyhookDamper",
37
+ "GroundhookDamper",
38
+ "HybridActiveDamper",
39
+ "SemiActiveWrapper",
25
40
  "MassSpringDamper",
26
41
  "SimplePendulum",
27
42
  "QuarterCarModel",
43
+ "HalfCarModel",
44
+ "RollCarModel",
45
+ "FullCarModel",
28
46
  "SeriesRLC",
29
47
  "ThermalRC",
30
48
  "TwoMassThermal",
@@ -0,0 +1,17 @@
1
+ from simweave.continuous.control.pid import (
2
+ PIDController,
3
+ )
4
+ from simweave.continuous.control.suspension import (
5
+ SkyhookDamper,
6
+ GroundhookDamper,
7
+ HybridActiveDamper,
8
+ SemiActiveWrapper,
9
+ )
10
+
11
+ __all__ = [
12
+ "PIDController",
13
+ "SkyhookDamper",
14
+ "GroundhookDamper",
15
+ "HybridActiveDamper",
16
+ "SemiActiveWrapper",
17
+ ]
@@ -0,0 +1,48 @@
1
+ # ---------------------------------------------------------------------------
2
+ # PID Controller for Control Systems Engineering
3
+ # ---------------------------------------------------------------------------
4
+
5
+ class PIDController:
6
+ """Generic PID controller.
7
+
8
+ u(t) = Kp * e + Ki * ∫e dt + Kd * de/dt
9
+ """
10
+
11
+ def __init__(
12
+ self,
13
+ Kp: float,
14
+ Ki: float = 0.0,
15
+ Kd: float = 0.0,
16
+ setpoint: float = 0.0,
17
+ ):
18
+ self.Kp = float(Kp)
19
+ self.Ki = float(Ki)
20
+ self.Kd = float(Kd)
21
+ self.setpoint = float(setpoint)
22
+
23
+ self._integral = 0.0
24
+ self._prev_error = None
25
+
26
+ def reset(self):
27
+ self._integral = 0.0
28
+ self._prev_error = None
29
+
30
+ def __call__(self, measurement: float, dt: float) -> float:
31
+ error = self.setpoint - measurement
32
+
33
+ # integral
34
+ self._integral += error * dt
35
+
36
+ # derivative
37
+ if self._prev_error is None:
38
+ derivative = 0.0
39
+ else:
40
+ derivative = (error - self._prev_error) / dt
41
+
42
+ self._prev_error = error
43
+
44
+ return (
45
+ self.Kp * error
46
+ + self.Ki * self._integral
47
+ + self.Kd * derivative
48
+ )
@@ -0,0 +1,54 @@
1
+ class SkyhookDamper:
2
+ """Skyhook damping controller.
3
+
4
+ Applies force proportional to body velocity.
5
+ """
6
+
7
+ def __init__(self, damping: float):
8
+ self.c = float(damping)
9
+
10
+ def force(self, body_velocity: float, wheel_velocity: float) -> float:
11
+ # For skyhook uses body velocity only, but signature makes modular/easy to toggle controller
12
+ return -self.c * body_velocity
13
+
14
+ class GroundhookDamper:
15
+ """Groundhook damping controller.
16
+
17
+ Applies force proportional to wheel velocity."""
18
+
19
+ def __init__(self, damping: float):
20
+ self.c = float(damping)
21
+
22
+ def force(self, body_velocity: float, wheel_velocity: float) -> float:
23
+ # For groundhook uses wheel velocity only, but signature makes modular/easy to toggle controller
24
+ return -self.c * wheel_velocity
25
+
26
+ class HybridActiveDamper:
27
+ "Hybrid Skyhook & Groundhook."
28
+ def __init__(self, c_sky: float, c_ground: float, alpha: float = 0.5):
29
+ self.sky = SkyhookDamper(c_sky)
30
+ self.ground = GroundhookDamper(c_ground)
31
+ self.alpha = float(alpha)
32
+
33
+ def force(self, body_v: float, wheel_v: float) -> float:
34
+ return (
35
+ self.alpha * self.sky.force(body_v)
36
+ + (1 - self.alpha) * self.ground.force(wheel_v)
37
+ )
38
+
39
+ class SemiActiveWrapper:
40
+ """Enforces passive (dissipative) behaviour: F * v_rel <= 0."""
41
+
42
+ def __init__(self, controller):
43
+ self.controller = controller
44
+
45
+ def force(self, body_velocity: float, wheel_velocity: float) -> float:
46
+ F = self.controller.force(body_velocity, wheel_velocity)
47
+
48
+ v_rel = body_velocity - wheel_velocity
49
+
50
+ # enforce dissipative constraint
51
+ if F * v_rel > 0:
52
+ return 0.0
53
+
54
+ return F
@@ -19,7 +19,7 @@ from typing import Callable, Iterable, Optional, Protocol, runtime_checkable
19
19
  import numpy as np
20
20
 
21
21
  from simweave.core.entity import Entity
22
-
22
+ from simweave.units.si import SIUnit
23
23
 
24
24
  ArrayLike = np.ndarray
25
25
  InputFunc = Callable[[float], np.ndarray | float | int | None]
@@ -32,6 +32,7 @@ class SimulationResult:
32
32
  state_labels: tuple[str, ...]
33
33
  system_name: str
34
34
  method: str
35
+ inputs: np.ndarray | None = None
35
36
 
36
37
  def final_state(self) -> np.ndarray:
37
38
  return self.state[-1].copy()
@@ -51,9 +52,29 @@ class SupportsDynamics(Protocol):
51
52
  ) -> np.ndarray: ...
52
53
 
53
54
 
55
+ class WrappedResult:
56
+ def __init__(self, data):
57
+ self._data = data
58
+
59
+ def __getitem__(self, key):
60
+ return self._data[key]
61
+
62
+
54
63
  class DynamicSystem:
55
64
  """Base class for first-order state-space dynamic systems."""
56
65
 
66
+ STATE_UNITS: dict[str, type] = {}
67
+
68
+ @staticmethod
69
+ def _val(x):
70
+ """ Allow SIUnits to be supplied despite float for internal calcs """
71
+
72
+ if isinstance(x, SIUnit):
73
+ return x.value
74
+ if isinstance(x, (int, float, np.ndarray)):
75
+ return x
76
+ raise TypeError(f"Unsupported type for numeric value: {type(x)}")
77
+
57
78
  def initial_state(self) -> np.ndarray:
58
79
  raise NotImplementedError
59
80
 
@@ -75,6 +96,25 @@ class DynamicSystem:
75
96
  def validate_state(self, state: np.ndarray) -> None:
76
97
  if state.ndim != 1:
77
98
  raise ValueError("State must be a 1D array.")
99
+
100
+ def get_state_unit(self, label: str):
101
+ return self.STATE_UNITS.get(label)
102
+
103
+ def wrap_states(self, result):
104
+ wrapped = {}
105
+
106
+ labels = result.state_labels
107
+ state = result.state
108
+
109
+ for i, label in enumerate(labels):
110
+ unit_cls = self.STATE_UNITS.get(label)
111
+
112
+ if unit_cls is not None:
113
+ wrapped[label] = unit_cls(state[:, i])
114
+ else:
115
+ wrapped[label] = state[:, i]
116
+
117
+ return WrappedResult(wrapped)
78
118
 
79
119
  @property
80
120
  def name(self) -> str:
@@ -152,10 +192,33 @@ def simulate(
152
192
  state = np.zeros((time.size, x_init.size), dtype=float)
153
193
  state[0] = x_init
154
194
 
195
+ t0_input = _resolve_inputs(system, inputs, time[0])
196
+ inputs_log = [np.atleast_1d(t0_input).astype(float) if t0_input is not None else np.zeros(1)]
197
+
155
198
  stepper = _STEPPERS[method]
156
199
  for i in range(1, time.size):
200
+ t_prev = time[i - 1]
201
+
202
+ # --- resolve inputs ---
203
+ if inputs is not None:
204
+ u = inputs(t_prev)
205
+ else:
206
+ u = system.inputs(t_prev)
207
+
208
+ # normalise to array for storage
209
+ if u is None:
210
+ u_arr = np.zeros(1)
211
+ else:
212
+ u_arr = np.atleast_1d(u).astype(float)
213
+
214
+ inputs_log.append(u_arr)
215
+
216
+ # --- step ---
157
217
  state[i] = stepper(system, time[i - 1], state[i - 1], dt, inputs)
158
218
 
219
+ # gather inputs
220
+ inputs_array = np.vstack(inputs_log)
221
+
159
222
  labels = (
160
223
  tuple(system.state_labels())
161
224
  if hasattr(system, "state_labels")
@@ -167,6 +230,7 @@ def simulate(
167
230
  state_labels=labels,
168
231
  system_name=getattr(system, "name", system.__class__.__name__),
169
232
  method=method,
233
+ inputs=inputs_array,
170
234
  )
171
235
 
172
236
 
@@ -234,4 +298,4 @@ class ContinuousProcess(Entity):
234
298
  else tuple(f"x{i}" for i in range(self.state.size)),
235
299
  system_name=getattr(self.system, "name", self.system.__class__.__name__),
236
300
  method=self.method,
237
- )
301
+ )