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}
{sec_msg}
{bocas_msg}
{prot_rule_msg}
{coord_msg}
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}
{sec_msg}
{bocas_msg}
{prot_rule_msg}
{coord_msg}
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 = """
Living / Dining: 1 lighting outlet (IUG) and 1 socket outlet (TUG) per 6m² (minimum 2).
Bedroom (<10m²): 1 IUG outlet and 2 TUG outlets.
Kitchen: 1 IUG outlet and 3 TUG outlets + outlets for extractor and refrigerator.
Bathroom: 1 IUG outlet and 1 TUG outlet (outside Zone 2).
Hallway / Vestibule: 1 IUG outlet and 1 TUG outlet per 5m of length.
"""
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 = """
Estar / Comedor: 1 boca de iluminación (IUG) y 1 boca de tomacorriente (TUG) cada 6m² (mínimo 2).
Dormitorio (<10m²): 1 boca de IUG y 2 bocas de TUG.
Cocina: 1 boca de IUG y 3 bocas de TUG + tomas para extractor y heladera.
Baño: 1 boca de IUG y 1 boca de TUG (fuera de Zona 2).
Pasillo / Vestíbulo: 1 boca de IUG y 1 de TUG cada 5m de longitud.
"""
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 = """
Living / Dining: 1 IUG outlet and 1 TUG outlet per 6m² (minimum 3).
Bedroom (>=10m²): 1 IUG outlet and 3 TUG outlets.
Kitchen: 2 IUG outlets and 3 TUG outlets + specific outlets (minimum 3 independent outlets).
Bathroom: 1 IUG outlet and 1 TUG outlet.
Hallway / Vestibule: 1 IUG outlet and 1 TUG outlet per 5m.
Estar / Comedor: Bocas abundantes. Mínimo 1 boca de IUG cada 18m² y 1 boca de TUG cada 6m² (mínimo 5).
Dormitorios: 1 boca de IUG y 4 bocas de TUG.
Cocina: Bocas de IUG y TUG abundantes (mínimo 4 bocas de tomacorriente generales).
Baño: 1 boca de IUG y 2 de TUG.
"""
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""""
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}.
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.
"""
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""""
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"""
{tbl_headers["circ"]}
{tbl_headers["type"]}
{tbl_headers["outlets"]}
{tbl_headers["power"]}
{tbl_headers["ib"]}
{tbl_headers["prot"]}
{tbl_headers["cond"]}
{tbody_rows}
"""
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)