167 lines
6.9 KiB
Python
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()
|