import os import re import json import subprocess import gradio as gr # ========================================== # CONSTANTES Y BASE DE DATOS DEL REGLAMENTO ELÉCTRICO ARGENTINO # ========================================== # Tabla 771.7.I - Resumen de tipos de circuitos CIRCUIT_RULES = { "IUG": { "name": "Iluminación de Uso General", "min_section": 1.5, "max_bocas": 15, "max_protection": 16, "desc": "Circuitos destinados a alimentar puntos de iluminación y tomacorrientes asociados si los hubiera." }, "TUG": { "name": "Tomacorrientes de Uso General", "min_section": 2.5, "max_bocas": 15, "max_protection": 20, "desc": "Circuitos destinados a alimentar tomas comunes para electrodomésticos estándar de hasta 10A." }, "IUE": { "name": "Iluminación de Uso Especial", "min_section": 2.5, "max_bocas": 12, "max_protection": 32, "desc": "Circuitos de iluminación a la intemperie o con condiciones ambientales específicas." }, "TUE": { "name": "Tomacorrientes de Uso Especial", "min_section": 2.5, "max_bocas": 12, "max_protection": 32, "desc": "Circuitos para consumos pesados (aires acondicionados, estufas, tomas de más de 10A)." } } # Tabla 771.16.I - Corrientes admisibles [A] para conductores de cobre termoplásticos (PVC) en cañerías a 40 °C # Formato: seccion: { 2x: corriente_bipolar, 3x: corriente_tripolar } CABLE_CURRENT_CAPACITY = { 1.5: {"2x": 15, "3x": 14}, 2.5: {"2x": 21, "3x": 18}, 4.0: {"2x": 28, "3x": 25}, 6.0: {"2x": 36, "3x": 32}, 10.0: {"2x": 50, "3x": 44}, 16.0: {"2x": 66, "3x": 59}, 25.0: {"2x": 88, "3x": 77}, 35.0: {"2x": 109, "3x": 96} } # Tabla 771.16.II.b - Factor de corrección por agrupamiento en un mismo caño GROUPING_FACTORS = { 1: 1.0, 2: 0.8, 3: 0.7, 4: 0.65, 5: 0.60, 6: 0.60, 7: 0.50, 8: 0.50, 9: 0.50 } # ========================================== # CÓDIGO DE RETRIEVAL (RAG LOCAL) # ========================================== MD_771_PATH = "Reglamento_Electrico_Argentino_771_Viviendas.md" MD_701_PATH = "Reglamento_Electrico_Argentino_701.md" def get_markdown_pages(md_path): """Lee un archivo Markdown y extrae las páginas delimitadas por marcadores --- PAGE X ---.""" if not os.path.exists(md_path): print(f"Error: No existe el archivo Markdown en {md_path}") return [] print(f"Cargando páginas desde Markdown: {md_path}...") try: with open(md_path, 'r', encoding='utf-8') as f: content = f.read() raw_parts = content.split("--- PAGE ") pages = [] for part in raw_parts: if not part.strip(): continue # Separar el número de página del texto lines = part.split("\n", 1) if len(lines) < 2: continue page_num_str = lines[0].replace("---", "").strip() page_text = lines[1].strip() try: page_num = int(page_num_str) except ValueError: page_num = len(pages) + 1 if page_text: pages.append({ "page": page_num, "text": page_text }) print(f"Cargadas {len(pages)} páginas desde {md_path}.") return pages except Exception as e: print(f"Error al leer archivo Markdown {md_path}: {e}") return [] # Inicializar bases de datos de texto pages_771 = get_markdown_pages(MD_771_PATH) pages_701 = get_markdown_pages(MD_701_PATH) import torch import numpy as np # Determinar dispositivo para la GPU device = "cuda" if torch.cuda.is_available() else "cpu" print(f"Dispositivo detectado para ejecución local: {device}") # Cargar modelo de embeddings local para búsqueda semántica embedding_model = None EMBEDDINGS_771_JSON = "reglamento_771_embeddings.json" EMBEDDINGS_701_JSON = "reglamento_701_embeddings.json" try: from sentence_transformers import SentenceTransformer print("Cargando modelo de embeddings sentence-transformers/all-MiniLM-L6-v2...") embedding_model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2", device="cpu") print("Modelo de embeddings cargado correctamente.") except Exception as e: print(f"Error al cargar el modelo de embeddings: {e}") def get_embeddings_for_pages(pages, cache_embeddings_path, emb_model): """Carga los embeddings desde el caché o los genera si no existen.""" if not emb_model: return pages if os.path.exists(cache_embeddings_path): try: with open(cache_embeddings_path, 'r', encoding='utf-8') as f: cached_data = json.load(f) for p in pages: p_str = str(p['page']) if p_str in cached_data: p['embedding'] = cached_data[p_str] print(f"Cargados embeddings desde caché {cache_embeddings_path}") return pages except Exception as e: print(f"Error al leer caché de embeddings {cache_embeddings_path}: {e}") print(f"Generando embeddings y guardando en {cache_embeddings_path} (esto puede tardar un momento)...") texts = [p['text'] for p in pages if p.get('text')] if texts: embeddings = emb_model.encode(texts, show_progress_bar=True) # Convertir a listas de flotantes para JSON embeddings_list = [emb.tolist() for emb in embeddings] cached_data = {} idx = 0 for p in pages: if p.get('text'): emb = embeddings_list[idx] p['embedding'] = emb cached_data[str(p['page'])] = emb idx += 1 try: with open(cache_embeddings_path, 'w', encoding='utf-8') as f: json.dump(cached_data, f) print(f"Embeddings guardados exitosamente en {cache_embeddings_path}") except Exception as e: print(f"Error al escribir caché de embeddings {cache_embeddings_path}: {e}") return pages # Inicializar y cachear embeddings if embedding_model: pages_771 = get_embeddings_for_pages(pages_771, EMBEDDINGS_771_JSON, embedding_model) pages_701 = get_embeddings_for_pages(pages_701, EMBEDDINGS_701_JSON, embedding_model) # Cargar modelo LLM local (llama.cpp) llm_model = None try: from huggingface_hub import hf_hub_download from llama_cpp import Llama print("Descargando/Verificando modelo Gemma 4 12B Instruct GGUF...") # Esto descarga del HF Hub al caché local y devuelve la ruta model_path = hf_hub_download( repo_id="bartowski/gemma-4-12B-it-GGUF", filename="gemma-4-12B-it-Q4_K_M.gguf" ) print(f"Modelo GGUF descargado en: {model_path}") print("Cargando modelo GGUF en llama.cpp...") # Cargar en GPU si cuda está disponible n_gpu = -1 if device == "cuda" else 0 llm_model = Llama( model_path=model_path, n_ctx=16384, # Contexto máximo aumentado n_gpu_layers=n_gpu, # -1 para pasar todas las capas a GPU, 0 para CPU verbose=False ) print("Modelo Gemma 4 12B Instruct GGUF cargado correctamente mediante llama.cpp.") except Exception as e: print(f"No se pudo cargar el LLM local con llama.cpp ({e}). El consultor usará el motor de búsqueda directa.") def normalize_text(text): if not text: return "" text = text.lower() replacements = { 'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u', 'ü': 'u', 'ñ': 'ñ' } for k, v in replacements.items(): text = text.replace(k, v) return text def local_search(query, num_results=3): """Busca en el texto de los reglamentos usando coincidencia híbrida (léxica + semántica).""" norm_query = normalize_text(query) raw_words = re.findall(r'\w+', norm_query) # Palabras vacías en español que no aportan significado semántico stopwords = { "que", "es", "el", "la", "los", "las", "un", "una", "unos", "unas", "de", "del", "al", "en", "para", "por", "con", "sin", "sobre", "entre", "este", "esta", "estos", "estas", "eso", "esa", "esos", "esas", "como", "cual", "cuales", "como", "donde", "cuando", "quien", "quienes", "que", "y", "o", "u", "e", "mas", "pero", "sino", "aunque", "hacer", "hace", "ser", "sido", "estar", "tiene", "tienen", "debe", "deben", "se", "del" } query_words = [w for w in raw_words if len(w) > 2 and w not in stopwords] if not query_words: query_words = [w for w in raw_words if len(w) > 2] unique_query_words = list(set(query_words)) # Diccionario de sinónimos reglamentarios para mejorar la recuperación SYNONYMS = { "tierra": ["tierra", "proteccion", "pe", "pat", "bpt", "jabalina"], "neutro": ["neutro", "celeste", "azul"], "fase": ["fase", "castaño", "marron", "negro", "rojo", "linea"], "bocas": ["boca", "bocas"], "tug": ["tug", "t.u.g."], "iug": ["iug", "i.u.g."], "tue": ["tue", "t.u.e."], "iue": ["iue", "i.u.e."] } # Obtener embedding de la consulta si el modelo está disponible query_emb = None if embedding_model is not None: try: query_emb = embedding_model.encode(query, convert_to_numpy=True) except Exception as e: print(f"Error al codificar la consulta '{query}': {e}") scored_results = [] def score_page(p): page_text = p['text'] norm_text = normalize_text(page_text) # 1. Coincidencia de palabras clave (con sinónimos) y recuento de únicas matched_unique_words = 0 lexical_score = 0 for word in unique_query_words: syns = SYNONYMS.get(word, [word]) word_matched = False for syn in syns: # Buscar como palabra completa o con plurales comunes en español (s, es) pattern = r'\b' + re.escape(syn) + r'(?:es|s)?\b' matches = re.findall(pattern, norm_text) if matches: word_matched = True count = len(matches) lexical_score += 5 + min(count, 5) * 1.0 break # Evitar doble coincidencia por varios sinónimos del mismo término if word_matched: matched_unique_words += 1 # 2. Factor de Coordinación (bonificación por coincidir más palabras distintas) coordination_bonus = 0 if len(unique_query_words) > 0: overlap_ratio = matched_unique_words / len(unique_query_words) if overlap_ratio == 1.0: coordination_bonus = 100.0 # Coincidencia perfecta de palabras clave elif overlap_ratio >= 0.75: coordination_bonus = 50.0 elif overlap_ratio >= 0.5: coordination_bonus = 20.0 # 3. Coincidencia exacta de la consulta completa (limpiando signos) exact_bonus = 0 clean_query = re.sub(r'[¿?¡!()]', '', norm_query).strip() clean_query_escaped = re.escape(clean_query) if len(clean_query) > 6 and re.search(r'\b' + clean_query_escaped + r'\b', norm_text): exact_bonus = 50.0 # 4. Similitud semántica semantic_score = 0 if query_emb is not None and 'embedding' in p: page_emb = np.array(p['embedding']) dot_product = np.dot(query_emb, page_emb) norm_q = np.linalg.norm(query_emb) norm_p = np.linalg.norm(page_emb) if norm_q * norm_p > 0: cosine_sim = dot_product / (norm_q * norm_p) semantic_score = float(cosine_sim) * 100.0 total_score = lexical_score + coordination_bonus + exact_bonus + semantic_score # Umbral mínimo de relevancia para evitar falsos positivos if matched_unique_words == 0 and semantic_score < 30: return 0 return round(total_score, 2) # Buscar en Reglamento General 771 for p in pages_771: score = score_page(p) if score > 0: scored_results.append((score, 771, p['page'], p['text'])) # Buscar en Reglamento Baños 701 for p in pages_701: score = score_page(p) if score > 0: scored_results.append((score, 701, p['page'], p['text'])) scored_results.sort(key=lambda x: x[0], reverse=True) return scored_results[:num_results] # ========================================== # FUNCIONES DE LÓGICA Y CÁLCULOS # ========================================== def audit_circuit(circuit_type, section, protection, bocas, grouped_circuits, lang="Español"): # Validaciones básicas rules = CIRCUIT_RULES.get(circuit_type) if not rules: return "Error: Invalid circuit type." if lang == "English" else "Error: Tipo de circuito no válido." section = float(section) protection = int(protection) bocas = int(bocas) grouped_circuits = int(grouped_circuits) # 1. Validación de sección de cable mínima min_sec = rules["min_section"] sec_ok = section >= min_sec if lang == "English": sec_msg = f"✅ Section of {section} mm² meets the minimum section of {min_sec} mm²." if sec_ok else f"❌ The mandatory minimum section for {circuit_type} circuits is {min_sec} mm² (you chose {section} mm²)." else: sec_msg = f"✅ Sección de {section} mm² cumple con la sección mínima de {min_sec} mm²." if sec_ok else f"❌ La sección mínima obligatoria para circuitos {circuit_type} es de {min_sec} mm² (elegiste {section} mm²)." # 2. Validación de cantidad máxima de bocas max_b = rules["max_bocas"] bocas_ok = bocas <= max_b if lang == "English": bocas_msg = f"✅ Number of outlets ({bocas}) within the regulatory limit (max {max_b})." if bocas_ok else f"❌ Excess outlets in the circuit. The standard specifies a maximum of {max_b} outlets per {circuit_type} circuit (you chose {bocas})." else: bocas_msg = f"✅ Cantidad de bocas ({bocas}) dentro del límite reglamentario (máx {max_b})." if bocas_ok else f"❌ Exceso de bocas en el circuito. La norma especifica un máximo de {max_b} bocas por circuito {circuit_type} (elegiste {bocas})." # 3. Validación de la protección (Térmica Máxima del circuito) max_p = rules["max_protection"] prot_rule_ok = protection <= max_p if lang == "English": prot_rule_msg = f"✅ Breaker rating ({protection}A) meets the maximum limit of {max_p}A for {circuit_type}." if prot_rule_ok else f"❌ The maximum protection for {circuit_type} circuits is {max_p}A (you chose {protection}A)." else: prot_rule_msg = f"✅ Calibre de la térmica ({protection}A) cumple con el límite máximo de {max_p}A para {circuit_type}." if prot_rule_ok else f"❌ La protección máxima para circuitos {circuit_type} es de {max_p}A (elegiste {protection}A)." # 4. Coordinación Cable - Térmica (Cálculo de corriente admisible) # Buscamos en la tabla capacity = CABLE_CURRENT_CAPACITY.get(section, {"2x": 0, "3x": 0}) # Asumimos monofásico (2 conductores cargados: 2x) base_current = capacity["2x"] # Aplicar factor de agrupamiento group_factor = GROUPING_FACTORS.get(grouped_circuits, 0.50) allowed_current_corrected = round(base_current * group_factor, 2) # Chequear condición fundamental: In <= Iz (Calibre de térmica <= Corriente admisible corregida del cable) coordination_ok = protection <= allowed_current_corrected if lang == "English": if coordination_ok: coord_msg = f"✅ **CORRECT PROTECTION COORDINATION:** The allowable cable current ({allowed_current_corrected}A, corrected for {grouped_circuits} grouped circuits) is greater than or equal to the breaker rating ({protection}A). The cable is protected against overloads." status_box = "APPROVED - COMPLIES WITH REGULATIONS" status_color = "green" else: coord_msg = f"⚠️ **FIRE HAZARD!:** The breaker of {protection}A is greater than the maximum allowable current supported by the cable ({allowed_current_corrected}A) with {grouped_circuits} circuits in the same conduit. **The cable could melt before the breaker trips.** The breaker must be reduced to maximum {int(allowed_current_corrected)}A or the conductor section must be increased." status_box = "REJECTED - OVERLOAD RISK" status_color = "red" else: if coordination_ok: coord_msg = f"✅ **COORDINACIÓN DE PROTECCIÓN CORRECTA:** La corriente admisible del cable ({allowed_current_corrected}A, corregida por {grouped_circuits} circuitos agrupados) es mayor o igual al calibre de la térmica ({protection}A). El cable está protegido contra sobrecargas." status_box = "APROBADO - CUMPLE LA REGLAMENTACIÓN" status_color = "green" else: coord_msg = f"⚠️ **¡PELIGRO DE INCENDIO!:** La térmica de {protection}A es mayor que la corriente admisible máxima que soporta el cable ({allowed_current_corrected}A) con {grouped_circuits} circuitos en la misma cañería. **El cable podría derretirse antes de que salte la térmica.** Se debe reducir la térmica a máximo {int(allowed_current_corrected)}A o aumentar la sección del conductor." status_box = "RECHAZADO - RIESGO DE SOBRECARGA" status_color = "red" # Texto detallado explicativo if lang == "English": detail_html = f"""

