@@ -1,268 +1,238 @@
# =============================
# 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 " : 400 0,
" rpm_fall_per_s " : 3000 ,
" throttle_curve " : " linear " ,
# Starter / Startlogik
" starter_rpm_nominal " : 250.0 ,
" starter_voltage_ min " : 1 0.5 ,
" start_rpm_threshold " : 2 10.0 ,
" stall _rpm" : 50 0.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 " : 700 0.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 ,
" idle_rpm " : 1200 ,
" idle_rpm " : 800 , # normaler Idle
" cold_idle _rpm" : 1050 , # Kaltlauf-Idle
" cold_idle_end_c " : 40. 0, # bis zu dieser Kühlmitteltemp gilt cold_idle
" idle_cold_mode " : " two_point " , # "two_point" | "slope"
" max_rpm " : 9000 ,
" rpm_rise_per_s " : 4000 ,
" rpm_fall_per_s " : 3000 ,
" throttle_curve " : " linear " ,
" starter_rpm_no minal " : 25 0.0 ,
" starter_voltage_min " : 10.5 ,
" start _rpm_threshold " : 21 0.0 ,
" stall_rpm " : 500.0 ,
" coolant_ambient_c " : 20.0 ,
" idle_cold_gain_per_deg " : 3 .0,
" idle_cold_gain_max " : 500 .0 ,
" oil_pressure_idle_bar " : 1.2 ,
" oil_pressure_slope_bar_per_krpm " : 0.8 ,
" oil_pressure_off_floor_bar " : 0.2 ,
" engine_power_kw " : 60.0 ,
" torque_peak_rpm " : 700 0.0 ,
" 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_idle_amp_rpm " : 12.0 ,
" rpm_jitter_high_amp_rpm " : 4.0 ,
" rpm_jitter_tau_s " : 0.2 0,
" rpm_jitter_off_threshold_rpm " : 250 .0,
" 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 . _oil_p_tau = 0.25
self . _plate_pct = 5.0
self . _tc_i = 0.0
# AR(1)-Noise
self . _rpm_noise = 0.0
# NEU: Gnadenfrist nach dem Anspringen
self . _post_start_s = 0.0
# ---- helpers -------------------------------------------------------- --
# -- helpers (unverändert) --
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 :
def _tmax_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 )
t_peak = ( 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 ) )
return max ( 0.0 , t_peak * 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_no m = 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_k rpm" , ENGINE_DEFAULTS [ " oil_pressure_slope_bar_per_k rpm" ] ) )
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 " ] ) )
# --- Config (wie gehabt) ---
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_rp m= 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 ) )
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 ) )
oil = float ( v . ensure ( " oil_temp " , ambient ) )
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
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 ) ) )
# 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 )
# Dashboard (wie gehabt)
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
# --- Startlogik + Post-Start-Grace ---
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_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 " ) :
if ign in ( " OFF " , " ACC " ) :
self . _running = False
target_rpm = 0.0
self . _post_start_s = 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 :
# wenn Schwelle erreicht: Motor gilt als angesprungen + Gnadenfrist
if not self . _running and crank _rpm > = start_rpm_th_eff and elx_v > starter_vmin :
self . _running = True
self . _post_start_s = 1.2
rpm = max ( rpm , crank_rpm ) # Starter dreht mit
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
self . _post_start_s = 1.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 )
if self . _running and self . _post_start_s > 0.0 :
self . _post_start_s = max ( 0 .0, self . _post_start_s - dt )
# --- Drehmomentmodell ---
tmax_rpm = self . _tmax_at_rpm ( power_kw , max ( 1.0 , rpm ) , peak_torque_rpm )
cold_add = max ( 0.0 , min ( ENGINE_DEFAULTS [ " idle_cold_gain_max " ] , ( 90.0 - cool ) * cold_gain_per_deg ) )
mode = str ( e . get ( " idle_cold_mode " , ENGINE_DEFAULTS [ " idle_cold_mode " ] ) ) . lower ( )
cold_idle = float ( e . get ( " cold_idle_rpm " , ENGINE_DEFAULTS [ " cold_idle_rpm " ] ) )
cold_end = float ( e . get ( " cold_idle_end_c " , ENGINE_DEFAULTS [ " cold_idle_end_c " ] ) )
if mode == " two_point " :
idle_eff = cold_idle if cool < cold_end else idle
else :
# Fallback: alte dynamische Rampe
cold_add = max ( 0.0 , min ( ENGINE_DEFAULTS [ " idle_cold_gain_max " ] ,
( 90.0 - cool ) * float ( e . get ( " idle_cold_gain_per_deg " ,
ENGINE_DEFAULTS [ " idle_cold_gain_per_deg " ] ) ) ) )
idle_eff = idle + cold_add
rpm_err = max ( 0.0 , idle_eff - rpm )
t_idle_cap = 0.35 * max ( 5.0 , self . _tmax_at_rpm ( power_kw , max ( 500.0 , idle_eff ) , peak_torque_rpm ) )
t_idle_req = t_idle_cap * min ( 1.0 , rpm_err / max ( 50.0 , 0.2 * idle_eff ) )
# --- 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
t_driver_req = demand * tmax_rpm
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 ) )
# WICHTIG: Momentfreigabe auch in START, wenn _running bereits True
running_mode = self . _running and ( ign in ( " ON " , " START " ) )
t_target = ( max ( t_idle_req , t_driver_req ) if running_mode else 0.0 )
if ign == " ON " and self . _running :
temp_derate = max ( 0.6 , 1.0 - max ( 0.0 , ( oil - 120.0 ) ) * 0.006 )
norm_target = 0.0 if tmax_rpm < = 1e-6 else max ( 0.0 , min ( 1.0 , t_target / ( tmax_rpm * temp_derate ) ) )
norm_avail = self . _plate_airflow_factor ( self . _plate_pct )
err = norm_target - norm_avail
if running_mode :
self . _tc_i + = err * torque_ki * dt
else :
self . _tc_i * = 0.95
self . _tc_i * = 0.9
plate_cmd = self . _plate_pct + ( torque_kp * err + self . _tc_i ) * 100.0
plate_cmd = max ( plate_target_min , min ( 100.0 , plate_cmd ) )
plate_min = plate_overrun if ( demand < 0.02 and rpm > idle_eff + 200.0 ) else plate_idle_min
plate_cmd = max ( plate_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
avail_torque = tmax_rpm * airflow * temp_derate
net_torque = max ( 0.0 , avail_torque - max ( 0.0 , torque_load ) )
# --- Wärmeleistung pushen (W) ---
# mechanische Leistung:
# Wärme (wie gehabt)
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 = 0.24 + 0.10 * demand
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_nor m * ( maxr - idle_eff )
# RPM-Dynamik
if running_mode :
torque_norm = 0.0 if tmax_rpm < = 1e-6 else max ( 0.0 , min ( 1.0 , net_torque / tmax_rpm ) )
free_target = idle_eff + torque_norm * ( maxr - idle_eff )
if rpm < free_target :
rpm = min ( free_target , rpm + rise * max ( 0.2 , torque_norm ) * dt )
else :
rpm = max ( free_target , rp m - fall * dt )
elif ign == " START " :
rpm = max ( rpm , crank_rpm )
else :
rpm * = 0.98
# 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 :
# Stall NUR wenn keine Gnadenfrist aktiv ist
if ign == " ON " and self . _running and self . _post_start_s < = 0.0 and rpm < stall_rpm :
self . _running = False
rpm = 0.0
# --- Öldruck ---
# Ö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 :
elif ign == " START " and 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 ---
# Jitter (wie gehabt)
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 )
@@ -273,12 +243,10 @@ class EngineModule(Module):
else :
self . _rpm_noise * = 0.9
# --- Clamp & Set ---
rpm = max ( 0.0 , min ( rpm , maxr ) )
# 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 ) )