# ============================= # 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