{status_box}


Technical Calculation (Table 771.16.I and II.b):
- Base allowable current of {section} mm² cable: {base_current}A.
- Grouping reduction factor ({grouped_circuits} circuit/s in conduit): x{group_factor}.
- Corrected maximum allowable current ($I_z$): {allowed_current_corrected}A.
- Chosen circuit breaker ($I_n$): {protection}A.

""" else: detail_html = f"""

{status_box}


Cálculo Técnico (Tabla 771.16.I y II.b):
- Corriente admisible base del cable de {section} mm²: {base_current}A.
- Factor de reducción por agrupamiento ({grouped_circuits} circuito/s en caño): x{group_factor}.
- Corriente máxima admitida corregida ($I_z$): {allowed_current_corrected}A.
- Llave térmica elegida ($I_n$): {protection}A.

""" return detail_html def audit_bathroom_zones(dist_horizontal, height, elem_type, lang="Español"): dist_horizontal = float(dist_horizontal) height = float(height) # Translate from English to internal Spanish keys if needed elem_mapping = { "Socket-outlet": "Tomacorriente", "Switch": "Interruptor", "Common luminaire": "Luminaria común", "Water heater": "Termotanque" } elem_type = elem_mapping.get(elem_type, elem_type) # 1. Determinar Zona zone = "Zona 3" # Zona 0 if dist_horizontal == 0 and height == 0: zone = "Zona 0" # Zona 1 elif dist_horizontal == 0 and height <= 225: zone = "Zona 1" # Zona 2 elif dist_horizontal <= 60 and height <= 225: zone = "Zona 2" # Zona 3 elif dist_horizontal > 60 and dist_horizontal <= 240 and height <= 225: zone = "Zona 3" else: # Fuera de las zonas del baño zone = "Fuera de volumen de peligro" # 2. Verificar permitidos por elemento permitted = False req_ip = "IPX0" safety_rule = "" if zone == "Zona 0": req_ip = "IPX7" if elem_type == "Termotanque": safety_rule = "Está terminantemente prohibido colocar calefones o termotanques dentro de la bañera o espacio de ducha (Zona 0)." if lang != "English" else "It is strictly forbidden to place water heaters inside the bathtub or shower space (Zone 0)." else: safety_rule = "Sólo se permiten equipos de Muy Baja Tensión de Seguridad (MBTS) de hasta 12V alternos, con fuente de seguridad fuera del baño." if lang != "English" else "Only Safety Extra-Low Voltage (SELV) equipment up to 12V AC is permitted, with safety source outside the bathroom." elif zone == "Zona 1": req_ip = "IPX5" if elem_type == "Termotanque": permitted = True safety_rule = "Se permite únicamente si es un calefón eléctrico o termotanque de fijación fija con alimentación dedicada y protección de disyuntor de 30mA." if lang != "English" else "Permitted only if it is a fixed electrical water heater with dedicated supply and 30mA residual current protection." elif elem_type == "Luminaria común": safety_rule = "Solo se permiten luminarias fijas protegidas por MBTS (hasta 12V) con fuente instalada en Zona 3 o fuera del baño." if lang != "English" else "Only fixed luminaires protected by SELV (up to 12V) are permitted, with source installed in Zone 3 or outside the bathroom." else: safety_rule = "Prohibido instalar interruptores o tomacorrientes en Zona 1. Tampoco se permite el paso de cañerías ajenas a este volumen." if lang != "English" else "Forbidden to install switches or outlets in Zone 1. Conduits unrelated to this volume are also not allowed to pass through." elif zone == "Zona 2": req_ip = "IPX4" if elem_type == "Tomacorriente": safety_rule = "No se permiten tomacorrientes comunes. Solo se permite el tomacorriente para máquinas de afeitar alimentado por transformador de aislación (según IRAM 2445)." if lang != "English" else "Common outlets are not permitted. Only shaver outlets powered by an isolation transformer (complying with IRAM 2445) are allowed." elif elem_type == "Interruptor": safety_rule = "No se permiten llaves de luz comunes. Deben estar fuera de esta zona o activarse por piolín/remoto de material aislante." if lang != "English" else "Common light switches are not permitted. They must be outside this zone or activated by insulating pull-cord/remote." elif elem_type == "Luminaria común": permitted = True safety_rule = "Permitido luminarias clase II con grado de protección IPX4 o mayor." if lang != "English" else "Permitted Class II luminaires with protection degree IPX4 or higher." else: permitted = True safety_rule = "Permitido termotanques y calentadores con IPX4 fijos." if lang != "English" else "Permitted fixed water heaters and heaters with IPX4." elif zone == "Zona 3": req_ip = "IPX1" permitted = True safety_rule = "Se permiten tomacorrientes, interruptores y luminarias comunes, SIEMPRE que cuenten con protección diferencial (disyuntor de hasta 30mA) y puesta a tierra obligatoria." if lang != "English" else "Common outlets, switches, and luminaires are permitted, PROVIDED they have residual current protection (breaker up to 30mA) and mandatory grounding." else: permitted = True safety_rule = "Fuera de la zonificación restrictiva del baño. Se aplican las reglas comunes de interiores de viviendas." if lang != "English" else "Outside the restrictive bathroom zoning. Common indoor residential rules apply." # Translate output labels if English if lang == "English": zone_trans = { "Zona 0": "Zone 0", "Zona 1": "Zone 1", "Zona 2": "Zone 2", "Zona 3": "Zone 3", "Fuera de volumen de peligro": "Outside danger volume" } zone = zone_trans.get(zone, zone) status_str = "COMPLIES WITH REGULATIONS" if permitted else "DOES NOT COMPLY WITH REGULATIONS" location_lbl = "Location" status_lbl = "Status" min_ip_lbl = "Minimum required protection rating (IP)" exp_lbl = "Argentine Electrical Code Explanation (Section 701)" else: status_str = "CUMPLE REGLAMENTO" if permitted else "NO CUMPLE REGLAMENTO" location_lbl = "Ubicación" status_lbl = "Estado" min_ip_lbl = "Índice de protección mínimo requerido" exp_lbl = "Explicación del Reglamento Eléctrico Argentino (Sección 701)" color = "#2e7d32" if permitted else "#c62828" html_res = f"""

{location_lbl}: {zone}

{status_lbl}: {status_str}

{min_ip_lbl}: {req_ip}


{exp_lbl}:
{safety_rule}

