Spaces:
Runtime error
Runtime error
File size: 8,596 Bytes
7599506 28927dd 7599506 28927dd 7599506 28927dd 7599506 28927dd 7599506 28927dd 7599506 28927dd 7599506 f6330ad 7599506 f6330ad 7599506 f6330ad 7599506 f6330ad 7599506 f6330ad 7599506 f6330ad 7599506 28927dd 7599506 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | """
models.py — Capa cognitiva (NLP zero-shot + Random Forest termodinámico).
Diferencia con el original: los modelos NO se cargan al importar el módulo
(eso obligaba a tener transformers/torch presentes solo para arrancar). Aquí se
cargan de forma PEREZOSA (lazy) la primera vez que se necesitan, vía
get_nlp_model() y get_tr_model(). La lógica de inferencia (calcular_ic_ia,
calcular_tr_y_ic_dinamico) es idéntica a app_tesis_final_V3.py.
"""
import warnings
import numpy as np
from .constants import ENFORCE_TR0_RESILIENTES
warnings.filterwarnings("ignore")
# ------------------------------------------------------------------
# Disponibilidad de librerías de IA (independientes entre sí).
# El Random Forest solo necesita sklearn; el NLP necesita transformers.
# ------------------------------------------------------------------
try:
from sklearn.ensemble import RandomForestRegressor # noqa: F401
ML_DISPONIBLE = True
except ImportError:
ML_DISPONIBLE = False
try:
from transformers import pipeline as _hf_pipeline # noqa: F401
NLP_DISPONIBLE = True
except ImportError:
NLP_DISPONIBLE = False
# Compatibilidad: "hay IA" si al menos uno de los dos modelos está disponible.
IA_DISPONIBLE = ML_DISPONIBLE or NLP_DISPONIBLE
# Singletons perezosos (se rellenan al primer uso).
_nlp_classifier = None
_nlp_cargado = False
_ml_tr_model = None
_ml_cargado = False
# Caché de predicciones del Random Forest (clave: entradas redondeadas).
_TR_CACHE: dict = {}
def get_nlp_model():
"""Carga perezosa del clasificador zero-shot. Devuelve None si no se puede."""
global _nlp_classifier, _nlp_cargado
if _nlp_cargado:
return _nlp_classifier
_nlp_cargado = True
if not NLP_DISPONIBLE:
_nlp_classifier = None
return None
try:
from transformers import pipeline
_nlp_classifier = pipeline(
"zero-shot-classification", model="Recognai/zeroshot_selectra_medium"
)
except Exception:
_nlp_classifier = None
return _nlp_classifier
def get_tr_model():
"""Carga/entrena perezosamente el Random Forest termodinámico (Tr en horas)."""
global _ml_tr_model, _ml_cargado
if _ml_cargado:
return _ml_tr_model
_ml_cargado = True
_ml_tr_model = _entrenar_modelo_termodinamico()
return _ml_tr_model
def _entrenar_modelo_termodinamico():
"""
Generador de datos sintéticos basados en física real (idéntico al original).
Entrena al Random Forest para que devuelva HORAS de secado respondiendo
correctamente a la temperatura y humedad.
"""
if not ML_DISPONIBLE:
return None
from sklearn.ensemble import RandomForestRegressor
np.random.seed(42)
X_train = []
y_train = []
# Creamos 2000 permutaciones físicas para entrenar a la IA
for _ in range(2000):
lluvia = np.random.uniform(0.1, 100.0)
temp = np.random.uniform(15.0, 45.0)
hum = np.random.uniform(30.0, 100.0)
suelo = np.random.choice([1, 2, 3])
# Fórmula física real de evaporación
evap_rate = max(0.1, (temp / 15.0) * ((100.0 - hum) / 40.0))
factor_suelo = {1: 1.5, 2: 1.0, 3: 0.5}[suelo] # 1 retiene más agua
tr_horas = (lluvia / evap_rate) * factor_suelo
X_train.append([lluvia, temp, hum, suelo])
y_train.append(min(96.0, tr_horas)) # Cap máximo de 96 horas
modelo = RandomForestRegressor(n_estimators=50, random_state=42)
modelo.fit(X_train, y_train)
return modelo
# ==============================================================================
# INFERENCIA SEMÁNTICA (Ic) — con caché por nombre y clasificación en lote.
# Los valores son idénticos a clasificar tarea por tarea; solo se evita repetir
# la inferencia para nombres iguales (clave para proyectos viales grandes).
# ==============================================================================
_IC_CACHE: dict = {}
_CATEGORIAS_IC = [
"estructuras de hormigón y acero",
"pavimento asfáltico y terminaciones",
"bases granulares y subbases",
"movimiento de tierras pesado y excavación",
]
_MAPA_IC = {_CATEGORIAS_IC[0]: 1.0, _CATEGORIAS_IC[1]: 1.5, _CATEGORIAS_IC[2]: 2.0, _CATEGORIAS_IC[3]: 3.0}
def _ic_por_palabras_clave(nombre_str: str) -> float:
"""Fallback determinista por RegEx (idéntico al original)."""
if any(w in nombre_str for w in ['acero', 'hormigon', 'hormigón', 'encofrado', 'vaciado', 'muro', 'alcantarilla', 'losa', 'zapata', 'columna', 'viga', 'platea', 'fundacion', 'fundación', 'estructura', 'paisajismo', 'limpieza', 'grama', 'terminacion', 'terminación']):
return 1.0
elif any(w in nombre_str for w in ['pintura', 'señalizacion', 'señalización']):
return 1.5
elif any(w in nombre_str for w in ['base', 'subbase', 'sub-base', 'granular', 'afirmado', 'asfalto', 'imprimacion', 'imprimación']):
return 2.0
elif any(w in nombre_str for w in ['corte', 'relleno', 'subrasante', 'tierra', 'excavacion', 'excavación']):
return 3.0
return 1.5
def precompute_ic_batch(nombres, usar_ia=True):
"""Clasifica en UN SOLO lote todos los nombres únicos y llena el caché.
Acelera drásticamente la corrida: una llamada al modelo en lugar de N. Si la IA
no está disponible no hace nada (el motor usará el fallback por palabras clave).
"""
if not usar_ia:
return
nlp = get_nlp_model()
if not nlp:
return
unicos = sorted({str(n).lower() for n in nombres if n is not None and str(n).strip()})
pendientes = [n for n in unicos if (n, True) not in _IC_CACHE]
if not pendientes:
return
try:
resultados = nlp(pendientes, _CATEGORIAS_IC)
if isinstance(resultados, dict): # el pipeline devuelve dict si es un solo texto
resultados = [resultados]
for nombre, res in zip(pendientes, resultados):
_IC_CACHE[(nombre, True)] = _MAPA_IC.get(res['labels'][0], 1.5)
except Exception:
pass
def calcular_ic_ia(nombre_tarea, usar_ia=True):
nombre_str = str(nombre_tarea).lower()
key = (nombre_str, bool(usar_ia))
cached = _IC_CACHE.get(key)
if cached is not None:
return cached
nlp_classifier = get_nlp_model() if usar_ia else None
if not usar_ia or not nlp_classifier:
val = _ic_por_palabras_clave(nombre_str)
_IC_CACHE[key] = val
return val
try:
res = nlp_classifier(nombre_str, _CATEGORIAS_IC)
val = _MAPA_IC.get(res['labels'][0], 1.5)
except Exception:
val = 1.5
_IC_CACHE[key] = val
return val
def calcular_tr_y_ic_dinamico(lluvia_mm, temp_c, humedad_pct, tipo_suelo_ic, usar_ia=True):
ml_tr_model = get_tr_model() if usar_ia else None
if not usar_ia or not ml_tr_model:
# [AUD-04] Fallback ciego (Ceguera Térmica, CFX-AUDIT-08): valores estáticos
# declarados en la tesis: 48 h arcillas, 12 h granulares, 6 h asfalto, 0 h estructuras.
if tipo_suelo_ic >= 3.0:
return 48.0, tipo_suelo_ic
elif tipo_suelo_ic >= 2.0:
return 12.0, tipo_suelo_ic
elif tipo_suelo_ic >= 1.5:
return 6.0, tipo_suelo_ic
else:
return 0.0, tipo_suelo_ic
# [AUD-05] Ec. 6.19: Δθ=0 ⟹ Tr=0 para hormigón/acero (modo estricto opcional).
if ENFORCE_TR0_RESILIENTES and tipo_suelo_ic <= 1.0:
return 0.0, tipo_suelo_ic
suelo_cat = 1 if tipo_suelo_ic >= 3.0 else (2 if tipo_suelo_ic >= 2.0 else 3)
# La IA predice HORAS reales. Cacheamos la predicción con las entradas redondeadas:
# sklearn tarda ~3.8 ms por predicción de UNA muestra, y el motor la invoca una vez
# por día lluvioso; en un proyecto grande eso son decenas de miles de llamadas
# idénticas. El redondeo (0.5 mm / 0.5 °C / 0.5 %) deja Tr prácticamente igual y
# colapsa miles de llamadas a unas pocas. Resultado: hasta cientos de veces más rápido.
key = (round(lluvia_mm * 2) / 2, round(temp_c * 2) / 2, round(humedad_pct * 2) / 2, suelo_cat)
tr_horas = _TR_CACHE.get(key)
if tr_horas is None:
tr_horas = float(ml_tr_model.predict([[key[0], key[1], key[2], suelo_cat]])[0])
_TR_CACHE[key] = tr_horas
# [AUD-07] Penalización dinámica del coeficiente — Ec. 6.22 de la tesis:
# Ic_dinámico = Q(Ic) = Ic_base + Tr/48 (constante de normalización 48 h)
tr_dias = tr_horas / 24.0
ic_dinamico = round(tipo_suelo_ic + (tr_dias * 0.5), 2)
return round(tr_horas, 1), ic_dinamico
|