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

@@ -0,0 +1,202 @@
# =============================
# app/simulation/modules/cooling.py
# =============================
from __future__ import annotations
from app.simulation.simulator import Module, Vehicle
import math
COOLING_DEFAULTS = {
# Thermostat
"thermostat_open_c": 85.0,
"thermostat_full_c": 100.0,
# Radiator & Fahrtwind (W/K)
"rad_base_u_w_per_k": 150.0,
"ram_air_gain_per_kmh": 5.0,
# Lüfterstufe 1
"fan1_on_c": 96.0,
"fan1_off_c": 92.0,
"fan1_power_w": 120.0,
"fan1_airflow_gain": 250.0,
# Lüfterstufe 2
"fan2_on_c": 102.0,
"fan2_off_c": 98.0,
"fan2_power_w": 180.0,
"fan2_airflow_gain": 400.0,
# Wärmekapazitäten (J/K)
"coolant_thermal_cap_j_per_k": 90_000.0,
"oil_thermal_cap_j_per_k": 75_000.0,
# Öl↔Kühlmittel Kopplung / kleine Öl-Abstrahlung
"oil_coolant_u_w_per_k": 120.0,
"oil_to_amb_u_w_per_k": 10.0,
# Anteil der Motorwärme ans Kühlmittel
"engine_heat_frac_to_coolant": 0.7,
# Versorgung / Nachlauf
"fan_power_feed": "elx", # "elx" oder "battery"
"fan_afterrun_enable": False,
"fan_afterrun_threshold_c": 105.0,
"fan_afterrun_max_s": 300.0
}
class CoolingModule(Module):
PRIO = 25
NAME = "cooling"
def apply(self, v: Vehicle, dt: float) -> None:
# --- Config lesen
cfg = dict(COOLING_DEFAULTS);
cfg.update(v.config.get("cooling", {}))
# --- Dashboard-Metriken registrieren (einmal pro Tick ist ok, Idempotenz erwartet) ---
# Temps
v.register_metric("coolant_temp", unit="°C", fmt=".1f", label="Kühlmitteltemp.", source="cooling", priority=30)
v.register_metric("oil_temp", unit="°C", fmt=".1f", label="Öltemperatur", source="cooling", priority=31)
# Thermostat & Kühlerwirkung
v.register_metric("thermostat_open_pct", unit="%", fmt=".0f", label="Thermostat Öffnung", source="cooling", priority=32)
v.register_metric("cooling_u_eff_w_per_k", unit="W/K", fmt=".0f", label="Eff. Kühlerleistung", source="cooling", priority=33)
# Lüfterzustände + Last
v.register_metric("fan1_on", unit="", fmt="", label="Lüfter 1", source="cooling", priority=34)
v.register_metric("fan2_on", unit="", fmt="", label="Lüfter 2", source="cooling", priority=35)
v.register_metric("cooling_fan_power_w", unit="W", fmt=".0f", label="Lüfterleistung", source="cooling", priority=36)
v.register_metric("cooling_fan_current_a", unit="A", fmt=".2f", label="Lüfterstrom", source="cooling", priority=37)
# --- Konfigurationsparameter ---
t_open = float(cfg.get("thermostat_open_c", COOLING_DEFAULTS["thermostat_open_c"]))
t_full = float(cfg.get("thermostat_full_c", COOLING_DEFAULTS["thermostat_full_c"]))
rad_base = float(cfg.get("rad_base_u_w_per_k", COOLING_DEFAULTS["rad_base_u_w_per_k"]))
ram_gain = float(cfg.get("ram_air_gain_per_kmh", COOLING_DEFAULTS["ram_air_gain_per_kmh"]))
f1_on = float(cfg.get("fan1_on_c", COOLING_DEFAULTS["fan1_on_c"])); f1_off = float(cfg.get("fan1_off_c", COOLING_DEFAULTS["fan1_off_c"]))
f1_w = float(cfg.get("fan1_power_w", COOLING_DEFAULTS["fan1_power_w"])); f1_air = float(cfg.get("fan1_airflow_gain", COOLING_DEFAULTS["fan1_airflow_gain"]))
f2_on = float(cfg.get("fan2_on_c", COOLING_DEFAULTS["fan2_on_c"])); f2_off = float(cfg.get("fan2_off_c", COOLING_DEFAULTS["fan2_off_c"]))
f2_w = float(cfg.get("fan2_power_w", COOLING_DEFAULTS["fan2_power_w"])); f2_air = float(cfg.get("fan2_airflow_gain", COOLING_DEFAULTS["fan2_airflow_gain"]))
Cc = float(cfg.get("coolant_thermal_cap_j_per_k", COOLING_DEFAULTS["coolant_thermal_cap_j_per_k"]))
Coil = float(cfg.get("oil_thermal_cap_j_per_k", COOLING_DEFAULTS["oil_thermal_cap_j_per_k"]))
Uoc = float(cfg.get("oil_coolant_u_w_per_k", COOLING_DEFAULTS["oil_coolant_u_w_per_k"]))
Uoil_amb = float(cfg.get("oil_to_amb_u_w_per_k", COOLING_DEFAULTS["oil_to_amb_u_w_per_k"]))
frac_to_coolant = float(cfg.get("engine_heat_frac_to_coolant", COOLING_DEFAULTS["engine_heat_frac_to_coolant"]))
# Versorgung / Nachlauf
feed = str(cfg.get("fan_power_feed", COOLING_DEFAULTS["fan_power_feed"]))
allow_ar = bool(cfg.get("fan_afterrun_enable", COOLING_DEFAULTS["fan_afterrun_enable"]))
ar_thr = float(cfg.get("fan_afterrun_threshold_c", COOLING_DEFAULTS["fan_afterrun_threshold_c"]))
ar_max = float(cfg.get("fan_afterrun_max_s", COOLING_DEFAULTS["fan_afterrun_max_s"]))
ign = str(v.ensure("ignition", "OFF"))
# --- State / Inputs ---
amb = float(v.ensure("ambient_c", 20.0))
speed = float(v.ensure("speed_kmh", 0.0))
elx_v = float(v.get("elx_voltage", 0.0)) or 0.0
batt_v= float(v.get("battery_voltage", 12.5)) or 12.5
# Temperaturen liegen hier (Cooling ist Owner)
Tcool = float(v.ensure("coolant_temp", amb))
Toil = float(v.ensure("oil_temp", amb))
# vom Motor gepushte Wärmeleistung (W); nur positive Leistung wird aufgeteilt
q_in_total = v.acc_total("thermal.heat_w")
q_cool_in = max(0.0, q_in_total) * frac_to_coolant
q_oil_in = max(0.0, q_in_total) * (1.0 - frac_to_coolant)
# --- Thermostat-Öffnung (0..1) ---
if Tcool <= t_open: tfrac = 0.0
elif Tcool >= t_full: tfrac = 1.0
else: tfrac = (Tcool - t_open) / max(1e-6, (t_full - t_open))
# --- Lüfter-Hysterese ---
fan1_on_prev = bool(v.ensure("fan1_on", False))
fan2_on_prev = bool(v.ensure("fan2_on", False))
fan1_on = fan1_on_prev
fan2_on = fan2_on_prev
if tfrac > 0.0:
if not fan1_on and Tcool >= f1_on: fan1_on = True
if fan1_on and Tcool <= f1_off: fan1_on = False
if not fan2_on and Tcool >= f2_on: fan2_on = True
if fan2_on and Tcool <= f2_off: fan2_on = False
else:
fan1_on = False; fan2_on = False
# --- Nachlauf-Entscheidung ---
# Basis: Lüfter je nach Temp/Hysterese an/aus (fan1_on/fan2_on).
# Jetzt prüfen, ob die *Versorgung* verfügbar ist:
# - feed=="elx": nur wenn ign in ("ON","START") und elx_v > 1V
# - feed=="battery": immer, aber bei OFF nur wenn allow_afterrun & heiß
fans_request = (fan1_on or fan2_on)
fans_powered = False
bus_for_fans = "elx"
bus_v = elx_v
if feed == "elx":
if ign in ("ON","START") and elx_v > 1.0 and fans_request:
fans_powered = True
bus_for_fans = "elx"; bus_v = elx_v
else: # battery
if ign in ("ON","START"):
if fans_request:
fans_powered = True
bus_for_fans = "batt"; bus_v = batt_v
self._afterrun_timer_s = 0.0
else:
# OFF/ACC -> Nachlauf, wenn erlaubt und heiß
hot = (Tcool >= ar_thr)
if allow_ar and (hot or self._afterrun_timer_s > 0.0):
if self._afterrun_timer_s <= 0.0:
self._afterrun_timer_s = ar_max
if fans_request or hot:
fans_powered = True
bus_for_fans = "batt"; bus_v = batt_v
self._afterrun_timer_s = max(0.0, self._afterrun_timer_s - dt)
else:
self._afterrun_timer_s = 0.0
# --- Eff. Kühlerleistung (W/K) ---
U_rad = (rad_base + ram_gain * max(0.0, speed)) * tfrac
if fan1_on: U_rad += f1_air
if fan2_on: U_rad += f2_air
# --- Elektrische Last je nach Bus ---
fan_power_w = 0.0
if fans_powered and bus_v > 1.0:
if fan1_on: fan_power_w += f1_w
if fan2_on: fan_power_w += f2_w
if fan_power_w > 0.0:
i = fan_power_w / bus_v
if bus_for_fans == "elx":
v.push("elec.current_elx", +i, source="fan")
else:
v.push("elec.current_batt", +i, source="fan_afterrun" if ign in ("OFF","ACC") else "fan")
# --- Wärmeströme (positiv Richtung Medium) ---
q_rad = - max(0.0, U_rad * (Tcool - amb)) # Kühler zieht aus Kühlmittel
q_oil_x = - Uoc * (Toil - Tcool) # Öl↔Kühlmittel
q_oil_amb = - max(0.0, Uoil_amb * (Toil - amb)) # Öl an Umgebung
# --- Integration ---
dT_cool = (q_cool_in + q_rad - q_oil_x) * dt / max(1e-3, Cc)
dT_oil = (q_oil_in + q_oil_x + q_oil_amb) * dt / max(1e-3, Coil)
Tcool += dT_cool
Toil += dT_oil
# --- Setzen & Dashboard-Infos ---
v.set("coolant_temp", float(Tcool))
v.set("oil_temp", float(Toil))
# Anzeige-friendly zusätzlich in %
v.set("thermostat_open_pct", float(tfrac * 100.0))
v.set("cooling_u_eff_w_per_k", float(U_rad))
v.set("fan1_on", bool(fan1_on))
v.set("fan2_on", bool(fan2_on))
v.set("cooling_fan_power_w", float(fan_power_w))
v.set("cooling_fan_current_a", float(fan_power_w / max(1.0, bus_v)))