neraly complete Model of Driving done, but needs tweaking
This commit is contained in:
202
app/simulation/modules/cooling.py
Normal file
202
app/simulation/modules/cooling.py
Normal 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)))
|
Reference in New Issue
Block a user