""" return html_res def calc_electrification(covered_m2, semi_covered_m2, lang="Español"): covered = float(covered_m2) semi = float(semi_covered_m2) # Límite de aplicación (LA) la = covered + (semi * 0.5) # Grado de electrificación (Tabla 771.8.I) grade = "" circuits_num = 0 circuits_desc = "" points_of_utilization = "" if la <= 60: if lang == "English": grade = "MINIMUM" circuits_desc = "Minimum 2 circuits: 1 of General Use Lighting (IUG) and 1 of General Use Outlets (TUG)." points_of_utilization = """ """ else: grade = "MÍNIMO" circuits_desc = "Mínimo 2 circuitos: 1 de Iluminación de Uso General (IUG) y 1 de Tomacorrientes de Uso General (TUG)." points_of_utilization = """ """ circuits_num = 2 elif la <= 130: if lang == "English": grade = "MEDIUM" circuits_desc = "Minimum 3 circuits: Various combinations allowed. Recommended: 1 IUG + 1 TUG + 1 free circuit (IUG, TUG, IUE, or TUE)." points_of_utilization = """ """ else: grade = "MEDIO" circuits_desc = "Mínimo 3 circuitos: Varias combinaciones permitidas. Ejemplo recomendado: 1 IUG + 1 TUG + 1 circuito libre (puede ser IUG, TUG, IUE o TUE)." points_of_utilization = """ """ circuits_num = 3 elif la <= 200: if lang == "English": grade = "HIGH" circuits_desc = "Minimum 5 circuits: 2 IUG + 2 TUG + 1 Special Use circuit (IUE or TUE)." points_of_utilization = """ """ else: grade = "ELEVADO" circuits_desc = "Mínimo 5 circuitos: 2 IUG + 2 TUG + 1 circuito de Uso Especial (IUE o TUE)." points_of_utilization = """ """ circuits_num = 5 else: if lang == "English": grade = "SUPERIOR" circuits_desc = "Minimum 6 circuits: 2 IUG + 2 TUG + 2 Special Use circuits (IUE and/or TUE)." points_of_utilization = """ """ else: grade = "SUPERIOR" circuits_desc = "Mínimo 6 circuitos: 2 IUG + 2 TUG + 2 circuitos de Uso Especial (IUE y/o TUE)." points_of_utilization = """ """ circuits_num = 6 if lang == "English": title_lbl = "Electrification Degree" limit_lbl = "Calculated Application Limit (LA)" min_circ_lbl = "Minimum number of mandatory circuits" config_lbl = "Required configuration" points_lbl = "Minimum Points of Utilization (Minimum outlets per room)" area_details = f"Covered: {covered} m², Semi-covered at 50%: {semi*0.5} m²" else: title_lbl = "Grado de Electrificación" limit_lbl = "Límite de Aplicación calculado (LA)" min_circ_lbl = "Número mínimo de circuitos obligatorios" config_lbl = "Configuración requerida" points_lbl = "Puntos Mínimos de Utilización (Bocas mínimas por ambiente)" area_details = f"Cubierta: {covered} m², Semicubierta al 50%: {semi*0.5} m²" html_res = f"""

{title_lbl}: {grade}

{limit_lbl}: {la} m² ({area_details})

{min_circ_lbl}: {circuits_num}

{config_lbl}:
{circuits_desc}


{points_lbl}:

{points_of_utilization}
""" return html_res def rag_query_response(message, history, lang="Español"): # Buscar pasajes relevantes matches = local_search(message, num_results=3) if not matches: if lang == "English": return "I did not find relevant articles in the Argentine Electrical Code regarding that topic. Try using keywords like 'cables', 'bathroom', 'outlets', 'grounding', 'RCD', or 'breaker'." else: return "No encontré pasajes relevantes en el Reglamento Eléctrico Argentino sobre ese tema. Probá usando palabras clave como 'cables', 'baño', 'bocas', 'toma', 'disyuntor' o 'térmica'." # Construir el contexto context_text = "" for score, doc_id, page, text in matches: if lang == "English": doc_name = "Section 771 (Housing)" if doc_id == 771 else "Section 701 (Bathrooms)" else: doc_name = "Sección 771 (Viviendas)" if doc_id == 771 else "Sección 701 (Baños)" context_text += f"\n--- {doc_name} - Página {page} (Relevancia: {score}) ---\n{text}\n" # Responder usando el modelo si está cargado if llm_model is not None: try: # Formatear usando la API de Chat Completions de llama.cpp if lang == "English": system_content = "You are an expert electrical engineering assistant specializing in the Argentine Electrical Code (AEA Sections 771 and 701). Answer the user's question using only the provided regulatory context. If the context does not contain the answer, state it clearly. Always mention the document and the page from which you obtained the response. Respond in English." else: system_content = "Eres un asistente de ingeniería eléctrica experto en el Reglamento Eléctrico Argentino (Secciones 771 y 701). Responde a la pregunta del usuario utilizando únicamente el contexto provisto del reglamento. Si el contexto no contiene la información para responder, indícalo claramente. Menciona siempre el documento y la página de donde obtienes la respuesta. Responde en español." response = llm_model.create_chat_completion( messages=[ { "role": "system", "content": system_content }, { "role": "user", "content": f"Contexto normativo extraído:\n{context_text}\n\nPregunta: {message}" } ], max_tokens=800, temperature=0.2 ) response_text = response["choices"][0]["message"]["content"] return response_text.strip() except Exception as e: print("Error en inferencia de modelo local con llama.cpp:", e) # Fallback si no está el LLM cargado: mostrar el pasaje directamente de forma prolija if lang == "English": response = "🤖 *Local RAG query motor (Direct search mode):*\n\n" response += "I found the following relevant articles and pages in the regulations:\n" for score, doc_id, page, text in matches: doc_name = "Argentine Electrical Code (Section 771 - Residences)" if doc_id == 771 else "Argentine Electrical Code (Section 701 - Bathrooms)" clean_text = "\n".join(text.split("\n")[:8]) response += f"\n📖 **{doc_name} — Page {page}** (Relevance: {score}):\n" response += f"```text\n{clean_text}...\n```\n" response += "\n*Note: You can view the full official PDF document in your project folder to read extended context.*" else: response = "🤖 *Motor de consulta RAG local (Modo búsqueda directa):*\n\n" response += "Encontré los siguientes artículos y páginas relevantes en el reglamento:\n" for score, doc_id, page, text in matches: doc_name = "Reglamento Eléctrico Argentino (Sección 771 - Viviendas)" if doc_id == 771 else "Reglamento Eléctrico Argentino (Sección 701 - Baños)" clean_text = "\n".join(text.split("\n")[:8]) response += f"\n📖 **{doc_name} — Página {page}** (Relevancia: {score}):\n" response += f"```text\n{clean_text}...\n```\n" response += "\n*Nota: Podés ver el documento oficial PDF completo en tu carpeta de proyecto para leer el contexto extendido.*" return response # ========================================== # NUEVAS HERRAMIENTAS DE LA SUITE ELÉCTRICA # ========================================== import math def get_standard_in(ib, max_in=100): breaks = [10, 16, 20, 25, 32, 40, 50, 63, 80, 100, 125, 160, 200, 250] for b in breaks: if b >= ib: return max_in if b > max_in else b return max_in def get_standard_diff(in_val): diffs = [25, 40, 63, 80, 100, 125, 160, 200, 250] for d in diffs: if d >= in_val: return d return in_val def get_cable(in_val, is_iug=False): cables = [ {"s": "1.5", "iz": 15}, {"s": "2.5", "iz": 21}, {"s": "4.0", "iz": 28}, {"s": "6.0", "iz": 36}, {"s": "10.0", "iz": 50}, {"s": "16.0", "iz": 66}, {"s": "25.0", "iz": 88}, {"s": "35.0", "iz": 109}, {"s": "50.0", "iz": 131}, {"s": "70.0", "iz": 167}, {"s": "95.0", "iz": 202}, {"s": "120.0", "iz": 234} ] start_idx = 0 if is_iug else 1 for i in range(start_idx, len(cables)): if cables[i]["iz"] >= in_val: return cables[i] return cables[-1] def draw_power_triangle_svg(P, cos_obs, cos_target, S, qc): phi_obs = math.acos(cos_obs) phi_target = math.acos(cos_target) width = 500 height = 300 padding_left = 65 padding_bottom = 50 max_width = width - padding_left - 50 max_height = height - padding_bottom - 40 q_original = math.sqrt(max(0.0, S**2 - P**2)) q_compensated = max(0.0, q_original - qc) s_compensated = P / cos_target max_power = max(P, q_original) scale = min(max_width / P, max_height / max_power) if max_power > 0 else 1.0 ox = padding_left oy = height - padding_bottom px = ox + (P * scale) py = oy qy_original = oy - (q_original * scale) qy_compensated = oy - (q_compensated * scale) svg = f""" """ # Draw Axes svg += f'' svg += f'' # Arrow heads svg += f'' svg += f'' # Active Power svg += f'' # Original Apparent Power svg += f'' svg += f'' # Corrected Apparent Power svg += f'' svg += f'' # Capacitor delta Qc svg += f'' # Text labels svg += f'P = {int(P):,}W' svg += f'Q = {int(q_original):,}VAR' svg += f'Qc = {int(qc):,}VAR' svg += f'S-Orig = {int(S):,}VA' svg += f'S-Corr = {int(s_compensated):,}VA' # Draw angle arcs r1 = 30 a1_x = ox + r1 * math.cos(-phi_obs) a1_y = oy + r1 * math.sin(-phi_obs) svg += f'' svg += f'φ1: {int(math.degrees(phi_obs))}°' r2 = 45 a2_x = ox + r2 * math.cos(-phi_target) a2_y = oy + r2 * math.sin(-phi_target) svg += f'' svg += f'φ2: {int(math.degrees(phi_target))}°' svg += "" return svg def calc_power_factor(P, I, V, target_cos, freq, lang="Español"): try: P = float(P) I = float(I) V = float(V) target_cos = float(target_cos) freq = int(freq) S = V * I if S == 0: err_msg = "Error: Voltage and current must be greater than 0." if lang == "English" else "Error: Tensión y corriente deben ser mayores a 0." return f"
{err_msg}
", "", "", "" cos_observed = P / S if cos_observed > 1.0: if lang == "English": err_msg = f"""

⚠️ Physical Inconsistency Detected

The resulting observed cos(φ) is {cos_observed:.3f}, which exceeds the theoretical limit of 1.0.
This is because active power ($P$) cannot be greater than apparent power ($S = V \times I$).
Suggestion: Increase the measured current or decrease the entered active power.

""" else: err_msg = f"""

⚠️ Inconsistencia Física Detectada

El cos(φ) observado resultante es {cos_observed:.3f}, el cual supera el límite teórico de 1.0.
Esto se debe a que la potencia activa ($P$) no puede ser mayor que la potencia aparente ($S = V \times I$).
Sugerencia: Aumente la corriente medida o disminuya la potencia activa ingresada.

""" return err_msg, "", "", "" phi_observed = math.acos(cos_observed) phi_target = math.acos(target_cos) k = math.tan(phi_observed) - math.tan(phi_target) qc = P * k if qc < 0: qc = 0.0 k = 0.0 omega = 314 if freq == 50 else 377 if qc > 0: Xc = (V ** 2) / qc C = 1000000.0 / (omega * Xc) else: Xc = 0.0 C = 0.0 s_compensated = P / target_cos i_new = P / (V * target_cos) i_old = I diff_amps = i_old - i_new percent_saved = (diff_amps / i_old) * 100 if i_old > 0 else 0.0 kva_saved = (S - s_compensated) / 1000.0 # Translate variable names in the table vars_dict = { "P": ("Active Power (P)", "Potencia Activa (P)"), "I": ("Current Intensity (I)", "Intensidad de Corriente (I)"), "V": ("Voltage (V)", "Tensión (V)"), "S": ("Original Apparent Power (S)", "Potencia Aparente Original (S)"), "cos_obs": ("Observed cos(φ)", "cos(φ) Observado"), "cos_tgt": ("Target cos(φ)", "cos(φ) Objetivo"), "k": ("k Multiplier", "Multiplicador k"), "qc": ("Compensating Reactive Power (qc)", "Reactiva de Compensación (qc)"), "Xc": ("Capacitive Reactance (Xc)", "Reactancia Capacitiva (Xc)"), "C": ("Capacitance (C)", "Capacitancia (C)") } idx = 0 if lang == "English" else 1 tbody_html = f""" {vars_dict["P"][idx]} {P:,.1f} W {vars_dict["I"][idx]} {I:,.1f} A {vars_dict["V"][idx]} {V:,.1f} V {vars_dict["S"][idx]} {S:,.1f} VA {vars_dict["cos_obs"][idx]} {cos_observed:.3f} {vars_dict["cos_tgt"][idx]} {target_cos:.2f} {vars_dict["k"][idx]} {k:.4f} {vars_dict["qc"][idx]} {qc:,.1f} VAR {vars_dict["Xc"][idx]} {Xc:,.2f} Ω {vars_dict["C"][idx]} {C:,.2f} μF """ if lang == "English": beneficios_html = f"""
Reduced Current
{diff_amps:.2f} A (-{percent_saved:.1f}%)

