Source code for simulator.tools.tool_bsfc_vs_displacement

from __future__ import annotations
from pathlib import Path
from typing import Any, Dict, List
import csv
import json

import plotly.graph_objects as go

from ..core import EngineSimulator, EngineGeometry

ROOT = Path(__file__).resolve().parents[1]
IN_DIR = ROOT / "in"
OUT_DIR = ROOT / "out"


def _load_base_config(name: str = "sample_si_engine.json") -> Dict[str, Any]:
    path = IN_DIR / name
    with path.open("r", encoding="utf-8") as f:
        cfg = json.load(f)
    print(f"[BSFC Vd] Loaded base config: {path}")
    return cfg


[docs] def main() -> None: # Qualitative BSFC vs displacement curve, similar to Pulkrabek Fig. 2-14. base_cfg = _load_base_config() OUT_DIR.mkdir(parents=True, exist_ok=True) # Base geometry / cylinder-count for scaling base_sim = EngineSimulator.from_dict(base_cfg) base_geom = base_sim.geometry base_op = base_sim.operating base_n_cyl = max(int(getattr(base_op, "num_cylinders", 4)), 1) # (total displacement [L], representative speed [rpm]) cases = [ (5.0, 4200.0), (10.0, 3600.0), (15.0, 3200.0), (20.0, 2800.0), (25.0, 2400.0), ] rows: List[List[float]] = [] disp_L: List[float] = [] bsfc_list: List[float] = [] for Vd_L, N in cases: cfg = json.loads(json.dumps(base_cfg)) geom_cfg = cfg.setdefault("geometry", {}) op = cfg.setdefault("operating", {}) op["engine_speed_rpm"] = float(N) op.setdefault("friction_mode", "passenger") # Determine cylinder count for this case (default to base) n_cyl = int(op.get("num_cylinders", base_n_cyl)) or base_n_cyl # Target total and per-cylinder displacement [m³] Vd_target_total_m3 = float(Vd_L) * 1e-3 Vd_target_cyl = Vd_target_total_m3 / float(n_cyl) # Current geometry (fallback to base if fields missing) geom_current = EngineGeometry( bore_m=float(geom_cfg.get("bore_m", base_geom.bore_m)), stroke_m=float(geom_cfg.get("stroke_m", base_geom.stroke_m)), con_rod_m=float(geom_cfg.get("con_rod_m", base_geom.con_rod_m)), compression_ratio=float( geom_cfg.get("compression_ratio", base_geom.compression_ratio) ), piston_pin_offset_m=float( geom_cfg.get("piston_pin_offset_m", base_geom.piston_pin_offset_m) ), ) Vd_base_cyl = geom_current.displacement_volume() if Vd_base_cyl > 0.0 and Vd_target_cyl > 0.0: # Scale bore, stroke and rod length with a single linear factor scale = (Vd_target_cyl / Vd_base_cyl) ** (1.0 / 3.0) else: scale = 1.0 geom_cfg["bore_m"] = geom_current.bore_m * scale geom_cfg["stroke_m"] = geom_current.stroke_m * scale geom_cfg["con_rod_m"] = geom_current.con_rod_m * scale geom_cfg["compression_ratio"] = geom_current.compression_ratio geom_cfg["piston_pin_offset_m"] = geom_current.piston_pin_offset_m sim = EngineSimulator.from_dict(cfg) result = sim.run(cycles=1).to_dict() bsfc = result.get("bsfc_g_per_kWh") if bsfc is None: continue disp_L.append(float(Vd_L)) bsfc_list.append(float(bsfc)) rows.append([float(Vd_L), float(N), float(bsfc)]) csv_path = OUT_DIR / "bsfc_vs_displacement_table.csv" with csv_path.open("w", newline="", encoding="utf-8") as f: w = csv.writer(f) w.writerow(["displacement_L", "representative_speed_rpm", "bsfc_g_per_kWh"]) for r in rows: w.writerow(r) print(f"[BSFC Vd] Wrote CSV: {csv_path}") fig = go.Figure() fig.add_trace( go.Scatter( x=disp_L, y=bsfc_list, mode="lines+markers", name="BSFC vs displacement", ) ) fig.update_layout( title="Brake specific fuel consumption vs displacement (qualitative)", xaxis_title="Displacement Vd [L]", yaxis_title="BSFC [g/kWh]", ) html_path = OUT_DIR / "bsfc_vs_displacement_plot.html" fig.write_html(str(html_path)) print(f"[BSFC Vd] Wrote HTML plot: {html_path}")
if __name__ == "__main__": # pragma: no cover main()