import os import re import json import subprocess import gradio as gr try: import spaces except ImportError: class spaces: @staticmethod def GPU(f=None, duration=None): if f is not None: return f def decorator(fn): return fn return decorator # ========================================== # 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=device) 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 llm_tokenizer = None llm_model = None try: from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig import torch print("Iniciando descarga/carga del modelo Qwen/Qwen3.5-9B...") llm_tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3.5-9B") # Configurar cuantización de 4 bits para reducir el uso de RAM en CPU a ~4.5 GB quant_cfg = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.float16 if device == "cpu" else torch.bfloat16, ) # Cargar siempre en CPU al inicio para cumplir con la política de ZeroGPU llm_model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen3.5-9B", quantization_config=quant_cfg, device_map="cpu" if "SPACE_ID" in os.environ else "auto", torch_dtype=torch.float16 ) print("Modelo Qwen3.5-9B cargado correctamente.") except Exception as e: print(f"No se pudo cargar el LLM local ({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" } 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] scored_results = [] # Extraer bigramas y trigramas contiguos de la consulta original para buscar frases query_phrases = [] if query_words: for length in [3, 2]: for i in range(len(raw_words) - length + 1): phrase = " ".join(raw_words[i:i+length]) if any(w in query_words for w in raw_words[i:i+length]): query_phrases.append((phrase, length)) # 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}") def score_page(p): page_text = p['text'] norm_text = normalize_text(page_text) lexical_score = 0 # 1. Coincidencia de palabras clave individuales for word in query_words: if word in norm_text: count = norm_text.count(word) lexical_score += 3 + min(count, 5) * 0.5 # 2. Coincidencia de frases contiguas for phrase, length in query_phrases: if phrase in norm_text: lexical_score += 15 * length # 3. Coincidencia exacta de la consulta completa (limpiando signos) clean_query = re.sub(r'[¿?¡!()]', '', norm_query).strip() if len(clean_query) > 5 and clean_query in norm_text: lexical_score += 50 # 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) # Escalar por 100 semantic_score = float(cosine_sim) * 100.0 total_score = lexical_score + semantic_score # Umbral mínimo de relevancia para evitar spam if total_score < 15: 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): # Validaciones básicas rules = CIRCUIT_RULES.get(circuit_type) if not rules: return "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 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 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 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 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 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): dist_horizontal = float(dist_horizontal) height = float(height) # 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)." 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." 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." 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." else: safety_rule = "Prohibido instalar interruptores o tomacorrientes en Zona 1. Tampoco se permite el paso de cañerías ajenas a este volumen." 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)." 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." elif elem_type == "Luminaria común": permitted = True safety_rule = "Permitido luminarias clase II con grado de protección IPX4 o mayor." else: permitted = True safety_rule = "Permitido termotanques y calentadores con IPX4 fijos." 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." else: permitted = True safety_rule = "Fuera de la zonificación restrictiva del baño. Se aplican las reglas comunes de interiores de viviendas." status_str = "CUMPLE REGLAMENTO" if permitted else "NO CUMPLE REGLAMENTO" color = "#2e7d32" if permitted else "#c62828" html_res = f"""

Ubicación: {zone}

Estado: {status_str}

Índice de protección mínimo requerido: {req_ip}


Explicación del Reglamento Eléctrico Argentino (Sección 701):
{safety_rule}

""" return html_res def calc_electrification(covered_m2, semi_covered_m2): 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: grade = "MÍNIMO" circuits_num = 2 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 = """ """ elif la <= 130: grade = "MEDIO" circuits_num = 3 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 = """ """ elif la <= 200: grade = "ELEVADO" circuits_num = 5 circuits_desc = "Mínimo 5 circuitos: 2 IUG + 2 TUG + 1 circuito de Uso Especial (IUE o TUE)." points_of_utilization = """ """ else: grade = "SUPERIOR" circuits_num = 6 circuits_desc = "Mínimo 6 circuitos: 2 IUG + 2 TUG + 2 circuitos de Uso Especial (IUE y/o TUE)." points_of_utilization = """ """ html_res = f"""

Grado de Electrificación: {grade}

Límite de Aplicación calculado (LA): {la} m² (Cubierta: {covered} m², Semicubierta al 50%: {semi*0.5} m²)

Número mínimo de circuitos obligatorios: {circuits_num}

Configuración requerida:
{circuits_desc}