Current will drop from {i_old:.1f}A to {i_new:.1f}A, reducing Joule effect losses.

Network Capacity Released
{kva_saved:.2f} kVA

Decreases apparent network distribution power, releasing load on transformers.

💡 Avoid Reactive Power Penalties

Power utilities severely penalize cos(φ) below 0.85 or 0.90. By installing a calibrated {C:.1f} μF capacitor, you guarantee operation under the regulatory target of {target_cos:.2f}.

""" else: beneficios_html = f"""
Corriente Reducida
{diff_amps:.2f} A (-{percent_saved:.1f}%)

Corriente bajará de {i_old:.1f}A a {i_new:.1f}A, reduciendo pérdidas por efecto Joule.

Capacidad de Red Liberada
{kva_saved:.2f} kVA

Disminuye la potencia de transporte aparente de la red, liberando carga en transformadores.

💡 Evita Penalizaciones de Reactiva

Las distribuidoras penalizan severamente cos(φ) inferiores a 0.85 o 0.90. Instalando un capacitor calibrado de {C:.1f} μF, garantizas operar bajo la meta reglamentaria de {target_cos:.2f}.

""" svg_triangle = draw_power_triangle_svg(P, cos_observed, target_cos, S, qc) table_hdr_var = "Variable" table_hdr_val = "Value" if lang == "English" else "Valor" table_wrapper = f"""
{tbody_html}
{table_hdr_var} {table_hdr_val}
""" if lang == "English": status_summary = f"""
Required Capacitance
{C:.2f} μF

To raise the power factor from observed cos(φ) of {cos_observed:.3f} to {target_cos:.2f}.

""" else: status_summary = f"""
Capacitancia Requerida
{C:.2f} μF

Para elevar el factor de potencia desde cos(φ) observado de {cos_observed:.3f} hasta {target_cos:.2f}.

""" return status_summary, table_wrapper, beneficios_html, svg_triangle except Exception as e: err_prefix = "Error in calculation: " if lang == "English" else "Error en cálculo: " return f"
{err_prefix}{str(e)}
", "", "", "" def calc_puesta_a_tierra(tipo, rho, L_jab, d_jab, L_hor, h_hor, d_hor, D_pla, h_pla, lang="Español"): try: rho = float(rho) if rho <= 0: err_msg = "Error: Soil resistivity (ρ) must be greater than 0." if lang == "English" else "Error: La resistividad (ρ) debe ser mayor a 0." return f"
{err_msg}
", "" # Translate selected electrode type from English to internal Spanish checked keys tipo_mapping = { "Vertically buried rod (771-C.10.1)": "Jabalina enterrada verticalmente (771-C.10.1)", "Horizontally buried bare conductor (771-C.10.2)": "Conductor desnudo enterrado horizontalmente (771-C.10.2)", "Vertically buried bare circular plate (771-C.10.3)": "Placa circular desnuda enterrada verticalmente (771-C.10.3)" } tipo = tipo_mapping.get(tipo, tipo) R = 0.0 formula_desc = "" aprox_text = "-" relacion_ld_text = "-" warning_text = "" if tipo == "Jabalina enterrada verticalmente (771-C.10.1)": L = float(L_jab) d = float(d_jab) if L <= 0 or d <= 0: err_msg = "Error: Length and diameter must be greater than 0." if lang == "English" else "Error: Longitud y diámetro deben ser mayores a 0." return f"
{err_msg}
", "" R = (rho / (2.0 * math.pi * L)) * (math.log((8.0 * L) / d) - 1.0) formula_desc = "R = (ρ / (2·π·L)) · [ ln(8·L / d) - 1 ]" relacion_ld = L / d relacion_ld_text = f"{relacion_ld:.2f}" if 25 <= relacion_ld <= 100: aprox_text = f"{(0.75 * rho / L):.2f} Ω" elif 100 < relacion_ld <= 600: aprox_text = f"{(rho / L):.2f} Ω" elif 600 < relacion_ld <= 3000: aprox_text = f"{(1.2 * rho / L):.2f} Ω" else: aprox_text = "Outside approximate standard range (25 ≤ L/d ≤ 3000)" if lang == "English" else "Fuera de rango aproximado normativo (25 ≤ L/d ≤ 3000)" elif tipo == "Conductor desnudo enterrado horizontalmente (771-C.10.2)": L = float(L_hor) h = float(h_hor) d = float(d_hor) if L <= 0 or h <= 0 or d <= 0: err_msg = "Error: Dimensions and depth must be greater than 0." if lang == "English" else "Error: Dimensiones y profundidad deben ser mayores a 0." return f"
{err_msg}
", "" t1 = math.log((4.0 * L) / d) t2 = math.log(L / h) t3 = - 2.0 t4 = (2.0 * h) / L t5 = - (h ** 2) / L t6 = (h ** 4) / (2.0 * L) R = (rho / (2.0 * math.pi * L)) * (t1 + t2 + t3 + t4 + t5 + t6) formula_desc = "R = (ρ / (2·π·L)) · [ ln(4L/d) + ln(L/h) - 2 + 2h/L - h²/L + h⁴/2L ]" elif tipo == "Placa circular desnuda enterrada verticalmente (771-C.10.3)": D = float(D_pla) h = float(h_pla) if D <= 0 or h <= 0: err_msg = "Error: Diameter and depth must be greater than 0." if lang == "English" else "Error: Diámetro y profundidad deben ser mayores a 0." return f"
{err_msg}
", "" if h < (D / 2.0): if lang == "English": warning_text = f"""
⚠️ Installation notice: The center depth of the plate ({h}m) should be greater than or equal to the radius ({D/2.0}m) to ensure adequate current dispersion.
""" else: warning_text = f"""
⚠️ Aviso de instalación: La profundidad del centro de la placa ({h}m) debería ser mayor o igual al radio ({D/2.0}m) para asegurar una dispersión de corriente adecuada.
""" rel = D / h p1 = 0.5 p2 = (1.0 / (4.0 * math.pi)) * rel p3 = (7.0 / 384.0) * (rel ** 3) p4 = (99.0 / 81920.0) * (rel ** 5) R = (rho / (2.0 * D)) * (p1 + p2 + p3 + p4) formula_desc = "R = (ρ / (2·D)) · [ 1/2 + 1/(4π) · (D/h) + 7/384 · (D/h)³ + 99/81920 · (D/h)⁵ ]" if R < 0: err_msg = "Error: Geometric values produce unrealistic impedance." if lang == "English" else "Error geométrico. Valores producen impedancia irreal." return f"
{err_msg}
", "" # Determine human-friendly method name if lang == "English": if "Jabalina" in tipo: method_name = "Vertically buried rod" elif "Conductor" in tipo: method_name = "Horizontal bare conductor" else: method_name = "Circular bare plate" res_title = "Grounding Resistance" method_lbl = "Method" else: method_name = tipo.split(' (')[0] res_title = "Resistencia de Puesta a Tierra" method_lbl = "Método" res_box_html = f""" {warning_text}
{res_title}
{R:.2f} Ω

{method_lbl}: {method_name}

