| 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 |
|
|
| |
| |
| |
|
|
| |
| 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)." |
| } |
| } |
|
|
| |
| |
| 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} |
| } |
|
|
| |
| 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 |
| } |
|
|
| |
| |
| |
|
|
| 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 |
| |
| 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 [] |
|
|
| |
| pages_771 = get_markdown_pages(MD_771_PATH) |
| pages_701 = get_markdown_pages(MD_701_PATH) |
|
|
| import torch |
| import numpy as np |
|
|
| |
| device = "cuda" if torch.cuda.is_available() else "cpu" |
| print(f"Dispositivo detectado para ejecución local: {device}") |
|
|
| |
| 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) |
| |
| 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 |
|
|
| |
| 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) |
|
|
| |
| 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") |
| |
| |
| 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, |
| ) |
| |
| |
| 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) |
| |
| |
| 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 = [] |
| |
| |
| 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)) |
|
|
| |
| 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 |
| |
| |
| for word in query_words: |
| if word in norm_text: |
| count = norm_text.count(word) |
| lexical_score += 3 + min(count, 5) * 0.5 |
| |
| |
| for phrase, length in query_phrases: |
| if phrase in norm_text: |
| lexical_score += 15 * length |
| |
| |
| clean_query = re.sub(r'[¿?¡!()]', '', norm_query).strip() |
| if len(clean_query) > 5 and clean_query in norm_text: |
| lexical_score += 50 |
|
|
| |
| 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 + semantic_score |
| |
| |
| if total_score < 15: |
| return 0 |
| |
| return round(total_score, 2) |
|
|
| |
| for p in pages_771: |
| score = score_page(p) |
| if score > 0: |
| scored_results.append((score, 771, p['page'], p['text'])) |
| |
| |
| 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] |
|
|
| |
| |
| |
|
|
| def audit_circuit(circuit_type, section, protection, bocas, grouped_circuits): |
| |
| 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) |
| |
| |
| 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²)." |
| |
| |
| 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})." |
| |
| |
| 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)." |
| |
| |
| |
| capacity = CABLE_CURRENT_CAPACITY.get(section, {"2x": 0, "3x": 0}) |
| |
| base_current = capacity["2x"] |
| |
| |
| group_factor = GROUPING_FACTORS.get(grouped_circuits, 0.50) |
| allowed_current_corrected = round(base_current * group_factor, 2) |
| |
| |
| 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" |
| |
| |
| 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) |
| |
| |
| zone = "Zona 3" |
| |
| |
| if dist_horizontal == 0 and height == 0: |
| zone = "Zona 0" |
| |
| elif dist_horizontal == 0 and height <= 225: |
| zone = "Zona 1" |
| |
| elif dist_horizontal <= 60 and height <= 225: |
| zone = "Zona 2" |
| |
| elif dist_horizontal > 60 and dist_horizontal <= 240 and height <= 225: |
| zone = "Zona 3" |
| else: |
| |
| zone = "Fuera de volumen de peligro" |
|
|
| |
| 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) |
| |
| |
| la = covered + (semi * 0.5) |
| |
| |
| 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 (<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 (>=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): |
| |
| 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'." |
| |
| |
| 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" |
| |
| |
| if llm_model is not None and llm_tokenizer is not None: |
| try: |
| |
| if torch.cuda.is_available(): |
| llm_model.to("cuda") |
| |
| |
| 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") |
| |
| |
| 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()} |
| |
| |
| 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) |
| |
| |
| 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 |
|
|
|
|
|
|
| |
| |
| |
|
|
| 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> |
| """ |
| |
| |
| 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" />' |
| |
| |
| 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" />' |
| |
| |
| svg += f'<path d="M {ox} {oy} L {px} {py}" class="line-active" />' |
| |
| |
| 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" />' |
| |
| |
| 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" />' |
| |
| |
| svg += f'<path d="M {px + 6} {qy_original} L {px + 6} {qy_compensated}" class="line-cap" />' |
| |
| |
| 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>' |
| |
| |
| 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> |
| """ |
| |
| |
| 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.") |
| |
| |
| 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.") |
| |
| |
| svg += f'<path d="M {center_x} 155 L {center_x} {busbar_y}" class="line" />' |
| |
| |
| 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" />' |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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 |
| |
| |
| 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>", "" |
| |
| |
| 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) |
|
|
| |
| |
| |
|
|
|
|
| |
| css_custom = "" |
| if os.path.exists("style.css"): |
| with open("style.css", "r", encoding="utf-8") as f: |
| css_custom = f.read() |
| else: |
| |
| 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(): |
| |
| 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"]) |
| |
| |
| with gr.Column(scale=4, elem_id="content-panel"): |
| |
| |
| 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 |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| 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] |
| ) |
| |
| |
| |
| 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]") |
| |
| |
| 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]") |
| |
| |
| 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]") |
| |
| |
| 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] |
| ) |
| |
| |
| 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] |
| ) |
| |
| |
| 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 |
| ) |
| |
| |
| 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?" |
| ] |
| ) |
|
|
| |
| 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() |