ParamManagerScripts/backend/script_groups/TwinCat/simple_lad_converter.py

835 lines
32 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Convertidor simplificado de LAD TwinCAT a pseudocódigo estructurado
Versión mejorada con SymPy para optimización de expresiones lógicas
"""
import re
import sympy
from sympy import symbols, And, Or, Not, simplify
from sympy.logic.boolalg import to_dnf
class SymbolManager:
"""Gestor de símbolos para SymPy"""
def __init__(self):
self._symbol_cache = {}
def get_symbol(self, name):
"""Obtener o crear símbolo SymPy"""
if name not in self._symbol_cache:
# Limpiar nombre para SymPy (sin espacios, caracteres especiales)
clean_name = re.sub(r'[^a-zA-Z0-9_]', '_', name)
self._symbol_cache[name] = symbols(clean_name)
return self._symbol_cache[name]
class SimpleLadConverter:
"""Convertidor simplificado de LAD a código estructurado"""
def __init__(self):
self.networks = []
self.current_network_id = 0
self.symbol_manager = SymbolManager()
self.sympy_expressions = {} # Mapeo de network_id -> sympy expression
# Nuevas propiedades para estructura SCL completa
self.program_name = ""
self.program_path = ""
self.var_sections = {} # VAR, VAR_INPUT, VAR_OUTPUT, etc.
self.actions = {} # Diccionario de ACTIONs
def parse_file(self, file_path):
"""Parse el archivo LAD completo incluyendo variables y ACTIONs"""
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
# Extraer información del header
self._parse_header_info(content)
# Extraer declaraciones de variables
self._parse_variable_declarations(content)
# Encontrar sección LAD
lad_start = content.find('_LD_BODY')
if lad_start != -1:
# Extraer contenido LAD hasta ACTION o END_PROGRAM
action_start = content.find('\nACTION', lad_start)
end_program = content.find('\nEND_PROGRAM', lad_start)
lad_end = action_start if action_start != -1 else end_program
if lad_end == -1:
lad_end = len(content)
lad_content = content[lad_start:lad_end]
lines = lad_content.split('\n')
self._parse_networks(lines)
else:
print("No se encontró _LD_BODY")
# Extraer ACTIONs
self._parse_actions(content)
def _parse_header_info(self, content):
"""Extraer información del header del programa"""
# Buscar PATH y nombre del programa
path_match = re.search(r'\(\* @PATH := \'([^\']+)\' \*\)', content)
if path_match:
self.program_path = path_match.group(1)
# Buscar nombre del programa/function_block
program_match = re.search(r'(PROGRAM|FUNCTION_BLOCK)\s+(\w+)', content)
if program_match:
self.program_name = program_match.group(2)
print(f"Programa encontrado: {self.program_name}")
if self.program_path:
print(f"Path: {self.program_path}")
def _parse_variable_declarations(self, content):
"""Extraer todas las declaraciones de variables"""
# Buscar todas las secciones VAR
var_patterns = [
(r'VAR_INPUT(.*?)END_VAR', 'VAR_INPUT'),
(r'VAR_OUTPUT(.*?)END_VAR', 'VAR_OUTPUT'),
(r'VAR_IN_OUT(.*?)END_VAR', 'VAR_IN_OUT'),
(r'VAR\s+(.*?)END_VAR', 'VAR'),
]
for pattern, var_type in var_patterns:
matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
if matches:
variables = []
for match in matches:
# Parsear cada línea de variable
for line in match.split('\n'):
line = line.strip()
if line and not line.startswith('(*') and ':' in line:
# Limpiar comentarios inline
if '(*' in line:
line = line[:line.find('(*')]
variables.append(line.strip())
if variables:
self.var_sections[var_type] = variables
print(f"Variables {var_type}: {len(variables)} encontradas")
def _parse_networks(self, lines):
"""Parse todas las redes"""
i = 0
while i < len(lines):
if lines[i].strip() == '_NETWORK':
self.current_network_id += 1
i = self._parse_network(lines, i)
else:
i += 1
def _parse_network(self, lines, start_idx):
"""Parse una red individual con soporte mejorado para operadores LAD"""
network = {
'id': self.current_network_id,
'comment': '',
'logic': None,
'target': '',
'function_blocks': []
}
i = start_idx + 1
# Parse comentario
if i < len(lines) and lines[i].strip() == '_COMMENT':
i, comment = self._parse_comment(lines, i)
network['comment'] = comment
# Parse contenido de la red
while i < len(lines):
line = lines[i].strip()
if line == '_NETWORK':
break
elif line == '_LD_ASSIGN':
i += 1
# Parsear la lógica LAD después de _LD_ASSIGN
i, logic = self._parse_lad_expression(lines, i)
network['logic'] = logic
elif line.startswith('_OUTPUT'):
# Buscar variable de salida
i += 1
while i < len(lines) and lines[i].strip().startswith('_'):
i += 1
if i < len(lines) and lines[i].strip() and 'ENABLELIST' not in lines[i]:
network['target'] = lines[i].strip()
i += 1
else:
i += 1
self.networks.append(network)
return i
def _parse_lad_expression(self, lines, start_idx):
"""Parse una expresión LAD recursivamente"""
i = start_idx
while i < len(lines):
line = lines[i].strip()
if line == '_LD_AND':
return self._parse_and_expression(lines, i + 1)
elif line == '_LD_OR':
return self._parse_or_expression(lines, i + 1)
elif line == '_LD_CONTACT':
return self._parse_contact(lines, i + 1)
elif line.startswith('_FUNCTIONBLOCK'):
return self._parse_function_block(lines, i)
elif line.startswith('_OUTPUT') or line == 'ENABLELIST : 0':
break
else:
i += 1
return i, None
def _parse_and_expression(self, lines, start_idx):
"""Parse una expresión AND"""
i = start_idx
operands = []
# Buscar operadores
if i < len(lines) and lines[i].strip().startswith('_LD_OPERATOR'):
# Extraer número de operandos
operator_line = lines[i].strip()
num_operands = int(operator_line.split(':')[-1].strip()) if ':' in operator_line else 2
i += 1
# Parse cada operando
for _ in range(num_operands):
i, operand = self._parse_lad_expression(lines, i)
if operand:
operands.append(operand)
return i, {'type': 'AND', 'operands': operands}
def _parse_or_expression(self, lines, start_idx):
"""Parse una expresión OR"""
i = start_idx
operands = []
# Buscar operadores
if i < len(lines) and lines[i].strip().startswith('_LD_OPERATOR'):
# Extraer número de operandos
operator_line = lines[i].strip()
num_operands = int(operator_line.split(':')[-1].strip()) if ':' in operator_line else 2
i += 1
# Parse cada operando
for _ in range(num_operands):
i, operand = self._parse_lad_expression(lines, i)
if operand:
operands.append(operand)
return i, {'type': 'OR', 'operands': operands}
def _parse_contact(self, lines, start_idx):
"""Parse un contacto LAD"""
i = start_idx
contact_name = ""
negated = False
# Obtener nombre del contacto
if i < len(lines):
contact_name = lines[i].strip()
i += 1
# Verificar si hay expresión
if i < len(lines) and lines[i].strip() == '_EXPRESSION':
i += 1
# Verificar si está negado
if i < len(lines):
if lines[i].strip() == '_NEGATIV':
negated = True
i += 1
elif lines[i].strip() == '_POSITIV':
i += 1
return i, {'type': 'CONTACT', 'name': contact_name, 'negated': negated}
def _parse_function_block(self, lines, start_idx):
"""Parse un bloque de función"""
i = start_idx + 1
fb_name = ""
inputs = []
if i < len(lines):
fb_name = lines[i].strip()
i += 1
# Parse inputs del function block
while i < len(lines) and not lines[i].strip().startswith('_OUTPUT'):
line = lines[i].strip()
if line.startswith('_OPERAND'):
i += 2 # Saltar _EXPRESSION
if i < len(lines):
inputs.append(lines[i].strip())
i += 1
else:
i += 1
return i, {'type': 'FUNCTION_BLOCK', 'name': fb_name, 'inputs': inputs}
def _parse_comment(self, lines, start_idx):
"""Parse comentario"""
i = start_idx + 1
comment_lines = []
while i < len(lines):
line = lines[i].strip()
if line == '_END_COMMENT':
break
if line and not line.startswith('_'):
comment_lines.append(line)
i += 1
return i + 1, ' '.join(comment_lines)
def convert_to_structured(self):
"""Convertir a código SCL completo con variables y ACTIONs"""
output = []
# Header del archivo
output.append("(* Código SCL generado desde LAD TwinCAT *)")
output.append("(* Convertidor mejorado con SymPy - Estructura DNF preferida *)")
if self.program_path:
output.append(f"(* Path original: {self.program_path} *)")
output.append("")
# Declaración del programa
program_name = self.program_name if self.program_name else "ConvertedProgram"
output.append(f"PROGRAM {program_name}")
# Declaraciones de variables
self._add_variable_sections(output)
# Inicio del código principal
output.append("")
output.append("(* === CÓDIGO PRINCIPAL === *)")
output.append("")
# Lógica de las redes LAD
for network in self.networks:
output.append(f" // Red {network['id']}")
if network['comment']:
output.append(f" // {network['comment']}")
if network['logic'] and network['target']:
# Usar expresión DNF optimizada si está disponible
if network['id'] in self.sympy_expressions:
sympy_expr = self.sympy_expressions[network['id']]
condition_str = self._format_dnf_for_lad(sympy_expr)
output.append(f" // Optimizada con SymPy DNF")
else:
# Fallback al método original
condition_str = self._convert_logic_to_string(network['logic'])
output.append(f" // Sin optimización SymPy")
if condition_str:
# Si hay saltos de línea en la condición (múltiples términos OR)
if '\n' in condition_str:
output.append(f" IF {condition_str} THEN")
else:
output.append(f" IF {condition_str} THEN")
output.append(f" {network['target']} := TRUE;")
output.append(" ELSE")
output.append(f" {network['target']} := FALSE;")
output.append(" END_IF;")
else:
output.append(f" {network['target']} := TRUE; // Logic no reconocida")
output.append("")
# Fin del programa principal
output.append("END_PROGRAM")
# Agregar ACTIONs como subfunciones
self._add_actions_as_procedures(output, program_name)
return '\n'.join(output)
def _add_variable_sections(self, output):
"""Agregar todas las secciones de variables al código"""
# Orden preferido de las secciones
section_order = ['VAR_INPUT', 'VAR_OUTPUT', 'VAR_IN_OUT', 'VAR']
for section_type in section_order:
if section_type in self.var_sections:
output.append(f"{section_type}")
for var_line in self.var_sections[section_type]:
# Asegurar que termine con punto y coma
if not var_line.endswith(';'):
var_line += ' ;'
output.append(f" {var_line}")
output.append("END_VAR")
output.append("")
def _add_actions_as_procedures(self, output, program_name):
"""Agregar ACTIONs como procedimientos independientes"""
if not self.actions:
return
output.append("")
output.append("(* === SUBFUNCIONES (ACTIONs convertidas) === *)")
output.append("")
for action_name, action_code in self.actions.items():
# Nombre completo como se usa en TwinCAT
full_name = f"{program_name}_{action_name}"
output.append(f"PROCEDURE {full_name}")
output.append("(* Convertida desde ACTION *)")
output.append("")
# Agregar el código de la ACTION con indentación
for line in action_code.split('\n'):
if line.strip():
output.append(f" {line.strip()}")
else:
output.append("")
output.append("")
output.append("END_PROCEDURE")
output.append("")
def _convert_logic_to_string(self, logic):
"""Convertir lógica LAD a string estructurado"""
if not logic:
return ""
if logic['type'] == 'CONTACT':
if logic['negated']:
return f"NOT {logic['name']}"
else:
return logic['name']
elif logic['type'] == 'AND':
operand_strings = []
for operand in logic['operands']:
operand_str = self._convert_logic_to_string(operand)
if operand_str:
operand_strings.append(operand_str)
if len(operand_strings) > 1:
return "(" + " AND ".join(operand_strings) + ")"
elif len(operand_strings) == 1:
return operand_strings[0]
else:
return ""
elif logic['type'] == 'OR':
operand_strings = []
for operand in logic['operands']:
operand_str = self._convert_logic_to_string(operand)
if operand_str:
operand_strings.append(operand_str)
if len(operand_strings) > 1:
return "(" + " OR ".join(operand_strings) + ")"
elif len(operand_strings) == 1:
return operand_strings[0]
else:
return ""
elif logic['type'] == 'FUNCTION_BLOCK':
inputs_str = ", ".join(logic['inputs']) if logic['inputs'] else ""
return f"{logic['name']}({inputs_str})"
return ""
def save_to_file(self, output_path):
"""Guardar código estructurado"""
structured_code = self.convert_to_structured()
with open(output_path, 'w', encoding='utf-8') as f:
f.write(structured_code)
print(f"Código guardado en: {output_path}")
return structured_code
def print_debug_info(self):
"""Mostrar información de debug sobre los networks parseados"""
print(f"\n=== DEBUG INFO - {len(self.networks)} networks encontrados ===")
for network in self.networks:
print(f"\nRed {network['id']}:")
if network['comment']:
print(f" Comentario: {network['comment']}")
print(f" Target: {network['target']}")
if network['logic']:
print(f" Lógica: {self._debug_logic_string(network['logic'])}")
condition_str = self._convert_logic_to_string(network['logic'])
print(f" Condición: {condition_str}")
else:
print(" Sin lógica")
def _debug_logic_string(self, logic, indent=0):
"""Crear string de debug para la lógica"""
if not logic:
return "None"
prefix = " " * indent
if logic['type'] == 'CONTACT':
neg_str = " (NEGADO)" if logic['negated'] else ""
return f"{prefix}CONTACT: {logic['name']}{neg_str}"
elif logic['type'] == 'AND':
result = f"{prefix}AND:\n"
for operand in logic['operands']:
result += self._debug_logic_string(operand, indent + 1) + "\n"
return result.rstrip()
elif logic['type'] == 'OR':
result = f"{prefix}OR:\n"
for operand in logic['operands']:
result += self._debug_logic_string(operand, indent + 1) + "\n"
return result.rstrip()
elif logic['type'] == 'FUNCTION_BLOCK':
return f"{prefix}FUNCTION_BLOCK: {logic['name']} inputs: {logic['inputs']}"
return f"{prefix}UNKNOWN: {logic}"
def _logic_to_sympy(self, logic):
"""Convertir lógica LAD a expresión SymPy"""
if not logic:
return None
if logic['type'] == 'CONTACT':
symbol = self.symbol_manager.get_symbol(logic['name'])
if logic['negated']:
return Not(symbol)
else:
return symbol
elif logic['type'] == 'AND':
sympy_operands = []
for operand in logic['operands']:
operand_sympy = self._logic_to_sympy(operand)
if operand_sympy is not None:
sympy_operands.append(operand_sympy)
if len(sympy_operands) > 1:
return And(*sympy_operands)
elif len(sympy_operands) == 1:
return sympy_operands[0]
else:
return None
elif logic['type'] == 'OR':
sympy_operands = []
for operand in logic['operands']:
operand_sympy = self._logic_to_sympy(operand)
if operand_sympy is not None:
sympy_operands.append(operand_sympy)
if len(sympy_operands) > 1:
return Or(*sympy_operands)
elif len(sympy_operands) == 1:
return sympy_operands[0]
else:
return None
elif logic['type'] == 'FUNCTION_BLOCK':
# Para function blocks, creamos un símbolo especial
fb_name = f"{logic['name']}({', '.join(logic['inputs'])})"
return self.symbol_manager.get_symbol(fb_name)
return None
def _sympy_to_structured_string(self, sympy_expr):
"""Convertir expresión SymPy a string estructurado"""
if sympy_expr is None:
return ""
# Crear un mapeo inverso de símbolos a nombres originales
symbol_to_name = {}
for original_name, symbol in self.symbol_manager._symbol_cache.items():
symbol_to_name[str(symbol)] = original_name
# Convertir la expresión a string
str_expr = str(sympy_expr)
# Reemplazar símbolos por nombres originales
for symbol_str, original_name in symbol_to_name.items():
str_expr = str_expr.replace(symbol_str, original_name)
# Reemplazar operadores SymPy por operadores IEC61131-3
str_expr = str_expr.replace('&', ' AND ')
str_expr = str_expr.replace('|', ' OR ')
str_expr = str_expr.replace('~', 'NOT ')
# Limpiar espacios múltiples
str_expr = re.sub(r'\s+', ' ', str_expr)
return str_expr.strip()
def optimize_expressions(self):
"""Optimizar todas las expresiones usando SymPy - Preferir DNF para LAD"""
print("\n=== Optimizando expresiones con SymPy (forzando DNF para LAD) ===")
for network in self.networks:
if network['logic']:
print(f"\nOptimizando Red {network['id']}:")
# Convertir a SymPy
sympy_expr = self._logic_to_sympy(network['logic'])
if sympy_expr:
print(f" Expresión original: {sympy_expr}")
# Simplificar primero
try:
simplified = simplify(sympy_expr)
print(f" Simplificada: {simplified}")
# SIEMPRE convertir a DNF para LAD (forma natural: (AND) OR (AND) OR (AND))
dnf_expr = to_dnf(simplified)
print(f" DNF (forma LAD preferida): {dnf_expr}")
# Post-procesar para eliminar contradicciones
final_expr = self._post_process_expression(dnf_expr)
# Verificar si el post-procesamiento cambió algo
if str(final_expr) != str(dnf_expr):
print(f" Post-procesada: {final_expr}")
self.sympy_expressions[network['id']] = final_expr
except Exception as e:
print(f" Error optimizando: {e}")
self.sympy_expressions[network['id']] = sympy_expr
def group_common_conditions(self):
"""Agrupar networks con condiciones similares o complementarias"""
print("\n=== Analizando agrupación de condiciones ===")
# Buscar networks que podrían agruparse
groupable_networks = []
for network in self.networks:
if (network['logic'] and network['target'] and
network['id'] in self.sympy_expressions):
groupable_networks.append(network)
if len(groupable_networks) < 2:
print("No hay suficientes networks para agrupar")
return
# Analizar condiciones comunes
print(f"Analizando {len(groupable_networks)} networks para agrupación:")
for i, net1 in enumerate(groupable_networks):
for j, net2 in enumerate(groupable_networks[i+1:], i+1):
expr1 = self.sympy_expressions[net1['id']]
expr2 = self.sympy_expressions[net2['id']]
# Buscar factores comunes
try:
# Extraer factores comunes si es posible
common_factors = self._find_common_factors(expr1, expr2)
if common_factors:
print(f" Red {net1['id']} y Red {net2['id']} comparten: {common_factors}")
# Verificar si son complementarias (útil para SET/RESET)
if self._are_complementary(expr1, expr2):
print(f" Red {net1['id']} y Red {net2['id']} son complementarias")
except Exception as e:
print(f" Error analizando Red {net1['id']} y Red {net2['id']}: {e}")
def _find_common_factors(self, expr1, expr2):
"""Encontrar factores comunes entre dos expresiones"""
try:
# Convertir a conjuntos de símbolos para análisis básico
symbols1 = expr1.free_symbols
symbols2 = expr2.free_symbols
common_symbols = symbols1.intersection(symbols2)
if len(common_symbols) > 1:
return f"{len(common_symbols)} símbolos comunes"
return None
except:
return None
def _are_complementary(self, expr1, expr2):
"""Verificar si dos expresiones son complementarias"""
try:
# Verificar si expr1 == NOT(expr2) simplificado
complement = Not(expr2)
simplified_complement = simplify(complement)
simplified_expr1 = simplify(expr1)
return simplified_expr1.equals(simplified_complement)
except:
return False
def _post_process_expression(self, expr):
"""Post-procesar expresión para eliminar contradicciones obvias"""
try:
# Detectar contradicciones como X & ~X que deberían ser False
cleaned_expr = expr
# Aplicar simplificaciones adicionales
cleaned_expr = simplify(cleaned_expr)
# Si la expresión contiene contradicciones obvias, intentar limpiar
free_symbols = cleaned_expr.free_symbols
for symbol in free_symbols:
# Verificar si tenemos symbol & ~symbol en alguna parte
contradiction = And(symbol, Not(symbol))
if cleaned_expr.has(contradiction):
print(f" Detectada contradicción eliminable: {symbol} AND NOT {symbol}")
# Reemplazar contradicción por False
cleaned_expr = cleaned_expr.replace(contradiction, sympy.false)
cleaned_expr = simplify(cleaned_expr)
return cleaned_expr
except:
return expr
def _format_dnf_for_lad(self, sympy_expr):
"""Formatear expresión DNF para código LAD más legible"""
if sympy_expr is None:
return ""
# Crear mapeo de símbolos a nombres originales
symbol_to_name = {}
for original_name, symbol in self.symbol_manager._symbol_cache.items():
symbol_to_name[str(symbol)] = original_name
# Convertir a string y analizar la estructura
str_expr = str(sympy_expr)
# Reemplazar símbolos por nombres originales
for symbol_str, original_name in symbol_to_name.items():
str_expr = str_expr.replace(symbol_str, original_name)
# Reemplazar operadores SymPy por IEC61131-3 primero
str_expr = str_expr.replace('&', ' AND ')
str_expr = str_expr.replace('|', ' OR ')
str_expr = str_expr.replace('~', 'NOT ')
# Limpiar espacios múltiples
str_expr = re.sub(r'\s+', ' ', str_expr).strip()
# Si es una expresión OR de términos AND principales, formatear cada término
# Buscar el patrón de OR principales (no anidados en paréntesis)
if ' OR ' in str_expr and not str_expr.startswith('('):
# Dividir por OR de nivel principal
# Esto es más complejo debido a paréntesis anidados
parts = self._split_main_or_terms(str_expr)
if len(parts) > 1:
formatted_terms = []
for part in parts:
part = part.strip()
# Asegurar que cada término tenga paréntesis si es complejo
if ' AND ' in part and not (part.startswith('(') and part.endswith(')')):
part = f"({part})"
formatted_terms.append(part)
# Unir con OR y saltos de línea para mejor legibilidad
return '\n OR '.join(formatted_terms)
return str_expr
def _split_main_or_terms(self, expr):
"""Dividir expresión por OR de nivel principal, respetando paréntesis"""
parts = []
current_part = ""
paren_level = 0
i = 0
while i < len(expr):
char = expr[i]
if char == '(':
paren_level += 1
current_part += char
elif char == ')':
paren_level -= 1
current_part += char
elif paren_level == 0 and expr[i:i+4] == ' OR ':
# OR de nivel principal encontrado
parts.append(current_part.strip())
current_part = ""
i += 3 # Saltar ' OR '
else:
current_part += char
i += 1
# Agregar la última parte
if current_part.strip():
parts.append(current_part.strip())
return parts if len(parts) > 1 else [expr]
def _parse_actions(self, content):
"""Extraer todas las ACTIONs del programa"""
# Buscar patrón ACTION nombre: ... END_ACTION
action_pattern = r'ACTION\s+(\w+)\s*:(.*?)END_ACTION'
action_matches = re.findall(action_pattern, content, re.DOTALL | re.IGNORECASE)
for action_name, action_code in action_matches:
# Limpiar el código de la ACTION
clean_code = action_code.strip()
self.actions[action_name] = clean_code
print(f"ACTION encontrada: {action_name} ({len(clean_code)} caracteres)")
print(f"Total ACTIONs: {len(self.actions)}")
def main():
"""Función principal"""
converter = SimpleLadConverter()
try:
print("=== Convertidor LAD a SCL con SymPy ===")
# Por ahora probar con SYRUPROOMCTRL que tiene variables y ACTIONs
file_path = ".example/SYRUPROOMCTRL.EXP"
output_name = "SYRUPROOMCTRL_scl"
print(f"Parseando archivo {file_path}...")
converter.parse_file(file_path)
print(f"Redes encontradas: {len(converter.networks)}")
print(f"Secciones de variables: {list(converter.var_sections.keys())}")
print(f"ACTIONs encontradas: {list(converter.actions.keys())}")
# Mostrar información de debug
converter.print_debug_info()
# NUEVO: Optimizar expresiones con SymPy
converter.optimize_expressions()
# NUEVO: Analizar agrupación de condiciones
converter.group_common_conditions()
# Convertir y guardar
print("\nGenerando código SCL completo...")
structured_code = converter.save_to_file(f"{output_name}.txt")
# Mostrar el código generado
lines = structured_code.split('\n')
print(f"\nCódigo SCL generado ({len(lines)} líneas):")
for i, line in enumerate(lines):
print(f"{i+1:3d}: {line}")
print(f"\n✓ Conversión SCL completada!")
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()