ArgenVolt / app.py
arey14's picture
Update app.py
1f1dc6c verified
Raw
History Blame Contribute Delete
96.8 kB
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"""
<div style="padding: 15px; border-radius: 12px; border: 2px solid {'#2e7d32' if status_color == 'green' else '#c62828'}; background-color: {'rgba(46,125,50,0.1)' if status_color == 'green' else 'rgba(198,40,40,0.1)'}; color: #fbf6e8; margin-bottom: 15px; font-family: 'Outfit', 'Inter', sans-serif;">
<h3 style="margin-top: 0; color: {'#a5d6a7' if status_color == 'green' else '#ef9a9a'}; font-family: 'Outfit', sans-serif;">{status_box}</h3>
<ul style="list-style-type: none; padding-left: 0; margin-bottom: 12px;">
<li style="margin-bottom: 8px;">{sec_msg}</li>
<li style="margin-bottom: 8px;">{bocas_msg}</li>
<li style="margin-bottom: 8px;">{prot_rule_msg}</li>
<li style="margin-bottom: 8px;">{coord_msg}</li>
</ul>
<hr style="border-color: rgba(251, 246, 232, 0.15); margin: 12px 0;"/>
<p style="font-size: 0.9em; opacity: 0.9; margin: 0; line-height: 1.5; color: #fbf6e8;">
<b>Cálculo Técnico (Tabla 771.16.I y II.b):</b><br/>
- Corriente admisible base del cable de {section} mm²: <b>{base_current}A</b>.<br/>
- Factor de reducción por agrupamiento ({grouped_circuits} circuito/s en caño): <b>x{group_factor}</b>.<br/>
- Corriente máxima admitida corregida ($I_z$): <b>{allowed_current_corrected}A</b>.<br/>
- Llave térmica elegida ($I_n$): <b>{protection}A</b>.
</p>
</div>
"""
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"""
<div style="padding: 15px; border-radius: 12px; border: 2px solid {color}; background-color: {color}1a; color: #fbf6e8; margin-top: 10px; font-family: 'Outfit', 'Inter', sans-serif;">
<h3 style="margin-top:0; color:{color}ee; font-family: 'Outfit', sans-serif;">Ubicación: {zone}</h3>
<p><b>Estado:</b> <span style="color:{color}; font-weight:bold;">{status_str}</span></p>
<p><b>Índice de protección mínimo requerido:</b> <span style="font-family: monospace; font-size: 1.1em; background: rgba(251, 246, 232, 0.1); padding: 2px 6px; border-radius: 4px; color: #fbf6e8;">{req_ip}</span></p>
<hr style="border-color: rgba(251, 246, 232, 0.15); margin: 10px 0;"/>
<p style="font-size: 0.95em; line-height: 1.5; color: #fbf6e8;"><b>Explicación del Reglamento Eléctrico Argentino (Sección 701):</b><br/>{safety_rule}</p>
</div>
"""
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 = """
<ul>
<li><b>Estar / Comedor:</b> 1 boca de iluminación (IUG) y 1 boca de tomacorriente (TUG) cada 6m² (mínimo 2).</li>
<li><b>Dormitorio (&lt;10m²):</b> 1 boca de IUG y 2 bocas de TUG.</li>
<li><b>Cocina:</b> 1 boca de IUG y 3 bocas de TUG + tomas para extractor y heladera.</li>
<li><b>Baño:</b> 1 boca de IUG y 1 boca de TUG (fuera de Zona 2).</li>
<li><b>Pasillo / Vestíbulo:</b> 1 boca de IUG y 1 de TUG cada 5m de longitud.</li>
</ul>
"""
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 = """
<ul>
<li><b>Estar / Comedor:</b> 1 boca de IUG y 1 boca de TUG cada 6m² (mínimo 3).</li>
<li><b>Dormitorio (&gt;=10m²):</b> 1 boca de IUG y 3 bocas de TUG.</li>
<li><b>Cocina:</b> 2 bocas de IUG y 3 bocas de TUG + tomas específicos (mínimo 3 tomas independientes).</li>
<li><b>Baño:</b> 1 boca de IUG y 1 de TUG.</li>
<li><b>Pasillo / Vestíbulo:</b> 1 boca de IUG y 1 de TUG cada 5m.</li>
</ul>
"""
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 = """
<ul>
<li><b>Estar / Comedor:</b> 1 boca de IUG cada 18m² y 1 boca de TUG cada 6m² (mínimo 4).</li>
<li><b>Dormitorios:</b> 1 boca de IUG y 3 bocas de TUG.</li>
<li><b>Cocina:</b> 2 bocas de IUG y 4 bocas de TUG + tomas especiales.</li>
<li><b>Baño:</b> 1 boca de IUG y 2 de TUG.</li>
<li><b>Pasillos amplios y lavadero:</b> bocas dedicadas de TUG e IUG.</li>
</ul>
"""
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 = """
<ul>
<li><b>Estar / Comedor:</b> Bocas abundantes. Mínimo 1 boca de IUG cada 18m² y 1 boca de TUG cada 6m² (mínimo 5).</li>
<li><b>Dormitorios:</b> 1 boca de IUG y 4 bocas de TUG.</li>
<li><b>Cocina:</b> Bocas de IUG y TUG abundantes (mínimo 4 bocas de tomacorriente generales).</li>
<li><b>Baño:</b> 1 boca de IUG y 2 de TUG.</li>
</ul>
"""
html_res = f"""
<div style="padding: 15px; border-radius: 12px; border: 1.5px solid #c98a3c; background-color: rgba(201,138,60,0.08); color: #fbf6e8; margin-top: 10px; font-family: 'Outfit', 'Inter', sans-serif;">
<h3 style="margin-top:0; color:#e6a85c; font-family: 'Outfit', sans-serif;">Grado de Electrificación: {grade}</h3>
<p><b>Límite de Aplicación calculado (LA):</b> {la} m² (Cubierta: {covered} m², Semicubierta al 50%: {semi*0.5} m²)</p>
<p><b>Número mínimo de circuitos obligatorios:</b> <b>{circuits_num}</b></p>
<p style="font-size: 0.95em; line-height: 1.5; color: #fbf6e8;"><b>Configuración requerida:</b><br/>{circuits_desc}</p>
<hr style="border-color: rgba(251, 246, 232, 0.15); margin: 12px 0;"/>
<h4 style="margin-top:0; color:#e6a85c; font-family: 'Outfit', sans-serif;">Puntos Mínimos de Utilización (Bocas mínimas por ambiente):</h4>
<div style="font-size: 0.9em; line-height: 1.5; color: #fbf6e8;">{points_of_utilization}</div>
</div>
"""
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"""<svg width="100%" height="auto" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg" style="background-color: #120d09; border-radius: 8px; border: 1px solid var(--walnut); font-family: 'Outfit', sans-serif;">
<style>
.axis {{ stroke: #5a3a22; stroke-width: 2; fill: none; }}
.line-active {{ stroke: #8a6a48; stroke-width: 3.5; stroke-linecap: round; }}
.line-original {{ stroke: #b8553a; stroke-width: 2.5; stroke-dasharray: 4,4; fill: none; }}
.line-corrected {{ stroke: #7a8c4a; stroke-width: 3; fill: none; }}
.line-cap {{ stroke: #c98a3c; stroke-width: 2.5; }}
.text-muted {{ font-size: 11px; fill: #b89f88; }}
.text-bold {{ font-size: 13px; fill: #fbf6e8; font-weight: bold; }}
.text-sun {{ font-size: 13px; fill: #e6a85c; font-weight: bold; }}
.text-rust {{ font-size: 13px; fill: #b8553a; font-weight: bold; }}
.text-moss {{ font-size: 13px; fill: #7a8c4a; font-weight: bold; }}
</style>
"""
# Draw Axes
svg += f'<path d="M {ox - 15} {oy} L {width - 20} {oy}" class="axis" />'
svg += f'<path d="M {ox} {oy + 15} L {ox} 20" class="axis" />'
# Arrow heads
svg += f'<path d="M {width - 20} {oy - 4} L {width - 12} {oy} L {width - 20} {oy + 4} Z" fill="#5a3a22" />'
svg += f'<path d="M {ox - 4} 20 L {ox} 12 L {ox + 4} 20 Z" fill="#5a3a22" />'
# Active Power
svg += f'<path d="M {ox} {oy} L {px} {py}" class="line-active" />'
# Original Apparent Power
svg += f'<path d="M {ox} {oy} L {px} {qy_original}" class="line-original" />'
svg += f'<path d="M {px} {qy_original} L {px} {py}" class="line-original" />'
# Corrected Apparent Power
svg += f'<path d="M {ox} {oy} L {px} {qy_compensated}" class="line-corrected" />'
svg += f'<path d="M {px} {qy_compensated} L {px} {py}" class="line-corrected" />'
# Capacitor delta Qc
svg += f'<path d="M {px + 6} {qy_original} L {px + 6} {qy_compensated}" class="line-cap" />'
# Text labels
svg += f'<text x="{ox + (px - ox)/2}" y="{oy + 18}" text-anchor="middle" class="text-bold">P = {int(P):,}W</text>'
svg += f'<text x="{px + 15}" y="{qy_original + (oy - qy_original)/2}" class="text-rust">Q = {int(q_original):,}VAR</text>'
svg += f'<text x="{px + 15}" y="{qy_compensated - (qy_compensated - qy_original)/2}" class="text-sun">Qc = {int(qc):,}VAR</text>'
svg += f'<text x="{ox + 15}" y="35" class="text-rust">S-Orig = {int(S):,}VA</text>'
svg += f'<text x="{ox + 15}" y="55" class="text-moss">S-Corr = {int(s_compensated):,}VA</text>'
# Draw angle arcs
r1 = 30
a1_x = ox + r1 * math.cos(-phi_obs)
a1_y = oy + r1 * math.sin(-phi_obs)
svg += f'<path d="M {ox + r1} {oy} A {r1} {r1} 0 0 0 {a1_x} {a1_y}" fill="none" stroke="#b8553a" stroke-width="1.5" />'
svg += f'<text x="{ox + 35}" y="{oy - 10}" class="text-rust">φ1: {int(math.degrees(phi_obs))}°</text>'
r2 = 45
a2_x = ox + r2 * math.cos(-phi_target)
a2_y = oy + r2 * math.sin(-phi_target)
svg += f'<path d="M {ox + r2} {oy} A {r2} {r2} 0 0 0 {a2_x} {a2_y}" fill="none" stroke="#7a8c4a" stroke-width="1.5" />'
svg += f'<text x="{ox + 50}" y="{oy - 28}" class="text-moss">φ2: {int(math.degrees(phi_target))}°</text>'
svg += "</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 "<div style='color: #ef9a9a; font-weight: bold;'>Error: Tensión y corriente deben ser mayores a 0.</div>", "", "", ""
cos_observed = P / S
if cos_observed > 1.0:
err_msg = f"""
<div style="padding: 15px; border-radius: 12px; border: 1.5px solid #c62828; background-color: rgba(198,40,40,0.1); color: #fbf6e8; font-family: 'Outfit', sans-serif;">
<h4 style="margin-top:0; color:#ef9a9a; font-family: 'Outfit', sans-serif;">⚠️ Inconsistencia Física Detectada</h4>
<p style="margin: 0; font-size: 0.95em;">
El cos(φ) observado resultante es <b>{cos_observed:.3f}</b>, el cual supera el límite teórico de 1.0.<br/>
Esto se debe a que la potencia activa ($P$) no puede ser mayor que la potencia aparente ($S = V \times I$).<br/>
<b>Sugerencia:</b> Aumente la corriente medida o disminuya la potencia activa ingresada.
</p>
</div>
"""
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"""
<tr style="border-bottom: 1px solid rgba(251,246,232,0.08);">
<td style="padding: 10px; font-weight: 500; text-align: left;">Potencia Activa (P)</td>
<td style="padding: 10px; text-align: right; font-family: monospace;">{P:,.1f} W</td>
</tr>
<tr style="border-bottom: 1px solid rgba(251,246,232,0.08);">
<td style="padding: 10px; font-weight: 500; text-align: left;">Intensidad de Corriente (I)</td>
<td style="padding: 10px; text-align: right; font-family: monospace;">{I:,.1f} A</td>
</tr>
<tr style="border-bottom: 1px solid rgba(251,246,232,0.08);">
<td style="padding: 10px; font-weight: 500; text-align: left;">Tensión (V)</td>
<td style="padding: 10px; text-align: right; font-family: monospace;">{V:,.1f} V</td>
</tr>
<tr style="border-bottom: 1px solid rgba(251,246,232,0.08);">
<td style="padding: 10px; font-weight: 500; text-align: left;">Potencia Aparente Original (S)</td>
<td style="padding: 10px; text-align: right; font-family: monospace;">{S:,.1f} VA</td>
</tr>
<tr style="background-color: rgba(230, 168, 92, 0.08); border-bottom: 1px solid rgba(251,246,232,0.08);">
<td style="padding: 10px; font-weight: bold; text-align: left; color: #e6a85c;">cos(φ) Observado</td>
<td style="padding: 10px; text-align: right; font-family: monospace; font-weight: bold; color: #e6a85c;">{cos_observed:.3f}</td>
</tr>
<tr style="border-bottom: 1px solid rgba(251,246,232,0.08);">
<td style="padding: 10px; font-weight: 500; text-align: left;">cos(φ) Objetivo</td>
<td style="padding: 10px; text-align: right; font-family: monospace;">{target_cos:.2f}</td>
</tr>
<tr style="border-bottom: 1px solid rgba(251,246,232,0.08);">
<td style="padding: 10px; font-weight: 500; text-align: left;">Multiplicador k</td>
<td style="padding: 10px; text-align: right; font-family: monospace;">{k:.4f}</td>
</tr>
<tr style="border-bottom: 1px solid rgba(251,246,232,0.08);">
<td style="padding: 10px; font-weight: 500; text-align: left;">Reactiva de Compensación (qc)</td>
<td style="padding: 10px; text-align: right; font-family: monospace;">{qc:,.1f} VAR</td>
</tr>
<tr style="border-bottom: 1px solid rgba(251,246,232,0.08);">
<td style="padding: 10px; font-weight: 500; text-align: left;">Reactancia Capacitiva (Xc)</td>
<td style="padding: 10px; text-align: right; font-family: monospace;">{Xc:,.2f} Ω</td>
</tr>
<tr style="background-color: rgba(230, 168, 92, 0.08); border-bottom: 1px solid rgba(251,246,232,0.08);">
<td style="padding: 10px; font-weight: bold; text-align: left; color: #e6a85c;">Capacitancia (C)</td>
<td style="padding: 10px; text-align: right; font-family: monospace; font-weight: bold; color: #e6a85c;">{C:,.2f} μF</td>
</tr>
"""
beneficios_html = f"""
<div style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); display: grid; gap: 15px; margin-bottom: 15px; font-family: 'Outfit', sans-serif;">
<div style="background: rgba(230, 168, 92, 0.03); border: 1.5px solid var(--walnut); padding: 15px; border-radius: 12px; text-align: left;">
<span style="font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.05em; color: var(--sun);">Corriente Reducida</span>
<div style="font-size: 1.6em; font-weight: 800; color: var(--sun); margin: 5px 0;">{diff_amps:.2f} A <span style="font-size: 0.6em; color: var(--cream); opacity: 0.8;">(-{percent_saved:.1f}%)</span></div>
<p style="font-size: 0.85em; margin: 0; opacity: 0.85; color: var(--cream);">Corriente bajará de <b>{i_old:.1f}A</b> a <b>{i_new:.1f}A</b>, reduciendo pérdidas por efecto Joule.</p>
</div>
<div style="background: rgba(230, 168, 92, 0.03); border: 1.5px solid var(--walnut); padding: 15px; border-radius: 12px; text-align: left;">
<span style="font-size: 0.8em; text-transform: uppercase; letter-spacing: 0.05em; color: var(--sun);">Capacidad de Red Liberada</span>
<div style="font-size: 1.6em; font-weight: 800; color: var(--sun); margin: 5px 0;">{kva_saved:.2f} kVA</div>
<p style="font-size: 0.85em; margin: 0; opacity: 0.85; color: var(--cream);">Disminuye la potencia de transporte aparente de la red, liberando carga en transformadores.</p>
</div>
</div>
<div style="background: rgba(251,246,232,0.04); border: 1px solid var(--walnut); border-radius: 12px; padding: 15px; font-family: 'Outfit', sans-serif; font-size: 0.9em; text-align: left;">
<h4 style="margin: 0 0 6px 0; color: var(--sun); font-family: 'Outfit', sans-serif;">💡 Evita Penalizaciones de Reactiva</h4>
<p style="margin: 0; opacity: 0.9; line-height: 1.45; color: var(--cream);">
Las distribuidoras penalizan severamente cos(φ) inferiores a 0.85 o 0.90.
Instalando un capacitor calibrado de <b>{C:.1f} μF</b>, garantizas operar bajo la meta reglamentaria de <b>{target_cos:.2f}</b>.
</p>
</div>
"""
svg_triangle = draw_power_triangle_svg(P, cos_observed, target_cos, S, qc)
table_wrapper = f"""
<div style="border: 1px solid var(--walnut); border-radius: 12px; overflow: hidden; font-family: 'Outfit', sans-serif;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.9em; color: var(--cream);">
<thead>
<tr style="background-color: rgba(251, 246, 232, 0.05); border-bottom: 2px solid var(--walnut); color: var(--sun);">
<th style="padding: 10px; text-align: left; font-weight: 600;">Variable</th>
<th style="padding: 10px; text-align: right; font-weight: 600;">Valor</th>
</tr>
</thead>
<tbody>
{tbody_html}
</tbody>
</table>
</div>
"""
status_summary = f"""
<div style="background: linear-gradient(135deg, #2a1b10, #15100c); border: 2px solid var(--walnut); border-radius: 12px; padding: 20px; text-align: center; font-family: 'Outfit', sans-serif;">
<span style="font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.1em; color: var(--sun); font-weight: 600;">Capacitancia Requerida</span>
<div style="font-size: 3.2em; font-weight: 800; color: var(--sun); margin: 10px 0; font-family: monospace;">{C:.2f} <span style="font-size: 0.5em; font-weight: 600; color: var(--cream);">μF</span></div>
<p style="font-size: 0.9em; margin: 0; opacity: 0.95; color: var(--cream);">
Para elevar el factor de potencia desde cos(φ) observed de <b>{cos_observed:.3f}</b> hasta <b>{target_cos:.2f}</b>.
</p>
</div>
"""
return status_summary, table_wrapper, beneficios_html, svg_triangle
except Exception as e:
return f"<div style='color: #ef9a9a; font-weight: bold;'>Error en cálculo: {str(e)}</div>", "", "", ""
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 "<div style='color: #ef9a9a; font-weight: bold;'>Error: La resistividad (ρ) debe ser mayor a 0.</div>", ""
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 "<div style='color: #ef9a9a; font-weight: bold;'>Error: Longitud y diámetro deben ser mayores a 0.</div>", ""
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 "<div style='color: #ef9a9a; font-weight: bold;'>Error: Dimensiones y profundidad deben ser mayores a 0.</div>", ""
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 "<div style='color: #ef9a9a; font-weight: bold;'>Error: Diámetro y profundidad deben ser mayores a 0.</div>", ""
if h < (D / 2.0):
warning_text = f"""
<div style="background: rgba(230, 168, 92, 0.08); border: 1.5px solid var(--copper); padding: 12px; border-radius: 8px; color: var(--sun); margin-bottom: 12px; font-size: 0.9em; font-family: 'Outfit', sans-serif;">
⚠️ <b>Aviso de instalación:</b> 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.
</div>
"""
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 "<div style='color: #ef9a9a; font-weight: bold;'>Error geométrico. Valores producen impedancia irreal.</div>", ""
res_box_html = f"""
{warning_text}
<div style="background-color: rgba(122, 140, 74, 0.08); border: 2px solid #7a8c4a; border-radius: 12px; padding: 20px; text-align: center; font-family: 'Outfit', sans-serif;">
<span style="font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.1em; color: var(--sun); font-weight: 600;">Resistencia de Puesta a Tierra</span>
<div style="font-size: 3.2em; font-weight: 800; color: #7a8c4a; margin: 10px 0; font-family: monospace;">{R:.2f} <span style="font-size: 0.5em; font-weight: 600; color: var(--cream);">Ω</span></div>
<p style="font-size: 0.85em; margin: 0; opacity: 0.85; color: var(--cream);">Método: {tipo.split(' (')[0]}</p>
</div>
"""
details_html = f"""
<div style="border: 1px solid var(--walnut); border-radius: 12px; padding: 15px; font-family: 'Outfit', sans-serif; font-size: 0.9em; color: var(--cream);">
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid rgba(251,246,232,0.08);">
<span style="color: var(--text-muted);">Relación L/d calculada:</span>
<span style="font-weight: bold;">{relacion_ld_text}</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid rgba(251,246,232,0.08);">
<span style="color: var(--text-muted);">Aproximación simplificada Norma:</span>
<span style="font-weight: bold;">{aprox_text}</span>
</div>
<div style="margin-top: 15px;">
<span style="display: block; font-size: 0.8em; font-weight: 600; color: var(--sun); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px;">Fórmula Matemática Aplicada</span>
<div style="background: #120d09; border: 1.5px solid var(--walnut); padding: 10px; border-radius: 8px; font-family: 'JetBrains Mono', monospace; font-size: 0.85em; color: var(--cream);">
{formula_desc}
</div>
</div>
</div>
"""
return res_box_html, details_html
except Exception as e:
return f"<div style='color: #ef9a9a; font-weight: bold;'>Error en cálculo: {str(e)}</div>", ""
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 "<div style='color: #ef9a9a; font-weight: bold;'>Error: El tiempo total debe ser mayor a 0 segundos.</div>", ""
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"""
<div style="background: rgba(230, 168, 92, 0.08); border: 1.5px solid var(--copper); padding: 12px; border-radius: 8px; color: var(--sun); margin-bottom: 12px; font-size: 0.9em; font-family: 'Outfit', sans-serif;">
⚠️ <b>Aviso:</b> 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.
</div>
"""
res_box_html = f"""
{warning_text}
<div style="background-color: rgba(184, 85, 58, 0.08); border: 2px solid var(--rust); border-radius: 12px; padding: 20px; text-align: center; font-family: 'Outfit', sans-serif;">
<span style="font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.1em; color: var(--sun); font-weight: 600;">Potencia Activa Medida</span>
<div style="font-size: 3.2em; font-weight: 800; color: var(--sun); margin: 5px 0; font-family: monospace;">{watts:.1f} <span style="font-size: 0.5em; font-weight: 600; color: var(--cream);">W</span></div>
<div style="font-size: 1.3em; font-weight: 700; color: var(--text-muted); font-family: monospace;">{watts/1000.0:.3f} kW</div>
</div>
"""
details_html = f"""
<div style="border: 1px solid var(--walnut); border-radius: 12px; padding: 15px; font-family: 'Outfit', sans-serif; font-size: 0.9em; color: var(--cream);">
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid rgba(251,246,232,0.08);">
<span style="color: var(--text-muted);">Tiempo total acumulado (t):</span>
<span style="font-weight: bold;">{total_seconds:.2f} seg</span>
</div>
<div style="display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid rgba(251,246,232,0.08);">
<span style="color: var(--text-muted);">Frecuencia del disco/pulsos:</span>
<span style="font-weight: bold;">{rev_speed:.4f} rev/seg</span>
</div>
<div style="margin-top: 15px;">
<span style="display: block; font-size: 0.8em; font-weight: 600; color: var(--sun); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px;">Ecuación Utilizada</span>
<div style="background: #120d09; border: 1.5px solid var(--walnut); padding: 10px; border-radius: 8px; font-family: 'JetBrains Mono', monospace; font-size: 0.85em; color: var(--cream);">
{formula_str}
</div>
</div>
</div>
"""
return res_box_html, details_html
except Exception as e:
return f"<div style='color: #ef9a9a; font-weight: bold;'>Error en cálculo: {str(e)}</div>", ""
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"""
<g transform="translate({x}, {y})">
<path d="M 0 0 L 0 10" stroke="#8a6a48" stroke-width="2" />
<circle cx="0" cy="10" r="2.5" fill="#e6a85c" />
<circle cx="0" cy="40" r="2.5" fill="#e6a85c" />
<path d="M 0 40 L 0 50" stroke="#8a6a48" stroke-width="2" />
<path d="M 0 40 L -14 12" stroke="#e6a85c" stroke-width="2.5" stroke-linecap="round" />
<path d="M -4 32 L -9 34.5 L -12 28.5 L -7 26" stroke="#8a6a48" fill="none" stroke-width="1.5" stroke-linejoin="round" />
<path d="M -9 22 L -15 25" stroke="#8a6a48" fill="none" stroke-width="1.5" stroke-linecap="round" />
<path d="M -12 22 L -15 25 L -12.5 27" stroke="#8a6a48" fill="none" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<text x="14" y="30" class="text-md-val">{label_in}A</text>
<text x="-25" y="30" class="text-xs-lbl" text-anchor="end">{label}</text>
</g>
"""
def draw_id_svg(x, y, label_in, sensitivity, label="ID"):
return f"""
<g transform="translate({x}, {y})">
<path d="M 0 0 L 0 10" stroke="#8a6a48" stroke-width="2" />
<circle cx="0" cy="10" r="2.5" fill="#e6a85c" />
<circle cx="0" cy="40" r="2.5" fill="#e6a85c" />
<path d="M 0 40 L 0 50" stroke="#8a6a48" stroke-width="2" />
<path d="M 0 40 L -14 12" stroke="#e6a85c" stroke-width="2.5" stroke-linecap="round" />
<ellipse cx="0" cy="45" rx="8" ry="4" stroke="#8a6a48" fill="none" stroke-width="1.5" />
<path d="M -8 45 L -18 45 L -18 10 L -4 10" stroke="#8a6a48" fill="none" stroke-width="1" stroke-dasharray="2,2" />
<rect x="-6" y="8" width="4" height="4" fill="#8a6a48" />
<text x="18" y="25" class="text-md-val">{label_in}A</text>
<text x="18" y="40" class="text-xs-sens">{sensitivity}</text>
<text x="-25" y="30" class="text-xs-lbl" text-anchor="end">{label}</text>
</g>
"""
def draw_gm_svg(x, y, label_in, label="GM"):
return f"""
<g transform="translate({x}, {y})">
<path d="M 0 0 L 0 10" stroke="#8a6a48" stroke-width="2" />
<circle cx="0" cy="10" r="2.5" fill="#e6a85c" />
<circle cx="0" cy="40" r="2.5" fill="#e6a85c" />
<path d="M 0 40 L 0 50" stroke="#8a6a48" stroke-width="2" />
<path d="M 0 20 L -10 8" stroke="#e6a85c" stroke-width="2.5" stroke-linecap="round" />
<path d="M -5 14 L -18 14" stroke="#8a6a48" fill="none" stroke-width="1.5" stroke-dasharray="2,2" />
<rect x="-26" y="10" width="8" height="8" stroke="#8a6a48" fill="#120d09" stroke-width="1.5" />
<path d="M -26 14 L -18 14 M -22 10 L -22 18" stroke="#8a6a48" fill="none" stroke-width="1.5" />
<path d="M -29 14 L -26 14" stroke="#8a6a48" stroke-width="1.5" />
<rect x="-10" y="20" width="20" height="20" stroke="#8a6a48" fill="#120d09" stroke-width="1.5" />
<path d="M -10 30 L 10 30" stroke="#8a6a48" stroke-width="1.5" />
<path d="M 0 20 L 0 22 L 5 22 L 5 28 L 0 28 L 0 30" stroke="#8a6a48" fill="none" stroke-width="1.5" stroke-linejoin="round" />
<path d="M -5 33 L -5 37 M -7 33 L -3 33 M -7 37 L -3 37" stroke="#8a6a48" fill="none" stroke-width="1.2" stroke-linecap="round" />
<path d="M 2 33 L 6 35 L 2 37" stroke="#8a6a48" fill="none" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" />
<text x="18" y="30" class="text-md-val">{label_in}A</text>
<text x="-34" y="30" class="text-xs-lbl" text-anchor="end">{label}</text>
</g>
"""
svg = f"""<svg width="100%" height="auto" viewBox="0 0 {width} {height}" xmlns="http://www.w3.org/2000/svg" style="background-color: #120d09; border-radius: 12px; border: 1.5px solid var(--walnut); font-family: 'Outfit', sans-serif;">
<style>
.line {{ stroke: #8a6a48; stroke-width: 2.5; fill: none; }}
.busbar {{ stroke: #e6a85c; stroke-width: 5; stroke-linecap: round; }}
.text-xs-lbl {{ font-size: 11px; fill: #b89f88; font-weight: 500; }}
.text-xs-sens {{ font-size: 11px; fill: #e6a85c; font-weight: bold; }}
.text-sm-prop {{ font-size: 12px; fill: #fbf6e8; }}
.text-md-val {{ font-size: 14px; fill: #fbf6e8; font-weight: bold; }}
.text-lg-title {{ font-size: 16px; fill: #e6a85c; font-weight: bold; }}
</style>
"""
# Corte Gral
svg += f'<path d="M {center_x} 15 L {center_x} 35" class="line" />'
svg += draw_tm_svg(center_x, 35, general["In"], "Corte Gral.")
# Dif Gral
svg += f'<path d="M {center_x} 85 L {center_x} 105" class="line" />'
svg += draw_id_svg(center_x, 105, general["diff"], "300mA", "Dif. Gral.")
# Connection to busbar
svg += f'<path d="M {center_x} 155 L {center_x} {busbar_y}" class="line" />'
# Barra Colectora
if len(circ) > 1:
svg += f'<path d="M {start_x} {busbar_y} L {end_x} {busbar_y}" class="busbar" />'
else:
svg += f'<circle cx="{center_x}" cy="{busbar_y}" r="5" fill="#e6a85c" />'
# Circuitos
for i, c in enumerate(circ):
cx = start_x + i * spacing
svg += f'<path d="M {cx} {busbar_y} L {cx} 220" class="line" />'
if len(circ) > 1:
svg += f'<circle cx="{cx}" cy="{busbar_y}" r="4" fill="#e6a85c" />'
if c["es_motor"]:
svg += draw_id_svg(cx, 220, c["diff"], "30mA", "ID")
svg += f'<path d="M {cx} 270 L {cx} 290" class="line" />'
svg += draw_gm_svg(cx, 290, c["In"], "GM")
svg += f'<path d="M {cx} 340 L {cx} 390" class="line" />'
else:
svg += draw_tm_svg(cx, 220, c["In"], "TM")
svg += f'<path d="M {cx} 270 L {cx} 390" class="line" />'
svg += f'<circle cx="{cx}" cy="{390}" r="4" fill="#8a6a48" />'
svg += f'<path d="M {cx - 6} 396 L {cx + 6} 396 M {cx - 4} 401 L {cx + 4} 401 M {cx - 2} 406 L {cx + 2} 406" stroke="#8a6a48" stroke-width="1.5" stroke-linecap="round"/>'
svg += f'<text x="{cx}" y="425" text-anchor="middle" class="text-lg-title">C{i+1}</text>'
badge_w = 50
svg += f'<rect x="{cx - badge_w/2}" y="435" width="{badge_w}" height="20" fill="{c["css_bg"]}" rx="10" stroke="var(--walnut)" stroke-width="1" />'
svg += f'<text x="{cx}" y="449" text-anchor="middle" class="text-xs-sens" fill="var(--cream)">{c["tipo"]}</text>'
svg += f'<text x="{cx}" y="475" text-anchor="middle" class="text-sm-prop" font-weight="bold">{c["polos"]}x{c["cable"]["s"]} mm²</text>'
svg += f'<text x="{cx}" y="492" text-anchor="middle" class="text-xs-lbl">{c["dpms"]:.0f} VA</text>'
svg += f'<text x="{cx}" y="508" text-anchor="middle" class="text-xs-lbl" style="fill:#e6a85c;">Fase: {c["fase"]}</text>'
svg += "</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 "<div style='color: var(--sun); font-weight: bold;'>Por favor, agregue algún elemento o bocas de circuito para comenzar.</div>", ""
# 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"""
<div style="grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); display: grid; gap: 15px; margin-bottom: 20px; font-family: 'Outfit', sans-serif;">
<div style="background: rgba(230, 168, 92, 0.04); border: 1.5px solid var(--walnut); padding: 15px; border-radius: 12px; text-align: center;">
<span style="font-size: 0.85em; text-transform: uppercase; color: var(--sun);">Potencia Simultánea (DPMS)</span>
<div style="font-size: 2.1em; font-weight: 800; color: var(--sun); margin: 5px 0; font-family: monospace;">{dpms_general:.0f} <span style="font-size: 0.6em; font-weight: 600; color: var(--cream);">VA</span></div>
<p style="font-size: 0.85em; margin: 0; opacity: 0.85; color: var(--cream);">Coeficiente de simultaneidad: <b>{coef:.2f}</b></p>
</div>
<div style="background: rgba(230, 168, 92, 0.04); border: 1.5px solid var(--walnut); padding: 15px; border-radius: 12px; text-align: center;">
<span style="font-size: 0.85em; text-transform: uppercase; color: var(--sun);">Alimentación General</span>
<div style="font-size: 1.6em; font-weight: 800; color: var(--sun); margin: 8px 0;">{'Trifásica (380V)' if is_trifasico else 'Monofásica (220V)'}</div>
<p style="font-size: 0.85em; margin: 0; opacity: 0.9; color: #ef9a9a;"><b>{f"Forzado: {force_reason}" if (has_tri_motor or over_63A) else ""}</b></p>
</div>
</div>
"""
tbody_rows = ""
tBocas = 0
for idx, c in enumerate(circuits):
tBocas += c["b"]
if c["es_motor"]:
prot_html = f"""
<div style="font-weight: bold; color: var(--cream);">GM: {c['polos']}x{c['In']}A</div>
<div style="font-size: 0.8em; color: var(--sun); font-weight: 500;">ID: {c['polos']}x{c['diff']}A (30mA)</div>
"""
else:
prot_html = f"<div style='font-weight: bold; color: var(--cream);'>TM: {c['polos']}x{c['In']}A</div>"
tbody_rows += f"""
<tr style="border-bottom: 1px solid rgba(251,246,232,0.08); font-family: 'Outfit', sans-serif;">
<td style="padding: 12px; font-weight: 600; color: var(--cream);">C{idx+1}</td>
<td style="padding: 12px; text-align: left;">
<span style="display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.8em; font-weight: bold; background-color: {c['css_bg']}; color: var(--cream);">{c['tipo']}</span>
<span style="margin-left: 8px; font-size: 0.9em; opacity: 0.85; color: var(--cream);">{c['desc']}</span>
</td>
<td style="padding: 12px; color: var(--cream);">{c['b']}</td>
<td style="padding: 12px; font-family: monospace; color: var(--cream);">{c['dpms']:.0f}</td>
<td style="padding: 12px; font-family: monospace; color: var(--cream);">{c['Ib']:.2f}</td>
<td style="padding: 12px;">{prot_html}</td>
<td style="padding: 12px;">
<div style="font-weight: bold; color: var(--cream);">{c['polos']}x{c['cable']['s']} mm²</div>
<div style="font-size: 0.8em; color: #7a8c4a;">Iz: {c['cable']['iz']}A</div>
</td>
</tr>
"""
tbody_rows += f"""
<tr style="border-top: 2px solid var(--walnut); background-color: rgba(251,246,232,0.03); font-family: 'Outfit', sans-serif; font-weight: bold;">
<td colspan="2" style="padding: 14px; text-align: right; text-transform: uppercase; font-size: 0.9em; color: var(--sun);">Línea Principal ({'Trifásica' if is_trifasico else 'Monofásica'})</td>
<td style="padding: 14px; color: var(--cream);">{tBocas}</td>
<td style="padding: 14px; font-family: monospace; color: var(--sun);">{dpms_general:.0f}</td>
<td style="padding: 14px; font-family: monospace; color: var(--sun);">{tIb:.2f}</td>
<td style="padding: 14px;">
<div style="font-size: 1.1em; color: var(--sun);">TM: {tPolos}x{tIn}A</div>
<div style="font-size: 0.9em; color: var(--rust); margin-top: 2px;">ID: {tPolos}x{tDiff}A (300mA)</div>
</td>
<td style="padding: 14px;">
<div style="font-size: 1.1em; color: #7a8c4a;">{tPolos}x{tCable['s']} mm²</div>
<div style="font-size: 0.85em; color: #7a8c4a; opacity: 0.8;">Iz: {tCable['iz']}A</div>
</td>
</tr>
"""
planilla_html = f"""
<div style="border: 2px solid var(--walnut); border-radius: 12px; overflow: hidden; font-family: 'Outfit', sans-serif; margin-top: 25px; background-color: #1a140f;">
<table style="width: 100%; border-collapse: collapse; font-size: 0.9em; text-align: center; color: var(--cream);">
<thead>
<tr style="background-color: rgba(251, 246, 232, 0.05); border-bottom: 2px solid var(--walnut); color: var(--sun);">
<th style="padding: 12px; font-weight: 600;">Circ.</th>
<th style="padding: 12px; font-weight: 600; text-align: left;">Tipo / Destino</th>
<th style="padding: 12px; font-weight: 600;">Bocas</th>
<th style="padding: 12px; font-weight: 600;">Potencia (VA)</th>
<th style="padding: 12px; font-weight: 600;">Ib (A)</th>
<th style="padding: 12px; font-weight: 600;">Protección</th>
<th style="padding: 12px; font-weight: 600;">Conductor / Iz</th>
</tr>
</thead>
<tbody>
{tbody_rows}
</tbody>
</table>
</div>
"""
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"<div style='color: #ef9a9a; font-weight: bold;'>Error en diseño de tableros: {str(e)}</div>", "", ""
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("""
<div style="text-align: center; margin-bottom: 25px; border-bottom: 2px double #8a6a48; padding-bottom: 15px; font-family: 'Outfit', sans-serif;">
<h1 style="color: #e6a85c; font-family: 'Outfit', sans-serif; margin: 0; font-size: 2.2em; font-weight: 700; letter-spacing: -0.02em;">⚡ ArgenVolt</h1>
<p style="font-style: italic; color: #fbf6e8; opacity: 0.95; margin: 5px 0 0 0; font-size: 0.95em;">Auditor y Consultor del Reglamento Eléctrico Argentino</p>
</div>
""")
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()