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.
- {simweave-0.5.0/src/simweave.egg-info → simweave-0.7.2}/PKG-INFO +15 -4
- {simweave-0.5.0 → simweave-0.7.2}/README.md +14 -3
- {simweave-0.5.0 → simweave-0.7.2}/pyproject.toml +1 -1
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/__init__.py +69 -1
- simweave-0.7.2/src/simweave/analysis/vehicle.py +207 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/__init__.py +18 -0
- simweave-0.7.2/src/simweave/continuous/control/__init__.py +17 -0
- simweave-0.7.2/src/simweave/continuous/control/pid.py +48 -0
- simweave-0.7.2/src/simweave/continuous/control/suspension.py +54 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/solver.py +66 -2
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/systems/__init__.py +6 -0
- simweave-0.7.2/src/simweave/continuous/systems/full_car.py +155 -0
- simweave-0.7.2/src/simweave/continuous/systems/half_car.py +143 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/systems/mass_spring_damper.py +13 -8
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/systems/pendulum.py +15 -10
- simweave-0.7.2/src/simweave/continuous/systems/quarter_car.py +82 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/systems/rlc.py +14 -9
- simweave-0.7.2/src/simweave/continuous/systems/roll_car.py +110 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/continuous/systems/thermal.py +40 -24
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/__init__.py +2 -0
- simweave-0.7.2/src/simweave/core/time_axis.py +236 -0
- simweave-0.7.2/src/simweave/reliability/__init__.py +34 -0
- simweave-0.7.2/src/simweave/reliability/entity.py +256 -0
- simweave-0.7.2/src/simweave/reliability/fleet.py +199 -0
- simweave-0.7.2/src/simweave/reliability/repair.py +160 -0
- simweave-0.7.2/src/simweave/reliability/sensitivity.py +211 -0
- simweave-0.7.2/src/simweave/reliability/subsystem.py +109 -0
- simweave-0.7.2/src/simweave/roads/__init__.py +52 -0
- simweave-0.7.2/src/simweave/roads/intersection.py +191 -0
- simweave-0.7.2/src/simweave/roads/network.py +149 -0
- simweave-0.7.2/src/simweave/roads/recorder.py +80 -0
- simweave-0.7.2/src/simweave/roads/road.py +203 -0
- simweave-0.7.2/src/simweave/roads/roundabout.py +220 -0
- simweave-0.7.2/src/simweave/roads/signal.py +107 -0
- simweave-0.7.2/src/simweave/roads/vehicle.py +116 -0
- simweave-0.7.2/src/simweave/units/__init__.py +71 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/units/si.py +378 -28
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/viz/__init__.py +12 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/viz/plots.py +375 -5
- simweave-0.7.2/src/simweave/viz/vehicle_dynamics.py +148 -0
- {simweave-0.5.0 → simweave-0.7.2/src/simweave.egg-info}/PKG-INFO +15 -4
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave.egg-info/SOURCES.txt +24 -1
- simweave-0.5.0/src/simweave/continuous/systems/quarter_car.py +0 -59
- simweave-0.5.0/src/simweave/units/__init__.py +0 -25
- {simweave-0.5.0 → simweave-0.7.2}/MANIFEST.in +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/requirements-dev.txt +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/requirements.txt +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/setup.cfg +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/agents/__init__.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/agents/agent.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/agents/compass.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/agents/routing.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/clock.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/entity.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/environment.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/logging.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/core/scheduler.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/currency/__init__.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/currency/codes.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/currency/format.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/currency/fx.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/currency/money.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/discrete/__init__.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/discrete/properties.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/discrete/queues.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/discrete/resources.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/discrete/services.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/mc/__init__.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/mc/runner.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/spatial/__init__.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/spatial/graph.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/supplychain/__init__.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/supplychain/inventory.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/supplychain/optimization.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/supplychain/warehouse.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/units/constants.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/viz/_plotly.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/viz/recorders.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave/viz/themes.py +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave.egg-info/dependency_links.txt +0 -0
- {simweave-0.5.0 → simweave-0.7.2}/src/simweave.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
+
)
|