Files
OBD2-Simulator/app/simulation/modules/engine.py

288 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# =============================
# app/simulation/modules/engine.py
# =============================
from __future__ import annotations
from app.simulation.simulator import Module, Vehicle
import math, random
ENGINE_DEFAULTS = {
# Basis
"idle_rpm": 1200,
"max_rpm": 9000,
"rpm_rise_per_s": 4000,
"rpm_fall_per_s": 3000,
"throttle_curve": "linear",
# Starter / Startlogik
"starter_rpm_nominal": 250.0,
"starter_voltage_min": 10.5,
"start_rpm_threshold": 210.0,
"stall_rpm": 500.0,
# Thermische Einflüsse (nur fürs Derating/Viskosität benutzt)
"coolant_ambient_c": 20.0,
"idle_cold_gain_per_deg": 3.0,
"idle_cold_gain_max": 500.0,
# Öl / Öldruck
"oil_pressure_idle_bar": 1.2,
"oil_pressure_slope_bar_per_krpm": 0.8,
"oil_pressure_off_floor_bar": 0.2,
# Leistungsdaten
"engine_power_kw": 60.0,
"torque_peak_rpm": 7000.0,
# Drive-by-wire / Regler
"throttle_plate_idle_min_pct": 6.0,
"throttle_plate_overrun_pct": 2.0,
"throttle_plate_tau_s": 0.08,
"torque_ctrl_kp": 1.2,
"torque_ctrl_ki": 0.6,
# RPM-Jitter
"rpm_jitter_idle_amp_rpm": 12.0,
"rpm_jitter_high_amp_rpm": 4.0,
"rpm_jitter_tau_s": 0.20,
"rpm_jitter_off_threshold_rpm": 250.0,
# UI
"throttle_pedal_pct": 0.0,
}
class EngineModule(Module):
PRIO = 20
NAME = "engine"
"""
Erweiterte Motormodellierung mit realistischem Jitter & Drive-by-Wire:
- OFF/ACC/ON/START Logik, Starten/Abwürgen
- Thermik (Kühlmittel/Öl), Öldruck ~ f(RPM)
- Startverhalten abhängig von Spannung & Öltemp
- Leistungsmodell via engine_power_kw + torque_peak_rpm
- Fahrerwunsch: throttle_pedal_pct (0..100) → Ziel-Leistungsanteil
* Drosselklappe (throttle_plate_pct) wird per PI-Regler geführt
* Mindestöffnung im Leerlauf, fast zu im Schubbetrieb
- Realistischer RPM-Jitter:
* bandbegrenztes Rauschen (1. Ordnung) mit Amplitude ~ f(RPM)
* kein Jitter unter einer Schwell-RPM oder wenn Motor aus
Outputs:
rpm, coolant_temp, oil_temp, oil_pressure
engine_available_torque_nm, engine_net_torque_nm
throttle_plate_pct (neu), throttle_pedal_pct (durchgereicht)
"""
def __init__(self):
self._running = False
self._oil_p_tau = 0.25 # Zeitkonstante Öldruck
# DBW intern
self._plate_pct = 5.0
self._tc_i = 0.0
# AR(1)-Noise
self._rpm_noise = 0.0
# ---- helpers ----------------------------------------------------------
def _curve(self, t: float, mode: str) -> float:
if mode == "progressive": return t**1.5
if mode == "aggressive": return t**0.7
return t
def _torque_at_rpm(self, power_kw: float, rpm: float, peak_rpm: float) -> float:
rpm = max(0.0, rpm)
t_max = (9550.0 * max(0.0, power_kw)) / max(500.0, peak_rpm)
x = min(math.pi, max(0.0, (rpm / max(1.0, peak_rpm)) * (math.pi/2)))
return max(0.0, t_max * math.sin(x))
def _plate_airflow_factor(self, plate_pct: float) -> float:
theta = max(0.0, min(90.0, (plate_pct/100.0)*90.0)) * math.pi/180.0
return math.sin(theta)**2
def _visco(self, temp_c: float) -> float:
# -10°C -> 0.6 … 20°C -> 0.8 … 90°C -> 1.0
if temp_c <= -10: return 0.6
if temp_c >= 90: return 1.0
if temp_c <= 20: return 0.6 + (temp_c + 10.0) * (0.2/30.0)
return 0.8 + (temp_c - 20.0) * (0.2/70.0)
# ---- main -------------------------------------------------------------
def apply(self, v: Vehicle, dt: float) -> None:
e = v.config.setdefault("engine", {})
# --- Config ---
idle = float(e.get("idle_rpm", ENGINE_DEFAULTS["idle_rpm"]))
maxr = float(e.get("max_rpm", ENGINE_DEFAULTS["max_rpm"]))
rise = float(e.get("rpm_rise_per_s", ENGINE_DEFAULTS["rpm_rise_per_s"]))
fall = float(e.get("rpm_fall_per_s", ENGINE_DEFAULTS["rpm_fall_per_s"]))
thr_curve = e.get("throttle_curve", ENGINE_DEFAULTS["throttle_curve"])
ambient = float(e.get("coolant_ambient_c", ENGINE_DEFAULTS["coolant_ambient_c"]))
cold_gain_per_deg = float(e.get("idle_cold_gain_per_deg", ENGINE_DEFAULTS["idle_cold_gain_per_deg"]))
cold_gain_max = float(e.get("idle_cold_gain_max", ENGINE_DEFAULTS["idle_cold_gain_max"]))
starter_nom = float(e.get("starter_rpm_nominal", ENGINE_DEFAULTS["starter_rpm_nominal"]))
starter_vmin= float(e.get("starter_voltage_min", ENGINE_DEFAULTS["starter_voltage_min"]))
start_rpm_th= float(e.get("start_rpm_threshold", ENGINE_DEFAULTS["start_rpm_threshold"]))
stall_rpm = float(e.get("stall_rpm", ENGINE_DEFAULTS["stall_rpm"]))
power_kw = float(e.get("engine_power_kw", ENGINE_DEFAULTS["engine_power_kw"]))
peak_torque_rpm = float(e.get("torque_peak_rpm", ENGINE_DEFAULTS["torque_peak_rpm"]))
oil_idle_bar = float(e.get("oil_pressure_idle_bar", ENGINE_DEFAULTS["oil_pressure_idle_bar"]))
oil_slope_bar_per_krpm = float(e.get("oil_pressure_slope_bar_per_krpm", ENGINE_DEFAULTS["oil_pressure_slope_bar_per_krpm"]))
oil_floor_off = float(e.get("oil_pressure_off_floor_bar", ENGINE_DEFAULTS["oil_pressure_off_floor_bar"]))
plate_idle_min = float(e.get("throttle_plate_idle_min_pct", ENGINE_DEFAULTS["throttle_plate_idle_min_pct"]))
plate_overrun = float(e.get("throttle_plate_overrun_pct", ENGINE_DEFAULTS["throttle_plate_overrun_pct"]))
plate_tau = float(e.get("throttle_plate_tau_s", ENGINE_DEFAULTS["throttle_plate_tau_s"]))
torque_kp = float(e.get("torque_ctrl_kp", ENGINE_DEFAULTS["torque_ctrl_kp"]))
torque_ki = float(e.get("torque_ctrl_ki", ENGINE_DEFAULTS["torque_ctrl_ki"]))
jitter_idle_amp= float(e.get("rpm_jitter_idle_amp_rpm", ENGINE_DEFAULTS["rpm_jitter_idle_amp_rpm"]))
jitter_hi_amp = float(e.get("rpm_jitter_high_amp_rpm", ENGINE_DEFAULTS["rpm_jitter_high_amp_rpm"]))
jitter_tau = float(e.get("rpm_jitter_tau_s", ENGINE_DEFAULTS["rpm_jitter_tau_s"]))
jitter_off_rpm = float(e.get("rpm_jitter_off_threshold_rpm", ENGINE_DEFAULTS["rpm_jitter_off_threshold_rpm"]))
# --- State ---
rpm = float(v.ensure("rpm", 0.0))
pedal = float(v.ensure("throttle_pedal_pct", float(e.get("throttle_pedal_pct", 0.0))))
pedal = max(0.0, min(100.0, pedal))
ign = str(v.ensure("ignition", "OFF"))
elx_v = float(v.ensure("elx_voltage", 0.0))
cool = float(v.ensure("coolant_temp", ambient)) # nur lesen
oil = float(v.ensure("oil_temp", ambient)) # nur lesen
oil_p = float(v.ensure("oil_pressure", 0.0))
# externe Momente (Alternator/Getriebe/…)
torque_load = max(0.0, v.acc_total("engine.torque_load_nm"))
torque_load = max(torque_load, float(v.get("engine_ext_torque_nm", 0.0))) # legacy fallback
# Dashboard-Metriken
v.register_metric("rpm", unit="RPM", fmt=".1f", label="Drehzahl", source="engine", priority=20)
v.register_metric("oil_pressure", unit="bar", fmt=".2f", label="Öldruck", source="engine", priority=42)
v.register_metric("engine_available_torque_nm", unit="Nm", fmt=".0f", label="Verfügbares Motormoment", source="engine", priority=43)
v.register_metric("engine_torque_load_nm", unit="Nm", fmt=".0f", label="Lastmoment ges.", source="engine", priority=44)
v.register_metric("engine_net_torque_nm", unit="Nm", fmt=".0f", label="Netto Motormoment", source="engine", priority=45)
v.register_metric("throttle_pedal_pct", unit="%", fmt=".0f", label="Gaspedal", source="engine", priority=46)
v.register_metric("throttle_plate_pct", unit="%", fmt=".0f", label="Drosselklappe", source="engine", priority=47)
# --- Start-/Ziel-RPM Logik ---
# Starter-Viskositätseinfluss
vfac = 0.0 if elx_v <= starter_vmin else min(1.2, (elx_v - starter_vmin) / max(0.3, (12.6 - starter_vmin)))
crank_rpm = starter_nom * vfac * self._visco(oil)
# effektive Startschwelle (15..45% Idle)
start_rpm_min = 0.15 * idle
start_rpm_max = 0.45 * idle
start_rpm_th_eff = max(start_rpm_min, min(start_rpm_th, start_rpm_max))
if ign in ("OFF", "ACC"):
self._running = False
target_rpm = 0.0
elif ign == "START":
target_rpm = crank_rpm
if not self._running and target_rpm >= start_rpm_th_eff and elx_v > starter_vmin:
self._running = True
else: # ON
if not self._running and rpm >= max(0.15*idle, start_rpm_th_eff*0.9):
self._running = True
if self._running:
cold_add = max(0.0, min(ENGINE_DEFAULTS["idle_cold_gain_max"],
(90.0 - cool) * cold_gain_per_deg))
idle_eff = idle + cold_add
target_rpm = max(idle_eff, min(maxr, rpm))
else:
target_rpm = 0.0
# --- Basis-Moment & Derating ---
base_torque = self._torque_at_rpm(power_kw, max(1.0, rpm), peak_torque_rpm)
temp_derate = max(0.7, 1.0 - max(0.0, (oil - 110.0)) * 0.005)
# --- DBW (PI auf Torque-Anteil) ---
demand = self._curve(pedal/100.0, thr_curve)
plate_target_min = plate_overrun if demand < 0.02 else plate_idle_min
airflow = self._plate_airflow_factor(self._plate_pct)
torque_avail = base_torque * airflow * temp_derate
torque_frac = 0.0 if base_torque <= 1e-6 else (torque_avail / (base_torque * temp_derate))
err = max(0.0, demand) - max(0.0, min(1.0, torque_frac))
if ign == "ON" and self._running:
self._tc_i += err * torque_ki * dt
else:
self._tc_i *= 0.95
plate_cmd = self._plate_pct + (torque_kp * err + self._tc_i) * 100.0
plate_cmd = max(plate_target_min, min(100.0, plate_cmd))
a_tau = min(1.0, dt / max(1e-3, plate_tau))
self._plate_pct = (1.0 - a_tau) * self._plate_pct + a_tau * plate_cmd
# aktualisiertes Moment
airflow = self._plate_airflow_factor(self._plate_pct)
avail_torque = base_torque * airflow * temp_derate
net_torque = max(0.0, avail_torque - max(0.0, torque_load))
# --- Wärmeleistung pushen (W) ---
# mechanische Leistung:
mech_power_w = net_torque * (2.0 * math.pi * rpm / 60.0)
# grober Wirkungsgrad (0.24..0.34 je nach Pedal/Kennlinie)
eta = 0.24 + 0.10 * self._curve(pedal/100.0, thr_curve)
eta = max(0.05, min(0.45, eta))
fuel_power_w = mech_power_w / max(1e-3, eta)
heat_w = max(0.0, fuel_power_w - mech_power_w)
# Idle-Basiswärme, damit im Leerlauf nicht auskühlt:
idle_heat_w = 1500.0 * (rpm / max(1.0, idle))
heat_w = max(heat_w, idle_heat_w)
v.push("thermal.heat_w", +heat_w, source="engine")
# --- Ziel-RPM aus Netto-Moment ---
if ign == "ON" and self._running:
cold_add = max(0.0, min(ENGINE_DEFAULTS["idle_cold_gain_max"],
(90.0 - cool) * cold_gain_per_deg))
idle_eff = idle + cold_add
denom = (base_torque * temp_derate + 1e-6)
torque_norm = 0.0 if denom <= 1e-8 else max(0.0, min(1.0, net_torque / denom))
target_rpm = idle_eff + torque_norm * (maxr - idle_eff)
# Inertia
if rpm < target_rpm: rpm = min(target_rpm, rpm + rise * dt)
else: rpm = max(target_rpm, rpm - fall * dt)
# Stall
if ign == "ON" and self._running and rpm < stall_rpm:
self._running = False
# --- Öldruck ---
if self._running and rpm > 0.0:
over_krpm = max(0.0, (rpm - idle)/1000.0)
oil_target = oil_idle_bar + oil_slope_bar_per_krpm * over_krpm
elif ign == "START" and target_rpm > 0.0:
oil_target = max(oil_floor_off, 0.4)
else:
oil_target = oil_floor_off
a = min(1.0, dt / max(0.05, self._oil_p_tau))
oil_p = (1-a) * oil_p + a * oil_target
# --- RPM-Jitter ---
if self._running and rpm >= jitter_off_rpm and ign == "ON":
b = min(1.0, dt / max(1e-3, jitter_tau))
eta_n = random.uniform(-1.0, 1.0)
self._rpm_noise = (1.0 - b) * self._rpm_noise + b * eta_n
k = max(0.0, min(1.0, rpm / max(1.0, maxr)))
amp = (1.0 - k)*jitter_idle_amp + k*jitter_hi_amp
rpm += self._rpm_noise * amp
else:
self._rpm_noise *= 0.9
# --- Clamp & Set ---
rpm = max(0.0, min(rpm, maxr))
oil_p = max(oil_floor_off, min(8.0, oil_p))
v.set("rpm", float(rpm))
# Temperaturen NICHT setzen CoolingModule ist owner!
v.set("oil_pressure", float(oil_p))
v.set("engine_available_torque_nm", float(avail_torque))
v.set("engine_torque_load_nm", float(torque_load))
v.set("engine_net_torque_nm", float(net_torque))
v.set("throttle_pedal_pct", float(pedal))
v.set("throttle_plate_pct", float(self._plate_pct))