228 lines
9.6 KiB
Python
228 lines
9.6 KiB
Python
# =============================
|
|
# app/simulation/modules/gearbox.py
|
|
# =============================
|
|
from __future__ import annotations
|
|
from app.simulation.simulator import Module, Vehicle
|
|
import math
|
|
|
|
GEARBOX_DEFAULTS = {
|
|
# Übersetzungen
|
|
"primary_ratio": 1.84, # Kurbelwelle -> Getriebeeingang
|
|
# Gangübersetzungen (Index 0 = Neutral/N = 0.0)
|
|
"gear_ratios": [0.0, 2.60, 1.90, 1.55, 1.35, 1.20, 1.07],
|
|
# Ketten-/Endübersetzung via Zähne
|
|
"front_sprocket_teeth": 16,
|
|
"rear_sprocket_teeth": 45,
|
|
|
|
# Rad/Reifen
|
|
"wheel_radius_m": 0.31, # dynamischer Halbmesser
|
|
"drivetrain_efficiency": 0.93, # Wirkungsgrad Kurbel -> Rad
|
|
"rpm_couple_gain": 0.20, # wie stark Engine-RPM zum Rad synchronisiert wird (0..1)
|
|
|
|
# Fahrzeug / Widerstände
|
|
"rolling_c": 0.015, # Rollwiderstandskoeff.
|
|
"air_density": 1.2, # kg/m^3
|
|
"aero_cd": 0.6,
|
|
"frontal_area_m2": 0.6,
|
|
|
|
# Kupplung (auto)
|
|
"clutch_max_torque_nm": 220.0, # max übertragbares Drehmoment (bei c=1)
|
|
"clutch_aggressiveness": 0.6, # 0..1 (0 = sehr sanft, 1 = sehr bissig)
|
|
"clutch_curve": "linear", # "linear" | "progressive" | "soft"
|
|
"clutch_drag_nm": 1.0, # Restschleppmoment bei getrennt
|
|
"shift_time_s": 0.15, # Schaltzeit, während der entkuppelt wird
|
|
"sync_rpm_band": 200.0, # RPM-Band, in dem als „synchron“ gilt
|
|
|
|
# Reifenhaftung (einfaches Limit)
|
|
"tire_mu_peak": 1.10, # statischer Reibkoeffizient (Peak)
|
|
"tire_mu_slide": 0.85, # Gleitreibung
|
|
"rear_static_weight_frac": 0.60 # statischer Lastanteil auf Antriebsrad
|
|
}
|
|
|
|
class GearboxModule(Module):
|
|
PRIO = 30
|
|
NAME = "gearbox"
|
|
|
|
def __init__(self):
|
|
# interner Zustand
|
|
self._clutch = 0.0 # 0..1
|
|
self._shift_t = 0.0
|
|
self._target_gear = None
|
|
self._wheel_v = 0.0 # m/s
|
|
|
|
def apply(self, v: Vehicle, dt: float) -> None:
|
|
# --- Dashboard-Registrierungen ---
|
|
v.register_metric("speed_kmh", label="Geschwindigkeit", unit="km/h", fmt=".1f", source="gearbox", priority=30)
|
|
v.register_metric("gear", label="Gang", fmt="", source="gearbox", priority=25)
|
|
v.register_metric("clutch_pct", label="Kupplung", unit="%", fmt=".0f", source="gearbox", priority=26)
|
|
v.register_metric("wheel_slip_pct", label="Reifenschlupf", unit="%", fmt=".0f", source="gearbox", priority=27)
|
|
|
|
# --- Config / Inputs ---
|
|
gb = dict(GEARBOX_DEFAULTS)
|
|
gb.update(v.config.get("gearbox", {}))
|
|
|
|
primary = float(gb["primary_ratio"])
|
|
gear_ratios = list(gb["gear_ratios"])
|
|
z_f = int(gb["front_sprocket_teeth"])
|
|
z_r = int(gb["rear_sprocket_teeth"])
|
|
final = (z_r / max(1, z_f))
|
|
|
|
r_w = float(gb["wheel_radius_m"])
|
|
eta = float(gb["drivetrain_efficiency"])
|
|
couple_gain = float(gb["rpm_couple_gain"])
|
|
|
|
c_rr = float(gb["rolling_c"])
|
|
rho = float(gb["air_density"])
|
|
cd = float(gb["aero_cd"])
|
|
A = float(gb["frontal_area_m2"])
|
|
|
|
clutch_Tmax = float(gb["clutch_max_torque_nm"])
|
|
clutch_agr = min(1.0, max(0.0, float(gb["clutch_aggressiveness"])))
|
|
clutch_curve= str(gb["clutch_curve"]).lower()
|
|
clutch_drag = float(gb["clutch_drag_nm"])
|
|
shift_time = float(gb["shift_time_s"])
|
|
sync_band = float(gb["sync_rpm_band"])
|
|
|
|
mu_peak = float(gb["tire_mu_peak"])
|
|
mu_slide= float(gb["tire_mu_slide"])
|
|
rear_w = float(gb["rear_static_weight_frac"])
|
|
|
|
m = float(v.config.get("vehicle", {}).get("mass_kg", 210.0))
|
|
g = 9.81
|
|
|
|
# State
|
|
gear = int(v.ensure("gear", 0))
|
|
ign = str(v.ensure("ignition", "OFF"))
|
|
rpm = float(v.ensure("rpm", 1200.0))
|
|
pedal= float(v.ensure("throttle_pedal_pct", 0.0))
|
|
pedal = max(0.0, min(100.0, pedal))
|
|
|
|
# verfügbare Motordaten
|
|
eng_avail_T = float(v.get("engine_available_torque_nm", 0.0)) # „kann liefern“
|
|
# Hinweis: die Engine zieht später v.acc_total("engine.torque_load_nm") ab.
|
|
|
|
# Pending Shift Commands (vom UI gesetzt und dann zurücksetzen)
|
|
up_req = bool(v.ensure("gear_shift_up", False))
|
|
down_req = bool(v.ensure("gear_shift_down", False))
|
|
to_N_req = bool(v.ensure("gear_set_neutral", False))
|
|
if up_req: v.set("gear_shift_up", False)
|
|
if down_req: v.set("gear_shift_down", False)
|
|
if to_N_req: v.set("gear_set_neutral", False)
|
|
|
|
# --- Schaltlogik ---
|
|
if self._shift_t > 0.0:
|
|
self._shift_t -= dt
|
|
# währenddessen Kupplung öffnen
|
|
self._clutch = max(0.0, self._clutch - self._rate_from_agr(1.0, clutch_agr) * dt)
|
|
if self._shift_t <= 0.0 and self._target_gear is not None:
|
|
gear = int(self._target_gear)
|
|
v.set("gear", gear)
|
|
self._target_gear = None
|
|
else:
|
|
# neue Requests annehmen, wenn nicht bereits am Limit
|
|
if to_N_req:
|
|
self._target_gear = 0
|
|
self._shift_t = shift_time
|
|
elif up_req and gear < min(6, len(gear_ratios)-1):
|
|
self._target_gear = gear + 1
|
|
self._shift_t = shift_time
|
|
elif down_req and gear > 0:
|
|
self._target_gear = gear - 1
|
|
self._shift_t = shift_time
|
|
|
|
# --- Gesamtübersetzung und Soll-Drehzahlbezug ---
|
|
gear_ratio = float(gear_ratios[gear]) if 0 <= gear < len(gear_ratios) else 0.0
|
|
overall = primary * gear_ratio * final # Kurbel -> Rad
|
|
wheel_omega = self._wheel_v / max(1e-6, r_w) # rad/s
|
|
eng_omega_from_wheel = wheel_omega * overall
|
|
rpm_from_wheel = eng_omega_from_wheel * 60.0 / (2.0 * math.pi)
|
|
|
|
# --- Kupplungs-Automat ---
|
|
# Zielschließung aus Schlupf und Fahrerwunsch
|
|
slip_rpm = abs(rpm - rpm_from_wheel)
|
|
slip_norm = min(1.0, slip_rpm / max(1.0, sync_band))
|
|
base_target = max(0.0, min(1.0, (pedal/100.0)*0.6 + (1.0 - slip_norm)*0.6))
|
|
target_c = self._shape(base_target, clutch_curve)
|
|
|
|
# Bei N oder ohne Übersetzung kein Kraftschluss
|
|
if gear == 0 or overall <= 1e-6 or ign in ("OFF","ACC"):
|
|
target_c = 0.0
|
|
|
|
# Sanfte Anti-Abwürg-Logik: ist RPM sehr niedrig und Radlast hoch -> etwas öffnen
|
|
if rpm < 1500.0 and slip_rpm > 200.0:
|
|
target_c = min(target_c, 0.6)
|
|
|
|
# Dynamik der Kupplung (Annäherung Richtung target_c)
|
|
rate = self._rate_from_agr(target_c, clutch_agr) # s^-1
|
|
self._clutch += (target_c - self._clutch) * min(1.0, rate * dt)
|
|
self._clutch = max(0.0, min(1.0, self._clutch))
|
|
|
|
# --- Übertragbares Motormoment durch Kupplung ---
|
|
clutch_cap = clutch_Tmax * self._clutch
|
|
T_engine_to_input = max(0.0, min(eng_avail_T, clutch_cap))
|
|
|
|
# --- Rad-Seite: aus Motor via Übersetzung ---
|
|
T_wheel_from_engine = T_engine_to_input * overall * eta if overall > 0.0 else 0.0 # Nm am Rad
|
|
|
|
# --- Reibungs-/Luftwiderstand ---
|
|
v_ms = max(0.0, self._wheel_v)
|
|
F_roll = m * g * c_rr
|
|
F_aero = 0.5 * rho * cd * A * v_ms * v_ms
|
|
F_res = F_roll + F_aero
|
|
|
|
# --- Reifen-Force-Limit & Schlupf ---
|
|
N_rear = m * g * rear_w
|
|
F_trac_cap = mu_peak * N_rear
|
|
F_from_engine = T_wheel_from_engine / max(1e-6, r_w)
|
|
|
|
slip = 0.0
|
|
F_trac = F_from_engine
|
|
if abs(F_from_engine) > F_trac_cap:
|
|
slip = min(1.0, (abs(F_from_engine) - F_trac_cap) / max(1.0, F_from_engine))
|
|
# im Schlupf auf Slide-Niveau kappen
|
|
F_trac = math.copysign(mu_slide * N_rear, F_from_engine)
|
|
|
|
# --- Fahrzeugdynamik: a = (F_trac - F_res)/m ---
|
|
a = (F_trac - F_res) / max(1.0, m)
|
|
self._wheel_v = max(0.0, self._wheel_v + a * dt)
|
|
speed_kmh = self._wheel_v * 3.6
|
|
v.set("speed_kmh", float(speed_kmh))
|
|
v.set("gear", int(gear))
|
|
v.set("clutch_pct", float(self._clutch * 100.0))
|
|
v.set("wheel_slip_pct", float(max(0.0, min(1.0, slip)) * 100.0))
|
|
|
|
# --- Reaktionsmoment zurück an den Motor (Last) ---
|
|
# aus tatsächlich wirkender Traktionskraft (nach Grip-Limit)
|
|
T_engine_load = 0.0
|
|
if overall > 0.0 and self._clutch > 0.0:
|
|
T_engine_load = (abs(F_trac) * r_w) / max(1e-6, (overall * eta))
|
|
# kleiner Schlepp bei getrennt
|
|
if self._clutch < 0.1:
|
|
T_engine_load += clutch_drag * (1.0 - self._clutch)
|
|
|
|
if T_engine_load > 0.0:
|
|
v.push("engine.torque_load_nm", +T_engine_load, source="driveline")
|
|
|
|
# --- RPM-Kopplung (sanfte Synchronisierung) ---
|
|
if overall > 0.0 and self._clutch > 0.2 and ign in ("ON","START"):
|
|
alpha = min(1.0, couple_gain * self._clutch * dt / max(1e-3, 0.1))
|
|
rpm = (1.0 - alpha) * rpm + alpha * rpm_from_wheel
|
|
v.set("rpm", float(rpm))
|
|
|
|
# ----- Helpers -----
|
|
def _rate_from_agr(self, target_c: float, agr: float) -> float:
|
|
"""Engage/Release-Geschwindigkeit [1/s] in Abhängigkeit der Aggressivität."""
|
|
# 0.05s (bissig) bis 0.5s (sanft) für ~63%-Annäherung
|
|
tau = 0.5 - 0.45 * agr
|
|
if target_c < 0.1: # Öffnen etwas flotter
|
|
tau *= 0.7
|
|
return 1.0 / max(0.05, tau)
|
|
|
|
def _shape(self, x: float, curve: str) -> float:
|
|
x = max(0.0, min(1.0, x))
|
|
if curve == "progressive":
|
|
return x * x
|
|
if curve == "soft":
|
|
return math.sqrt(x)
|
|
return x # linear
|