""" if lang == "English": ld_lbl = "Calculated L/d ratio:" aprox_lbl = "Standard simplified approximation:" formula_lbl = "Applied Mathematical Formula" else: ld_lbl = "Relación L/d calculada:" aprox_lbl = "Aproximación simplificada Norma:" formula_lbl = "Fórmula Matemática Aplicada" details_html = f"""
{ld_lbl} {relacion_ld_text}
{aprox_lbl} {aprox_text}
{formula_lbl}
{formula_desc}
""" return res_box_html, details_html except Exception as e: err_prefix = "Error in calculation: " if lang == "English" else "Error en cálculo: " return f"
{err_prefix}{str(e)}
", "" def calc_medidor_potencia(K, k_unit, N, min_val, sec_val, lang="Español"): try: K = float(K) N = float(N) min_val = float(min_val) if min_val else 0.0 sec_val = float(sec_val) if sec_val else 0.0 total_seconds = (min_val * 60.0) + sec_val if total_seconds <= 0: err_msg = "Error: Total time must be greater than 0 seconds." if lang == "English" else "Error: El tiempo total debe ser mayor a 0 segundos." return f"
{err_msg}
", "" rev_speed = N / total_seconds if k_unit == "rev / kWh": watts = (3600.0 * N * 1000.0) / (K * total_seconds) formula_str = "Power (W) = (3600 · N · 1000) / (K · t)" if lang == "English" else "Potencia (W) = (3600 · N · 1000) / (K · t)" else: watts = (3600.0 * N * K) / total_seconds formula_str = "Power (W) = (3600 · N · K) / t" if lang == "English" else "Potencia (W) = (3600 · N · K) / t" warning_text = "" if watts > 25000: if lang == "English": warning_text = f"""
⚠️ Notice: A high power of {watts/1000.0:.3f} kW has been calculated. Verify that the values entered for K, N, or time are correct.
""" else: warning_text = f"""
⚠️ Aviso: Se ha calculado una potencia elevada de {watts/1000.0:.3f} kW. Verifique que los valores ingresados de K, N o el tiempo sean correctos.
""" res_title = "Measured Active Power" if lang == "English" else "Potencia Activa Medida" res_box_html = f""" {warning_text}
{res_title}
{watts:.1f} W
{watts/1000.0:.3f} kW
""" if lang == "English": time_lbl = "Total accumulated time (t):" freq_lbl = "Disk/pulse frequency:" eq_lbl = "Equation Used" else: time_lbl = "Tiempo total acumulado (t):" freq_lbl = "Frecuencia del disco/pulsos:" eq_lbl = "Ecuación Utilizada" details_html = f"""
{time_lbl} {total_seconds:.2f} seg
{freq_lbl} {rev_speed:.4f} rev/seg
{eq_lbl}
{formula_str}
""" return res_box_html, details_html except Exception as e: err_prefix = "Error in calculation: " if lang == "English" else "Error en cálculo: " return f"
{err_prefix}{str(e)}
", "" def generate_unifilar_svg(circ, general, is_trifasico, lang="Español"): if not circ: return "" spacing = 135 content_width = (len(circ) - 1) * spacing margin_x = 120 width = max(600, content_width + (margin_x * 2)) height = 560 busbar_y = 190 center_x = width / 2.0 start_x = center_x - (content_width / 2.0) end_x = center_x + (content_width / 2.0) def draw_tm_svg(x, y, label_in, label="TM"): return f""" {label_in}A {label} """ def draw_id_svg(x, y, label_in, sensitivity, label="ID"): return f""" {label_in}A {sensitivity} {label} """ def draw_gm_svg(x, y, label_in, label="GM"): return f""" {label_in}A {label} """ svg = f""" """ # Corte Gral / Main Breaker main_breaker_lbl = "Main Breaker" if lang == "English" else "Corte Gral." main_rcd_lbl = "Main RCD" if lang == "English" else "Dif. Gral." phase_lbl = "Phase" if lang == "English" else "Fase" svg += f'' svg += draw_tm_svg(center_x, 35, general["In"], main_breaker_lbl) # Dif Gral / Main RCD svg += f'' svg += draw_id_svg(center_x, 105, general["diff"], "300mA", main_rcd_lbl) # Connection to busbar svg += f'' # Barra Colectora if len(circ) > 1: svg += f'' else: svg += f'' # Circuitos for i, c in enumerate(circ): cx = start_x + i * spacing svg += f'' if len(circ) > 1: svg += f'' if c["es_motor"]: svg += draw_id_svg(cx, 220, c["diff"], "30mA", "ID") svg += f'' svg += draw_gm_svg(cx, 290, c["In"], "GM") svg += f'' else: svg += draw_tm_svg(cx, 220, c["In"], "TM") svg += f'' svg += f'' svg += f'' svg += f'C{i+1}' badge_w = 50 svg += f'' svg += f'{c["tipo"]}' svg += f'{c["polos"]}x{c["cable"]["s"]} mm²' svg += f'{c["dpms"]:.0f} VA' svg += f'{phase_lbl}: {c["fase"]}' svg += "" return svg def calc_tableros_designer(iug_b, tug_b, iue_b, tue_b, motor_hp, motor_fases, motor_cant, mbt_w, mbt_cant, coef, suministro, lang="Español"): try: iug_b = int(iug_b) if iug_b else 0 tug_b = int(tug_b) if tug_b else 0 iue_b = int(iue_b) if iue_b else 0 tue_b = int(tue_b) if tue_b else 0 motor_hp = float(motor_hp) motor_fases = int(motor_fases) motor_cant = int(motor_cant) if motor_cant else 0 mbt_w = float(mbt_w) mbt_cant = int(mbt_cant) if mbt_cant else 0 coef = float(coef) suministro = str(suministro) if suministro in ["Single-phase", "Monofásico"]: suministro = "Monofásico" else: suministro = "Trifásico" circuits = [] total_dpms = 0.0 # Labels definition based on language iug_desc = "Gen. Lighting" if lang == "English" else "Ilum. Gral" tug_desc = "Gen. Outlets" if lang == "English" else "Tomas Gral" iue_desc = "Spec. Lighting" if lang == "English" else "Ilum. Esp." tue_desc = "Spec. Outlets" if lang == "English" else "Tomas Esp." mbt_desc = "Extra-low Voltage" if lang == "English" else "Muy Baja Tensión" # 1. IUG if iug_b > 0: iug_count = math.ceil(iug_b / 15.0) for i in range(iug_count): b = (iug_b % 15) if (i == iug_count - 1 and iug_b % 15 != 0) else 15 dpms = b * 60 * 0.66 Ib = dpms / 220.0 In = get_standard_in(Ib, 16) cable = get_cable(In, is_iug=True) circuits.append({ "tipo": "IUG", "desc": iug_desc, "b": b, "dpms": dpms, "Ib": Ib, "In": In, "cable": cable, "polos": 2, "es_motor": False, "css_bg": "#3a3212", }) total_dpms += dpms # 2. TUG if tug_b > 0: tug_count = math.ceil(tug_b / 15.0) for i in range(tug_count): b = (tug_b % 15) if (i == tug_count - 1 and tug_b % 15 != 0) else 15 dpms = 2200.0 Ib = dpms / 220.0 In = get_standard_in(Ib, 20) cable = get_cable(In, is_iug=False) circuits.append({ "tipo": "TUG", "desc": tug_desc, "b": b, "dpms": dpms, "Ib": Ib, "In": In, "cable": cable, "polos": 2, "es_motor": False, "css_bg": "#12253a", }) total_dpms += dpms # 3. IUE if iue_b > 0: iue_count = math.ceil(iue_b / 12.0) for i in range(iue_count): b = (iue_b % 12) if (i == iue_count - 1 and iue_b % 12 != 0) else 12 dpms = b * 500 * 0.66 Ib = dpms / 220.0 In = get_standard_in(Ib, 32) cable = get_cable(In, is_iug=False) circuits.append({ "tipo": "IUE", "desc": iue_desc, "b": b, "dpms": dpms, "Ib": Ib, "In": In, "cable": cable, "polos": 2, "es_motor": False, "css_bg": "#3a2812", }) total_dpms += dpms # 4. TUE if tue_b > 0: tue_count = math.ceil(tue_b / 12.0) for i in range(tue_count): b = (tue_b % 12) if (i == tue_count - 1 and tue_b % 12 != 0) else 12 dpms = 3300.0 Ib = dpms / 220.0 In = get_standard_in(Ib, 32) cable = get_cable(In, is_iug=False) circuits.append({ "tipo": "TUE", "desc": tue_desc, "b": b, "dpms": dpms, "Ib": Ib, "In": In, "cable": cable, "polos": 2, "es_motor": False, "css_bg": "#1d123a", }) total_dpms += dpms # 5. Motores if motor_cant > 0: for i in range(motor_cant): dpms = (motor_hp * 746) / 0.8 fases = motor_fases if fases == 1: Ib = dpms / 220.0 In = get_standard_in(Ib, 63) polos = 2 tipo_lbl = "ACU-M" motor_type_str = "Single-phase" if lang == "English" else "Mono" else: Ib = dpms / (math.sqrt(3) * 380.0) In = get_standard_in(Ib, 63) polos = 4 tipo_lbl = "ACU-T" motor_type_str = "Three-phase" if lang == "English" else "Tri" diff = get_standard_diff(In) cable = get_cable(In, is_iug=False) circuits.append({ "tipo": tipo_lbl, "desc": f"Motor {motor_hp} HP ({motor_type_str})", "b": 1, "dpms": dpms, "Ib": Ib, "In": In, "diff": diff, "cable": cable, "polos": polos, "es_motor": True, "fases": fases, "css_bg": "#3a1812", }) total_dpms += dpms # 6. MBT if mbt_cant > 0: cur_mbt_b = 0 cur_mbt_dpms = 0.0 mbt_chunks = [] for i in range(mbt_cant): if cur_mbt_b >= 15 or (cur_mbt_dpms + mbt_w) > 2200.0: if cur_mbt_b > 0: mbt_chunks.append({"b": cur_mbt_b, "dpms": cur_mbt_dpms}) cur_mbt_b = 0 cur_mbt_dpms = 0.0 cur_mbt_b += 1 cur_mbt_dpms += mbt_w if cur_mbt_b > 0: mbt_chunks.append({"b": cur_mbt_b, "dpms": cur_mbt_dpms}) for chunk in mbt_chunks: Ib = chunk["dpms"] / 220.0 In = get_standard_in(Ib, 20) cable = get_cable(In, is_iug=True) circuits.append({ "tipo": "MBTF", "desc": mbt_desc, "b": chunk["b"], "dpms": chunk["dpms"], "Ib": Ib, "In": In, "cable": cable, "polos": 2, "es_motor": False, "css_bg": "#33123a", }) total_dpms += chunk["dpms"] if len(circuits) == 0: err_msg = "Please add some elements or circuit outlets to begin." if lang == "English" else "Por favor, agregue algún elemento o bocas de circuito para comenzar." return f"
{err_msg}
", "", "" # Asignación de fase phases = ["R/N", "S/N", "T/N"] phase_idx = 0 for c in circuits: if c.get("fases") == 3: c["fase"] = "RST" else: c["fase"] = phases[phase_idx % 3] phase_idx += 1 dpms_general = total_dpms * coef has_tri_motor = any(c.get("fases") == 3 for c in circuits) temp_mono_ib = dpms_general / 220.0 over_63A = temp_mono_ib > 63.0 is_trifasico = False force_reason = "" if has_tri_motor: is_trifasico = True force_reason = "Three-phase motor detected" if lang == "English" else "Motor trifásico detectado" elif over_63A: is_trifasico = True force_reason = "Current > 63A" if lang == "English" else "Corriente > 63A" else: is_trifasico = (suministro == "Trifásico") if is_trifasico: tIb = dpms_general / (math.sqrt(3) * 380.0) tPolos = 4 else: tIb = dpms_general / 220.0 tPolos = 2 tIn = get_standard_in(tIb, 250) tDiff = get_standard_diff(tIn) tCable = get_cable(tIn, is_iug=False) if lang == "English": sim_pwr_lbl = "Simultaneous Power (DPMS)" sim_coef_lbl = "Simultaneity factor" main_supply_lbl = "Main Supply" tri_lbl = "Three-phase (380V)" mono_lbl = "Single-phase (220V)" forced_lbl = "Forced" else: sim_pwr_lbl = "Potencia Simultánea (DPMS)" sim_coef_lbl = "Coeficiente de simultaneidad" main_supply_lbl = "Alimentación General" tri_lbl = "Trifásica (380V)" mono_lbl = "Monofásica (220V)" forced_lbl = "Forzado" res_summary = f"""
{sim_pwr_lbl}
{dpms_general:.0f} VA

{sim_coef_lbl}: {coef:.2f}

{main_supply_lbl}
{tri_lbl if is_trifasico else mono_lbl}

{f"{forced_lbl}: {force_reason}" if (has_tri_motor or over_63A) else ""}

