neraly complete Model of Driving done, but needs tweaking
This commit is contained in:
@@ -1,146 +1,185 @@
|
||||
# =============================
|
||||
# app/simulation/modules/basic.py
|
||||
# =============================
|
||||
|
||||
from __future__ import annotations
|
||||
from app.simulation.simulator import Module, Vehicle
|
||||
import bisect
|
||||
import bisect, math
|
||||
|
||||
def _ocv_from_soc(soc: float, table: dict[float, float]) -> float:
|
||||
# table: {SOC: OCV} unsortiert → linear interpolieren
|
||||
xs = sorted(table.keys())
|
||||
ys = [table[x] for x in xs]
|
||||
s = max(0.0, min(1.0, soc))
|
||||
s = 0.0 if soc is None else max(0.0, min(1.0, float(soc)))
|
||||
i = bisect.bisect_left(xs, s)
|
||||
if i <= 0: return ys[0]
|
||||
if i >= len(xs): return ys[-1]
|
||||
x0, x1 = xs[i-1], xs[i]
|
||||
y0, y1 = ys[i-1], ys[i]
|
||||
x0, x1 = xs[i-1], xs[i]; y0, y1 = ys[i-1], ys[i]
|
||||
t = 0.0 if x1 == x0 else (s - x0) / (x1 - x0)
|
||||
return y0 + t*(y1 - y0)
|
||||
return y0 + t * (y1 - y0)
|
||||
|
||||
class BasicModule(Module):
|
||||
PRIO = 10
|
||||
PRIO = 90
|
||||
NAME = "basic"
|
||||
"""
|
||||
- Zündungslogik inkl. START→ON nach crank_time_s
|
||||
- Ambient-Temperatur als globale Umweltgröße
|
||||
- Elektrik:
|
||||
* Load/Source-Aggregation via Vehicle-Helpers
|
||||
* Lichtmaschine drehzahlabhängig, Regler auf alternator_reg_v
|
||||
* Batterie: Kapazität (Ah), Innenwiderstand, OCV(SOC); I_batt > 0 => Entladung
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.crank_time_s = 2.7
|
||||
self._crank_timer = 0.0
|
||||
|
||||
def apply(self, v: Vehicle, dt: float) -> None:
|
||||
# ----- Dashboard registration (unverändert) -----
|
||||
v.register_metric("ignition", label="Zündung", source="basic", priority=5)
|
||||
v.register_metric("ambient_c", label="Umgebung", unit="°C", fmt=".1f", source="basic", priority=7)
|
||||
v.register_metric("battery_voltage", label="Batteriespannung", unit="V", fmt=".2f", source="basic", priority=8)
|
||||
v.register_metric("elx_voltage", label="ELX-Spannung", unit="V", fmt=".2f", source="basic", priority=10)
|
||||
v.register_metric("system_voltage", label="Systemspannung", unit="V", fmt=".2f", source="basic", priority=11)
|
||||
v.register_metric("battery_soc", label="Batterie SOC", unit="", fmt=".2f", source="basic", priority=12)
|
||||
v.register_metric("battery_current_a", label="Batterie Strom", unit="A", fmt=".2f", source="basic", priority=13)
|
||||
v.register_metric("alternator_current_a", label="Lima Strom", unit="A", fmt=".2f", source="basic", priority=14)
|
||||
v.register_metric("elec_load_total_a", label="Verbrauch ges.", unit="A", fmt=".2f", source="basic", priority=15)
|
||||
# Dashboard
|
||||
v.register_metric("ignition", label="Zündung", source="basic", priority=5)
|
||||
v.register_metric("ambient_c", label="Umgebung", unit="°C", fmt=".1f", source="basic", priority=7)
|
||||
v.register_metric("battery_voltage", label="Batteriespannung", unit="V", fmt=".2f", source="basic", priority=8)
|
||||
v.register_metric("elx_voltage", label="ELX-Spannung", unit="V", fmt=".2f", source="basic", priority=10)
|
||||
v.register_metric("system_voltage", label="Systemspannung", unit="V", fmt=".2f", source="basic", priority=11)
|
||||
v.register_metric("battery_soc", label="Batterie SOC", fmt=".3f", source="basic", priority=12)
|
||||
v.register_metric("battery_current_a", label="Batterie Strom", unit="A", fmt=".2f", source="basic", priority=13)
|
||||
v.register_metric("alternator_current_a", label="Lima Strom", unit="A", fmt=".2f", source="basic", priority=14)
|
||||
v.register_metric("elec_load_total_a", label="Verbrauch netto", unit="A", fmt=".2f", source="basic", priority=15)
|
||||
# neue Detailmetriken (optional in UI)
|
||||
v.register_metric("elec_load_elx_a", label="Verbrauch ELX", unit="A", fmt=".2f", source="basic", priority=16)
|
||||
v.register_metric("elec_load_batt_a", label="Verbrauch Batt", unit="A", fmt=".2f", source="basic", priority=17)
|
||||
|
||||
# ----- Read config/state -----
|
||||
econf = v.config.get("electrical", {})
|
||||
alt_reg_v = float(econf.get("alternator_reg_v", 14.2))
|
||||
alt_rated_a = float(econf.get("alternator_rated_a", 20.0))
|
||||
alt_cut_in = int(econf.get("alt_cut_in_rpm", 1500))
|
||||
alt_full = int(econf.get("alt_full_rpm", 4000))
|
||||
# Config
|
||||
econf = v.config.get("electrical", {})
|
||||
alt_reg_v = float(econf.get("alternator_reg_v", 14.2))
|
||||
alt_rated_a = float(econf.get("alternator_rated_a", 20.0))
|
||||
alt_cut_in = int(econf.get("alt_cut_in_rpm", 1500))
|
||||
alt_full = int(econf.get("alt_full_rpm", 4000))
|
||||
|
||||
batt_cap_ah = float(econf.get("battery_capacity_ah", 8.0))
|
||||
batt_rint = float(econf.get("battery_r_int_ohm", 0.020))
|
||||
batt_ocv_tbl= dict(econf.get("battery_ocv_v", {})) or {
|
||||
alt_eta_mech = float(econf.get("alternator_mech_efficiency", 0.55))
|
||||
alt_ratio = float(econf.get("alternator_pulley_ratio", 1.0))
|
||||
alt_drag_c0 = float(econf.get("alternator_drag_nm_idle", 0.15))
|
||||
alt_drag_c1 = float(econf.get("alternator_drag_nm_per_krpm", 0.05))
|
||||
|
||||
batt_cap_ah = float(econf.get("battery_capacity_ah", 8.0))
|
||||
batt_rint = float(econf.get("battery_r_int_ohm", 0.020))
|
||||
batt_ocv_tbl = dict(econf.get("battery_ocv_v", {})) or {
|
||||
0.0: 11.8, 0.1: 12.0, 0.2: 12.1, 0.3: 12.2, 0.4: 12.3,
|
||||
0.5: 12.45, 0.6: 12.55, 0.7: 12.65, 0.8: 12.75, 0.9: 12.85, 1.0: 12.95
|
||||
}
|
||||
|
||||
ign = v.ensure("ignition", "ON")
|
||||
rpm = float(v.ensure("rpm", 1200))
|
||||
soc = float(v.ensure("battery_soc", 0.80))
|
||||
# State
|
||||
prev_ign = str(v.ensure("prev_ignition", v.get("ignition", "ON")))
|
||||
ign = v.ensure("ignition", "ON")
|
||||
v.set("prev_ignition", ign)
|
||||
|
||||
rpm = float(v.ensure("rpm", 1200.0))
|
||||
soc = float(v.ensure("battery_soc", 0.80))
|
||||
v.set("ambient_c", float(v.ensure("ambient_c", v.get("ambient_c", 20.0))))
|
||||
|
||||
# ----- START auto-fall to ON -----
|
||||
# START → ON Auto-Übergang
|
||||
if ign == "START":
|
||||
if self._crank_timer <= 0.0:
|
||||
self._crank_timer = float(self.crank_time_s)
|
||||
else:
|
||||
self._crank_timer -= dt
|
||||
if self._crank_timer <= 0.0:
|
||||
v.set("ignition", "ON")
|
||||
ign = "ON"
|
||||
v.set("ignition", "ON"); ign = "ON"
|
||||
else:
|
||||
self._crank_timer = 0.0
|
||||
|
||||
# ----- Früh-Exit: OFF/ACC -> Bus AUS, Batterie „ruht“ -----
|
||||
# --- Akkumulierte Lasten aus beiden Bussen ---
|
||||
# Verbraucher pushen jetzt wahlweise:
|
||||
# - v.push("elec.current_elx", +A, source="...")
|
||||
# - v.push("elec.current_batt", +A, source="...")
|
||||
elx_load_a = max(0.0, v.acc_total("elec.current_elx"))
|
||||
batt_load_a = max(0.0, v.acc_total("elec.current_batt"))
|
||||
|
||||
# Grundlast hängt an ELX (nur bei ON/START aktiv)
|
||||
if ign in ("ON", "START"):
|
||||
v.push("elec.current_elx", +0.5, source="basic:base")
|
||||
|
||||
# --- OFF/ACC: ELX tot, Batterie lebt weiter ---
|
||||
if ign in ("OFF", "ACC"):
|
||||
# nur Batteriepfad zählt
|
||||
total_batt_a = max(0.0, v.acc_total("elec.current_batt"))
|
||||
ocv = _ocv_from_soc(soc, batt_ocv_tbl)
|
||||
# Batterie entspannt sich langsam gegen OCV (optional, super simpel):
|
||||
# (man kann hier auch gar nichts tun; ich halte batt_v = ocv für okay)
|
||||
batt_v = ocv
|
||||
v.set("battery_voltage", round(batt_v, 2))
|
||||
v.set("elx_voltage", 0.0)
|
||||
v.set("system_voltage", 0.0)
|
||||
v.set("battery_current_a", 0.0)
|
||||
|
||||
# Batterie entlädt nach I*dt
|
||||
batt_i = total_batt_a
|
||||
soc = max(0.0, min(1.0, soc - (batt_i * dt) / (3600.0 * max(0.1, batt_cap_ah))))
|
||||
batt_v = ocv - batt_i * batt_rint
|
||||
batt_v = max(10.0, min(15.5, batt_v))
|
||||
|
||||
v.set("battery_voltage", batt_v)
|
||||
v.set("elx_voltage", 0.0) # Bus aus
|
||||
v.set("system_voltage", batt_v) # für „alles was noch lebt“ = Batterie
|
||||
v.set("battery_soc", soc)
|
||||
v.set("battery_current_a", batt_i)
|
||||
v.set("alternator_current_a", 0.0)
|
||||
v.set("elec_load_total_a", 0.0)
|
||||
v.set("battery_soc", round(soc, 3))
|
||||
v.set("elec_load_elx_a", 0.0)
|
||||
v.set("elec_load_batt_a", total_batt_a)
|
||||
v.set("elec_load_total_a", total_batt_a)
|
||||
|
||||
# keine Limamechanik aktiv
|
||||
v.set("engine_ext_torque_nm", 0.0)
|
||||
return
|
||||
|
||||
# ----- ON/START: Elektrik-Bilanz -----
|
||||
# Beiträge anderer Module summieren
|
||||
loads_a, sources_a = v.elec_totals()
|
||||
# Grundlasten (z.B. ECU, Relais)
|
||||
base_load = 0.5 if ign == "ON" else 0.6 # START leicht höher
|
||||
loads_a += base_load
|
||||
# Quellen anderer Module (z.B. DC-DC) können sources_a > 0 machen
|
||||
# Wir ziehen Quellen von der Last ab – was übrig bleibt, muss Lima/Batterie liefern
|
||||
net_load_a = max(0.0, loads_a - sources_a)
|
||||
# --- Ab hier: Zündung ON/START (ELX aktiv) ---
|
||||
elx_load_a = max(0.0, v.acc_total("elec.current_elx"))
|
||||
batt_load_a = max(0.0, v.acc_total("elec.current_batt"))
|
||||
net_load_a = elx_load_a + batt_load_a # Gesamtverbrauch
|
||||
|
||||
# Lima-Fähigkeit aus rpm
|
||||
if rpm >= alt_cut_in:
|
||||
frac = 0.0 if rpm <= alt_cut_in else (rpm - alt_cut_in) / max(1, (alt_full - alt_cut_in))
|
||||
frac = max(0.0, min(1.0, frac))
|
||||
alt_cap_a = alt_rated_a * frac
|
||||
else:
|
||||
# 3) Lima-Kapazität
|
||||
if rpm < alt_cut_in:
|
||||
alt_cap_a = 0.0
|
||||
|
||||
# Batterie-OCV
|
||||
ocv = _ocv_from_soc(soc, batt_ocv_tbl)
|
||||
|
||||
# Ziel: Regler hält alt_reg_v – aber nur, wenn die Lima überhaupt aktiv ist
|
||||
desired_charge_a = max(0.0, (alt_reg_v - ocv) / max(1e-4, batt_rint)) if alt_cap_a > 0.0 else 0.0
|
||||
alt_needed_a = net_load_a + desired_charge_a
|
||||
alt_i = min(alt_needed_a, alt_cap_a)
|
||||
|
||||
# Batterie-Bilanz
|
||||
if alt_cap_a > 0.0 and alt_i >= net_load_a:
|
||||
# Lima deckt alles; Überschuss lädt Batterie
|
||||
batt_i = -(alt_i - net_load_a) # negativ = lädt
|
||||
bus_v = alt_reg_v
|
||||
elif rpm >= alt_full:
|
||||
alt_cap_a = alt_rated_a
|
||||
else:
|
||||
# Lima (falls vorhanden) reicht nicht -> Batterie liefert Defizit
|
||||
deficit = net_load_a - alt_i
|
||||
batt_i = max(0.0, deficit) # positiv = entlädt
|
||||
bus_v = ocv - batt_i * batt_rint
|
||||
frac = (rpm - alt_cut_in) / max(1, (alt_full - alt_cut_in))
|
||||
alt_cap_a = alt_rated_a * max(0.0, min(1.0, frac))
|
||||
|
||||
# SOC-Update (Ah-Bilanz)
|
||||
ocv = _ocv_from_soc(soc, batt_ocv_tbl)
|
||||
desired_charge_a = ((alt_reg_v - ocv) / batt_rint) if alt_cap_a > 0.0 else 0.0
|
||||
if desired_charge_a < 0.0: desired_charge_a = 0.0
|
||||
|
||||
alt_needed_a = net_load_a + desired_charge_a
|
||||
alt_i = min(alt_needed_a, alt_cap_a) if alt_cap_a > 0.0 else 0.0
|
||||
|
||||
# Lima liefert in ELX-Bus (Quelle = negativ)
|
||||
if alt_i > 0.0:
|
||||
v.push("elec.current_elx", -alt_i, source="alternator")
|
||||
|
||||
# Rest geht von Batterie (angenommen gleicher Bus)
|
||||
remaining = net_load_a - alt_i
|
||||
if alt_cap_a > 0.0 and remaining <= 0.0:
|
||||
# Überschuss -> lädt Batt (wir zählen Lade-Strom negativ am Batterieklemmen)
|
||||
batt_i = remaining # ≤ 0
|
||||
bus_v = alt_reg_v
|
||||
else:
|
||||
batt_i = max(0.0, remaining)
|
||||
bus_v = ocv - batt_i * batt_rint
|
||||
|
||||
# SOC integrieren
|
||||
soc = max(0.0, min(1.0, soc - (batt_i * dt) / (3600.0 * max(0.1, batt_cap_ah))))
|
||||
batt_v = ocv - (batt_i * batt_rint)
|
||||
batt_v = ocv - batt_i * batt_rint
|
||||
|
||||
# Klammern/Spiegeln
|
||||
# Clamps
|
||||
batt_v = max(10.0, min(15.5, batt_v))
|
||||
bus_v = max(0.0, min(15.5, bus_v))
|
||||
v.set("battery_voltage", round(batt_v, 2))
|
||||
v.set("elx_voltage", round(bus_v, 2))
|
||||
v.set("system_voltage", round(bus_v, 2))
|
||||
v.set("battery_soc", round(soc, 3))
|
||||
v.set("battery_current_a", round(batt_i, 2))
|
||||
v.set("alternator_current_a", round(min(alt_i, alt_cap_a), 2))
|
||||
v.set("elec_load_total_a", round(net_load_a, 2))
|
||||
bus_v = max(0.0, min(15.5, bus_v))
|
||||
|
||||
# Mechanische Last Lima
|
||||
tau_base = 0.0
|
||||
if rpm > 0.0:
|
||||
tau_base = alt_drag_c0 + (rpm / 1000.0) * alt_drag_c1
|
||||
omega_engine = 2.0 * math.pi * max(0.0, rpm) / 60.0
|
||||
omega_alt = omega_engine * max(0.1, alt_ratio)
|
||||
tau_el = 0.0
|
||||
if alt_i > 0.0 and omega_alt > 1e-2 and alt_eta_mech > 0.05:
|
||||
p_el = alt_i * bus_v
|
||||
p_mech = p_el / alt_eta_mech
|
||||
tau_el = p_mech / omega_alt
|
||||
tau_alt = max(0.0, tau_base) + max(0.0, tau_el)
|
||||
if tau_alt > 0.0:
|
||||
v.push("engine.torque_load_nm", +tau_alt, source="alternator")
|
||||
|
||||
# Outputs
|
||||
v.set("battery_voltage", batt_v)
|
||||
v.set("elx_voltage", bus_v)
|
||||
v.set("system_voltage", bus_v)
|
||||
v.set("battery_soc", soc)
|
||||
v.set("battery_current_a", batt_i)
|
||||
v.set("alternator_current_a", min(alt_i, alt_cap_a))
|
||||
v.set("elec_load_elx_a", elx_load_a)
|
||||
v.set("elec_load_batt_a", batt_load_a)
|
||||
v.set("elec_load_total_a", net_load_a)
|
||||
|
Reference in New Issue
Block a user