Puntos Mínimos de Utilización (Bocas mínimas por ambiente):

{points_of_utilization}
""" return html_res @spaces.GPU def rag_query_response(message, history): # Buscar pasajes relevantes matches = local_search(message, num_results=3) if not matches: 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: 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 and llm_tokenizer is not None: try: # Si corre en ZeroGPU, movemos el modelo a CUDA dentro de la función decorada if torch.cuda.is_available(): llm_model.to("cuda") # Formatear el prompt en base al contexto prompt = f"<|im_start|>system\nEres 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.\n<|im_end|>\n<|im_start|>user\nContexto normativo extraído:\n{context_text}\n\nPregunta: {message}\n<|im_end|>\n<|im_start|>assistant\n" inputs = llm_tokenizer(prompt, return_tensors="pt") # Mover inputs a GPU if torch.cuda.is_available(): inputs = {k: v.to("cuda") for k, v in inputs.items()} elif hasattr(llm_model, "device"): inputs = {k: v.to(llm_model.device) for k, v in inputs.items()} else: inputs = {k: v.to(device) for k, v in inputs.items()} # Generación con max_new_tokens de 350 outputs = llm_model.generate( **inputs, max_new_tokens=350, temperature=0.2, do_sample=False, pad_token_id=llm_tokenizer.eos_token_id ) response_text = llm_tokenizer.decode(outputs[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True) return response_text.strip() except Exception as e: print("Error en inferencia de modelo local:", e) # Fallback si no está el LLM cargado: mostrar el pasaje directamente de forma prolija 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)" # Extraer las primeras 3 líneas o 200 caracteres para mostrar un extracto limpio 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): try: P = float(P) I = float(I) V = float(V) target_cos = float(target_cos) freq = int(freq) S = V * I if S == 0: return "
Error: Tensión y corriente deben ser mayores a 0.
", "", "", "" cos_observed = P / S if cos_observed > 1.0: 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 tbody_html = f""" Potencia Activa (P) {P:,.1f} W Intensidad de Corriente (I) {I:,.1f} A Tensión (V) {V:,.1f} V Potencia Aparente Original (S) {S:,.1f} VA cos(φ) Observado {cos_observed:.3f} cos(φ) Objetivo {target_cos:.2f} Multiplicador k {k:.4f} Reactiva de Compensación (qc) {qc:,.1f} VAR Reactancia Capacitiva (Xc) {Xc:,.2f} Ω Capacitancia (C) {C:,.2f} μF """ 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_wrapper = f"""
{tbody_html}
Variable Valor
""" status_summary = f"""
Capacitancia Requerida
{C:.2f} μF

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

""" return status_summary, table_wrapper, beneficios_html, svg_triangle except Exception as e: return f"
Error en cálculo: {str(e)}
", "", "", "" def calc_puesta_a_tierra(tipo, rho, L_jab, d_jab, L_hor, h_hor, d_hor, D_pla, h_pla): try: rho = float(rho) if rho <= 0: return "
Error: La resistividad (ρ) debe ser mayor a 0.
", "" 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: return "
Error: Longitud y diámetro deben ser mayores a 0.
", "" 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 = "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: return "
Error: Dimensiones y profundidad deben ser mayores a 0.
", "" 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: return "
Error: Diámetro y profundidad deben ser mayores a 0.
", "" if h < (D / 2.0): 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: return "
Error geométrico. Valores producen impedancia irreal.
", "" res_box_html = f""" {warning_text}
Resistencia de Puesta a Tierra
{R:.2f} Ω

Método: {tipo.split(' (')[0]}

