| import os |
| import re |
| import json |
| import subprocess |
| import gradio as gr |
|
|
| |
| |
| |
|
|
| |
| 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="cpu") |
| print("Modelo de embeddings cargado correctamente.") |
| except Exception as e: |
| print(f"Error al cargar el modelo de embeddings: {e}") |
|
|
| def get_embeddings_for_pages(pages, cache_embeddings_path, emb_model): |
| """Carga los embeddings desde el caché o los genera si no existen.""" |
| if not emb_model: |
| return pages |
| |
| if os.path.exists(cache_embeddings_path): |
| try: |
| with open(cache_embeddings_path, 'r', encoding='utf-8') as f: |
| cached_data = json.load(f) |
| for p in pages: |
| p_str = str(p['page']) |
| if p_str in cached_data: |
| p['embedding'] = cached_data[p_str] |
| print(f"Cargados embeddings desde caché {cache_embeddings_path}") |
| return pages |
| except Exception as e: |
| print(f"Error al leer caché de embeddings {cache_embeddings_path}: {e}") |
| |
| print(f"Generando embeddings y guardando en {cache_embeddings_path} (esto puede tardar un momento)...") |
| texts = [p['text'] for p in pages if p.get('text')] |
| if texts: |
| embeddings = emb_model.encode(texts, show_progress_bar=True) |
| |
| 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_model = None |
| try: |
| from huggingface_hub import hf_hub_download |
| from llama_cpp import Llama |
| |
| print("Descargando/Verificando modelo Gemma 4 12B Instruct GGUF...") |
| |
| model_path = hf_hub_download( |
| repo_id="bartowski/gemma-4-12B-it-GGUF", |
| filename="gemma-4-12B-it-Q4_K_M.gguf" |
| ) |
| print(f"Modelo GGUF descargado en: {model_path}") |
| |
| print("Cargando modelo GGUF en llama.cpp...") |
| |
| n_gpu = -1 if device == "cuda" else 0 |
| llm_model = Llama( |
| model_path=model_path, |
| n_ctx=16384, |
| n_gpu_layers=n_gpu, |
| verbose=False |
| ) |
| print("Modelo Gemma 4 12B Instruct GGUF cargado correctamente mediante llama.cpp.") |
| except Exception as e: |
| print(f"No se pudo cargar el LLM local con llama.cpp ({e}). El consultor usará el motor de búsqueda directa.") |
|
|
| def normalize_text(text): |
| if not text: |
| return "" |
| text = text.lower() |
| replacements = { |
| 'á': 'a', 'é': 'e', 'í': 'i', 'ó': 'o', 'ú': 'u', 'ü': 'u', |
| 'ñ': 'ñ' |
| } |
| for k, v in replacements.items(): |
| text = text.replace(k, v) |
| return text |
|
|
| def local_search(query, num_results=3): |
| """Busca en el texto de los reglamentos usando coincidencia híbrida (léxica + semántica).""" |
| norm_query = normalize_text(query) |
| raw_words = re.findall(r'\w+', norm_query) |
| |
| |
| stopwords = { |
| "que", "es", "el", "la", "los", "las", "un", "una", "unos", "unas", |
| "de", "del", "al", "en", "para", "por", "con", "sin", "sobre", "entre", |
| "este", "esta", "estos", "estas", "eso", "esa", "esos", "esas", "como", |
| "cual", "cuales", "como", "donde", "cuando", "quien", "quienes", "que", |
| "y", "o", "u", "e", "mas", "pero", "sino", "aunque", "hacer", "hace", |
| "ser", "sido", "estar", "tiene", "tienen", "debe", "deben", "se", "del" |
| } |
| |
| query_words = [w for w in raw_words if len(w) > 2 and w not in stopwords] |
| if not query_words: |
| query_words = [w for w in raw_words if len(w) > 2] |
| |
| unique_query_words = list(set(query_words)) |
| |
| |
| SYNONYMS = { |
| "tierra": ["tierra", "proteccion", "pe", "pat", "bpt", "jabalina"], |
| "neutro": ["neutro", "celeste", "azul"], |
| "fase": ["fase", "castaño", "marron", "negro", "rojo", "linea"], |
| "bocas": ["boca", "bocas"], |
| "tug": ["tug", "t.u.g."], |
| "iug": ["iug", "i.u.g."], |
| "tue": ["tue", "t.u.e."], |
| "iue": ["iue", "i.u.e."] |
| } |
| |
| |
| query_emb = None |
| if embedding_model is not None: |
| try: |
| query_emb = embedding_model.encode(query, convert_to_numpy=True) |
| except Exception as e: |
| print(f"Error al codificar la consulta '{query}': {e}") |
| |
| scored_results = [] |
| |
| def score_page(p): |
| page_text = p['text'] |
| norm_text = normalize_text(page_text) |
| |
| |
| matched_unique_words = 0 |
| lexical_score = 0 |
| |
| for word in unique_query_words: |
| syns = SYNONYMS.get(word, [word]) |
| word_matched = False |
| for syn in syns: |
| |
| pattern = r'\b' + re.escape(syn) + r'(?:es|s)?\b' |
| matches = re.findall(pattern, norm_text) |
| if matches: |
| word_matched = True |
| count = len(matches) |
| lexical_score += 5 + min(count, 5) * 1.0 |
| break |
| if word_matched: |
| matched_unique_words += 1 |
| |
| |
| coordination_bonus = 0 |
| if len(unique_query_words) > 0: |
| overlap_ratio = matched_unique_words / len(unique_query_words) |
| if overlap_ratio == 1.0: |
| coordination_bonus = 100.0 |
| elif overlap_ratio >= 0.75: |
| coordination_bonus = 50.0 |
| elif overlap_ratio >= 0.5: |
| coordination_bonus = 20.0 |
| |
| |
| exact_bonus = 0 |
| clean_query = re.sub(r'[¿?¡!()]', '', norm_query).strip() |
| clean_query_escaped = re.escape(clean_query) |
| if len(clean_query) > 6 and re.search(r'\b' + clean_query_escaped + r'\b', norm_text): |
| exact_bonus = 50.0 |
|
|
| |
| semantic_score = 0 |
| if query_emb is not None and 'embedding' in p: |
| page_emb = np.array(p['embedding']) |
| dot_product = np.dot(query_emb, page_emb) |
| norm_q = np.linalg.norm(query_emb) |
| norm_p = np.linalg.norm(page_emb) |
| if norm_q * norm_p > 0: |
| cosine_sim = dot_product / (norm_q * norm_p) |
| semantic_score = float(cosine_sim) * 100.0 |
|
|
| total_score = lexical_score + coordination_bonus + exact_bonus + semantic_score |
| |
| |
| if matched_unique_words == 0 and semantic_score < 30: |
| 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, lang="Español"): |
| |
| rules = CIRCUIT_RULES.get(circuit_type) |
| if not rules: |
| return "Error: Invalid circuit type." if lang == "English" else "Error: Tipo de circuito no válido." |
| |
| section = float(section) |
| protection = int(protection) |
| bocas = int(bocas) |
| grouped_circuits = int(grouped_circuits) |
| |
| |
| min_sec = rules["min_section"] |
| sec_ok = section >= min_sec |
| if lang == "English": |
| sec_msg = f"✅ Section of {section} mm² meets the minimum section of {min_sec} mm²." if sec_ok else f"❌ The mandatory minimum section for {circuit_type} circuits is {min_sec} mm² (you chose {section} mm²)." |
| else: |
| sec_msg = f"✅ Sección de {section} mm² cumple con la sección mínima de {min_sec} mm²." if sec_ok else f"❌ La sección mínima obligatoria para circuitos {circuit_type} es de {min_sec} mm² (elegiste {section} mm²)." |
| |
| |
| max_b = rules["max_bocas"] |
| bocas_ok = bocas <= max_b |
| if lang == "English": |
| bocas_msg = f"✅ Number of outlets ({bocas}) within the regulatory limit (max {max_b})." if bocas_ok else f"❌ Excess outlets in the circuit. The standard specifies a maximum of {max_b} outlets per {circuit_type} circuit (you chose {bocas})." |
| else: |
| bocas_msg = f"✅ Cantidad de bocas ({bocas}) dentro del límite reglamentario (máx {max_b})." if bocas_ok else f"❌ Exceso de bocas en el circuito. La norma especifica un máximo de {max_b} bocas por circuito {circuit_type} (elegiste {bocas})." |
| |
| |
| max_p = rules["max_protection"] |
| prot_rule_ok = protection <= max_p |
| if lang == "English": |
| prot_rule_msg = f"✅ Breaker rating ({protection}A) meets the maximum limit of {max_p}A for {circuit_type}." if prot_rule_ok else f"❌ The maximum protection for {circuit_type} circuits is {max_p}A (you chose {protection}A)." |
| else: |
| prot_rule_msg = f"✅ Calibre de la térmica ({protection}A) cumple con el límite máximo de {max_p}A para {circuit_type}." if prot_rule_ok else f"❌ La protección máxima para circuitos {circuit_type} es de {max_p}A (elegiste {protection}A)." |
| |
| |
| |
| 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 lang == "English": |
| if coordination_ok: |
| coord_msg = f"✅ **CORRECT PROTECTION COORDINATION:** The allowable cable current ({allowed_current_corrected}A, corrected for {grouped_circuits} grouped circuits) is greater than or equal to the breaker rating ({protection}A). The cable is protected against overloads." |
| status_box = "APPROVED - COMPLIES WITH REGULATIONS" |
| status_color = "green" |
| else: |
| coord_msg = f"⚠️ **FIRE HAZARD!:** The breaker of {protection}A is greater than the maximum allowable current supported by the cable ({allowed_current_corrected}A) with {grouped_circuits} circuits in the same conduit. **The cable could melt before the breaker trips.** The breaker must be reduced to maximum {int(allowed_current_corrected)}A or the conductor section must be increased." |
| status_box = "REJECTED - OVERLOAD RISK" |
| status_color = "red" |
| else: |
| if coordination_ok: |
| coord_msg = f"✅ **COORDINACIÓN DE PROTECCIÓN CORRECTA:** La corriente admisible del cable ({allowed_current_corrected}A, corregida por {grouped_circuits} circuitos agrupados) es mayor o igual al calibre de la térmica ({protection}A). El cable está protegido contra sobrecargas." |
| status_box = "APROBADO - CUMPLE LA REGLAMENTACIÓN" |
| status_color = "green" |
| else: |
| coord_msg = f"⚠️ **¡PELIGRO DE INCENDIO!:** La térmica de {protection}A es mayor que la corriente admisible máxima que soporta el cable ({allowed_current_corrected}A) con {grouped_circuits} circuitos en la misma cañería. **El cable podría derretirse antes de que salte la térmica.** Se debe reducir la térmica a máximo {int(allowed_current_corrected)}A o aumentar la sección del conductor." |
| status_box = "RECHAZADO - RIESGO DE SOBRECARGA" |
| status_color = "red" |
| |
| |
| if lang == "English": |
| 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>Technical Calculation (Table 771.16.I and II.b):</b><br/> |
| - Base allowable current of {section} mm² cable: <b>{base_current}A</b>.<br/> |
| - Grouping reduction factor ({grouped_circuits} circuit/s in conduit): <b>x{group_factor}</b>.<br/> |
| - Corrected maximum allowable current ($I_z$): <b>{allowed_current_corrected}A</b>.<br/> |
| - Chosen circuit breaker ($I_n$): <b>{protection}A</b>. |
| </p> |
| </div> |
| """ |
| else: |
| 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, lang="Español"): |
| dist_horizontal = float(dist_horizontal) |
| height = float(height) |
| |
| |
| elem_mapping = { |
| "Socket-outlet": "Tomacorriente", |
| "Switch": "Interruptor", |
| "Common luminaire": "Luminaria común", |
| "Water heater": "Termotanque" |
| } |
| elem_type = elem_mapping.get(elem_type, elem_type) |
| |
| |
| 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)." if lang != "English" else "It is strictly forbidden to place water heaters inside the bathtub or shower space (Zone 0)." |
| else: |
| safety_rule = "Sólo se permiten equipos de Muy Baja Tensión de Seguridad (MBTS) de hasta 12V alternos, con fuente de seguridad fuera del baño." if lang != "English" else "Only Safety Extra-Low Voltage (SELV) equipment up to 12V AC is permitted, with safety source outside the bathroom." |
| |
| elif zone == "Zona 1": |
| req_ip = "IPX5" |
| if elem_type == "Termotanque": |
| permitted = True |
| safety_rule = "Se permite únicamente si es un calefón eléctrico o termotanque de fijación fija con alimentación dedicada y protección de disyuntor de 30mA." if lang != "English" else "Permitted only if it is a fixed electrical water heater with dedicated supply and 30mA residual current protection." |
| elif elem_type == "Luminaria común": |
| safety_rule = "Solo se permiten luminarias fijas protegidas por MBTS (hasta 12V) con fuente instalada en Zona 3 o fuera del baño." if lang != "English" else "Only fixed luminaires protected by SELV (up to 12V) are permitted, with source installed in Zone 3 or outside the bathroom." |
| else: |
| safety_rule = "Prohibido instalar interruptores o tomacorrientes en Zona 1. Tampoco se permite el paso de cañerías ajenas a este volumen." if lang != "English" else "Forbidden to install switches or outlets in Zone 1. Conduits unrelated to this volume are also not allowed to pass through." |
| |
| elif zone == "Zona 2": |
| req_ip = "IPX4" |
| if elem_type == "Tomacorriente": |
| safety_rule = "No se permiten tomacorrientes comunes. Solo se permite el tomacorriente para máquinas de afeitar alimentado por transformador de aislación (según IRAM 2445)." if lang != "English" else "Common outlets are not permitted. Only shaver outlets powered by an isolation transformer (complying with IRAM 2445) are allowed." |
| elif elem_type == "Interruptor": |
| safety_rule = "No se permiten llaves de luz comunes. Deben estar fuera de esta zona o activarse por piolín/remoto de material aislante." if lang != "English" else "Common light switches are not permitted. They must be outside this zone or activated by insulating pull-cord/remote." |
| elif elem_type == "Luminaria común": |
| permitted = True |
| safety_rule = "Permitido luminarias clase II con grado de protección IPX4 o mayor." if lang != "English" else "Permitted Class II luminaires with protection degree IPX4 or higher." |
| else: |
| permitted = True |
| safety_rule = "Permitido termotanques y calentadores con IPX4 fijos." if lang != "English" else "Permitted fixed water heaters and heaters with IPX4." |
| |
| elif zone == "Zona 3": |
| req_ip = "IPX1" |
| permitted = True |
| safety_rule = "Se permiten tomacorrientes, interruptores y luminarias comunes, SIEMPRE que cuenten con protección diferencial (disyuntor de hasta 30mA) y puesta a tierra obligatoria." if lang != "English" else "Common outlets, switches, and luminaires are permitted, PROVIDED they have residual current protection (breaker up to 30mA) and mandatory grounding." |
| |
| else: |
| permitted = True |
| safety_rule = "Fuera de la zonificación restrictiva del baño. Se aplican las reglas comunes de interiores de viviendas." if lang != "English" else "Outside the restrictive bathroom zoning. Common indoor residential rules apply." |
|
|
| |
| if lang == "English": |
| zone_trans = { |
| "Zona 0": "Zone 0", |
| "Zona 1": "Zone 1", |
| "Zona 2": "Zone 2", |
| "Zona 3": "Zone 3", |
| "Fuera de volumen de peligro": "Outside danger volume" |
| } |
| zone = zone_trans.get(zone, zone) |
| status_str = "COMPLIES WITH REGULATIONS" if permitted else "DOES NOT COMPLY WITH REGULATIONS" |
| location_lbl = "Location" |
| status_lbl = "Status" |
| min_ip_lbl = "Minimum required protection rating (IP)" |
| exp_lbl = "Argentine Electrical Code Explanation (Section 701)" |
| else: |
| status_str = "CUMPLE REGLAMENTO" if permitted else "NO CUMPLE REGLAMENTO" |
| location_lbl = "Ubicación" |
| status_lbl = "Estado" |
| min_ip_lbl = "Índice de protección mínimo requerido" |
| exp_lbl = "Explicación del Reglamento Eléctrico Argentino (Sección 701)" |
| |
| color = "#2e7d32" if permitted else "#c62828" |
| |
| html_res = f""" |
| <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;">{location_lbl}: {zone}</h3> |
| <p><b>{status_lbl}:</b> <span style="color:{color}; font-weight:bold;">{status_str}</span></p> |
| <p><b>{min_ip_lbl}:</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>{exp_lbl}:</b><br/>{safety_rule}</p> |
| </div> |
| """ |
| return html_res |
|
|
| def calc_electrification(covered_m2, semi_covered_m2, lang="Español"): |
| 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: |
| if lang == "English": |
| grade = "MINIMUM" |
| circuits_desc = "Minimum 2 circuits: 1 of General Use Lighting (IUG) and 1 of General Use Outlets (TUG)." |
| points_of_utilization = """ |
| <ul> |
| <li><b>Living / Dining:</b> 1 lighting outlet (IUG) and 1 socket outlet (TUG) per 6m² (minimum 2).</li> |
| <li><b>Bedroom (<10m²):</b> 1 IUG outlet and 2 TUG outlets.</li> |
| <li><b>Kitchen:</b> 1 IUG outlet and 3 TUG outlets + outlets for extractor and refrigerator.</li> |
| <li><b>Bathroom:</b> 1 IUG outlet and 1 TUG outlet (outside Zone 2).</li> |
| <li><b>Hallway / Vestibule:</b> 1 IUG outlet and 1 TUG outlet per 5m of length.</li> |
| </ul> |
| """ |
| else: |
| grade = "MÍNIMO" |
| circuits_desc = "Mínimo 2 circuitos: 1 de Iluminación de Uso General (IUG) y 1 de Tomacorrientes de Uso General (TUG)." |
| points_of_utilization = """ |
| <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> |
| """ |
| circuits_num = 2 |
| elif la <= 130: |
| if lang == "English": |
| grade = "MEDIUM" |
| circuits_desc = "Minimum 3 circuits: Various combinations allowed. Recommended: 1 IUG + 1 TUG + 1 free circuit (IUG, TUG, IUE, or TUE)." |
| points_of_utilization = """ |
| <ul> |
| <li><b>Living / Dining:</b> 1 IUG outlet and 1 TUG outlet per 6m² (minimum 3).</li> |
| <li><b>Bedroom (>=10m²):</b> 1 IUG outlet and 3 TUG outlets.</li> |
| <li><b>Kitchen:</b> 2 IUG outlets and 3 TUG outlets + specific outlets (minimum 3 independent outlets).</li> |
| <li><b>Bathroom:</b> 1 IUG outlet and 1 TUG outlet.</li> |
| <li><b>Hallway / Vestibule:</b> 1 IUG outlet and 1 TUG outlet per 5m.</li> |
| </ul> |
| """ |
| else: |
| grade = "MEDIO" |
| circuits_desc = "Mínimo 3 circuitos: Varias combinaciones permitidas. Ejemplo recomendado: 1 IUG + 1 TUG + 1 circuito libre (puede ser IUG, TUG, IUE o TUE)." |
| points_of_utilization = """ |
| <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> |
| """ |
| circuits_num = 3 |
| elif la <= 200: |
| if lang == "English": |
| grade = "HIGH" |
| circuits_desc = "Minimum 5 circuits: 2 IUG + 2 TUG + 1 Special Use circuit (IUE or TUE)." |
| points_of_utilization = """ |
| <ul> |
| <li><b>Living / Dining:</b> 1 IUG outlet per 18m² and 1 TUG outlet per 6m² (minimum 4).</li> |
| <li><b>Bedrooms:</b> 1 IUG outlet and 3 TUG outlets.</li> |
| <li><b>Kitchen:</b> 2 IUG outlets and 4 TUG outlets + special outlets.</li> |
| <li><b>Bathroom:</b> 1 IUG outlet and 2 TUG outlets.</li> |
| <li><b>Wide hallways and laundry room:</b> dedicated TUG and IUG outlets.</li> |
| </ul> |
| """ |
| else: |
| grade = "ELEVADO" |
| 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> |
| """ |
| circuits_num = 5 |
| else: |
| if lang == "English": |
| grade = "SUPERIOR" |
| circuits_desc = "Minimum 6 circuits: 2 IUG + 2 TUG + 2 Special Use circuits (IUE and/or TUE)." |
| points_of_utilization = """ |
| <ul> |
| <li><b>Living / Dining:</b> Abundant outlets. Minimum 1 IUG outlet per 18m² and 1 TUG outlet per 6m² (minimum 5).</li> |
| <li><b>Bedrooms:</b> 1 IUG outlet and 4 TUG outlets.</li> |
| <li><b>Kitchen:</b> Abundant IUG and TUG outlets (minimum 4 general socket outlets).</li> |
| <li><b>Bathroom:</b> 1 IUG outlet and 2 TUG outlets.</li> |
| </ul> |
| """ |
| else: |
| grade = "SUPERIOR" |
| circuits_desc = "Mínimo 6 circuitos: 2 IUG + 2 TUG + 2 circuitos de Uso Especial (IUE y/o TUE)." |
| points_of_utilization = """ |
| <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> |
| """ |
| circuits_num = 6 |
| |
| if lang == "English": |
| title_lbl = "Electrification Degree" |
| limit_lbl = "Calculated Application Limit (LA)" |
| min_circ_lbl = "Minimum number of mandatory circuits" |
| config_lbl = "Required configuration" |
| points_lbl = "Minimum Points of Utilization (Minimum outlets per room)" |
| area_details = f"Covered: {covered} m², Semi-covered at 50%: {semi*0.5} m²" |
| else: |
| title_lbl = "Grado de Electrificación" |
| limit_lbl = "Límite de Aplicación calculado (LA)" |
| min_circ_lbl = "Número mínimo de circuitos obligatorios" |
| config_lbl = "Configuración requerida" |
| points_lbl = "Puntos Mínimos de Utilización (Bocas mínimas por ambiente)" |
| area_details = f"Cubierta: {covered} m², Semicubierta al 50%: {semi*0.5} m²" |
| |
| html_res = f""" |
| <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;">{title_lbl}: {grade}</h3> |
| <p><b>{limit_lbl}:</b> {la} m² ({area_details})</p> |
| <p><b>{min_circ_lbl}:</b> <b>{circuits_num}</b></p> |
| <p style="font-size: 0.95em; line-height: 1.5; color: #fbf6e8;"><b>{config_lbl}:</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;">{points_lbl}:</h4> |
| <div style="font-size: 0.9em; line-height: 1.5; color: #fbf6e8;">{points_of_utilization}</div> |
| </div> |
| """ |
| return html_res |
|
|
| def rag_query_response(message, history, lang="Español"): |
| |
| matches = local_search(message, num_results=3) |
| |
| if not matches: |
| if lang == "English": |
| return "I did not find relevant articles in the Argentine Electrical Code regarding that topic. Try using keywords like 'cables', 'bathroom', 'outlets', 'grounding', 'RCD', or 'breaker'." |
| else: |
| return "No encontré pasajes relevantes en el Reglamento Eléctrico Argentino sobre ese tema. Probá usando palabras clave como 'cables', 'baño', 'bocas', 'toma', 'disyuntor' o 'térmica'." |
| |
| |
| context_text = "" |
| for score, doc_id, page, text in matches: |
| if lang == "English": |
| doc_name = "Section 771 (Housing)" if doc_id == 771 else "Section 701 (Bathrooms)" |
| else: |
| doc_name = "Sección 771 (Viviendas)" if doc_id == 771 else "Sección 701 (Baños)" |
| context_text += f"\n--- {doc_name} - Página {page} (Relevancia: {score}) ---\n{text}\n" |
| |
| |
| if llm_model is not None: |
| try: |
| |
| if lang == "English": |
| system_content = "You are an expert electrical engineering assistant specializing in the Argentine Electrical Code (AEA Sections 771 and 701). Answer the user's question using only the provided regulatory context. If the context does not contain the answer, state it clearly. Always mention the document and the page from which you obtained the response. Respond in English." |
| else: |
| system_content = "Eres un asistente de ingeniería eléctrica experto en el Reglamento Eléctrico Argentino (Secciones 771 y 701). Responde a la pregunta del usuario utilizando únicamente el contexto provisto del reglamento. Si el contexto no contiene la información para responder, indícalo claramente. Menciona siempre el documento y la página de donde obtienes la respuesta. Responde en español." |
| |
| response = llm_model.create_chat_completion( |
| messages=[ |
| { |
| "role": "system", |
| "content": system_content |
| }, |
| { |
| "role": "user", |
| "content": f"Contexto normativo extraído:\n{context_text}\n\nPregunta: {message}" |
| } |
| ], |
| max_tokens=800, |
| temperature=0.2 |
| ) |
| response_text = response["choices"][0]["message"]["content"] |
| return response_text.strip() |
| except Exception as e: |
| print("Error en inferencia de modelo local con llama.cpp:", e) |
| |
| |
| if lang == "English": |
| response = "🤖 *Local RAG query motor (Direct search mode):*\n\n" |
| response += "I found the following relevant articles and pages in the regulations:\n" |
| for score, doc_id, page, text in matches: |
| doc_name = "Argentine Electrical Code (Section 771 - Residences)" if doc_id == 771 else "Argentine Electrical Code (Section 701 - Bathrooms)" |
| clean_text = "\n".join(text.split("\n")[:8]) |
| response += f"\n📖 **{doc_name} — Page {page}** (Relevance: {score}):\n" |
| response += f"```text\n{clean_text}...\n```\n" |
| response += "\n*Note: You can view the full official PDF document in your project folder to read extended context.*" |
| else: |
| response = "🤖 *Motor de consulta RAG local (Modo búsqueda directa):*\n\n" |
| response += "Encontré los siguientes artículos y páginas relevantes en el reglamento:\n" |
| for score, doc_id, page, text in matches: |
| doc_name = "Reglamento Eléctrico Argentino (Sección 771 - Viviendas)" if doc_id == 771 else "Reglamento Eléctrico Argentino (Sección 701 - Baños)" |
| clean_text = "\n".join(text.split("\n")[:8]) |
| response += f"\n📖 **{doc_name} — Página {page}** (Relevancia: {score}):\n" |
| response += f"```text\n{clean_text}...\n```\n" |
| response += "\n*Nota: Podés ver el documento oficial PDF completo en tu carpeta de proyecto para leer el contexto extendido.*" |
| return response |
|
|
| |
| |
| |
|
|
| 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, lang="Español"): |
| try: |
| P = float(P) |
| I = float(I) |
| V = float(V) |
| target_cos = float(target_cos) |
| freq = int(freq) |
| |
| S = V * I |
| if S == 0: |
| err_msg = "Error: Voltage and current must be greater than 0." if lang == "English" else "Error: Tensión y corriente deben ser mayores a 0." |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_msg}</div>", "", "", "" |
| |
| cos_observed = P / S |
| if cos_observed > 1.0: |
| if lang == "English": |
| 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;">⚠️ Physical Inconsistency Detected</h4> |
| <p style="margin: 0; font-size: 0.95em;"> |
| The resulting observed cos(φ) is <b>{cos_observed:.3f}</b>, which exceeds the theoretical limit of 1.0.<br/> |
| This is because active power ($P$) cannot be greater than apparent power ($S = V \times I$).<br/> |
| <b>Suggestion:</b> Increase the measured current or decrease the entered active power. |
| </p> |
| </div> |
| """ |
| else: |
| 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 |
| |
| |
| vars_dict = { |
| "P": ("Active Power (P)", "Potencia Activa (P)"), |
| "I": ("Current Intensity (I)", "Intensidad de Corriente (I)"), |
| "V": ("Voltage (V)", "Tensión (V)"), |
| "S": ("Original Apparent Power (S)", "Potencia Aparente Original (S)"), |
| "cos_obs": ("Observed cos(φ)", "cos(φ) Observado"), |
| "cos_tgt": ("Target cos(φ)", "cos(φ) Objetivo"), |
| "k": ("k Multiplier", "Multiplicador k"), |
| "qc": ("Compensating Reactive Power (qc)", "Reactiva de Compensación (qc)"), |
| "Xc": ("Capacitive Reactance (Xc)", "Reactancia Capacitiva (Xc)"), |
| "C": ("Capacitance (C)", "Capacitancia (C)") |
| } |
| idx = 0 if lang == "English" else 1 |
| |
| tbody_html = f""" |
| <tr style="border-bottom: 1px solid rgba(251,246,232,0.08);"> |
| <td style="padding: 10px; font-weight: 500; text-align: left;">{vars_dict["P"][idx]}</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;">{vars_dict["I"][idx]}</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;">{vars_dict["V"][idx]}</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;">{vars_dict["S"][idx]}</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;">{vars_dict["cos_obs"][idx]}</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;">{vars_dict["cos_tgt"][idx]}</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;">{vars_dict["k"][idx]}</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;">{vars_dict["qc"][idx]}</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;">{vars_dict["Xc"][idx]}</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;">{vars_dict["C"][idx]}</td> |
| <td style="padding: 10px; text-align: right; font-family: monospace; font-weight: bold; color: #e6a85c;">{C:,.2f} μF</td> |
| </tr> |
| """ |
| |
| if lang == "English": |
| 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);">Reduced Current</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);">Current will drop from <b>{i_old:.1f}A</b> to <b>{i_new:.1f}A</b>, reducing Joule effect losses.</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);">Network Capacity Released</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);">Decreases apparent network distribution power, releasing load on transformers.</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;">💡 Avoid Reactive Power Penalties</h4> |
| <p style="margin: 0; opacity: 0.9; line-height: 1.45; color: var(--cream);"> |
| Power utilities severely penalize cos(φ) below 0.85 or 0.90. |
| By installing a calibrated <b>{C:.1f} μF</b> capacitor, you guarantee operation under the regulatory target of <b>{target_cos:.2f}</b>. |
| </p> |
| </div> |
| """ |
| else: |
| 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_hdr_var = "Variable" |
| table_hdr_val = "Value" if lang == "English" else "Valor" |
| 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;">{table_hdr_var}</th> |
| <th style="padding: 10px; text-align: right; font-weight: 600;">{table_hdr_val}</th> |
| </tr> |
| </thead> |
| <tbody> |
| {tbody_html} |
| </tbody> |
| </table> |
| </div> |
| """ |
| |
| if lang == "English": |
| 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;">Required Capacitance</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);"> |
| To raise the power factor from observed cos(φ) of <b>{cos_observed:.3f}</b> to <b>{target_cos:.2f}</b>. |
| </p> |
| </div> |
| """ |
| else: |
| 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(φ) observado 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: |
| err_prefix = "Error in calculation: " if lang == "English" else "Error en cálculo: " |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_prefix}{str(e)}</div>", "", "", "" |
|
|
| def calc_puesta_a_tierra(tipo, rho, L_jab, d_jab, L_hor, h_hor, d_hor, D_pla, h_pla, lang="Español"): |
| try: |
| rho = float(rho) |
| if rho <= 0: |
| err_msg = "Error: Soil resistivity (ρ) must be greater than 0." if lang == "English" else "Error: La resistividad (ρ) debe ser mayor a 0." |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_msg}</div>", "" |
| |
| |
| tipo_mapping = { |
| "Vertically buried rod (771-C.10.1)": "Jabalina enterrada verticalmente (771-C.10.1)", |
| "Horizontally buried bare conductor (771-C.10.2)": "Conductor desnudo enterrado horizontalmente (771-C.10.2)", |
| "Vertically buried bare circular plate (771-C.10.3)": "Placa circular desnuda enterrada verticalmente (771-C.10.3)" |
| } |
| tipo = tipo_mapping.get(tipo, tipo) |
| |
| R = 0.0 |
| formula_desc = "" |
| aprox_text = "-" |
| relacion_ld_text = "-" |
| warning_text = "" |
| |
| if tipo == "Jabalina enterrada verticalmente (771-C.10.1)": |
| L = float(L_jab) |
| d = float(d_jab) |
| if L <= 0 or d <= 0: |
| err_msg = "Error: Length and diameter must be greater than 0." if lang == "English" else "Error: Longitud y diámetro deben ser mayores a 0." |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_msg}</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 = "Outside approximate standard range (25 ≤ L/d ≤ 3000)" if lang == "English" else "Fuera de rango aproximado normativo (25 ≤ L/d ≤ 3000)" |
| |
| elif tipo == "Conductor desnudo enterrado horizontalmente (771-C.10.2)": |
| L = float(L_hor) |
| h = float(h_hor) |
| d = float(d_hor) |
| if L <= 0 or h <= 0 or d <= 0: |
| err_msg = "Error: Dimensions and depth must be greater than 0." if lang == "English" else "Error: Dimensiones y profundidad deben ser mayores a 0." |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_msg}</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: |
| err_msg = "Error: Diameter and depth must be greater than 0." if lang == "English" else "Error: Diámetro y profundidad deben ser mayores a 0." |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_msg}</div>", "" |
| if h < (D / 2.0): |
| if lang == "English": |
| 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>Installation notice:</b> The center depth of the plate ({h}m) should be greater than or equal to the radius ({D/2.0}m) to ensure adequate current dispersion. |
| </div> |
| """ |
| else: |
| 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: |
| err_msg = "Error: Geometric values produce unrealistic impedance." if lang == "English" else "Error geométrico. Valores producen impedancia irreal." |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_msg}</div>", "" |
| |
| |
| if lang == "English": |
| if "Jabalina" in tipo: |
| method_name = "Vertically buried rod" |
| elif "Conductor" in tipo: |
| method_name = "Horizontal bare conductor" |
| else: |
| method_name = "Circular bare plate" |
| res_title = "Grounding Resistance" |
| method_lbl = "Method" |
| else: |
| method_name = tipo.split(' (')[0] |
| res_title = "Resistencia de Puesta a Tierra" |
| method_lbl = "Método" |
| |
| res_box_html = f""" |
| {warning_text} |
| <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;">{res_title}</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);">{method_lbl}: {method_name}</p> |
| </div> |
| """ |
| |
| if lang == "English": |
| ld_lbl = "Calculated L/d ratio:" |
| aprox_lbl = "Standard simplified approximation:" |
| formula_lbl = "Applied Mathematical Formula" |
| else: |
| ld_lbl = "Relación L/d calculada:" |
| aprox_lbl = "Aproximación simplificada Norma:" |
| formula_lbl = "Fórmula Matemática Aplicada" |
| |
| details_html = f""" |
| <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);">{ld_lbl}</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);">{aprox_lbl}</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;">{formula_lbl}</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: |
| err_prefix = "Error in calculation: " if lang == "English" else "Error en cálculo: " |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_prefix}{str(e)}</div>", "" |
|
|
| def calc_medidor_potencia(K, k_unit, N, min_val, sec_val, lang="Español"): |
| try: |
| K = float(K) |
| N = float(N) |
| min_val = float(min_val) if min_val else 0.0 |
| sec_val = float(sec_val) if sec_val else 0.0 |
| |
| total_seconds = (min_val * 60.0) + sec_val |
| if total_seconds <= 0: |
| err_msg = "Error: Total time must be greater than 0 seconds." if lang == "English" else "Error: El tiempo total debe ser mayor a 0 segundos." |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_msg}</div>", "" |
| |
| rev_speed = N / total_seconds |
| |
| if k_unit == "rev / kWh": |
| watts = (3600.0 * N * 1000.0) / (K * total_seconds) |
| formula_str = "Power (W) = (3600 · N · 1000) / (K · t)" if lang == "English" else "Potencia (W) = (3600 · N · 1000) / (K · t)" |
| else: |
| watts = (3600.0 * N * K) / total_seconds |
| formula_str = "Power (W) = (3600 · N · K) / t" if lang == "English" else "Potencia (W) = (3600 · N · K) / t" |
| |
| warning_text = "" |
| if watts > 25000: |
| if lang == "English": |
| warning_text = f""" |
| <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>Notice:</b> A high power of {watts/1000.0:.3f} kW has been calculated. Verify that the values entered for K, N, or time are correct. |
| </div> |
| """ |
| else: |
| 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_title = "Measured Active Power" if lang == "English" else "Potencia Activa Medida" |
| 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;">{res_title}</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> |
| """ |
| |
| if lang == "English": |
| time_lbl = "Total accumulated time (t):" |
| freq_lbl = "Disk/pulse frequency:" |
| eq_lbl = "Equation Used" |
| else: |
| time_lbl = "Tiempo total acumulado (t):" |
| freq_lbl = "Frecuencia del disco/pulsos:" |
| eq_lbl = "Ecuación Utilizada" |
| |
| details_html = f""" |
| <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);">{time_lbl}</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);">{freq_lbl}</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;">{eq_lbl}</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: |
| err_prefix = "Error in calculation: " if lang == "English" else "Error en cálculo: " |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_prefix}{str(e)}</div>", "" |
|
|
| def generate_unifilar_svg(circ, general, is_trifasico, lang="Español"): |
| if not circ: |
| return "" |
| |
| spacing = 135 |
| content_width = (len(circ) - 1) * spacing |
| margin_x = 120 |
| width = max(600, content_width + (margin_x * 2)) |
| height = 560 |
| |
| busbar_y = 190 |
| center_x = width / 2.0 |
| |
| start_x = center_x - (content_width / 2.0) |
| end_x = center_x + (content_width / 2.0) |
| |
| def draw_tm_svg(x, y, label_in, label="TM"): |
| return f""" |
| <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> |
| """ |
| |
| |
| main_breaker_lbl = "Main Breaker" if lang == "English" else "Corte Gral." |
| main_rcd_lbl = "Main RCD" if lang == "English" else "Dif. Gral." |
| phase_lbl = "Phase" if lang == "English" else "Fase" |
| |
| svg += f'<path d="M {center_x} 15 L {center_x} 35" class="line" />' |
| svg += draw_tm_svg(center_x, 35, general["In"], main_breaker_lbl) |
| |
| |
| svg += f'<path d="M {center_x} 85 L {center_x} 105" class="line" />' |
| svg += draw_id_svg(center_x, 105, general["diff"], "300mA", main_rcd_lbl) |
| |
| |
| 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;">{phase_lbl}: {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, lang="Español"): |
| try: |
| iug_b = int(iug_b) if iug_b else 0 |
| tug_b = int(tug_b) if tug_b else 0 |
| iue_b = int(iue_b) if iue_b else 0 |
| tue_b = int(tue_b) if tue_b else 0 |
| |
| motor_hp = float(motor_hp) |
| motor_fases = int(motor_fases) |
| motor_cant = int(motor_cant) if motor_cant else 0 |
| |
| mbt_w = float(mbt_w) |
| mbt_cant = int(mbt_cant) if mbt_cant else 0 |
| |
| coef = float(coef) |
| suministro = str(suministro) |
| if suministro in ["Single-phase", "Monofásico"]: |
| suministro = "Monofásico" |
| else: |
| suministro = "Trifásico" |
| |
| circuits = [] |
| total_dpms = 0.0 |
| |
| |
| iug_desc = "Gen. Lighting" if lang == "English" else "Ilum. Gral" |
| tug_desc = "Gen. Outlets" if lang == "English" else "Tomas Gral" |
| iue_desc = "Spec. Lighting" if lang == "English" else "Ilum. Esp." |
| tue_desc = "Spec. Outlets" if lang == "English" else "Tomas Esp." |
| mbt_desc = "Extra-low Voltage" if lang == "English" else "Muy Baja Tensión" |
| |
| |
| if iug_b > 0: |
| iug_count = math.ceil(iug_b / 15.0) |
| for i in range(iug_count): |
| b = (iug_b % 15) if (i == iug_count - 1 and iug_b % 15 != 0) else 15 |
| dpms = b * 60 * 0.66 |
| Ib = dpms / 220.0 |
| In = get_standard_in(Ib, 16) |
| cable = get_cable(In, is_iug=True) |
| circuits.append({ |
| "tipo": "IUG", |
| "desc": iug_desc, |
| "b": b, |
| "dpms": dpms, |
| "Ib": Ib, |
| "In": In, |
| "cable": cable, |
| "polos": 2, |
| "es_motor": False, |
| "css_bg": "#3a3212", |
| }) |
| total_dpms += dpms |
| |
| |
| if tug_b > 0: |
| tug_count = math.ceil(tug_b / 15.0) |
| for i in range(tug_count): |
| b = (tug_b % 15) if (i == tug_count - 1 and tug_b % 15 != 0) else 15 |
| dpms = 2200.0 |
| Ib = dpms / 220.0 |
| In = get_standard_in(Ib, 20) |
| cable = get_cable(In, is_iug=False) |
| circuits.append({ |
| "tipo": "TUG", |
| "desc": tug_desc, |
| "b": b, |
| "dpms": dpms, |
| "Ib": Ib, |
| "In": In, |
| "cable": cable, |
| "polos": 2, |
| "es_motor": False, |
| "css_bg": "#12253a", |
| }) |
| total_dpms += dpms |
| |
| |
| if iue_b > 0: |
| iue_count = math.ceil(iue_b / 12.0) |
| for i in range(iue_count): |
| b = (iue_b % 12) if (i == iue_count - 1 and iue_b % 12 != 0) else 12 |
| dpms = b * 500 * 0.66 |
| Ib = dpms / 220.0 |
| In = get_standard_in(Ib, 32) |
| cable = get_cable(In, is_iug=False) |
| circuits.append({ |
| "tipo": "IUE", |
| "desc": iue_desc, |
| "b": b, |
| "dpms": dpms, |
| "Ib": Ib, |
| "In": In, |
| "cable": cable, |
| "polos": 2, |
| "es_motor": False, |
| "css_bg": "#3a2812", |
| }) |
| total_dpms += dpms |
| |
| |
| if tue_b > 0: |
| tue_count = math.ceil(tue_b / 12.0) |
| for i in range(tue_count): |
| b = (tue_b % 12) if (i == tue_count - 1 and tue_b % 12 != 0) else 12 |
| dpms = 3300.0 |
| Ib = dpms / 220.0 |
| In = get_standard_in(Ib, 32) |
| cable = get_cable(In, is_iug=False) |
| circuits.append({ |
| "tipo": "TUE", |
| "desc": tue_desc, |
| "b": b, |
| "dpms": dpms, |
| "Ib": Ib, |
| "In": In, |
| "cable": cable, |
| "polos": 2, |
| "es_motor": False, |
| "css_bg": "#1d123a", |
| }) |
| total_dpms += dpms |
| |
| |
| if motor_cant > 0: |
| for i in range(motor_cant): |
| dpms = (motor_hp * 746) / 0.8 |
| fases = motor_fases |
| if fases == 1: |
| Ib = dpms / 220.0 |
| In = get_standard_in(Ib, 63) |
| polos = 2 |
| tipo_lbl = "ACU-M" |
| motor_type_str = "Single-phase" if lang == "English" else "Mono" |
| else: |
| Ib = dpms / (math.sqrt(3) * 380.0) |
| In = get_standard_in(Ib, 63) |
| polos = 4 |
| tipo_lbl = "ACU-T" |
| motor_type_str = "Three-phase" if lang == "English" else "Tri" |
| |
| diff = get_standard_diff(In) |
| cable = get_cable(In, is_iug=False) |
| circuits.append({ |
| "tipo": tipo_lbl, |
| "desc": f"Motor {motor_hp} HP ({motor_type_str})", |
| "b": 1, |
| "dpms": dpms, |
| "Ib": Ib, |
| "In": In, |
| "diff": diff, |
| "cable": cable, |
| "polos": polos, |
| "es_motor": True, |
| "fases": fases, |
| "css_bg": "#3a1812", |
| }) |
| total_dpms += dpms |
| |
| |
| if mbt_cant > 0: |
| cur_mbt_b = 0 |
| cur_mbt_dpms = 0.0 |
| mbt_chunks = [] |
| for i in range(mbt_cant): |
| if cur_mbt_b >= 15 or (cur_mbt_dpms + mbt_w) > 2200.0: |
| if cur_mbt_b > 0: |
| mbt_chunks.append({"b": cur_mbt_b, "dpms": cur_mbt_dpms}) |
| cur_mbt_b = 0 |
| cur_mbt_dpms = 0.0 |
| cur_mbt_b += 1 |
| cur_mbt_dpms += mbt_w |
| if cur_mbt_b > 0: |
| mbt_chunks.append({"b": cur_mbt_b, "dpms": cur_mbt_dpms}) |
| |
| for chunk in mbt_chunks: |
| Ib = chunk["dpms"] / 220.0 |
| In = get_standard_in(Ib, 20) |
| cable = get_cable(In, is_iug=True) |
| circuits.append({ |
| "tipo": "MBTF", |
| "desc": mbt_desc, |
| "b": chunk["b"], |
| "dpms": chunk["dpms"], |
| "Ib": Ib, |
| "In": In, |
| "cable": cable, |
| "polos": 2, |
| "es_motor": False, |
| "css_bg": "#33123a", |
| }) |
| total_dpms += chunk["dpms"] |
| |
| if len(circuits) == 0: |
| err_msg = "Please add some elements or circuit outlets to begin." if lang == "English" else "Por favor, agregue algún elemento o bocas de circuito para comenzar." |
| return f"<div style='color: var(--sun); font-weight: bold;'>{err_msg}</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 = "Three-phase motor detected" if lang == "English" else "Motor trifásico detectado" |
| elif over_63A: |
| is_trifasico = True |
| force_reason = "Current > 63A" if lang == "English" else "Corriente > 63A" |
| else: |
| is_trifasico = (suministro == "Trifásico") |
| |
| if is_trifasico: |
| tIb = dpms_general / (math.sqrt(3) * 380.0) |
| tPolos = 4 |
| else: |
| tIb = dpms_general / 220.0 |
| tPolos = 2 |
| |
| tIn = get_standard_in(tIb, 250) |
| tDiff = get_standard_diff(tIn) |
| tCable = get_cable(tIn, is_iug=False) |
| |
| if lang == "English": |
| sim_pwr_lbl = "Simultaneous Power (DPMS)" |
| sim_coef_lbl = "Simultaneity factor" |
| main_supply_lbl = "Main Supply" |
| tri_lbl = "Three-phase (380V)" |
| mono_lbl = "Single-phase (220V)" |
| forced_lbl = "Forced" |
| else: |
| sim_pwr_lbl = "Potencia Simultánea (DPMS)" |
| sim_coef_lbl = "Coeficiente de simultaneidad" |
| main_supply_lbl = "Alimentación General" |
| tri_lbl = "Trifásica (380V)" |
| mono_lbl = "Monofásica (220V)" |
| forced_lbl = "Forzado" |
| |
| res_summary = f""" |
| <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);">{sim_pwr_lbl}</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);">{sim_coef_lbl}: <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);">{main_supply_lbl}</span> |
| <div style="font-size: 1.6em; font-weight: 800; color: var(--sun); margin: 8px 0;">{tri_lbl if is_trifasico else mono_lbl}</div> |
| <p style="font-size: 0.85em; margin: 0; opacity: 0.9; color: #ef9a9a;"><b>{f"{forced_lbl}: {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> |
| """ |
| |
| main_line_lbl = "Main Line (Three-phase)" if lang == "English" else "Línea Principal (Trifásica)" |
| if not is_trifasico: |
| main_line_lbl = "Main Line (Single-phase)" if lang == "English" else "Línea Principal (Monofásica)" |
| |
| tbody_rows += f""" |
| <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);">{main_line_lbl}</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> |
| """ |
| |
| tbl_headers = { |
| "circ": "Circ.", |
| "type": "Type / Destination" if lang == "English" else "Tipo / Destino", |
| "outlets": "Outlets" if lang == "English" else "Bocas", |
| "power": "Power (VA)" if lang == "English" else "Potencia (VA)", |
| "ib": "Ib (A)", |
| "prot": "Protection" if lang == "English" else "Protección", |
| "cond": "Conductor / Iz" |
| } |
| |
| planilla_html = f""" |
| <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;">{tbl_headers["circ"]}</th> |
| <th style="padding: 12px; font-weight: 600; text-align: left;">{tbl_headers["type"]}</th> |
| <th style="padding: 12px; font-weight: 600;">{tbl_headers["outlets"]}</th> |
| <th style="padding: 12px; font-weight: 600;">{tbl_headers["power"]}</th> |
| <th style="padding: 12px; font-weight: 600;">{tbl_headers["ib"]}</th> |
| <th style="padding: 12px; font-weight: 600;">{tbl_headers["prot"]}</th> |
| <th style="padding: 12px; font-weight: 600;">{tbl_headers["cond"]}</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, lang) |
| return res_summary, planilla_html, svg_unifilar |
| except Exception as e: |
| err_msg = "Error in board design: " if lang == "English" else "Error en diseño de tableros: " |
| return f"<div style='color: #ef9a9a; font-weight: bold;'>{err_msg}{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, lang="Español"): |
| try: |
| _, _, svg_unifilar = calc_tableros_designer( |
| iug_b, tug_b, iue_b, tue_b, motor_hp, motor_fases, motor_cant, mbt_w, mbt_cant, coef, suministro, lang |
| ) |
| if not svg_unifilar or "Error" in svg_unifilar: |
| return None |
| |
| import io |
| import re |
| from svglib.svglib import svg2rlg |
| from reportlab.graphics import renderPDF |
| |
| svg_cleaned = svg_unifilar |
| viewbox_match = re.search(r'viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"', svg_cleaned) |
| if viewbox_match: |
| w, h = viewbox_match.groups() |
| svg_cleaned = svg_cleaned.replace('height="auto"', f'height="{h}"') |
| svg_cleaned = svg_cleaned.replace('width="100%"', f'width="{w}"') |
| |
| svg_cleaned = svg_cleaned.replace('var(--walnut)', '#5a3a22') |
| svg_cleaned = svg_cleaned.replace('var(--cream)', '#fbf6e8') |
| svg_cleaned = svg_cleaned.replace('var(--sun)', '#e6a85c') |
| svg_cleaned = svg_cleaned.replace('var(--rust)', '#8a4a2b') |
| |
| svg_io = io.BytesIO(svg_cleaned.encode('utf-8')) |
| drawing = svg2rlg(svg_io) |
| |
| pdf_path = "esquema_unifilar.pdf" |
| renderPDF.drawToFile(drawing, pdf_path) |
| |
| return gr.update(value=pdf_path, visible=True) |
| except Exception as e: |
| print("Error exportando a PDF:", e) |
| return None |
|
|
| def on_design_click(iug_b, tug_b, iue_b, tue_b, motor_hp, motor_fases, motor_cant, mbt_w, mbt_cant, coef, suministro, lang="Español"): |
| res_summary, planilla_html, svg_unifilar = calc_tableros_designer( |
| iug_b, tug_b, iue_b, tue_b, motor_hp, motor_fases, motor_cant, mbt_w, mbt_cant, coef, suministro, lang |
| ) |
| if svg_unifilar and "Error" not in svg_unifilar: |
| return res_summary, planilla_html, svg_unifilar, gr.update(visible=True), gr.update(visible=False) |
| return res_summary, planilla_html, svg_unifilar, gr.update(visible=False), gr.update(visible=False) |
|
|
| |
| |
| |
|
|
| |
| 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) |
|
|
| |
| |
| |
|
|
| TRANSLATIONS = { |
| "Español": { |
| "nav_circuitos": "🔎 Auditor de Circuitos", |
| "nav_banos": "🚿 Baños y Zonas", |
| "nav_electrif": "📐 Grado de Electrificación", |
| "nav_fp": "🎛️ Corrector de Factor de Potencia", |
| "nav_pat": "🌱 Puesta a Tierra (PAT)", |
| "nav_medidor": "🔌 Medidor de Potencia", |
| "nav_tableros": "⚡ Diseñador de Tableros", |
| "nav_rag": "📖 Consultor Normativo (RAG)", |
| |
| "circuit_title": "### Auditoría de Cables y Protecciones Térmicas (Reglamento Eléctrico Argentino Sección 771)", |
| "circuit_in_label": "Tipo de Circuito", |
| "circuit_in_info": "IUG (Luz), TUG (Tomas comunes), IUE/TUE (Especiales/Aire Acon.)", |
| "section_in_label": "Sección del Conductor (Cobre) [mm²]", |
| "section_in_info": "Sección nominal de los cables en la cañería.", |
| "protection_in_label": "Calibre de la Llave Térmica (In) [A]", |
| "bocas_in_label": "Cantidad de Bocas en el Circuito", |
| "grouped_in_label": "Circuitos agrupados en la misma cañería", |
| "grouped_in_info": "Afecta directamente la corriente admisible por sobrecalentamiento.", |
| "btn_audit": "Verificar Circuito", |
| "audit_results_title": "#### Resultado de la Auditoría", |
| |
| "bathroom_title": "### Auditor de Seguridad en Cuartos de Baño (Reglamento Eléctrico Argentino Sección 701)", |
| "bathroom_desc": "Ingrese la distancia a la bañera/ducha para calcular si cumple la normativa y qué IP se requiere.", |
| "elem_in_label": "Tipo de Artefacto/Elemento", |
| "elem_in_choices": ["Tomacorriente", "Interruptor", "Luminaria común", "Termotanque"], |
| "dist_in_label": "Distancia horizontal al borde de bañera/ducha [cm]", |
| "height_in_label": "Altura desde el nivel del piso terminado [cm]", |
| "btn_bathroom": "Analizar Zona", |
| "bathroom_results_title": "#### Análisis de Zonas de Seguridad", |
| |
| "electrif_title": "### Cómputo de Grado de Electrificación y Bocas Mínimas", |
| "covered_in_label": "Superficie Cubierta del Inmueble [m²]", |
| "semi_covered_in_label": "Superficie Semicubierta [m²] (Terrazas, Balcones techados)", |
| "btn_calc": "Calcular Grado", |
| "electrif_results_title": "#### Requerimientos Mínimos del Inmueble", |
| |
| "fp_title": "### Calculadora de Factor de Potencia y Capacitancia", |
| "fp_p_label": "Potencia Activa (P) [W]", |
| "fp_i_label": "Intensidad Medida (I) [A]", |
| "fp_v_label": "Tensión de Red (V) [V]", |
| "fp_target_label": "Coseno φ Objetivo", |
| "fp_freq_label": "Frecuencia Eléctrica (f) [Hz]", |
| "btn_fp": "Calcular Corrección", |
| |
| "pat_title": "### Resistencia de Puesta a Tierra (Reglamento Eléctrico Argentino Sección 771)", |
| "pat_tipo_label": "Tipo de Electrodo", |
| "pat_tipo_choices": [ |
| "Jabalina enterrada verticalmente (771-C.10.1)", |
| "Conductor desnudo enterrado horizontalmente (771-C.10.2)", |
| "Placa circular desnuda enterrada verticalmente (771-C.10.3)" |
| ], |
| "pat_rho_label": "Resistividad del Terreno (ρ) [Ω·m]", |
| "jab_L_label": "Longitud de la Jabalina (L) [m]", |
| "jab_d_label": "Diámetro de la Jabalina (d) [m]", |
| "hor_L_label": "Longitud del Conductor (L) [m]", |
| "hor_h_label": "Profundidad de Enterrado (h) [m]", |
| "hor_d_label": "Diámetro del Conductor (d) [m]", |
| "pla_D_label": "Diámetro de la Placa (D) [m]", |
| "pla_h_label": "Profundidad hasta el Centro (h) [m]", |
| "btn_pat": "Calcular PAT", |
| |
| "medidor_title": "### Medición de Potencia Activa por Medidor de Inducción", |
| "med_k_unit_label": "Unidad de la Constante (K)", |
| "med_K_label": "Valor de la Constante (K)", |
| "med_N_label": "Vueltas Contadas (N)", |
| "med_min_label": "Minutos", |
| "med_sec_label": "Segundos", |
| "btn_med": "Calcular Potencia Medida", |
| |
| "tableros_title": "### Dimensionamiento de Distribución y Protecciones de Tableros", |
| "tableros_bocas_title": "#### Bocas del Proyecto", |
| "tab_iug_label": "Bocas de Iluminación Gral. (IUG)", |
| "tab_tug_label": "Bocas de Tomacorrientes Gral. (TUG)", |
| "tab_iue_label": "Bocas de Iluminación Especial (IUE)", |
| "tab_tue_label": "Bocas de Tomacorrientes Especial (TUE)", |
| "tableros_motores_title": "#### Cargas de Motores (ACU)", |
| "tab_motor_hp_label": "Potencia Motor [HP]", |
| "tab_motor_fases_label": "Fases Motor", |
| "tab_motor_cant_label": "Cantidad de Motores", |
| "tableros_mbt_title": "#### Muy Baja Tensión (MBT)", |
| "tab_mbt_w_label": "Consumo por Carga [W]", |
| "tab_mbt_cant_label": "Cantidad de Cargas", |
| "tableros_params_title": "#### Parámetros Generales", |
| "tab_coef_label": "Coeficiente de Simultaneidad", |
| "tab_sum_label": "Suministro Sugerido", |
| "tab_sum_choices": ["Monofásico", "Trifásico"], |
| "btn_tab": "Diseñar Tablero", |
| "btn_pdf": "Exportar a PDF", |
| "pdf_output_label": "Descargar Diagrama Unifilar PDF", |
| "tableros_unifilar_title": "#### Diagrama Unifilar Dinámico (Esquema Eléctrico)", |
| |
| "rag_title": "### Consultor del Reglamento Eléctrico Argentino", |
| "rag_desc": "Escribí una duda técnica o palabra clave sobre la reglamentación. El motor buscará de manera semántica e indexada en los PDFs cargados y responderá.", |
| "chat_textbox_label": "Pregunta", |
| "chat_textbox_placeholder": "Escribí una duda técnica o palabra clave..." |
| }, |
| "English": { |
| "nav_circuitos": "🔎 Circuit Auditor", |
| "nav_banos": "🚿 Bathroom Zones", |
| "nav_electrif": "📐 Electrification Degree", |
| "nav_fp": "🎛️ Power Factor Corrector", |
| "nav_pat": "🌱 Grounding (PAT)", |
| "nav_medidor": "🔌 Disk Power Meter", |
| "nav_tableros": "⚡ Board Designer", |
| "nav_rag": "📖 Normative Advisor (RAG)", |
| |
| "circuit_title": "### Cables and Circuit Breaker Auditing (Argentine Electrical Code Section 771)", |
| "circuit_in_label": "Circuit Type", |
| "circuit_in_info": "IUG (Lighting), TUG (General Outlets), IUE/TUE (Special/AC)", |
| "section_in_label": "Conductor Section (Copper) [mm²]", |
| "section_in_info": "Nominal section of cables in the conduit.", |
| "protection_in_label": "Circuit Breaker Rating (In) [A]", |
| "bocas_in_label": "Number of Outlets in the Circuit", |
| "grouped_in_label": "Grouped circuits in the same conduit", |
| "grouped_in_info": "Directly affects allowable current due to thermal grouping.", |
| "btn_audit": "Verify Circuit", |
| "audit_results_title": "#### Audit Result", |
| |
| "bathroom_title": "### Bathroom Safety Auditor (Argentine Electrical Code Section 701)", |
| "bathroom_desc": "Enter the distance to the bathtub/shower to calculate compliance and required IP rating.", |
| "elem_in_label": "Appliance / Element Type", |
| "elem_in_choices": ["Socket-outlet", "Switch", "Common luminaire", "Water heater"], |
| "dist_in_label": "Horizontal distance to bathtub/shower border [cm]", |
| "height_in_label": "Height from finished floor level [cm]", |
| "btn_bathroom": "Analyze Zone", |
| "bathroom_results_title": "#### Safety Zones Analysis", |
| |
| "electrif_title": "### Electrification Degree & Minimum Outlets Calculation", |
| "covered_in_label": "Covered Area of the Property [m²]", |
| "semi_covered_in_label": "Semi-covered Area [m²] (Patios, Roofed Balconies)", |
| "btn_calc": "Calculate Degree", |
| "electrif_results_title": "#### Minimum Property Requirements", |
| |
| "fp_title": "### Power Factor and Capacitance Calculator", |
| "fp_p_label": "Active Power (P) [W]", |
| "fp_i_label": "Measured Current (I) [A]", |
| "fp_v_label": "Grid Voltage (V) [V]", |
| "fp_target_label": "Target Cos φ (Power Factor)", |
| "fp_freq_label": "Grid Frequency (f) [Hz]", |
| "btn_fp": "Calculate Correction", |
| |
| "pat_title": "### Grounding Resistance (Argentine Electrical Code Section 771)", |
| "pat_tipo_label": "Electrode Type", |
| "pat_tipo_choices": [ |
| "Vertically buried rod (771-C.10.1)", |
| "Horizontally buried bare conductor (771-C.10.2)", |
| "Vertically buried bare circular plate (771-C.10.3)" |
| ], |
| "pat_rho_label": "Soil Resistivity (ρ) [Ω·m]", |
| "jab_L_label": "Rod Length (L) [m]", |
| "jab_d_label": "Rod Diameter (d) [m]", |
| "hor_L_label": "Conductor Length (L) [m]", |
| "hor_h_label": "Burial Depth (h) [m]", |
| "hor_d_label": "Conductor Diameter (d) [m]", |
| "pla_D_label": "Plate Diameter (D) [m]", |
| "pla_h_label": "Depth to Center (h) [m]", |
| "btn_pat": "Calculate Grounding", |
| |
| "medidor_title": "### Active Power Measurement via Induction Meter", |
| "med_k_unit_label": "Constant Unit (K)", |
| "med_K_label": "Constant Value (K)", |
| "med_N_label": "Counted Revolutions (N)", |
| "med_min_label": "Minutes", |
| "med_sec_label": "Seconds", |
| "btn_med": "Calculate Measured Power", |
| |
| "tableros_title": "### Board Distribution and Protections Sizing", |
| "tableros_bocas_title": "#### Project Outlets", |
| "tab_iug_label": "General Lighting Outlets (IUG)", |
| "tab_tug_label": "General Socket Outlets (TUG)", |
| "tab_iue_label": "Special Lighting Outlets (IUE)", |
| "tab_tue_label": "Special Socket Outlets (TUE)", |
| "tableros_motores_title": "#### Motor Loads (ACU)", |
| "tab_motor_hp_label": "Motor Power [HP]", |
| "tab_motor_fases_label": "Motor Phases", |
| "tab_motor_cant_label": "Number of Motors", |
| "tableros_mbt_title": "#### Extra Low Voltage (ELV)", |
| "tab_mbt_w_label": "Consumption per Load [W]", |
| "tab_mbt_cant_label": "Number of Loads", |
| "tableros_params_title": "#### General Parameters", |
| "tab_coef_label": "Simultaneity Coeff.", |
| "tab_sum_label": "Suggested Supply", |
| "tab_sum_choices": ["Single-phase", "Three-phase"], |
| "btn_tab": "Design Board", |
| "btn_pdf": "Export to PDF", |
| "pdf_output_label": "Download Single-line Diagram PDF", |
| "tableros_unifilar_title": "#### Dynamic Single-Line Diagram (Electrical Scheme)", |
| |
| "rag_title": "### Argentine Electrical Code Advisor", |
| "rag_desc": "Write a technical question or keyword about the regulations. The engine will perform a semantic and indexed search on the loaded PDFs and answer.", |
| "chat_textbox_label": "Question", |
| "chat_textbox_placeholder": "Write a technical question or keyword..." |
| } |
| } |
|
|
| def change_language(lang): |
| t = TRANSLATIONS[lang] |
| return { |
| nav_btn_circuitos: gr.update(value=t["nav_circuitos"]), |
| nav_btn_banos: gr.update(value=t["nav_banos"]), |
| nav_btn_electrif: gr.update(value=t["nav_electrif"]), |
| nav_btn_fp: gr.update(value=t["nav_fp"]), |
| nav_btn_pat: gr.update(value=t["nav_pat"]), |
| nav_btn_medidor: gr.update(value=t["nav_medidor"]), |
| nav_btn_tableros: gr.update(value=t["nav_tableros"]), |
| nav_btn_rag: gr.update(value=t["nav_rag"]), |
| |
| circuit_title: gr.update(value=t["circuit_title"]), |
| circuit_in: gr.update(label=t["circuit_in_label"], info=t["circuit_in_info"]), |
| section_in: gr.update(label=t["section_in_label"], info=t["section_in_info"]), |
| protection_in: gr.update(label=t["protection_in_label"]), |
| bocas_in: gr.update(label=t["bocas_in_label"]), |
| grouped_in: gr.update(label=t["grouped_in_label"], info=t["grouped_in_info"]), |
| btn_audit: gr.update(value=t["btn_audit"]), |
| audit_results_title: gr.update(value=t["audit_results_title"]), |
| |
| bathroom_title: gr.update(value=t["bathroom_title"]), |
| bathroom_desc: gr.update(value=t["bathroom_desc"]), |
| elem_in: gr.update(label=t["elem_in_label"], choices=t["elem_in_choices"], value=t["elem_in_choices"][0]), |
| dist_in: gr.update(label=t["dist_in_label"]), |
| height_in: gr.update(label=t["height_in_label"]), |
| btn_bathroom: gr.update(value=t["btn_bathroom"]), |
| bathroom_results_title: gr.update(value=t["bathroom_results_title"]), |
| |
| electrif_title: gr.update(value=t["electrif_title"]), |
| covered_in: gr.update(label=t["covered_in_label"]), |
| semi_covered_in: gr.update(label=t["semi_covered_in_label"]), |
| btn_calc: gr.update(value=t["btn_calc"]), |
| electrif_results_title: gr.update(value=t["electrif_results_title"]), |
| |
| fp_title: gr.update(value=t["fp_title"]), |
| fp_p: gr.update(label=t["fp_p_label"]), |
| fp_i: gr.update(label=t["fp_i_label"]), |
| fp_v: gr.update(label=t["fp_v_label"]), |
| fp_target: gr.update(label=t["fp_target_label"]), |
| fp_freq: gr.update(label=t["fp_freq_label"]), |
| btn_fp: gr.update(value=t["btn_fp"]), |
| |
| pat_title: gr.update(value=t["pat_title"]), |
| pat_tipo: gr.update(label=t["pat_tipo_label"], choices=t["pat_tipo_choices"], value=t["pat_tipo_choices"][0]), |
| pat_rho: gr.update(label=t["pat_rho_label"]), |
| jab_L: gr.update(label=t["jab_L_label"]), |
| jab_d: gr.update(label=t["jab_d_label"]), |
| hor_L: gr.update(label=t["hor_L_label"]), |
| hor_h: gr.update(label=t["hor_h_label"]), |
| hor_d: gr.update(label=t["hor_d_label"]), |
| pla_D: gr.update(label=t["pla_D_label"]), |
| pla_h: gr.update(label=t["pla_h_label"]), |
| btn_pat: gr.update(value=t["btn_pat"]), |
| |
| medidor_title: gr.update(value=t["medidor_title"]), |
| med_k_unit: gr.update(label=t["med_k_unit_label"]), |
| med_K: gr.update(label=t["med_K_label"]), |
| med_N: gr.update(label=t["med_N_label"]), |
| med_min: gr.update(label=t["med_min_label"]), |
| med_sec: gr.update(label=t["med_sec_label"]), |
| btn_med: gr.update(value=t["btn_med"]), |
| |
| tableros_title: gr.update(value=t["tableros_title"]), |
| tableros_bocas_title: gr.update(value=t["tableros_bocas_title"]), |
| tab_iug: gr.update(label=t["tab_iug_label"]), |
| tab_tug: gr.update(label=t["tab_tug_label"]), |
| tab_iue: gr.update(label=t["tab_iue_label"]), |
| tab_tue: gr.update(label=t["tab_tue_label"]), |
| tableros_motores_title: gr.update(value=t["tableros_motores_title"]), |
| tab_motor_hp: gr.update(label=t["tab_motor_hp_label"]), |
| tab_motor_fases: gr.update(label=t["tab_motor_fases_label"]), |
| tab_motor_cant: gr.update(label=t["tab_motor_cant_label"]), |
| tableros_mbt_title: gr.update(value=t["tableros_mbt_title"]), |
| tab_mbt_w: gr.update(label=t["tab_mbt_w_label"]), |
| tab_mbt_cant: gr.update(label=t["tab_mbt_cant_label"]), |
| tableros_params_title: gr.update(value=t["tableros_params_title"]), |
| tab_coef: gr.update(label=t["tab_coef_label"]), |
| tab_sum: gr.update(label=t["tab_sum_label"], choices=t["tab_sum_choices"], value=t["tab_sum_choices"][0]), |
| btn_tab: gr.update(value=t["btn_tab"]), |
| btn_pdf: gr.update(value=t["btn_pdf"]), |
| pdf_output: gr.update(label=t["pdf_output_label"]), |
| tableros_unifilar_title: gr.update(value=t["tableros_unifilar_title"]), |
| |
| rag_title: gr.update(value=t["rag_title"]), |
| rag_desc: gr.update(value=t["rag_desc"]), |
| chat_textbox: gr.update(label=t["chat_textbox_label"], placeholder=t["chat_textbox_placeholder"]) |
| } |
|
|
| with gr.Blocks(title="ArgenVolt - Auditor de Reglamento Eléctrico Argentino", css=css_custom) as demo: |
| with gr.Row(): |
| |
| 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"]) |
| |
| gr.HTML("<hr style='border-color: rgba(251, 246, 232, 0.15); margin: 15px 0;'/>") |
| lang_dropdown = gr.Dropdown( |
| choices=["Español", "English"], |
| value="Español", |
| label="Idioma / Language", |
| interactive=True |
| ) |
| |
| |
| with gr.Column(scale=4, elem_id="content-panel"): |
| |
| |
| with gr.Column(visible=True) as col_circuitos: |
| circuit_title = gr.Markdown("### Auditoría de Cables y Protecciones Térmicas (Reglamento Eléctrico Argentino Sección 771)") |
| with gr.Row(): |
| with gr.Column(scale=1): |
| circuit_in = gr.Dropdown( |
| choices=["IUG", "TUG", "IUE", "TUE"], |
| value="TUG", |
| label="Tipo de Circuito", |
| info="IUG (Luz), TUG (Tomas comunes), IUE/TUE (Especiales/Aire Acon.)" |
| ) |
| section_in = gr.Dropdown( |
| choices=["1.5", "2.5", "4.0", "6.0", "10.0"], |
| value="2.5", |
| label="Sección del Conductor (Cobre) [mm²]", |
| info="Sección nominal de los cables en la cañería." |
| ) |
| protection_in = gr.Dropdown( |
| choices=["10", "16", "20", "25", "32", "40"], |
| value="20", |
| label="Calibre de la Llave Térmica (In) [A]" |
| ) |
| bocas_in = gr.Number( |
| value=10, |
| label="Cantidad de Bocas en el Circuito", |
| precision=0 |
| ) |
| grouped_in = gr.Slider( |
| minimum=1, |
| maximum=6, |
| value=1, |
| step=1, |
| label="Circuitos agrupados en la misma cañería", |
| info="Afecta directamente la corriente admisible por sobrecalentamiento." |
| ) |
| btn_audit = gr.Button("Verificar Circuito", variant="primary") |
| |
| with gr.Column(scale=1): |
| audit_results_title = gr.Markdown("#### Resultado de la Auditoría") |
| audit_out = gr.HTML(label="Informe de Conformidad") |
| |
| btn_audit.click( |
| fn=audit_circuit, |
| inputs=[circuit_in, section_in, protection_in, bocas_in, grouped_in, lang_dropdown], |
| outputs=audit_out |
| ) |
| |
| |
| with gr.Column(visible=False) as col_banos: |
| bathroom_title = gr.Markdown("### Auditor de Seguridad en Cuartos de Baño (Reglamento Eléctrico Argentino Sección 701)") |
| bathroom_desc = gr.Markdown("Ingrese la distancia a la bañera/ducha para calcular si cumple la normativa y qué IP se requiere.") |
| with gr.Row(): |
| with gr.Column(scale=1): |
| elem_in = gr.Dropdown( |
| choices=["Tomacorriente", "Interruptor", "Luminaria común", "Termotanque"], |
| value="Tomacorriente", |
| label="Tipo de Artefacto/Elemento" |
| ) |
| dist_in = gr.Slider( |
| minimum=0, |
| maximum=300, |
| value=40, |
| step=10, |
| label="Distancia horizontal al borde de bañera/ducha [cm]" |
| ) |
| height_in = gr.Slider( |
| minimum=0, |
| maximum=300, |
| value=180, |
| step=10, |
| label="Altura desde el nivel del piso terminado [cm]" |
| ) |
| btn_bathroom = gr.Button("Analizar Zona", variant="primary") |
| |
| with gr.Column(scale=1): |
| bathroom_results_title = gr.Markdown("#### Análisis de Zonas de Seguridad") |
| bathroom_out = gr.HTML(label="Informe de Baños") |
| |
| btn_bathroom.click( |
| fn=audit_bathroom_zones, |
| inputs=[dist_in, height_in, elem_in, lang_dropdown], |
| outputs=bathroom_out |
| ) |
| |
| |
| with gr.Column(visible=False) as col_electrif: |
| electrif_title = gr.Markdown("### Cómputo de Grado de Electrificación y Bocas Mínimas") |
| with gr.Row(): |
| with gr.Column(scale=1): |
| covered_in = gr.Number( |
| value=80, |
| label="Superficie Cubierta del Inmueble [m²]", |
| precision=1 |
| ) |
| semi_covered_in = gr.Number( |
| value=20, |
| label="Superficie Semicubierta [m²] (Terrazas, Balcones techados)", |
| precision=1 |
| ) |
| btn_calc = gr.Button("Calcular Grado", variant="primary") |
| |
| with gr.Column(scale=1): |
| electrif_results_title = gr.Markdown("#### Requerimientos Mínimos del Inmueble") |
| calc_out = gr.HTML(label="Requisitos Mínimos") |
| |
| btn_calc.click( |
| fn=calc_electrification, |
| inputs=[covered_in, semi_covered_in, lang_dropdown], |
| outputs=calc_out |
| ) |
| |
| |
| with gr.Column(visible=False) as col_fp: |
| fp_title = gr.Markdown("### Calculadora de Factor de Potencia y Capacitancia") |
| with gr.Row(): |
| with gr.Column(scale=1): |
| fp_p = gr.Slider(minimum=100, maximum=50000, value=4000, step=100, label="Potencia Activa (P) [W]") |
| fp_i = gr.Slider(minimum=1, maximum=250, value=28, step=1, label="Intensidad Medida (I) [A]") |
| fp_v = gr.Slider(minimum=100, maximum=480, value=220, step=5, label="Tensión de Red (V) [V]") |
| fp_target = gr.Slider(minimum=0.8, maximum=1.0, value=0.98, step=0.01, label="Coseno φ Objetivo") |
| fp_freq = gr.Radio(choices=[50, 60], value=50, label="Frecuencia Eléctrica (f) [Hz]") |
| btn_fp = gr.Button("Calcular Corrección", variant="primary") |
| |
| with gr.Column(scale=1): |
| fp_status = gr.HTML(label="Resultado Crítico") |
| fp_triangle = gr.HTML(label="Triángulo de Potencias") |
| fp_table = gr.HTML(label="Variables Calculadas") |
| fp_benefits = gr.HTML(label="Beneficios de Corrección") |
| |
| btn_fp.click( |
| fn=calc_power_factor, |
| inputs=[fp_p, fp_i, fp_v, fp_target, fp_freq, lang_dropdown], |
| outputs=[fp_status, fp_table, fp_benefits, fp_triangle] |
| ) |
| |
| |
| with gr.Column(visible=False) as col_pat: |
| pat_title = gr.Markdown("### Resistencia de Puesta a Tierra (Reglamento Eléctrico Argentino Sección 771)") |
| with gr.Row(): |
| with gr.Column(scale=1): |
| pat_tipo = gr.Dropdown( |
| choices=[ |
| "Jabalina enterrada verticalmente (771-C.10.1)", |
| "Conductor desnudo enterrado horizontalmente (771-C.10.2)", |
| "Placa circular desnuda enterrada verticalmente (771-C.10.3)" |
| ], |
| value="Jabalina enterrada verticalmente (771-C.10.1)", |
| label="Tipo de Electrodo" |
| ) |
| pat_rho = gr.Number(value=100.0, label="Resistividad del Terreno (ρ) [Ω·m]") |
| |
| |
| 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 or "rod" in choice: |
| return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False) |
| elif "Conductor" in choice or "conductor" in choice: |
| return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False) |
| else: |
| return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True) |
| |
| pat_tipo.change( |
| fn=update_pat_fields, |
| inputs=[pat_tipo], |
| outputs=[pat_group_jab, pat_group_hor, pat_group_pla] |
| ) |
| |
| btn_pat.click( |
| fn=calc_puesta_a_tierra, |
| inputs=[pat_tipo, pat_rho, jab_L, jab_d, hor_L, hor_h, hor_d, pla_D, pla_h, lang_dropdown], |
| outputs=[pat_res, pat_details] |
| ) |
| |
| |
| with gr.Column(visible=False) as col_medidor: |
| medidor_title = gr.Markdown("### Medición de Potencia Activa por Medidor de Inducción") |
| with gr.Row(): |
| with gr.Column(scale=1): |
| med_k_unit = gr.Radio(choices=["rev / kWh", "Wh / rev"], value="rev / kWh", label="Unidad de la Constante (K)") |
| med_K = gr.Number(value=150.0, label="Valor de la Constante (K)") |
| med_N = gr.Number(value=10.0, label="Vueltas Contadas (N)") |
| with gr.Row(): |
| med_min = gr.Number(value=1.0, label="Minutos") |
| med_sec = gr.Number(value=15.0, label="Segundos") |
| btn_med = gr.Button("Calcular Potencia Medida", variant="primary") |
| |
| with gr.Column(scale=1): |
| med_res = gr.HTML(label="Potencia Calculada") |
| med_details = gr.HTML(label="Detalles del Ensayo") |
| |
| btn_med.click( |
| fn=calc_medidor_potencia, |
| inputs=[med_K, med_k_unit, med_N, med_min, med_sec, lang_dropdown], |
| outputs=[med_res, med_details] |
| ) |
| |
| |
| with gr.Column(visible=False) as col_tableros: |
| tableros_title = gr.Markdown("### Dimensionamiento de Distribución y Protecciones de Tableros") |
| with gr.Row(): |
| with gr.Column(scale=1): |
| tableros_bocas_title = gr.Markdown("#### Bocas del Proyecto") |
| tab_iug = gr.Number(value=10, label="Bocas de Iluminación Gral. (IUG)", precision=0) |
| tab_tug = gr.Number(value=15, label="Bocas de Tomacorrientes Gral. (TUG)", precision=0) |
| tab_iue = gr.Number(value=0, label="Bocas de Iluminación Especial (IUE)", precision=0) |
| tab_tue = gr.Number(value=0, label="Bocas de Tomacorrientes Especial (TUE)", precision=0) |
| |
| tableros_motores_title = gr.Markdown("#### Cargas de Motores (ACU)") |
| with gr.Row(): |
| tab_motor_hp = gr.Dropdown(choices=["0.5", "0.75", "1.0", "1.5", "2.0", "3.0", "5.0"], value="1.0", label="Potencia Motor [HP]") |
| tab_motor_fases = gr.Radio(choices=[1, 3], value=1, label="Fases Motor") |
| tab_motor_cant = gr.Number(value=0, label="Cantidad de Motores", precision=0) |
| |
| tableros_mbt_title = gr.Markdown("#### Muy Baja Tensión (MBT)") |
| with gr.Row(): |
| tab_mbt_w = gr.Number(value=100, label="Consumo por Carga [W]", precision=0) |
| tab_mbt_cant = gr.Number(value=0, label="Cantidad de Cargas", precision=0) |
| |
| tableros_params_title = gr.Markdown("#### Parámetros Generales") |
| with gr.Row(): |
| tab_coef = gr.Slider(minimum=0.5, maximum=1.0, value=0.8, step=0.05, label="Coeficiente de Simultaneidad") |
| tab_sum = gr.Radio(choices=["Monofásico", "Trifásico"], value="Monofásico", label="Suministro Sugerido") |
| |
| with gr.Row(): |
| btn_tab = gr.Button("Diseñar Tablero", variant="primary") |
| btn_pdf = gr.Button("Exportar a PDF", variant="secondary", visible=False) |
| |
| pdf_output = gr.File(label="Descargar Diagrama Unifilar PDF", visible=False) |
| |
| with gr.Column(scale=1): |
| tab_summary = gr.HTML(label="Resumen de Potencia") |
| tab_planilla = gr.HTML(label="Planilla de Carga") |
| |
| with gr.Row(): |
| with gr.Column(): |
| tableros_unifilar_title = gr.Markdown("#### Diagrama Unifilar Dinámico (Esquema Eléctrico)") |
| tab_svg = gr.HTML(label="Esquema Unifilar") |
| |
| btn_tab.click( |
| fn=on_design_click, |
| inputs=[tab_iug, tab_tug, tab_iue, tab_tue, tab_motor_hp, tab_motor_fases, tab_motor_cant, tab_mbt_w, tab_mbt_cant, tab_coef, tab_sum, lang_dropdown], |
| outputs=[tab_summary, tab_planilla, tab_svg, btn_pdf, pdf_output] |
| ) |
| |
| btn_pdf.click( |
| fn=export_unifilar_to_pdf, |
| inputs=[tab_iug, tab_tug, tab_iue, tab_tue, tab_motor_hp, tab_motor_fases, tab_motor_cant, tab_mbt_w, tab_mbt_cant, tab_coef, tab_sum, lang_dropdown], |
| outputs=pdf_output |
| ) |
| |
| |
| with gr.Column(visible=False) as col_rag: |
| rag_title = gr.Markdown("### Consultor del Reglamento Eléctrico Argentino") |
| rag_desc = gr.Markdown("Escribí una duda técnica o palabra clave sobre la reglamentación. El motor buscará de manera semántica e indexada en los PDFs cargados y responderá.") |
| |
| chat_textbox = gr.Textbox(placeholder="Escribí una duda técnica o palabra clave...", label="Pregunta") |
| |
| chat_interface = gr.ChatInterface( |
| fn=rag_query_response, |
| chatbot=gr.Chatbot(height=750), |
| textbox=chat_textbox, |
| additional_inputs=[lang_dropdown], |
| examples=[ |
| ["¿De qué color es el conductor neutro y el de tierra?"], |
| ["¿Cuál es el límite de bocas para un circuito TUG?"], |
| ["¿Qué se permite instalar en la Zona 1 del baño?"], |
| ["¿Qué sección mínima debe tener el cable de puesta a tierra?"], |
| ["¿Qué es el límite de aplicación para electrificación?"] |
| ] |
| ) |
|
|
| |
| 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) |
|
|
| |
| lang_dropdown.change( |
| fn=change_language, |
| inputs=[lang_dropdown], |
| outputs=[ |
| nav_btn_circuitos, nav_btn_banos, nav_btn_electrif, nav_btn_fp, |
| nav_btn_pat, nav_btn_medidor, nav_btn_tableros, nav_btn_rag, |
| circuit_title, circuit_in, section_in, protection_in, bocas_in, grouped_in, |
| btn_audit, audit_results_title, |
| bathroom_title, bathroom_desc, elem_in, dist_in, height_in, |
| btn_bathroom, bathroom_results_title, |
| electrif_title, covered_in, semi_covered_in, btn_calc, electrif_results_title, |
| fp_title, fp_p, fp_i, fp_v, fp_target, fp_freq, btn_fp, |
| pat_title, pat_tipo, pat_rho, jab_L, jab_d, hor_L, hor_h, hor_d, pla_D, pla_h, btn_pat, |
| medidor_title, med_k_unit, med_K, med_N, med_min, med_sec, btn_med, |
| tableros_title, tableros_bocas_title, tab_iug, tab_tug, tab_iue, tab_tue, |
| tableros_motores_title, tab_motor_hp, tab_motor_fases, tab_motor_cant, |
| tableros_mbt_title, tab_mbt_w, tab_mbt_cant, |
| tableros_params_title, tab_coef, tab_sum, btn_tab, btn_pdf, pdf_output, |
| tableros_unifilar_title, |
| rag_title, rag_desc, chat_textbox |
| ] |
| ) |
|
|
| if __name__ == "__main__": |
| |
| if "SPACE_ID" in os.environ: |
| demo.launch(server_name="0.0.0.0", server_port=7860) |
| else: |
| demo.launch(server_name="0.0.0.0", server_port=7861) |
|
|