import argparse import json from pathlib import Path from typing import List, Optional, Tuple from models.ics_simlab_config import Config from models.ir_v1 import ( IRHIL, IRPLC, IRSpec, TankLevelBlock, BottleLineBlock, HysteresisFillRule, ThresholdOutputRule, ) 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 bottle_fill_mapping_plc(inputs: List[str], outputs: List[str]) -> Tuple[Optional[str], Optional[str], bool]: fill_level, lvl_hit = pick_by_keywords(inputs, ["bottle_fill_level", "fill_level"]) fill_req, req_hit = pick_by_keywords(outputs, ["fill_request"]) ok = bool(fill_level and fill_req and lvl_hit and req_hit) return fill_level, fill_req, ok def tank_mapping_hil(pv_inputs: List[str], pv_outputs: List[str]) -> Tuple[Optional[str], Optional[str], Optional[str], bool]: level_out, level_hit = pick_by_keywords(pv_outputs, ["water_tank_level_output", "tank_level_output", "tank_level_value", "tank_level", "level"]) inlet_in, inlet_hit = pick_by_keywords(pv_inputs, ["tank_input_valve_input", "input_valve_input", "tank_input_valve", "inlet"]) remaining = [i for i in pv_inputs if i != inlet_in] outlet_in, outlet_hit = pick_by_keywords(remaining, ["tank_output_valve_input", "output_valve_input", "tank_output_valve", "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 bottle_line_mapping_hil(pv_inputs: List[str], pv_outputs: List[str]) -> Tuple[Optional[str], Optional[str], Optional[str], bool]: conveyor_cmd, c_hit = pick_by_keywords(pv_inputs, ["conveyor_belt_input", "conveyor_input", "conveyor"]) at_out, a_hit = pick_by_keywords(pv_outputs, ["bottle_at_filler_output", "bottle_at_filler", "at_filler"]) fill_out, f_hit = pick_by_keywords(pv_outputs, ["bottle_fill_level_output", "bottle_level", "fill_level"]) ok = bool(conveyor_cmd and at_out and fill_out and c_hit and a_hit and f_hit) return conveyor_cmd, at_out, fill_out, ok def write_json(path: Path, obj: dict, 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(json.dumps(obj, indent=2, ensure_ascii=False), encoding="utf-8") def main() -> None: ap = argparse.ArgumentParser(description="Create IR v1 from configuration.json (deterministic draft)") ap.add_argument("--config", required=True, help="Path to configuration.json") ap.add_argument("--out", required=True, help="Path to output IR json") ap.add_argument("--model", default="tank", choices=["tank"], help="Heuristic model to propose in IR") ap.add_argument("--overwrite", action="store_true", help="Overwrite existing file") args = ap.parse_args() cfg_text = Path(args.config).read_text(encoding="utf-8") cfg = Config.model_validate_json(cfg_text) ir = IRSpec() # PLCs for plc in cfg.plcs: plc_name = plc.label logic = (plc.logic or "").strip() if not logic: continue inputs, outputs = plc.io_ids() rules = [] if args.model == "tank": level, inlet, outlet, ok_tank = tank_mapping_plc(inputs, outputs) if ok_tank: enable_in = "fill_request" if "fill_request" in inputs else None rules.append( HysteresisFillRule( level_in=level, low=0.2, high=0.8, inlet_out=inlet, outlet_out=outlet, enable_input=enable_in, signal_max=1000.0, # Tank level range: 0-1000 ) ) fill_level, fill_req, ok_bottle = bottle_fill_mapping_plc(inputs, outputs) if ok_bottle: rules.append( ThresholdOutputRule( input_id=fill_level, threshold=0.2, op="lt", output_id=fill_req, true_value=1, false_value=0, signal_max=200.0, # Bottle fill range: 0-200 ) ) ir.plcs.append(IRPLC(name=plc_name, logic=logic, rules=rules)) # HILs for hil in cfg.hils: hil_name = hil.label logic = (hil.logic or "").strip() if not logic: continue pv_inputs, pv_outputs = hil.pv_io() outputs_init = {oid: 0.0 for oid in pv_outputs} blocks = [] if args.model == "tank": # Tank block level_out, inlet_in, outlet_in, ok_tank = tank_mapping_hil(pv_inputs, pv_outputs) if ok_tank: outputs_init[level_out] = 0.5 blocks.append( TankLevelBlock( level_out=level_out, inlet_cmd=inlet_in, outlet_cmd=outlet_in, initial_level=outputs_init[level_out], ) ) # Bottle line block conveyor_cmd, at_out, fill_out, ok_bottle = bottle_line_mapping_hil(pv_inputs, pv_outputs) if ok_bottle: outputs_init.setdefault(at_out, 0.0) outputs_init.setdefault(fill_out, 0.0) blocks.append( BottleLineBlock( conveyor_cmd=conveyor_cmd, bottle_at_filler_out=at_out, bottle_fill_level_out=fill_out, initial_fill=float(outputs_init.get(fill_out, 0.0)), ) ) ir.hils.append(IRHIL(name=hil_name, logic=logic, outputs_init=outputs_init, blocks=blocks)) write_json(Path(args.out), ir.model_dump(), overwrite=bool(args.overwrite)) print(f"Wrote IR: {args.out}") if __name__ == "__main__": main()