""" tbody_rows = "" tBocas = 0 for idx, c in enumerate(circuits): tBocas += c["b"] if c["es_motor"]: prot_html = f"""
GM: {c['polos']}x{c['In']}A
ID: {c['polos']}x{c['diff']}A (30mA)
""" else: prot_html = f"
TM: {c['polos']}x{c['In']}A
" tbody_rows += f""" C{idx+1} {c['tipo']} {c['desc']} {c['b']} {c['dpms']:.0f} {c['Ib']:.2f} {prot_html}
{c['polos']}x{c['cable']['s']} mm²
Iz: {c['cable']['iz']}A
""" main_line_lbl = "Main Line (Three-phase)" if lang == "English" else "Línea Principal (Trifásica)" if not is_trifasico: main_line_lbl = "Main Line (Single-phase)" if lang == "English" else "Línea Principal (Monofásica)" tbody_rows += f""" {main_line_lbl} {tBocas} {dpms_general:.0f} {tIb:.2f}
TM: {tPolos}x{tIn}A
ID: {tPolos}x{tDiff}A (300mA)
{tPolos}x{tCable['s']} mm²
Iz: {tCable['iz']}A
""" tbl_headers = { "circ": "Circ.", "type": "Type / Destination" if lang == "English" else "Tipo / Destino", "outlets": "Outlets" if lang == "English" else "Bocas", "power": "Power (VA)" if lang == "English" else "Potencia (VA)", "ib": "Ib (A)", "prot": "Protection" if lang == "English" else "Protección", "cond": "Conductor / Iz" } planilla_html = f"""
{tbody_rows}
{tbl_headers["circ"]} {tbl_headers["type"]} {tbl_headers["outlets"]} {tbl_headers["power"]} {tbl_headers["ib"]} {tbl_headers["prot"]} {tbl_headers["cond"]}
""" svg_unifilar = generate_unifilar_svg(circuits, {"In": tIn, "diff": tDiff, "cable": tCable, "polos": tPolos}, is_trifasico, lang) return res_summary, planilla_html, svg_unifilar except Exception as e: err_msg = "Error in board design: " if lang == "English" else "Error en diseño de tableros: " return f"
{err_msg}{str(e)}
", "", "" def export_unifilar_to_pdf(iug_b, tug_b, iue_b, tue_b, motor_hp, motor_fases, motor_cant, mbt_w, mbt_cant, coef, suministro, lang="Español"): try: _, _, svg_unifilar = calc_tableros_designer( iug_b, tug_b, iue_b, tue_b, motor_hp, motor_fases, motor_cant, mbt_w, mbt_cant, coef, suministro, lang ) if not svg_unifilar or "Error" in svg_unifilar: return None import io import re from svglib.svglib import svg2rlg from reportlab.graphics import renderPDF svg_cleaned = svg_unifilar viewbox_match = re.search(r'viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"', svg_cleaned) if viewbox_match: w, h = viewbox_match.groups() svg_cleaned = svg_cleaned.replace('height="auto"', f'height="{h}"') svg_cleaned = svg_cleaned.replace('width="100%"', f'width="{w}"') svg_cleaned = svg_cleaned.replace('var(--walnut)', '#5a3a22') svg_cleaned = svg_cleaned.replace('var(--cream)', '#fbf6e8') svg_cleaned = svg_cleaned.replace('var(--sun)', '#e6a85c') svg_cleaned = svg_cleaned.replace('var(--rust)', '#8a4a2b') svg_io = io.BytesIO(svg_cleaned.encode('utf-8')) drawing = svg2rlg(svg_io) pdf_path = "esquema_unifilar.pdf" renderPDF.drawToFile(drawing, pdf_path) return gr.update(value=pdf_path, visible=True) except Exception as e: print("Error exportando a PDF:", e) return None def on_design_click(iug_b, tug_b, iue_b, tue_b, motor_hp, motor_fases, motor_cant, mbt_w, mbt_cant, coef, suministro, lang="Español"): res_summary, planilla_html, svg_unifilar = calc_tableros_designer( iug_b, tug_b, iue_b, tue_b, motor_hp, motor_fases, motor_cant, mbt_w, mbt_cant, coef, suministro, lang ) if svg_unifilar and "Error" not in svg_unifilar: return res_summary, planilla_html, svg_unifilar, gr.update(visible=True), gr.update(visible=False) return res_summary, planilla_html, svg_unifilar, gr.update(visible=False), gr.update(visible=False) # ========================================== # INTERFAZ GRADIO (DISEÑO PREMIUM AESTHETICS) # ========================================== # Cargar estilos CSS personalizados desde el archivo de diseño css_custom = "" if os.path.exists("style.css"): with open("style.css", "r", encoding="utf-8") as f: css_custom = f.read() else: # Fallback básico si el archivo no existe css_custom = """ body, .gradio-container { background-color: #1a1510 !important; color: #f6efe1 !important; } """ def select_tab(tab_index): col_updates = [gr.update(visible=(i == tab_index)) for i in range(8)] btn_updates = [gr.update(variant="primary" if i == tab_index else "secondary") for i in range(8)] return tuple(col_updates + btn_updates) # ========================================== # BILINGUAL INTERFACE TRANSLATIONS # ========================================== TRANSLATIONS = { "Español": { "nav_circuitos": "🔎 Auditor de Circuitos", "nav_banos": "🚿 Baños y Zonas", "nav_electrif": "📐 Grado de Electrificación", "nav_fp": "🎛️ Corrector de Factor de Potencia", "nav_pat": "🌱 Puesta a Tierra (PAT)", "nav_medidor": "🔌 Medidor de Potencia", "nav_tableros": "⚡ Diseñador de Tableros", "nav_rag": "📖 Consultor Normativo (RAG)", "circuit_title": "### Auditoría de Cables y Protecciones Térmicas (Reglamento Eléctrico Argentino Sección 771)", "circuit_in_label": "Tipo de Circuito", "circuit_in_info": "IUG (Luz), TUG (Tomas comunes), IUE/TUE (Especiales/Aire Acon.)", "section_in_label": "Sección del Conductor (Cobre) [mm²]", "section_in_info": "Sección nominal de los cables en la cañería.", "protection_in_label": "Calibre de la Llave Térmica (In) [A]", "bocas_in_label": "Cantidad de Bocas en el Circuito", "grouped_in_label": "Circuitos agrupados en la misma cañería", "grouped_in_info": "Afecta directamente la corriente admisible por sobrecalentamiento.", "btn_audit": "Verificar Circuito", "audit_results_title": "#### Resultado de la Auditoría", "bathroom_title": "### Auditor de Seguridad en Cuartos de Baño (Reglamento Eléctrico Argentino Sección 701)", "bathroom_desc": "Ingrese la distancia a la bañera/ducha para calcular si cumple la normativa y qué IP se requiere.", "elem_in_label": "Tipo de Artefacto/Elemento", "elem_in_choices": ["Tomacorriente", "Interruptor", "Luminaria común", "Termotanque"], "dist_in_label": "Distancia horizontal al borde de bañera/ducha [cm]", "height_in_label": "Altura desde el nivel del piso terminado [cm]", "btn_bathroom": "Analizar Zona", "bathroom_results_title": "#### Análisis de Zonas de Seguridad", "electrif_title": "### Cómputo de Grado de Electrificación y Bocas Mínimas", "covered_in_label": "Superficie Cubierta del Inmueble [m²]", "semi_covered_in_label": "Superficie Semicubierta [m²] (Terrazas, Balcones techados)", "btn_calc": "Calcular Grado", "electrif_results_title": "#### Requerimientos Mínimos del Inmueble", "fp_title": "### Calculadora de Factor de Potencia y Capacitancia", "fp_p_label": "Potencia Activa (P) [W]", "fp_i_label": "Intensidad Medida (I) [A]", "fp_v_label": "Tensión de Red (V) [V]", "fp_target_label": "Coseno φ Objetivo", "fp_freq_label": "Frecuencia Eléctrica (f) [Hz]", "btn_fp": "Calcular Corrección", "pat_title": "### Resistencia de Puesta a Tierra (Reglamento Eléctrico Argentino Sección 771)", "pat_tipo_label": "Tipo de Electrodo", "pat_tipo_choices": [ "Jabalina enterrada verticalmente (771-C.10.1)", "Conductor desnudo enterrado horizontalmente (771-C.10.2)", "Placa circular desnuda enterrada verticalmente (771-C.10.3)" ], "pat_rho_label": "Resistividad del Terreno (ρ) [Ω·m]", "jab_L_label": "Longitud de la Jabalina (L) [m]", "jab_d_label": "Diámetro de la Jabalina (d) [m]", "hor_L_label": "Longitud del Conductor (L) [m]", "hor_h_label": "Profundidad de Enterrado (h) [m]", "hor_d_label": "Diámetro del Conductor (d) [m]", "pla_D_label": "Diámetro de la Placa (D) [m]", "pla_h_label": "Profundidad hasta el Centro (h) [m]", "btn_pat": "Calcular PAT", "medidor_title": "### Medición de Potencia Activa por Medidor de Inducción", "med_k_unit_label": "Unidad de la Constante (K)", "med_K_label": "Valor de la Constante (K)", "med_N_label": "Vueltas Contadas (N)", "med_min_label": "Minutos", "med_sec_label": "Segundos", "btn_med": "Calcular Potencia Medida", "tableros_title": "### Dimensionamiento de Distribución y Protecciones de Tableros", "tableros_bocas_title": "#### Bocas del Proyecto", "tab_iug_label": "Bocas de Iluminación Gral. (IUG)", "tab_tug_label": "Bocas de Tomacorrientes Gral. (TUG)", "tab_iue_label": "Bocas de Iluminación Especial (IUE)", "tab_tue_label": "Bocas de Tomacorrientes Especial (TUE)", "tableros_motores_title": "#### Cargas de Motores (ACU)", "tab_motor_hp_label": "Potencia Motor [HP]", "tab_motor_fases_label": "Fases Motor", "tab_motor_cant_label": "Cantidad de Motores", "tableros_mbt_title": "#### Muy Baja Tensión (MBT)", "tab_mbt_w_label": "Consumo por Carga [W]", "tab_mbt_cant_label": "Cantidad de Cargas", "tableros_params_title": "#### Parámetros Generales", "tab_coef_label": "Coeficiente de Simultaneidad", "tab_sum_label": "Suministro Sugerido", "tab_sum_choices": ["Monofásico", "Trifásico"], "btn_tab": "Diseñar Tablero", "btn_pdf": "Exportar a PDF", "pdf_output_label": "Descargar Diagrama Unifilar PDF", "tableros_unifilar_title": "#### Diagrama Unifilar Dinámico (Esquema Eléctrico)", "rag_title": "### Consultor del Reglamento Eléctrico Argentino", "rag_desc": "Escribí una duda técnica o palabra clave sobre la reglamentación. El motor buscará de manera semántica e indexada en los PDFs cargados y responderá.", "chat_textbox_label": "Pregunta", "chat_textbox_placeholder": "Escribí una duda técnica o palabra clave..." }, "English": { "nav_circuitos": "🔎 Circuit Auditor", "nav_banos": "🚿 Bathroom Zones", "nav_electrif": "📐 Electrification Degree", "nav_fp": "🎛️ Power Factor Corrector", "nav_pat": "🌱 Grounding (PAT)", "nav_medidor": "🔌 Disk Power Meter", "nav_tableros": "⚡ Board Designer", "nav_rag": "📖 Normative Advisor (RAG)", "circuit_title": "### Cables and Circuit Breaker Auditing (Argentine Electrical Code Section 771)", "circuit_in_label": "Circuit Type", "circuit_in_info": "IUG (Lighting), TUG (General Outlets), IUE/TUE (Special/AC)", "section_in_label": "Conductor Section (Copper) [mm²]", "section_in_info": "Nominal section of cables in the conduit.", "protection_in_label": "Circuit Breaker Rating (In) [A]", "bocas_in_label": "Number of Outlets in the Circuit", "grouped_in_label": "Grouped circuits in the same conduit", "grouped_in_info": "Directly affects allowable current due to thermal grouping.", "btn_audit": "Verify Circuit", "audit_results_title": "#### Audit Result", "bathroom_title": "### Bathroom Safety Auditor (Argentine Electrical Code Section 701)", "bathroom_desc": "Enter the distance to the bathtub/shower to calculate compliance and required IP rating.", "elem_in_label": "Appliance / Element Type", "elem_in_choices": ["Socket-outlet", "Switch", "Common luminaire", "Water heater"], "dist_in_label": "Horizontal distance to bathtub/shower border [cm]", "height_in_label": "Height from finished floor level [cm]", "btn_bathroom": "Analyze Zone", "bathroom_results_title": "#### Safety Zones Analysis", "electrif_title": "### Electrification Degree & Minimum Outlets Calculation", "covered_in_label": "Covered Area of the Property [m²]", "semi_covered_in_label": "Semi-covered Area [m²] (Patios, Roofed Balconies)", "btn_calc": "Calculate Degree", "electrif_results_title": "#### Minimum Property Requirements", "fp_title": "### Power Factor and Capacitance Calculator", "fp_p_label": "Active Power (P) [W]", "fp_i_label": "Measured Current (I) [A]", "fp_v_label": "Grid Voltage (V) [V]", "fp_target_label": "Target Cos φ (Power Factor)", "fp_freq_label": "Grid Frequency (f) [Hz]", "btn_fp": "Calculate Correction", "pat_title": "### Grounding Resistance (Argentine Electrical Code Section 771)", "pat_tipo_label": "Electrode Type", "pat_tipo_choices": [ "Vertically buried rod (771-C.10.1)", "Horizontally buried bare conductor (771-C.10.2)", "Vertically buried bare circular plate (771-C.10.3)" ], "pat_rho_label": "Soil Resistivity (ρ) [Ω·m]", "jab_L_label": "Rod Length (L) [m]", "jab_d_label": "Rod Diameter (d) [m]", "hor_L_label": "Conductor Length (L) [m]", "hor_h_label": "Burial Depth (h) [m]", "hor_d_label": "Conductor Diameter (d) [m]", "pla_D_label": "Plate Diameter (D) [m]", "pla_h_label": "Depth to Center (h) [m]", "btn_pat": "Calculate Grounding", "medidor_title": "### Active Power Measurement via Induction Meter", "med_k_unit_label": "Constant Unit (K)", "med_K_label": "Constant Value (K)", "med_N_label": "Counted Revolutions (N)", "med_min_label": "Minutes", "med_sec_label": "Seconds", "btn_med": "Calculate Measured Power", "tableros_title": "### Board Distribution and Protections Sizing", "tableros_bocas_title": "#### Project Outlets", "tab_iug_label": "General Lighting Outlets (IUG)", "tab_tug_label": "General Socket Outlets (TUG)", "tab_iue_label": "Special Lighting Outlets (IUE)", "tab_tue_label": "Special Socket Outlets (TUE)", "tableros_motores_title": "#### Motor Loads (ACU)", "tab_motor_hp_label": "Motor Power [HP]", "tab_motor_fases_label": "Motor Phases", "tab_motor_cant_label": "Number of Motors", "tableros_mbt_title": "#### Extra Low Voltage (ELV)", "tab_mbt_w_label": "Consumption per Load [W]", "tab_mbt_cant_label": "Number of Loads", "tableros_params_title": "#### General Parameters", "tab_coef_label": "Simultaneity Coeff.", "tab_sum_label": "Suggested Supply", "tab_sum_choices": ["Single-phase", "Three-phase"], "btn_tab": "Design Board", "btn_pdf": "Export to PDF", "pdf_output_label": "Download Single-line Diagram PDF", "tableros_unifilar_title": "#### Dynamic Single-Line Diagram (Electrical Scheme)", "rag_title": "### Argentine Electrical Code Advisor", "rag_desc": "Write a technical question or keyword about the regulations. The engine will perform a semantic and indexed search on the loaded PDFs and answer.", "chat_textbox_label": "Question", "chat_textbox_placeholder": "Write a technical question or keyword..." } } def change_language(lang): t = TRANSLATIONS[lang] return { nav_btn_circuitos: gr.update(value=t["nav_circuitos"]), nav_btn_banos: gr.update(value=t["nav_banos"]), nav_btn_electrif: gr.update(value=t["nav_electrif"]), nav_btn_fp: gr.update(value=t["nav_fp"]), nav_btn_pat: gr.update(value=t["nav_pat"]), nav_btn_medidor: gr.update(value=t["nav_medidor"]), nav_btn_tableros: gr.update(value=t["nav_tableros"]), nav_btn_rag: gr.update(value=t["nav_rag"]), circuit_title: gr.update(value=t["circuit_title"]), circuit_in: gr.update(label=t["circuit_in_label"], info=t["circuit_in_info"]), section_in: gr.update(label=t["section_in_label"], info=t["section_in_info"]), protection_in: gr.update(label=t["protection_in_label"]), bocas_in: gr.update(label=t["bocas_in_label"]), grouped_in: gr.update(label=t["grouped_in_label"], info=t["grouped_in_info"]), btn_audit: gr.update(value=t["btn_audit"]), audit_results_title: gr.update(value=t["audit_results_title"]), bathroom_title: gr.update(value=t["bathroom_title"]), bathroom_desc: gr.update(value=t["bathroom_desc"]), elem_in: gr.update(label=t["elem_in_label"], choices=t["elem_in_choices"], value=t["elem_in_choices"][0]), dist_in: gr.update(label=t["dist_in_label"]), height_in: gr.update(label=t["height_in_label"]), btn_bathroom: gr.update(value=t["btn_bathroom"]), bathroom_results_title: gr.update(value=t["bathroom_results_title"]), electrif_title: gr.update(value=t["electrif_title"]), covered_in: gr.update(label=t["covered_in_label"]), semi_covered_in: gr.update(label=t["semi_covered_in_label"]), btn_calc: gr.update(value=t["btn_calc"]), electrif_results_title: gr.update(value=t["electrif_results_title"]), fp_title: gr.update(value=t["fp_title"]), fp_p: gr.update(label=t["fp_p_label"]), fp_i: gr.update(label=t["fp_i_label"]), fp_v: gr.update(label=t["fp_v_label"]), fp_target: gr.update(label=t["fp_target_label"]), fp_freq: gr.update(label=t["fp_freq_label"]), btn_fp: gr.update(value=t["btn_fp"]), pat_title: gr.update(value=t["pat_title"]), pat_tipo: gr.update(label=t["pat_tipo_label"], choices=t["pat_tipo_choices"], value=t["pat_tipo_choices"][0]), pat_rho: gr.update(label=t["pat_rho_label"]), jab_L: gr.update(label=t["jab_L_label"]), jab_d: gr.update(label=t["jab_d_label"]), hor_L: gr.update(label=t["hor_L_label"]), hor_h: gr.update(label=t["hor_h_label"]), hor_d: gr.update(label=t["hor_d_label"]), pla_D: gr.update(label=t["pla_D_label"]), pla_h: gr.update(label=t["pla_h_label"]), btn_pat: gr.update(value=t["btn_pat"]), medidor_title: gr.update(value=t["medidor_title"]), med_k_unit: gr.update(label=t["med_k_unit_label"]), med_K: gr.update(label=t["med_K_label"]), med_N: gr.update(label=t["med_N_label"]), med_min: gr.update(label=t["med_min_label"]), med_sec: gr.update(label=t["med_sec_label"]), btn_med: gr.update(value=t["btn_med"]), tableros_title: gr.update(value=t["tableros_title"]), tableros_bocas_title: gr.update(value=t["tableros_bocas_title"]), tab_iug: gr.update(label=t["tab_iug_label"]), tab_tug: gr.update(label=t["tab_tug_label"]), tab_iue: gr.update(label=t["tab_iue_label"]), tab_tue: gr.update(label=t["tab_tue_label"]), tableros_motores_title: gr.update(value=t["tableros_motores_title"]), tab_motor_hp: gr.update(label=t["tab_motor_hp_label"]), tab_motor_fases: gr.update(label=t["tab_motor_fases_label"]), tab_motor_cant: gr.update(label=t["tab_motor_cant_label"]), tableros_mbt_title: gr.update(value=t["tableros_mbt_title"]), tab_mbt_w: gr.update(label=t["tab_mbt_w_label"]), tab_mbt_cant: gr.update(label=t["tab_mbt_cant_label"]), tableros_params_title: gr.update(value=t["tableros_params_title"]), tab_coef: gr.update(label=t["tab_coef_label"]), tab_sum: gr.update(label=t["tab_sum_label"], choices=t["tab_sum_choices"], value=t["tab_sum_choices"][0]), btn_tab: gr.update(value=t["btn_tab"]), btn_pdf: gr.update(value=t["btn_pdf"]), pdf_output: gr.update(label=t["pdf_output_label"]), tableros_unifilar_title: gr.update(value=t["tableros_unifilar_title"]), rag_title: gr.update(value=t["rag_title"]), rag_desc: gr.update(value=t["rag_desc"]), chat_textbox: gr.update(label=t["chat_textbox_label"], placeholder=t["chat_textbox_placeholder"]) } with gr.Blocks(title="ArgenVolt - Auditor de Reglamento Eléctrico Argentino", css=css_custom) as demo: with gr.Row(): # COLUMNA 1: SIDEBAR DE NAVEGACIÓN with gr.Column(scale=1, min_width=280, elem_id="sidebar-panel"): gr.HTML("""

⚡ ArgenVolt

Auditor y Consultor del Reglamento Eléctrico Argentino

""") nav_btn_circuitos = gr.Button("🔎 Auditor de Circuitos", variant="primary", elem_classes=["nav-btn"]) nav_btn_banos = gr.Button("🚿 Baños y Zonas", variant="secondary", elem_classes=["nav-btn"]) nav_btn_electrif = gr.Button("📐 Grado de Electrificación", variant="secondary", elem_classes=["nav-btn"]) nav_btn_fp = gr.Button("🎛️ Corrector de Factor de Potencia", variant="secondary", elem_classes=["nav-btn"]) nav_btn_pat = gr.Button("🌱 Puesta a Tierra (PAT)", variant="secondary", elem_classes=["nav-btn"]) nav_btn_medidor = gr.Button("🔌 Medidor de Potencia", variant="secondary", elem_classes=["nav-btn"]) nav_btn_tableros = gr.Button("⚡ Diseñador de Tableros", variant="secondary", elem_classes=["nav-btn"]) nav_btn_rag = gr.Button("📖 Consultor Normativo (RAG)", variant="secondary", elem_classes=["nav-btn"]) gr.HTML("
") lang_dropdown = gr.Dropdown( choices=["Español", "English"], value="Español", label="Idioma / Language", interactive=True ) # COLUMNA 2: PANEL DE CONTENIDO with gr.Column(scale=4, elem_id="content-panel"): # --- TAB 0: AUDITOR DE CIRCUITOS --- with gr.Column(visible=True) as col_circuitos: circuit_title = gr.Markdown("### Auditoría de Cables y Protecciones Térmicas (Reglamento Eléctrico Argentino Sección 771)") with gr.Row(): with gr.Column(scale=1): circuit_in = gr.Dropdown( choices=["IUG", "TUG", "IUE", "TUE"], value="TUG", label="Tipo de Circuito", info="IUG (Luz), TUG (Tomas comunes), IUE/TUE (Especiales/Aire Acon.)" ) section_in = gr.Dropdown( choices=["1.5", "2.5", "4.0", "6.0", "10.0"], value="2.5", label="Sección del Conductor (Cobre) [mm²]", info="Sección nominal de los cables en la cañería." ) protection_in = gr.Dropdown( choices=["10", "16", "20", "25", "32", "40"], value="20", label="Calibre de la Llave Térmica (In) [A]" ) bocas_in = gr.Number( value=10, label="Cantidad de Bocas en el Circuito", precision=0 ) grouped_in = gr.Slider( minimum=1, maximum=6, value=1, step=1, label="Circuitos agrupados en la misma cañería", info="Afecta directamente la corriente admisible por sobrecalentamiento." ) btn_audit = gr.Button("Verificar Circuito", variant="primary") with gr.Column(scale=1): audit_results_title = gr.Markdown("#### Resultado de la Auditoría") audit_out = gr.HTML(label="Informe de Conformidad") btn_audit.click( fn=audit_circuit, inputs=[circuit_in, section_in, protection_in, bocas_in, grouped_in, lang_dropdown], outputs=audit_out ) # --- TAB 1: ZONIFICACIÓN DE BAÑOS --- with gr.Column(visible=False) as col_banos: bathroom_title = gr.Markdown("### Auditor de Seguridad en Cuartos de Baño (Reglamento Eléctrico Argentino Sección 701)") bathroom_desc = gr.Markdown("Ingrese la distancia a la bañera/ducha para calcular si cumple la normativa y qué IP se requiere.") with gr.Row(): with gr.Column(scale=1): elem_in = gr.Dropdown( choices=["Tomacorriente", "Interruptor", "Luminaria común", "Termotanque"], value="Tomacorriente", label="Tipo de Artefacto/Elemento" ) dist_in = gr.Slider( minimum=0, maximum=300, value=40, step=10, label="Distancia horizontal al borde de bañera/ducha [cm]" ) height_in = gr.Slider( minimum=0, maximum=300, value=180, step=10, label="Altura desde el nivel del piso terminado [cm]" ) btn_bathroom = gr.Button("Analizar Zona", variant="primary") with gr.Column(scale=1): bathroom_results_title = gr.Markdown("#### Análisis de Zonas de Seguridad") bathroom_out = gr.HTML(label="Informe de Baños") btn_bathroom.click( fn=audit_bathroom_zones, inputs=[dist_in, height_in, elem_in, lang_dropdown], outputs=bathroom_out ) # --- TAB 2: GRADO DE ELECTRIFICACIÓN --- with gr.Column(visible=False) as col_electrif: electrif_title = gr.Markdown("### Cómputo de Grado de Electrificación y Bocas Mínimas") with gr.Row(): with gr.Column(scale=1): covered_in = gr.Number( value=80, label="Superficie Cubierta del Inmueble [m²]", precision=1 ) semi_covered_in = gr.Number( value=20, label="Superficie Semicubierta [m²] (Terrazas, Balcones techados)", precision=1 ) btn_calc = gr.Button("Calcular Grado", variant="primary") with gr.Column(scale=1): electrif_results_title = gr.Markdown("#### Requerimientos Mínimos del Inmueble") calc_out = gr.HTML(label="Requisitos Mínimos") btn_calc.click( fn=calc_electrification, inputs=[covered_in, semi_covered_in, lang_dropdown], outputs=calc_out ) # --- TAB 3: FACTOR DE POTENCIA --- with gr.Column(visible=False) as col_fp: fp_title = gr.Markdown("### Calculadora de Factor de Potencia y Capacitancia") with gr.Row(): with gr.Column(scale=1): fp_p = gr.Slider(minimum=100, maximum=50000, value=4000, step=100, label="Potencia Activa (P) [W]") fp_i = gr.Slider(minimum=1, maximum=250, value=28, step=1, label="Intensidad Medida (I) [A]") fp_v = gr.Slider(minimum=100, maximum=480, value=220, step=5, label="Tensión de Red (V) [V]") fp_target = gr.Slider(minimum=0.8, maximum=1.0, value=0.98, step=0.01, label="Coseno φ Objetivo") fp_freq = gr.Radio(choices=[50, 60], value=50, label="Frecuencia Eléctrica (f) [Hz]") btn_fp = gr.Button("Calcular Corrección", variant="primary") with gr.Column(scale=1): fp_status = gr.HTML(label="Resultado Crítico") fp_triangle = gr.HTML(label="Triángulo de Potencias") fp_table = gr.HTML(label="Variables Calculadas") fp_benefits = gr.HTML(label="Beneficios de Corrección") btn_fp.click( fn=calc_power_factor, inputs=[fp_p, fp_i, fp_v, fp_target, fp_freq, lang_dropdown], outputs=[fp_status, fp_table, fp_benefits, fp_triangle] ) # --- TAB 4: PUESTA A TIERRA (PAT) --- with gr.Column(visible=False) as col_pat: pat_title = gr.Markdown("### Resistencia de Puesta a Tierra (Reglamento Eléctrico Argentino Sección 771)") with gr.Row(): with gr.Column(scale=1): pat_tipo = gr.Dropdown( choices=[ "Jabalina enterrada verticalmente (771-C.10.1)", "Conductor desnudo enterrado horizontalmente (771-C.10.2)", "Placa circular desnuda enterrada verticalmente (771-C.10.3)" ], value="Jabalina enterrada verticalmente (771-C.10.1)", label="Tipo de Electrodo" ) pat_rho = gr.Number(value=100.0, label="Resistividad del Terreno (ρ) [Ω·m]") # Jabalina parameters with gr.Group(visible=True) as pat_group_jab: jab_L = gr.Number(value=1.5, label="Longitud de la Jabalina (L) [m]") jab_d = gr.Number(value=0.016, label="Diámetro de la Jabalina (d) [m]") # Horizontal parameters with gr.Group(visible=False) as pat_group_hor: hor_L = gr.Number(value=45.0, label="Longitud del Conductor (L) [m]") hor_h = gr.Number(value=0.7, label="Profundidad de Enterrado (h) [m]") hor_d = gr.Number(value=0.008, label="Diámetro del Conductor (d) [m]") # Placa parameters with gr.Group(visible=False) as pat_group_pla: pla_D = gr.Number(value=1.0, label="Diámetro de la Placa (D) [m]") pla_h = gr.Number(value=1.5, label="Profundidad hasta el Centro (h) [m]") btn_pat = gr.Button("Calcular PAT", variant="primary") with gr.Column(scale=1): pat_res = gr.HTML(label="Resistencia Resultante") pat_details = gr.HTML(label="Detalles de Cálculo") def update_pat_fields(choice): if "Jabalina" in choice or "rod" in choice: return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) elif "Conductor" in choice or "conductor" in choice: return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False) else: return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True) pat_tipo.change( fn=update_pat_fields, inputs=[pat_tipo], outputs=[pat_group_jab, pat_group_hor, pat_group_pla] ) btn_pat.click( fn=calc_puesta_a_tierra, inputs=[pat_tipo, pat_rho, jab_L, jab_d, hor_L, hor_h, hor_d, pla_D, pla_h, lang_dropdown], outputs=[pat_res, pat_details] ) # --- TAB 5: MEDIDOR DE POTENCIA --- with gr.Column(visible=False) as col_medidor: medidor_title = gr.Markdown("### Medición de Potencia Activa por Medidor de Inducción") with gr.Row(): with gr.Column(scale=1): med_k_unit = gr.Radio(choices=["rev / kWh", "Wh / rev"], value="rev / kWh", label="Unidad de la Constante (K)") med_K = gr.Number(value=150.0, label="Valor de la Constante (K)") med_N = gr.Number(value=10.0, label="Vueltas Contadas (N)") with gr.Row(): med_min = gr.Number(value=1.0, label="Minutos") med_sec = gr.Number(value=15.0, label="Segundos") btn_med = gr.Button("Calcular Potencia Medida", variant="primary") with gr.Column(scale=1): med_res = gr.HTML(label="Potencia Calculada") med_details = gr.HTML(label="Detalles del Ensayo") btn_med.click( fn=calc_medidor_potencia, inputs=[med_K, med_k_unit, med_N, med_min, med_sec, lang_dropdown], outputs=[med_res, med_details] ) # --- TAB 6: DISEÑADOR DE TABLEROS --- with gr.Column(visible=False) as col_tableros: tableros_title = gr.Markdown("### Dimensionamiento de Distribución y Protecciones de Tableros") with gr.Row(): with gr.Column(scale=1): tableros_bocas_title = gr.Markdown("#### Bocas del Proyecto") tab_iug = gr.Number(value=10, label="Bocas de Iluminación Gral. (IUG)", precision=0) tab_tug = gr.Number(value=15, label="Bocas de Tomacorrientes Gral. (TUG)", precision=0) tab_iue = gr.Number(value=0, label="Bocas de Iluminación Especial (IUE)", precision=0) tab_tue = gr.Number(value=0, label="Bocas de Tomacorrientes Especial (TUE)", precision=0) tableros_motores_title = gr.Markdown("#### Cargas de Motores (ACU)") with gr.Row(): tab_motor_hp = gr.Dropdown(choices=["0.5", "0.75", "1.0", "1.5", "2.0", "3.0", "5.0"], value="1.0", label="Potencia Motor [HP]") tab_motor_fases = gr.Radio(choices=[1, 3], value=1, label="Fases Motor") tab_motor_cant = gr.Number(value=0, label="Cantidad de Motores", precision=0) tableros_mbt_title = gr.Markdown("#### Muy Baja Tensión (MBT)") with gr.Row(): tab_mbt_w = gr.Number(value=100, label="Consumo por Carga [W]", precision=0) tab_mbt_cant = gr.Number(value=0, label="Cantidad de Cargas", precision=0) tableros_params_title = gr.Markdown("#### Parámetros Generales") with gr.Row(): tab_coef = gr.Slider(minimum=0.5, maximum=1.0, value=0.8, step=0.05, label="Coeficiente de Simultaneidad") tab_sum = gr.Radio(choices=["Monofásico", "Trifásico"], value="Monofásico", label="Suministro Sugerido") with gr.Row(): btn_tab = gr.Button("Diseñar Tablero", variant="primary") btn_pdf = gr.Button("Exportar a PDF", variant="secondary", visible=False) pdf_output = gr.File(label="Descargar Diagrama Unifilar PDF", visible=False) with gr.Column(scale=1): tab_summary = gr.HTML(label="Resumen de Potencia") tab_planilla = gr.HTML(label="Planilla de Carga") with gr.Row(): with gr.Column(): tableros_unifilar_title = gr.Markdown("#### Diagrama Unifilar Dinámico (Esquema Eléctrico)") tab_svg = gr.HTML(label="Esquema Unifilar") btn_tab.click( fn=on_design_click, inputs=[tab_iug, tab_tug, tab_iue, tab_tue, tab_motor_hp, tab_motor_fases, tab_motor_cant, tab_mbt_w, tab_mbt_cant, tab_coef, tab_sum, lang_dropdown], outputs=[tab_summary, tab_planilla, tab_svg, btn_pdf, pdf_output] ) btn_pdf.click( fn=export_unifilar_to_pdf, inputs=[tab_iug, tab_tug, tab_iue, tab_tue, tab_motor_hp, tab_motor_fases, tab_motor_cant, tab_mbt_w, tab_mbt_cant, tab_coef, tab_sum, lang_dropdown], outputs=pdf_output ) # --- TAB 7: CONSULTOR CHAT RAG --- with gr.Column(visible=False) as col_rag: rag_title = gr.Markdown("### Consultor del Reglamento Eléctrico Argentino") rag_desc = gr.Markdown("Escribí una duda técnica o palabra clave sobre la reglamentación. El motor buscará de manera semántica e indexada en los PDFs cargados y responderá.") chat_textbox = gr.Textbox(placeholder="Escribí una duda técnica o palabra clave...", label="Pregunta") chat_interface = gr.ChatInterface( fn=rag_query_response, chatbot=gr.Chatbot(height=750), textbox=chat_textbox, additional_inputs=[lang_dropdown], examples=[ ["¿De qué color es el conductor neutro y el de tierra?"], ["¿Cuál es el límite de bocas para un circuito TUG?"], ["¿Qué se permite instalar en la Zona 1 del baño?"], ["¿Qué sección mínima debe tener el cable de puesta a tierra?"], ["¿Qué es el límite de aplicación para electrificación?"] ] ) # Registro de navegación nav_cols = [col_circuitos, col_banos, col_electrif, col_fp, col_pat, col_medidor, col_tableros, col_rag] nav_btns = [nav_btn_circuitos, nav_btn_banos, nav_btn_electrif, nav_btn_fp, nav_btn_pat, nav_btn_medidor, nav_btn_tableros, nav_btn_rag] nav_outputs = nav_cols + nav_btns nav_btn_circuitos.click(fn=lambda: select_tab(0), outputs=nav_outputs) nav_btn_banos.click(fn=lambda: select_tab(1), outputs=nav_outputs) nav_btn_electrif.click(fn=lambda: select_tab(2), outputs=nav_outputs) nav_btn_fp.click(fn=lambda: select_tab(3), outputs=nav_outputs) nav_btn_pat.click(fn=lambda: select_tab(4), outputs=nav_outputs) nav_btn_medidor.click(fn=lambda: select_tab(5), outputs=nav_outputs) nav_btn_tableros.click(fn=lambda: select_tab(6), outputs=nav_outputs) nav_btn_rag.click(fn=lambda: select_tab(7), outputs=nav_outputs) # Registro de cambio de idioma lang_dropdown.change( fn=change_language, inputs=[lang_dropdown], outputs=[ nav_btn_circuitos, nav_btn_banos, nav_btn_electrif, nav_btn_fp, nav_btn_pat, nav_btn_medidor, nav_btn_tableros, nav_btn_rag, circuit_title, circuit_in, section_in, protection_in, bocas_in, grouped_in, btn_audit, audit_results_title, bathroom_title, bathroom_desc, elem_in, dist_in, height_in, btn_bathroom, bathroom_results_title, electrif_title, covered_in, semi_covered_in, btn_calc, electrif_results_title, fp_title, fp_p, fp_i, fp_v, fp_target, fp_freq, btn_fp, pat_title, pat_tipo, pat_rho, jab_L, jab_d, hor_L, hor_h, hor_d, pla_D, pla_h, btn_pat, medidor_title, med_k_unit, med_K, med_N, med_min, med_sec, btn_med, tableros_title, tableros_bocas_title, tab_iug, tab_tug, tab_iue, tab_tue, tableros_motores_title, tab_motor_hp, tab_motor_fases, tab_motor_cant, tableros_mbt_title, tab_mbt_w, tab_mbt_cant, tableros_params_title, tab_coef, tab_sum, btn_tab, btn_pdf, pdf_output, tableros_unifilar_title, rag_title, rag_desc, chat_textbox ] ) if __name__ == "__main__": # Detect if running in a Hugging Face Space to use default server port and binding if "SPACE_ID" in os.environ: demo.launch(server_name="0.0.0.0", server_port=7860) else: demo.launch(server_name="0.0.0.0", server_port=7861)