""" details_html = f"""
Relación L/d calculada: {relacion_ld_text}
Aproximación simplificada Norma: {aprox_text}
Fórmula Matemática Aplicada
{formula_desc}
""" return res_box_html, details_html except Exception as e: return f"
Error en cálculo: {str(e)}
", "" def calc_medidor_potencia(K, k_unit, N, min_val, sec_val): 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: return "
Error: El tiempo total debe ser mayor a 0 segundos.
", "" rev_speed = N / total_seconds if k_unit == "rev / kWh": watts = (3600.0 * N * 1000.0) / (K * total_seconds) formula_str = "Potencia (W) = (3600 · N · 1000) / (K · t)" else: watts = (3600.0 * N * K) / total_seconds formula_str = "Potencia (W) = (3600 · N · K) / t" warning_text = "" if watts > 25000: 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_box_html = f""" {warning_text}
Potencia Activa Medida
{watts:.1f} W
{watts/1000.0:.3f} kW
""" details_html = f"""
Tiempo total acumulado (t): {total_seconds:.2f} seg
Frecuencia del disco/pulsos: {rev_speed:.4f} rev/seg
Ecuación Utilizada
{formula_str}
""" return res_box_html, details_html except Exception as e: return f"
Error en cálculo: {str(e)}
", "" def generate_unifilar_svg(circ, general, is_trifasico): 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 svg += f'' svg += draw_tm_svg(center_x, 35, general["In"], "Corte Gral.") # Dif Gral svg += f'' svg += draw_id_svg(center_x, 105, general["diff"], "300mA", "Dif. Gral.") # 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'Fase: {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): 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) circuits = [] total_dpms = 0.0 # 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": "Ilum. Gral", "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": "Tomas Gral", "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": "Ilum. Esp.", "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": "Tomas Esp.", "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" else: Ib = dpms / (math.sqrt(3) * 380.0) In = get_standard_in(Ib, 63) polos = 4 tipo_lbl = "ACU-T" diff = get_standard_diff(In) cable = get_cable(In, is_iug=False) circuits.append({ "tipo": tipo_lbl, "desc": f"Motor {motor_hp} HP ({'Mono' if fases == 1 else 'Tri'})", "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": "Muy Baja Tensión", "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: return "
Por favor, agregue algún elemento o bocas de circuito para comenzar.
", "" # 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 = "Motor trifásico detectado" elif over_63A: is_trifasico = True force_reason = "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) res_summary = f"""
Potencia Simultánea (DPMS)
{dpms_general:.0f} VA

Coeficiente de simultaneidad: {coef:.2f}

Alimentación General
{'Trifásica (380V)' if is_trifasico else 'Monofásica (220V)'}

{f"Forzado: {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
""" tbody_rows += f""" Línea Principal ({'Trifásica' if is_trifasico else 'Monofásica'}) {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
""" planilla_html = f"""
{tbody_rows}
Circ. Tipo / Destino Bocas Potencia (VA) Ib (A) Protección Conductor / Iz
""" svg_unifilar = generate_unifilar_svg(circuits, {"In": tIn, "diff": tDiff, "cable": tCable, "polos": tPolos}, is_trifasico) return res_summary, planilla_html, svg_unifilar except Exception as e: return f"
Error en diseño de tableros: {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): 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 ) 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): 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 ) 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) with gr.Blocks(title="ArgenVolt - Auditor de Reglamento Eléctrico Argentino") 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"]) # 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: 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): 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], outputs=audit_out ) # --- TAB 1: ZONIFICACIÓN DE BAÑOS --- with gr.Column(visible=False) as col_banos: gr.Markdown("### Auditor de Seguridad en Cuartos de Baño (Reglamento Eléctrico Argentino Sección 701)") 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): 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], outputs=bathroom_out ) # --- TAB 2: GRADO DE ELECTRIFICACIÓN --- with gr.Column(visible=False) as col_electrif: 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): 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], outputs=calc_out ) # --- TAB 3: FACTOR DE POTENCIA --- with gr.Column(visible=False) as col_fp: 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], outputs=[fp_status, fp_table, fp_benefits, fp_triangle] ) # --- TAB 4: PUESTA A TIERRA Y MEDIDOR --- # --- TAB 4: PUESTA A TIERRA (PAT) --- with gr.Column(visible=False) as col_pat: 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: return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) elif "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], outputs=[pat_res, pat_details] ) # --- TAB 5: MEDIDOR DE POTENCIA --- with gr.Column(visible=False) as col_medidor: 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], outputs=[med_res, med_details] ) # --- TAB 5: DISEÑADOR DE TABLEROS --- with gr.Column(visible=False) as col_tableros: gr.Markdown("### Dimensionamiento de Distribución y Protecciones de Tableros") with gr.Row(): with gr.Column(scale=1): 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) 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) 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) 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(): 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], 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], outputs=pdf_output ) # --- TAB 6: CONSULTOR CHAT RAG --- with gr.Column(visible=False) as col_rag: gr.Markdown("### Consultor del Reglamento Eléctrico Argentino") 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_interface = gr.ChatInterface( fn=rag_query_response, chatbot=gr.Chatbot(height=750), 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) if __name__ == "__main__": demo.launch()