from __future__ import annotations import xml.etree.ElementTree as ET import pandas as pd from ..utils.common import natural_sort_key __all__ = ["create_outputs_routine"] def create_outputs_routine(routines: ET.Element, zones_df: pd.DataFrame, sto_df: pd.DataFrame) -> None: """Append OUTPUTS routine (R011_OUTPUTS) to the given `` element. Logic copied from original generate_l5x.py implementation with no behavioural changes; only external dependencies (natural_sort_key) imported from utilities. """ routine = ET.SubElement(routines, "Routine") routine.set("Use", "Target") routine.set("Name", "R011_OUTPUTS") routine.set("Type", "RLL") rll_content = ET.SubElement(routine, "RLLContent") rung_num = 0 # Check if zones_df is empty or missing NAME column if zones_df.empty: print("Warning: zones DataFrame is empty, skipping outputs routine") return if "NAME" not in zones_df.columns: print(f"Warning: zones DataFrame missing NAME column. Available columns: {list(zones_df.columns)}") return # Process each zone (sorted alphabetically) for _, zone in zones_df.sort_values("NAME").iterrows(): # Skip MCM zone (no conventional device range) if zone["NAME"] in ["MCM", "MCM01"]: continue if pd.isna(zone["START"]) or pd.isna(zone["END"]) or zone["START"] == "" or zone["END"] == "": continue zone_name = zone["NAME"].replace(" ", "_").replace("-", "_") zone_ok_tag = f"EStop_{zone_name}_OK" # Check if START and END follow numeric device pattern try: start_parts = zone["START"].split("_") end_parts = zone["END"].split("_") if len(start_parts) < 2 or len(end_parts) < 2: print(f"Warning: Skipping zone {zone_name} in outputs - invalid device name format") continue start_prefix = start_parts[0] start_num = int(start_parts[1]) end_prefix = end_parts[0] end_num = int(end_parts[1]) except (ValueError, IndexError): print(f"Warning: Skipping zone {zone_name} in outputs - unable to parse device numbers") continue # Get unique device outputs for this zone zone_outputs: list[str] = [] # Collect all STO devices from the dataframe for _, sto_row in sto_df.iterrows(): try: device_parts = sto_row["TAGNAME"].split("_") if len(device_parts) < 2: continue device_prefix = device_parts[0] # Extract device number, handling A/B suffixes device_num_str = device_parts[1] device_num = int(device_num_str[:-1]) if device_num_str[-1] in ["A", "B"] else int(device_num_str) except (ValueError, IndexError): continue # Check if this device is within the zone range in_zone = False if start_prefix == end_prefix: if device_prefix == start_prefix and start_num <= device_num <= end_num: in_zone = True else: if ( (device_prefix == start_prefix and device_num >= start_num) or (device_prefix == end_prefix and device_num <= end_num) or (device_prefix > start_prefix and device_prefix < end_prefix) ): in_zone = True if in_zone: if "STO" in sto_row["DESCA"]: zone_outputs.append(sto_row["IO_PATH"]) else: zone_outputs.append(f"{sto_row['TAGNAME']}:SO.STOOutput") # Create rung for zone if zone_outputs: # Zone has devices - create rung with interlock logic and device outputs rung = ET.SubElement(rll_content, "Rung") rung.set("Number", str(rung_num)) rung.set("Type", "N") text = ET.SubElement(rung, "Text") # Build XIC conditions - always include EStop_MCM01_OK first xic_conditions = "XIC(EStop_MCM01_OK)" # Add interlock if specified and different from MCM01 if pd.notna(zone["INTERLOCK"]) and zone["INTERLOCK"] != "": interlock_zone = zone["INTERLOCK"].replace(" ", "_").replace("-", "_") interlock_tag = f"EStop_{interlock_zone}_OK" # Only add interlock if it's different from the hardcoded MCM01 if interlock_tag != "EStop_MCM01_OK": xic_conditions = f"XIC({interlock_tag}){xic_conditions}" # Add the zone's own OK tag xic_conditions = f"{xic_conditions}XIC({zone_ok_tag})" # Remove duplicates and filter out non-STO outputs unique_outputs: list[str] = [] for output in zone_outputs: if (":SO.STOOutput" in output or ":SO.Out00Output" in output) and output not in unique_outputs: unique_outputs.append(output) if len(unique_outputs) == 1: output = unique_outputs[0] text.text = f"{xic_conditions}OTE({output});" elif len(unique_outputs) > 1: ote_list = [f"OTE({output})" for output in sorted(unique_outputs, key=natural_sort_key)] ote_chain = "[" + ",".join(ote_list) + "]" text.text = f"{xic_conditions}{ote_chain};" rung_num += 1 else: # Zone has no devices - just set the zone OK tag (no interlock conditions) rung = ET.SubElement(rll_content, "Rung") rung.set("Number", str(rung_num)) rung.set("Type", "N") text = ET.SubElement(rung, "Text") text.text = f"OTE({zone_ok_tag});" rung_num += 1