ics-simlab-config-gen-claude/tools/make_ir_from_config.py

167 lines
6.9 KiB
Python

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