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()