126 lines
4.9 KiB
Python
126 lines
4.9 KiB
Python
import argparse
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
from models.ics_simlab_config import Config
|
|
from templates.tank import (
|
|
TankParams,
|
|
render_hil_stub,
|
|
render_hil_tank,
|
|
render_plc_stub,
|
|
render_plc_threshold,
|
|
)
|
|
|
|
|
|
def pick_by_keywords(ids: List[str], keywords: List[str]) -> Tuple[Optional[str], bool]:
|
|
low_ids = [(s, s.lower()) for s in ids]
|
|
for kw in keywords:
|
|
kwl = kw.lower()
|
|
for original, lowered in low_ids:
|
|
if kwl in lowered:
|
|
return original, True
|
|
return None, False
|
|
|
|
|
|
def tank_mapping_plc(inputs: List[str], outputs: List[str]) -> Tuple[Optional[str], Optional[str], Optional[str], bool]:
|
|
level, level_hit = pick_by_keywords(inputs, ["water_tank_level", "tank_level", "level"])
|
|
inlet, inlet_hit = pick_by_keywords(outputs, ["tank_input_valve", "input_valve", "inlet"])
|
|
remaining = [o for o in outputs if o != inlet]
|
|
outlet, outlet_hit = pick_by_keywords(remaining, ["tank_output_valve", "output_valve", "outlet"])
|
|
ok = bool(level and inlet and outlet and level_hit and inlet_hit and outlet_hit and inlet != outlet)
|
|
return level, inlet, outlet, ok
|
|
|
|
|
|
def tank_mapping_hil(inputs: List[str], outputs: List[str]) -> Tuple[Optional[str], Optional[str], Optional[str], bool]:
|
|
level_out, level_hit = pick_by_keywords(outputs, ["water_tank_level_output", "tank_level_output", "tank_level_value", "level"])
|
|
inlet_in, inlet_hit = pick_by_keywords(inputs, ["tank_input_valve_input", "input_valve_input", "inlet"])
|
|
remaining = [i for i in inputs if i != inlet_in]
|
|
outlet_in, outlet_hit = pick_by_keywords(remaining, ["tank_output_valve_input", "output_valve_input", "outlet"])
|
|
ok = bool(level_out and inlet_in and outlet_in and level_hit and inlet_hit and outlet_hit and inlet_in != outlet_in)
|
|
return level_out, inlet_in, outlet_in, ok
|
|
|
|
|
|
def write_text(path: Path, content: str, overwrite: bool) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
if path.exists() and not overwrite:
|
|
raise SystemExit(f"Refusing to overwrite existing file: {path} (use --overwrite)")
|
|
path.write_text(content, encoding="utf-8")
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser(description="Generate logic/*.py deterministically from configuration.json")
|
|
ap.add_argument("--config", required=True, help="Path to configuration.json")
|
|
ap.add_argument("--out-dir", required=True, help="Directory where .py files will be written")
|
|
ap.add_argument("--model", default="tank", choices=["tank"], help="Deterministic model template to use")
|
|
ap.add_argument("--overwrite", action="store_true", help="Overwrite existing files")
|
|
args = ap.parse_args()
|
|
|
|
cfg_text = Path(args.config).read_text(encoding="utf-8")
|
|
cfg = Config.model_validate_json(cfg_text)
|
|
out_dir = Path(args.out_dir)
|
|
|
|
# duplicate logic filename guard
|
|
seen: Dict[str, str] = {}
|
|
for plc in cfg.plcs:
|
|
lf = (plc.logic or "").strip()
|
|
if lf:
|
|
key = f"plc:{plc.label}"
|
|
if lf in seen:
|
|
raise SystemExit(f"Duplicate logic filename '{lf}' used by: {seen[lf]} and {key}")
|
|
seen[lf] = key
|
|
for hil in cfg.hils:
|
|
lf = (hil.logic or "").strip()
|
|
if lf:
|
|
key = f"hil:{hil.label}"
|
|
if lf in seen:
|
|
raise SystemExit(f"Duplicate logic filename '{lf}' used by: {seen[lf]} and {key}")
|
|
seen[lf] = key
|
|
|
|
# PLCs
|
|
for plc in cfg.plcs:
|
|
logic_name = (plc.logic or "").strip()
|
|
if not logic_name:
|
|
continue
|
|
|
|
inputs, outputs = plc.io_ids()
|
|
level, inlet, outlet, ok = tank_mapping_plc(inputs, outputs)
|
|
|
|
if args.model == "tank" and ok:
|
|
content = render_plc_threshold(plc.label, level, inlet, outlet, low=0.2, high=0.8)
|
|
else:
|
|
content = render_plc_stub(plc.label)
|
|
|
|
write_text(out_dir / logic_name, content, overwrite=bool(args.overwrite))
|
|
print(f"Wrote PLC logic: {out_dir / logic_name}")
|
|
|
|
# HILs
|
|
for hil in cfg.hils:
|
|
logic_name = (hil.logic or "").strip()
|
|
if not logic_name:
|
|
continue
|
|
|
|
inputs, outputs = hil.pv_io()
|
|
required_outputs = list(outputs)
|
|
|
|
level_out, inlet_in, outlet_in, ok = tank_mapping_hil(inputs, outputs)
|
|
|
|
if args.model == "tank" and ok:
|
|
content = render_hil_tank(
|
|
hil.label,
|
|
level_out_id=level_out,
|
|
inlet_cmd_in_id=inlet_in,
|
|
outlet_cmd_in_id=outlet_in,
|
|
required_output_ids=required_outputs,
|
|
params=TankParams(),
|
|
initial_level=None,
|
|
)
|
|
else:
|
|
content = render_hil_stub(hil.label, required_output_ids=required_outputs)
|
|
|
|
write_text(out_dir / logic_name, content, overwrite=bool(args.overwrite))
|
|
print(f"Wrote HIL logic: {out_dir / logic_name}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|