202 lines
9.1 KiB
Python
202 lines
9.1 KiB
Python
# =============================
|
|
# 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))) |