Continuous¶
Fixed-step ODE integration plus a small library of canonical example systems used throughout the demos and tests.
All continuous models use SI units: - Displacement: metres (m) - Velocity: m/s - Angles: radians (rad)
Quick example¶
import numpy as np
import simweave as sw
msd = sw.MassSpringDamper(mass=1.0, damping=0.4, stiffness=4.0)
res = sw.simulate(msd, t_span=(0.0, 12.0), dt=0.01, x0=np.array([1.0, 0.0]))
Available systems¶
MassSpringDamperSimplePendulumQuarterCarModelHalfCarModelRollCarModelFullCarModelSeriesRLCThermalRCTwoMassThermal
Each implements the DynamicSystem / SupportsDynamics protocol so
custom systems plug into simulate() the same way.
Hybrid: continuous + discrete¶
Wrap a system in ContinuousProcess to register it on a
SimEnvironment. The integrator advances one tick per env.tick()
call, so continuous physics share the clock with queues and agents.
PID Control¶
Please see demo 20_PID_thermal_system.py to see a PID controller applied to a simple ThermalRC system,
run as a sampled-data controller (simulate(..., controller=pid, control_dt=...), described below).
The older ThermalRC(controller=...) argument is deprecated.
A temperature regulation system responding to workload changes could be helpful in real-world cases like:
- CPUs
- batteries
- industrial heaters
- HVAC systems
Closed-loop control in simulate¶
simulate accepts a sampled-data controller. Every control_dt seconds it
calls controller.update(t, state, control_dt) and holds the result constant
(zero-order hold) while the system integrates, as a real digital controller
does. control_dt must be a whole multiple of dt and defaults to dt.
pid = PIDController(Kp=4.0, Ki=2.0, setpoint=1.0, output_limits=(-10, 10))
result = simulate(plant, (0, 20), dt=0.01, controller=pid, control_dt=0.02)
PIDController also supports output_limits, anti-windup
(anti_windup="clamp", the default when limits are set),
derivative_on_measurement and a derivative filter derivative_tau.
ContinuousProcess takes the same controller= / control_dt= arguments. A
system may define post_step(state) -> state, applied after every
integration step (for example to clamp a ground contact).
Vehicle dynamics examples¶
Quarter car¶
A 2 DOF (Degrees of Freedom) model with 4 variables (position + velocity).
- z_s → sprung mass (body)
- z_u → unsprung mass (wheel)
d = sw.QuarterCarModel(250, 40, 15000, 1500, 200000)
r = sw.simulate(d, (0.0, 2.0), dt=0.001, inputs=lambda t: 0.01)
demos/10_quarter_car.py
Half car (pitch)¶
A 4 DOF (Degrees of Freedom) model for simulating pitch movement between front and back.
- z_s → vertical body motion
- theta → pitch
- z_uf → front wheel
- z_ur → rear wheel
model = sw.HalfCarModel(
1200, 2500, 60, 60,
20000, 20000,
1500, 1500,
150000, 150000,
1.2, 1.6
)
r = sw.simulate(model, (0.0, 2.0), dt=0.001,
inputs=lambda t: (0.01, 0.01))
demos/17_half_car_pitch.py
Roll model (left/right)¶
A 4 DOF (Degrees of Freedom) model for simulating roll movement between left and right.
- z_s → vertical body motion
- phi → roll
- z_ul → left wheel
- z_ur → right wheel
model = sw.RollCarModel(
1200, 2200, 60, 60,
20000, 20000,
1500, 1500,
150000, 150000,
1.6
)
r = sw.simulate(model, (0.0, 2.0), dt=0.001,
inputs=lambda t: (0.01, 0.0))
demos/18_half_car_roll_model.py
Full Car Model¶
A 7 DOF (Degrees of Freedom) model is included, extending prior examples.
- z_s → vertical body motion
- theta → pitch
- phi → roll
- z_ufl → front-left
- z_ufr → front-right
- z_url → rear-left
- z_urr → rear-right
model = sw.FullCarModel(
sprung_mass=1200,
pitch_inertia=1500,
roll_inertia=800,
unsprung_mass=40,
k_s=20000,
c_s=1500,
k_t=200000,
a=1.2,
b=1.3,
track_width=1.6,
)
result = simulate(
model, (0.0, 3.0),
dt=0.001,
inputs=lambda t: np.array([0.01, 0.01, 0.0, 0.0]) if t > 1 else np.zeros(4),
)
fig = sw.plot_vehicle_metrics(
res,
title="Full car response (bump input)",
model=model,
)
demos/19_full_car_dynamics.py
Suspension Controllers¶
From v0.6.0, SimWeave includes controllers to help optimise dynamics according to prefered performance criteria: - Comfort (Acceleration of Vertical Vibrations) - Grip (Dynamic Tyre Loading) - Hybrid
These are realised by Skyhook and Groundhook control. See: - simweave.continuous.control.suspension.SkyhookDamper - simweave.continuous.control.suspension.GroundhookDamper - simweave.continuous.control.suspension.HybridActiveDamper
The former imagines one end of a damper 'connected to the sky' (an inertial reference point). The latter imagines one end affixed to the ground. This ensure dissipative force applies with respect to body movement, or wheel movement only as opposed to the releative velocities.
Example controller usage:
passive = QuarterCarModel(250, 40, 15000, 1500, 200000)
controlled = QuarterCarModel(250, 40, 15000, 1500, 200000, controller=SkyhookDamper(1500))
r_passive = simulate(passive, (0, 2), dt=0.001, inputs=lambda t: 0.01)
r_control = simulate(controlled, (0, 2), dt=0.001, inputs=lambda t: 0.01)
z_passive = r_passive.state[:, 0]
z_control = r_control.state[:, 0]
print(f"Standard deviation controlled system: {z_control.std()}")
print(f"Standard deviation passive system: {z_passive.std()}")
A difference between an idealised behaviour, and the passive suspension can be used as a control signal. This could be used by an actuator in place of a damper, capable of adding energy into the system as a 'Fully Active' system. Alternatively, it could be a variable damping coefficient e.g. magnetorheological (MR) dampers.
To ensure fidelity with a real-world suspension that is only dissipative in nature, a wrapper allows for controller force to only apply when Force * relative velocity is negative.
See: simweave.continuous.control.suspension.SemiActiveWrapper
Example wrapper usage:API¶
Continuous-time dynamics: state-space solver, plug-in systems, hybrid wrapper.
DynamicSystem
¶
Base class for first-order state-space dynamic systems.
SupportsDynamics
¶
Bases: Protocol
Protocol for plug-in dynamic systems.
Controller
¶
Bases: Protocol
Protocol for sampled-data (digital) controllers.
update is called once per control period with the current time and
state and returns the control input, which the solver holds constant
(zero-order hold) until the next control tick. reset is optional and,
when present, is called at the start of every run.
ContinuousProcess
¶
ContinuousProcess(system: SupportsDynamics, method: str = 'rk4', n_substeps: int = 1, inputs: InputFunc | None = None, name: str | None = None, controller: Controller | None = None, control_dt: float | None = None)
Bases: Entity
Drive a DynamicSystem from a SimEnvironment's tick loop.
Each tick, integrates n_substeps sub-intervals of length dt / n_substeps.
The previous state is appended to history_state with the corresponding
time in history_time.
Source code in src/simweave/continuous/solver.py
PIDController
¶
PIDController(Kp: float, Ki: float = 0.0, Kd: float = 0.0, setpoint: float = 0.0, *, output_limits: tuple[float | None, float | None] | None = None, anti_windup: str = 'clamp', derivative_on_measurement: bool = False, derivative_tau: float | None = None)
Generic PID controller.
u(t) = Kp * e + Ki * ∫e dt + Kd * de/dt
With only Kp, Ki, Kd and setpoint given it behaves as a
plain PID. The keyword options below are all off by default.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
output_limits
|
tuple[float | None, float | None] | None
|
|
None
|
anti_windup
|
str
|
|
'clamp'
|
derivative_on_measurement
|
bool
|
Differentiate the measurement instead of the error, which avoids a derivative kick when the setpoint changes. |
False
|
derivative_tau
|
float | None
|
Time constant of a first-order low-pass filter on the derivative
term; |
None
|
Source code in src/simweave/continuous/control/pid.py
update
¶
:class:~simweave.continuous.solver.Controller adapter.
Only valid for single-state plants: the measurement is state[0].
For other layouts wrap the controller in a small class.
Source code in src/simweave/continuous/control/pid.py
SkyhookDamper
¶
GroundhookDamper
¶
HybridActiveDamper
¶
SemiActiveWrapper
¶
MassSpringDamper
¶
MassSpringDamper(mass: float | Mass, damping: float | Damping, stiffness: float | SpringStiffness, x0: tuple[float, float] = (0.0, 0.0))
Bases: DynamicSystem
Single degree-of-freedom mass-spring-damper.
State x = [displacement, velocity] Equation: m x'' + c x' + k x = F(t)
Source code in src/simweave/continuous/systems/mass_spring_damper.py
SimplePendulum
¶
SimplePendulum(length: float | Distance, mass: float | Mass = 1.0, gravity: float | Acceleration = 9.81, damping: float | Damping = 0.0, x0: tuple[float, float] = (0.0, 0.0))
Bases: DynamicSystem
Simple pendulum with optional viscous damping and external torque.
State x = [theta, theta_dot] Equation: theta'' + (c / (m l^2)) theta' + (g/l) sin(theta) = tau / (m l^2)
Source code in src/simweave/continuous/systems/pendulum.py
QuarterCarModel
¶
QuarterCarModel(sprung_mass: float | Mass, unsprung_mass: float | Mass, suspension_stiffness: float | SpringStiffness, damping: float | Damping, tyre_stiffness: float | SpringStiffness, x0: tuple[float, float, float, float] = (0.0, 0.0, 0.0, 0.0), controller=None)
Bases: DynamicSystem
Two degree-of-freedom quarter-car suspension model.
State vector x = [z_s, z_s_dot, z_u, z_u_dot] where: z_s: sprung mass displacement z_u: unsprung mass displacement
Source code in src/simweave/continuous/systems/quarter_car.py
HalfCarModel
¶
HalfCarModel(sprung_mass: float | Mass, pitch_inertia: float | Inertia, unsprung_mass_front: float | Mass, unsprung_mass_rear: float | Mass, k_sf: float | SpringStiffness, k_sr: float | SpringStiffness, c_sf: float | Damping, c_sr: float | Damping, k_tf: float | SpringStiffness, k_tr: float | SpringStiffness, a: float | Distance, b: float | Distance, x0: tuple[float, ...] = (0.0,) * 8, controller=None)
Bases: DynamicSystem
Half-car suspension model with pitch dynamics.
State vector: x = [z_s, z_s_dot, theta, theta_dot, z_uf, z_uf_dot, z_ur, z_ur_dot]
Source code in src/simweave/continuous/systems/half_car.py
RollCarModel
¶
RollCarModel(sprung_mass: float | Mass, roll_inertia: float | Inertia, unsprung_mass_left: float | Mass, unsprung_mass_right: float | Mass, k_sl: float | SpringStiffness, k_sr: float | SpringStiffness, c_sl: float | Damping, c_sr: float | Damping, k_tl: float | SpringStiffness, k_tr: float | SpringStiffness, track_width: float | Distance, x0: tuple[float, ...] = (0.0,) * 8, controller=None)
Bases: DynamicSystem
Roll-only vehicle model (left-right dynamics).
Source code in src/simweave/continuous/systems/roll_car.py
FullCarModel
¶
FullCarModel(sprung_mass: float | Mass, pitch_inertia: float | Inertia, roll_inertia: float | Inertia, unsprung_mass: float | Mass, k_s: float | SpringStiffness, c_s: float | Damping, k_t: float | SpringStiffness, a: float | Distance, b: float | Distance, track_width: float | Distance, x0: tuple[float, ...] = (0.0,) * 14, controller=None)
Bases: DynamicSystem
Full car model with pitch and roll dynamics.
Source code in src/simweave/continuous/systems/full_car.py
SeriesRLC
¶
SeriesRLC(resistance: float | Resistance, inductance: float | Inductance, capacitance: float | Capacitance, x0: tuple[float, float] = (0.0, 0.0))
Bases: DynamicSystem
Series RLC circuit in charge-current form.
State x = [q, i] Equation: L q'' + R q' + (1/C) q = V(t)
Source code in src/simweave/continuous/systems/rlc.py
ThermalRC
¶
ThermalRC(thermal_resistance: float | ThermalResistance, thermal_capacitance: float | ThermalCapacitance, ambient_temperature: float | Temperature = 293.15, initial_temperature: float | Temperature = 293.15, controller: None | PIDController = None)
Bases: DynamicSystem
First-order lumped-capacitance thermal model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
thermal_resistance
|
float | ThermalResistance
|
R_th in K/W. Larger => slower equilibration with ambient. |
required |
thermal_capacitance
|
float | ThermalCapacitance
|
C_th in J/K. Larger => more thermal inertia. |
required |
ambient_temperature
|
float | Temperature
|
T_inf in Kelvin (or any consistent unit). Default 293.15 K (20 C). |
293.15
|
initial_temperature
|
float | Temperature
|
Starting body temperature, same unit as |
293.15
|
Source code in src/simweave/continuous/systems/thermal.py
time_constant
property
¶
First-order time constant tau = R_th * C_th (seconds).
reset_controls
¶
Clear the embedded controller's state at the start of a run.
control_update
¶
Run the embedded controller once per solver step with the true dt.
Called by :func:~simweave.continuous.solver.simulate and
:class:~simweave.continuous.solver.ContinuousProcess before each
integration step. The output is held (zero-order hold) across that step
and added to any external heat input, so the closed loop no longer
depends on how many times the integrator evaluates derivatives.
Source code in src/simweave/continuous/systems/thermal.py
TwoMassThermal
¶
TwoMassThermal(C_core: float | ThermalCapacitance, C_sink: float | ThermalCapacitance, k_core_to_sink: float | ThermalConductance, R_sink_to_ambient: float | ThermalResistance, ambient_temperature: float | Temperature = 293.15, initial_core: float | Temperature = 293.15, initial_sink: float | Temperature = 293.15)
Bases: DynamicSystem
Two-lump thermal model: core <-> sink <-> ambient.
State: [T_core, T_sink], both in the same unit as ambient.
Q_in(t) is applied to the core. Sink dissipates to ambient via
R_sink_to_ambient; core couples to sink via conductance
k_core_to_sink (W/K).
Source code in src/simweave/continuous/systems/thermal.py
simulate
¶
simulate(system: SupportsDynamics, t_span: tuple[float, float], dt: float, x0: Iterable[float] | ndarray | None = None, method: str = 'rk4', inputs: Optional[InputFunc] = None, controller: Optional[Controller] = None, control_dt: float | None = None) -> SimulationResult
Integrate a system over t_span with fixed step dt.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
controller
|
Optional[Controller]
|
Optional sampled-data controller (see :class: |
None
|
control_dt
|
float | None
|
Control period; must be a whole multiple of |
None
|
Notes
If the system defines post_step(state) -> state it is applied after
every integration step (for example to clamp a ground contact or
renormalise a quaternion).
If the system defines control_update(t, state, dt) it is called once
before every integration step with the true step size dt (and
reset_controls(), if present, is called at the start of the run). A
system that carries its own feedback law should use this hook instead of
evaluating a stateful controller inside derivatives, which is called
several times per step and so cannot advance an integrator correctly.
Source code in src/simweave/continuous/solver.py
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | |