neraly complete Model of Driving done, but needs tweaking

This commit is contained in:
2025-09-05 14:54:29 +02:00
parent 0276a3fb3c
commit 6108413d7e
12 changed files with 1469 additions and 726 deletions

View File

@@ -4,9 +4,8 @@
from __future__ import annotations
from app.simulation.simulator import Module, Vehicle
import random, math
import math, random
# Ein einziger Wahrheitsanker für alle Defaults:
ENGINE_DEFAULTS = {
# Basis
"idle_rpm": 1200,
@@ -14,38 +13,41 @@ ENGINE_DEFAULTS = {
"rpm_rise_per_s": 4000,
"rpm_fall_per_s": 3000,
"throttle_curve": "linear",
# Starter
# Starter / Startlogik
"starter_rpm_nominal": 250.0,
"starter_voltage_min": 10.5,
"start_rpm_threshold": 250.0, # <- fix niedriger, damit anspringt
"start_rpm_threshold": 210.0,
"stall_rpm": 500.0,
# Thermik
# Thermische Einflüsse (nur fürs Derating/Viskosität benutzt)
"coolant_ambient_c": 20.0,
"coolant_warm_rate_c_per_s": 0.35,
"coolant_cool_rate_c_per_s": 0.06,
"oil_warm_rate_c_per_s": 0.30,
"oil_cool_rate_c_per_s": 0.05,
"idle_cold_gain_per_deg": 3.0,
"idle_cold_gain_max": 500.0,
# Öl
# Öl / Öldruck
"oil_pressure_idle_bar": 1.2,
"oil_pressure_slope_bar_per_krpm": 0.8,
"oil_pressure_off_floor_bar": 0.2,
# Leistung
# Leistungsdaten
"engine_power_kw": 60.0,
"torque_peak_rpm": 7000.0,
# DBW
# 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,
# Jitter
# 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-Startwert (nur Anzeige)
# UI
"throttle_pedal_pct": 0.0,
}
@@ -71,17 +73,15 @@ class EngineModule(Module):
"""
def __init__(self):
self._target = None
self._running = False
self._oil_p_tau = 0.25 # s, Annäherung Öldruck
# Drive-by-Wire interner Zustand
self._plate_pct = 5.0 # Startwert, leicht geöffnet
self._tc_i = 0.0 # Integrator PI-Regler
# bandbegrenztes RPM-Rauschen (AR(1))
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
@@ -90,34 +90,34 @@ class EngineModule(Module):
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)
# einfache „Glocke“
x = min(math.pi, max(0.0, (rpm / max(1.0, peak_rpm)) * (math.pi/2)))
shape = math.sin(x)
return max(0.0, t_max * shape)
return max(0.0, t_max * math.sin(x))
def _plate_airflow_factor(self, plate_pct: float) -> float:
"""
Näherung Volumenstrom ~ sin^2(θ) mit θ aus 0..90° (hier 0..100%).
0% ≈ geschlossen (fast null), 100% ≈ voll offen (~1.0).
"""
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 / Defaults ---
idle = int(e.get("idle_rpm", ENGINE_DEFAULTS["idle_rpm"]))
maxr = int(e.get("max_rpm", ENGINE_DEFAULTS["max_rpm"]))
rise = int(e.get("rpm_rise_per_s", ENGINE_DEFAULTS["rpm_rise_per_s"]))
fall = int(e.get("rpm_fall_per_s", ENGINE_DEFAULTS["rpm_fall_per_s"]))
thr_curve = e.get("throttle_curve", ENGINE_DEFAULTS["throttle_curve"])
# --- 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"]))
warm_c = float(e.get("coolant_warm_rate_c_per_s", ENGINE_DEFAULTS["coolant_warm_rate_c_per_s"]))
cool_c = float(e.get("coolant_cool_rate_c_per_s", ENGINE_DEFAULTS["coolant_cool_rate_c_per_s"]))
warm_o = float(e.get("oil_warm_rate_c_per_s", ENGINE_DEFAULTS["oil_warm_rate_c_per_s"]))
cool_o = float(e.get("oil_cool_rate_c_per_s", ENGINE_DEFAULTS["oil_cool_rate_c_per_s"]))
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"]))
@@ -127,9 +127,6 @@ class EngineModule(Module):
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"]))
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"]))
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"]))
@@ -146,183 +143,145 @@ class EngineModule(Module):
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))
# Fahrerwunsch (kommt aus dem UI-Schieber)
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))
load = float(v.ensure("engine_load", 0.0))
ign = str(v.ensure("ignition", "OFF"))
ign = str(v.ensure("ignition", "OFF"))
elx_v = float(v.ensure("elx_voltage", 0.0))
cool = float(v.ensure("coolant_temp", ambient))
oil = float(v.ensure("oil_temp", ambient))
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))
ext_torque = float(v.ensure("engine_ext_torque_nm", 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", label="Drehzahl", unit="RPM", source="engine", priority=20)
v.register_metric("coolant_temp", label="Kühlmitteltemp", unit="°C", fmt=".1f", source="engine", priority=40)
v.register_metric("oil_temp", label="Öltemp", unit="°C", fmt=".1f", source="engine", priority=41)
v.register_metric("oil_pressure", label="Öldruck", unit="bar", fmt=".2f", source="engine", priority=42)
v.register_metric("engine_available_torque_nm", label="Verfügbares Motormoment", unit="Nm", fmt=".0f", source="engine", priority=43)
v.register_metric("engine_net_torque_nm", label="Netto Motormoment", unit="Nm", fmt=".0f", source="engine", priority=44)
v.register_metric("throttle_pedal_pct", label="Gaspedal", unit="%", fmt=".0f", source="engine", priority=45)
v.register_metric("throttle_plate_pct", label="Drosselklappe", unit="%", fmt=".0f", source="engine", priority=46)
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)
# Hilfsfunktionen
def visco(temp_c: float) -> float:
# -10°C -> 0.6, 20°C -> 0.8, 90°C -> 1.0 (linear segmentiert)
if temp_c <= -10: return 0.6
if temp_c >= 90: return 1.0
if temp_c <= 20:
# -10..20°C: 0.6 -> 0.8 (30 K Schritt → +0.2 => +0.006666.. pro K)
return 0.6 + (temp_c + 10.0) * (0.2 / 30.0)
# 20..90°C: 0.8 -> 1.0 (70 K Schritt → +0.2)
return 0.8 + (temp_c - 20.0) * (0.2 / 70.0)
# Spannungsfaktor: unter vmin kein Crank, bei 12.6V ~1.0
# --- 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 * visco(oil)
crank_rpm = starter_nom * vfac * self._visco(oil)
# sinnvolle effektive Startschwelle (unabhängig von stall)
start_rpm_min = 0.15 * idle # 15 % vom Idle
start_rpm_max = 0.45 * idle # 45 % vom Idle
# 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))
# --- Ziel-RPM bestimmen ---
if ign in ("OFF", "ACC"):
self._running = False
target_rpm = 0.0
elif ign == "START":
target_rpm = crank_rpm # wie gehabt
# Greifen, sobald Schwelle erreicht und Spannung reicht
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
# Catch-on-ON: wenn beim Umschalten noch genug Drehzahl anliegt
if not self._running and rpm >= max(0.15 * idle, start_rpm_th_eff * 0.9):
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(cold_gain_max, (90.0 - cool) * cold_gain_per_deg))
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
# --- verfügbare Motorleistung / Moment (ohne Last) ---
# --- 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)
# Drive-by-Wire / PI auf Drehmomentanteil -----------------------------------
# Fahrerwunsch in "Leistungsanteil" (0..1) transformieren (Kennlinie)
demand = self._curve(pedal/100.0, thr_curve) # 0..1
# Overrun-Logik: bei sehr geringem Wunsch → nahezu zu (aber nie ganz)
# --- 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
# Regler-Soll: gewünschter Torque-Anteil relativ zum maximal möglichen bei aktueller Drehzahl
# Wir approximieren: torque_avail = base_torque * airflow * temp_derate
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)) # ~airflow
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))
# PI: Integrator nur wenn Motor an
if ign == "ON" and self._running:
self._tc_i += err * torque_ki * dt
else:
self._tc_i *= 0.95 # langsam abbauen
self._tc_i *= 0.95
plate_cmd = self._plate_pct + (torque_kp * err + self._tc_i) * 100.0 # in %-Punkte
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
# Aktuator-Trägheit (1. Ordnung)
if plate_tau <= 1e-3:
self._plate_pct = plate_cmd
else:
a = min(1.0, dt / plate_tau)
self._plate_pct = (1.0 - a) * self._plate_pct + a * plate_cmd
# Update airflow nach Stellgröße
# 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, ext_torque))
net_torque = max(0.0, avail_torque - max(0.0, torque_load))
# --- Ziel-RPM aus Netto-Moment (sehr simple Dynamik) -----------------------
# Näherung: mehr Netto-Moment → RPM-Ziel steigt innerhalb der Bandbreite
# Wir skalieren zwischen (idle_eff) und maxr
# --- 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(cold_gain_max, (90.0 - cool) * cold_gain_per_deg))
cold_add = max(0.0, min(ENGINE_DEFAULTS["idle_cold_gain_max"],
(90.0 - cool) * cold_gain_per_deg))
idle_eff = idle + cold_add
torque_norm = 0.0 if base_torque <= 1e-6 else max(0.0, min(1.0, net_torque / (base_torque * temp_derate + 1e-6)))
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)
# --- RPM an Ziel annähern (mechanische Trägheit) --------------------------
if rpm < target_rpm:
rpm = min(target_rpm, rpm + rise * dt)
else:
rpm = max(target_rpm, rpm - fall * dt)
# Inertia
if rpm < target_rpm: rpm = min(target_rpm, rpm + rise * dt)
else: rpm = max(target_rpm, rpm - fall * dt)
# Stall: in ON, wenn laufend und RPM < stall ohne Starter → aus
# Stall
if ign == "ON" and self._running and rpm < stall_rpm:
self._running = False
# --- Temperaturen ----------------------------------------------------------
heat = (rpm/maxr)*0.8 + load*0.6
if (ign in ("ON","START")) and (self._running or target_rpm > 0):
cool += warm_c * heat * dt
oil += warm_o * heat * dt
else:
cool += (ambient - cool) * min(1.0, dt * cool_c)
oil += (ambient - oil) * min(1.0, dt * cool_o)
# --- Öldruck ---------------------------------------------------------------
if self._running and rpm > 0:
# --- Ö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:
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
# --- Realistischer RPM-Jitter ---------------------------------------------
# bandbegrenztes Rauschen: x[n] = (1 - b)*x[n-1] + b*eta, b ~ dt/tau
# --- RPM-Jitter ---
if self._running and rpm >= jitter_off_rpm and ign == "ON":
b = min(1.0, dt / max(1e-3, jitter_tau))
eta = random.uniform(-1.0, 1.0) # weißes Rauschen
self._rpm_noise = (1.0 - b) * self._rpm_noise + b * eta
# Amplitude linear zwischen idle_amp und hi_amp
# bezogen auf aktuelles Drehzahlniveau (klein aber sichtbar)
amp_idle = jitter_idle_amp
amp_hi = jitter_hi_amp
# Interpolation über 0..maxr
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)*amp_idle + k*amp_hi
amp = (1.0 - k)*jitter_idle_amp + k*jitter_hi_amp
rpm += self._rpm_noise * amp
else:
# Kein Jitter: Noise langsam abklingen
self._rpm_noise *= 0.9
# --- Klammern & Setzen -----------------------------------------------------
# --- Clamp & Set ---
rpm = max(0.0, min(rpm, maxr))
cool = max(-40.0, min(cool, 120.0))
oil = max(-40.0, min(oil, 150.0))
oil_p = max(oil_floor_off if not self._running else oil_floor_off, min(8.0, oil_p))
oil_p = max(oil_floor_off, min(8.0, oil_p))
v.set("rpm", int(rpm))
# WICHTIG: NICHT runden das macht das Dashboard per fmt
v.set("coolant_temp", float(cool))
v.set("oil_temp", float(oil))
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))
v.set("throttle_plate_pct", float(self._plate_pct))