Zapata De Tanque Elevado

OBRA IE 344 INICIAL - AYACUCHO

Author

Ing. Paolo De la calle (CIP: 154396)

Zapata de Tanque Elevado
import numpy as np
import pandas as pd

class CalculadoraMetrado:
    def __init__(self, in_elevado, in_soporte, in_cisterna, in_materiales):
        # 1. Asignación de Inputs Clasificados
        self.Elev = in_elevado      # Datos Tanque Elevado
        self.Sop = in_soporte       # Datos Soporte (Vigas/Col)
        self.Cis = in_cisterna      # Datos Cisterna
        self.Mat = in_materiales    # Materiales Generales
        
        # 2. Geometría Derivada (Calculada una sola vez)
        # Ejes y dimensiones exteriores basados en el Tanque Elevado
        self.Leje = self.Elev['L_luz'] + 0.50 
        self.L_Cis_X = self.Leje + 0.50
        self.L_Cis_Y = self.Leje + 0.50

    def Ejecutar(self):
        """
        Realiza el metrado separando Cargas Vivas (CV) y Muertas (CM).
        """
        # ==========================================
        # 1. CARGAS VIVAS (CV) - PESO DEL AGUA
        # ==========================================
        # A. Agua Tanque Elevado
        # Volumen = Area_Luz * Altura_Agua
        P_AguaE = (self.Elev['L_luz']**2 * self.Elev['HL_agua']) * self.Mat['GammaW']
        
        # B. Agua Cisterna
        # Volumen = (Largo_Ext - 2*Muros) * Altura_Ent * Ancho...
        L_int_cis = self.L_Cis_X - 2*self.Cis['Tw_muro']
        P_AguaC = (L_int_cis**2 * self.Cis['H_ent']) * self.Mat['GammaW']
        
        Total_CV = P_AguaE + P_AguaC

        # ==========================================
        # 2. CARGAS MUERTAS (CM) - CONCRETO
        # ==========================================
        
        # A. Tanque Elevado (Recipiente)
        # Paredes (4 lados) + Losa Fondo + Losa Techo
        Vol_Paredes_E = (self.Elev['L_luz'] * 4) * self.Elev['H_cajon'] * self.Elev['Tw_par']
        Vol_Losas_E   = (self.Elev['L_luz']**2) * (self.Elev['Tf_fondo'] + self.Elev['Tr_techo'])
        
        P_ConcE = (Vol_Paredes_E + Vol_Losas_E) * self.Mat['GammaC']
        
        # B. Soporte (Columnas y Vigas)
        # Altura total de columnas = Altura_piso * N_pisos
        H_Total_Cols = self.Sop['H_entrepiso'] * self.Sop['N_pisos']
        
        # Volumen Columnas = Area_Seccion * H_Total * Num_Cols
        Vol_Cols = self.Sop['Area_Col_Unit'] * H_Total_Cols * self.Sop['Num_Cols']
        
        # Volumen Vigas = (Seccion_Viga * Longitud_Total) * N_pisos
        Longitud_Vigas_Piso = self.Leje * 4 # Perímetro a ejes
        Vol_Vigas = (self.Sop['Viga_b'] * self.Sop['Viga_h']) * Longitud_Vigas_Piso * self.Sop['N_pisos']
        
        P_Soporte = (Vol_Cols + Vol_Vigas) * self.Mat['GammaC']
        
        # C. Cisterna (Casco Estructural)
        # Muros Perimetrales + Losa Techo (La zapata se calcula en el paso de Geotecnia)
        Vol_Muros_C = (self.L_Cis_X * 4) * self.Cis['H_ent'] * self.Cis['Tw_muro']
        Vol_Techo_C = (self.L_Cis_X**2) * self.Cis['Tr_techo']
        
        P_ConcC = (Vol_Muros_C + Vol_Techo_C) * self.Mat['GammaC']
        
        # Total Carga Muerta
        Total_CM = P_ConcE + P_Soporte + P_ConcC
        
        # Carga Total de Servicio
        P_Servicio = Total_CM + Total_CV
        
        # ==========================================
        # 3. EMPAQUETADO DE DATOS (OUTPUT)
        # ==========================================
        
        # DataFrame para visualización
        data = {
            "ID": ["AGUA_ELEV", "AGUA_CIS", "CONC_ELEV", "SOPORTE", "CONC_CIS", "TOTAL_CM", "TOTAL_CV", "TOTAL_SERV"],
            "Descripcion": [
                "Agua Tanque Elevado", 
                "Agua Cisterna", 
                "Concreto Tanque Elevado", 
                "Soporte (Columnas + Vigas)", 
                "Casco Cisterna (Muros+Techo)", 
                "TOTAL CARGA MUERTA", 
                "TOTAL CARGA VIVA", 
                "TOTAL SERVICIO"
            ],
            "Valor": [P_AguaE, P_AguaC, P_ConcE, P_Soporte, P_ConcC, Total_CM, Total_CV, P_Servicio],
            "Tipo": ["CV", "CV", "CM", "CM", "CM", "RESUMEN", "RESUMEN", "RESUMEN"]
        }
        df = pd.DataFrame(data)
        
        # Retorno estructurado para R
        return {
            "tabla": df,
            "parametros": {
                "Total_CM": Total_CM,
                "Total_CV": Total_CV,
                "P_Servicio": P_Servicio,
                "L_Cis_X": self.L_Cis_X,
                "H_Zapata": self.Cis['H_zap']
            }
        }

# --- DEFINICIÓN CLARA DE INPUTS ---

# 1. Tanque Elevado
Input_Elevado = {
    'L_luz': 2.35,      # Luz interior
    'HL_agua': 2.50,    # Altura de agua
    'H_cajon': 2.50,    # Altura muros (HL + Borde Libre implícito o explícito)
    'Tw_par': 0.20,     # Espesor paredes
    'Tf_fondo': 0.20,   # Espesor losa fondo
    'Tr_techo': 0.15    # Espesor losa techo
}

# 2. Soporte (Vigas y Columnas)
Input_Soporte = {
    'H_entrepiso': 2.60,
    'N_pisos': 4,
    'Num_Cols': 4,
    # Área de Columna L (0.50x0.50x0.25) -> (0.50*0.25) + (0.25*0.25) = 0.1875 m2
    'Area_Col_Unit': 0.1875, 
    'Viga_b': 0.25,
    'Viga_h': 0.40
}

# 3. Cisterna
Input_Cisterna = {
    'H_ent': 3.00,      # Altura enterramiento
    'H_zap': 0.50,      # Altura zapata (para geotecnia posterior)
    'Tw_muro': 0.25,    # Espesor muros
    'Tr_techo': 0.15    # Espesor techo
}

# 4. Materiales
Input_Materiales = {
    'GammaC': 2.4,      # Concreto Armado
    'GammaW': 1.0       # Agua
}

# --- EJECUCIÓN ---
metrado = CalculadoraMetrado(Input_Elevado, Input_Soporte, Input_Cisterna, Input_Materiales)
resultados_py = metrado.Ejecutar()
library(reticulate)
library(flextable)
library(dplyr)

# 1. Extracción Segura de Datos (Clean Source)
# Convertimos el DF de Python a una lista pura antes de traerla a R
py_run_string("export_metrado = resultados_py['tabla'].to_dict(orient='list')")
raw_data <- py$export_metrado

# 2. Reconstrucción del DataFrame en R
df_reporte <- data.frame(
  Descripcion = unlist(raw_data$Descripcion),
  Valor       = as.numeric(unlist(raw_data$Valor)),
  Tipo        = unlist(raw_data$Tipo),
  stringsAsFactors = FALSE
)

# 3. Renderizado de Tabla Profesional
df_reporte %>%
  select(Descripcion, Valor, Tipo) %>%
  flextable() %>%
  # Encabezados
  set_caption("Tabla 1: Metrado de Cargas por Componente") %>%
  set_header_labels(
    Descripcion = "Elemento Estructural", 
    Valor = "Carga (Ton)", 
    Tipo = "Clasif."
  ) %>%
  # Formatos
  colformat_double(j = "Valor", digits = 2) %>%
  theme_box() %>%
  autofit() %>%
  # Estilos:
  # - Filas de Resumen (Totales) en amarillo
  bg(i = ~ Tipo == "RESUMEN", bg = "#FFF2CC") %>%
  bold(i = ~ Tipo == "RESUMEN") %>%
  # - Cargas Vivas en azul muy claro (opcional, para diferenciar)
  bg(i = ~ Tipo == "CV", bg = "#F0F8FF") %>%
  align(j = c("Valor", "Tipo"), align = "center", part = "all")
Table 1

Elemento Estructural

Carga (Ton)

Clasif.

Agua Tanque Elevado

13.81

CV

Agua Cisterna

24.37

CV

Concreto Tanque Elevado

15.92

CM

Soporte (Columnas + Vigas)

29.66

CM

Casco Cisterna (Muros+Techo)

28.16

CM

TOTAL CARGA MUERTA

73.74

RESUMEN

TOTAL CARGA VIVA

38.17

RESUMEN

TOTAL SERVICIO

111.92

RESUMEN

class CalculadoraGeotecnia:
    def __init__(self, obj_input, suelo, dim_user=None):
        # 1. Recuperación de Datos (Desde el diccionario 'parametros')
        params = obj_input['parametros']
        
        self.P_Servicio = params['P_Servicio']
        self.Total_CM   = params['Total_CM']
        self.Total_CV   = params['Total_CV']
        
        # Geometría de la Cisterna (Base para el mínimo)
        # Por ahora asumimos cisterna cuadrada en planta base, pero preparada para X/Y
        self.L_Cis_X = params['L_Cis_X']
        self.L_Cis_Y = params['L_Cis_X'] # Asumimos cuadrada si no se definió Y
        self.H_Zap   = params['H_Zapata']
        
        self.Suelo = suelo
        self.DimUser = dim_user # Tupla opcional (Lx, Ly) si el usuario quiere forzar medidas
        
    def Ejecutar(self):
        # 1. Capacidad Portante Neta
        # Sigma_Neto = Sigma_t - (Sobrecarga Relleno 3m) - (Peso Propio Zapata)
        # Asumimos relleno promedio gamma=1.8
        Sigma_Neto = self.Suelo['Sigma_t'] - (3.00 * 1.8) - (self.H_Zap * 2.4)
        
        if Sigma_Neto <= 0:
             raise ValueError(f"ERROR: Capacidad Neta Negativa ({Sigma_Neto}). Suelo muy pobre o zapata muy profunda.")
             
        # 2. Área Requerida
        Area_Req = self.P_Servicio / Sigma_Neto
        
        # 3. Dimensionamiento (Lógica de Selección)
        if self.DimUser:
            # A. Usuario define dimensiones
            L_Final_X, L_Final_Y = self.DimUser
            Nota = "Definido por Usuario"
            
            if (L_Final_X * L_Final_Y) < Area_Req:
                print(f"⚠️ ADVERTENCIA: El área ingresada ({L_Final_X*L_Final_Y:.2f} m2) es menor a la requerida ({Area_Req:.2f} m2).")
        else:
            # B. Cálculo Automático (Zapata Cuadrada Óptima)
            L_Teorico = np.sqrt(Area_Req)
            
            # El ancho no puede ser menor que la cisterna
            L_Min_Arq = max(self.L_Cis_X, self.L_Cis_Y)
            
            # Seleccionamos el mayor y redondeamos a 0.05m
            L_Calc = max(L_Teorico, L_Min_Arq)
            L_Final = np.ceil(L_Calc * 20) / 20 
            
            L_Final_X = L_Final
            L_Final_Y = L_Final
            Nota = "Cálculo Automático"
            
        # 4. Presión de Contacto Real (Servicio)
        Area_Final = L_Final_X * L_Final_Y
        Presion_Real = self.P_Servicio / Area_Final
        
        # 5. Estructuración de Salida
        data = {
            "Descripcion": [
                "Capacidad Neta Disponible", 
                "Área Requerida (Suelo)", 
                "Lado Mínimo (Cisterna)", 
                "LADO ZAPATA X", 
                "LADO ZAPATA Y", 
                "Presión Real Contacto"
            ],
            "Valor": [
                Sigma_Neto, 
                Area_Req, 
                max(self.L_Cis_X, self.L_Cis_Y), 
                L_Final_X, 
                L_Final_Y, 
                Presion_Real
            ],
            "Unidad": ["Ton/m2", "m2", "m", "m", "m", "Ton/m2"]
        }
        df = pd.DataFrame(data)
        
        # Retorno compatible con Fase 3 (Diseño Acero)
        return {
            "tabla": df,
            "parametros": {
                "Lx_Zap": L_Final_X,
                "Ly_Zap": L_Final_Y,
                "Lx_Cis": self.L_Cis_X,
                "Ly_Cis": self.L_Cis_Y,
                "H_Zap": self.H_Zap,
                "Total_CM": self.Total_CM, # <--- CLAVE: Pasamos cargas separadas
                "Total_CV": self.Total_CV,
                "Presion_Serv": Presion_Real
            }
        }

# --- INPUTS ---
SueloDatos = {'Sigma_t': 11.0} # Ton/m2

# Opción A: Automático
# dim_user = None 
# Opción B: Forzar dimensiones (ej. 4.50 x 4.50)
dim_user = (7.60, 4.45)

geotecnia = CalculadoraGeotecnia(resultados_py, SueloDatos, dim_user)
resultados_geo = geotecnia.Ejecutar()
library(reticulate)
library(flextable)
library(dplyr)

# 1. Extracción Segura
py_run_string("export_geo = resultados_geo['tabla'].to_dict(orient='list')")
raw_geo <- py$export_geo

# 2. Reconstrucción
df_geo <- data.frame(
  Parametro = unlist(raw_geo$Descripcion),
  Valor     = as.numeric(unlist(raw_geo$Valor)),
  Unidad    = unlist(raw_geo$Unidad),
  stringsAsFactors = FALSE
)

# 3. Tabla
flextable(df_geo) %>%
  set_caption("Tabla 2: Dimensionamiento Geotécnico") %>%
  set_header_labels(Parametro = "Parámetro", Valor = "Resultado") %>%
  colformat_double(j = "Valor", digits = 2) %>%
  theme_box() %>%
  autofit() %>%
  # Resaltar Lados Finales
  bg(i = ~ Parametro %in% c("LADO ZAPATA X", "LADO ZAPATA Y"), bg = "#E2EFDA") %>%
  bold(i = ~ Parametro %in% c("LADO ZAPATA X", "LADO ZAPATA Y")) %>%
  align(j = c("Valor", "Unidad"), align = "center", part = "all")
Table 2

Parámetro

Resultado

Unidad

Capacidad Neta Disponible

4.40

Ton/m2

Área Requerida (Suelo)

25.44

m2

Lado Mínimo (Cisterna)

3.35

m

LADO ZAPATA X

7.60

m

LADO ZAPATA Y

4.45

m

Presión Real Contacto

3.31

Ton/m2

class CalculadoraConcretoDetallada:
    def __init__(self, geo_output, mat, acero_config, vuelos_reales):
        params = geo_output['parametros']
        
        self.Lx_Zap = params['Lx_Zap']
        self.Ly_Zap = params['Ly_Zap']
        self.Lx_Cis = params['Lx_Cis']
        self.Ly_Cis = params['Ly_Cis']
        self.H_Zap  = params['H_Zap']
        
        self.Total_CM = params['Total_CM']
        self.Total_CV = params['Total_CV']
        
        self.Mat = mat
        self.Barra = acero_config
        self.Vuelos = vuelos_reales

    def _disenar_eje(self, Vuelo_1, Vuelo_2, L_Zap_Transversal, qu):
        
        # 1. Momento
        Vuelo_Critico = max(Vuelo_1, Vuelo_2)
        Lado = "Izquierda/Arriba" if Vuelo_1 > Vuelo_2 else "Derecha/Abajo"
        
        Mu = (qu * Vuelo_Critico**2) / 2
            
        # 2. Acero por Cálculo (Flexión)
        Rec = 7.5 
        d = (self.H_Zap * 100) - Rec - (self.Barra['diam_cm'] / 2)
        b = 100 
        
        a = 2.0
        for _ in range(10):
            As_Calc = (Mu * 100000) / (0.90 * self.Mat['Fy'] * (d - a/2))
            a = (As_Calc * self.Mat['Fy']) / (0.85 * self.Mat['Fc'] * b)
            
        # 3. Acero Mínimo (Temperatura - Norma E.060/ACI)
        # As_min = 0.0018 * b * h
        As_Min = 0.0018 * b * (self.H_Zap * 100)
        
        # 4. Decisión
        if As_Min >= As_Calc:
            As_Final = As_Min
            Control = "MÍNIMO (Temp)"
        else:
            As_Final = As_Calc
            Control = "CÁLCULO (Flexión)"
        
        # 5. Distribución
        s_calc = (self.Barra['area_cm2'] / As_Final) * 100
        s_final = np.floor(s_calc) / 100
        N_barras = int(L_Zap_Transversal / s_final) + 1
        
        Detalle = f"{N_barras} \u03C6 {self.Barra['nombre']} @ {s_final:.2f} m"
        
        return {
            "Vuelo": Vuelo_Critico,
            "Lado": Lado,
            "Mu": Mu,
            "As_Calc": As_Calc,
            "As_Min": As_Min,
            "As_Final": As_Final,
            "Control": Control,
            "Detalle": Detalle
        }

    def Ejecutar(self):
        # Cargas
        Pu = (1.4 * self.Total_CM) + (1.7 * self.Total_CV)
        Area_Zap = self.Lx_Zap * self.Ly_Zap
        qu = Pu / Area_Zap 
        
        # Diseños
        # Eje X (Transversal Ly)
        Res_X = self._disenar_eje(self.Vuelos['Ix'], self.Vuelos['Dx'], self.Ly_Zap, qu)
        # Eje Y (Transversal Lx)
        Res_Y = self._disenar_eje(self.Vuelos['Iy'], self.Vuelos['Dy'], self.Lx_Zap, qu)
        
        # Tabla Detallada
        data = {
            "Concepto": [
                "Carga Factorada (Pu)", "Presión Diseño (qu)",
                "--- EJE X (Horizontal) ---",
                f"Vuelo Crítico ({Res_X['Lado']})",
                "Momento Último (Mu)",
                "As Requerido (Por Cálculo)",   # <--- NUEVO
                "As Mínimo (Norma)",            # <--- NUEVO
                f"AS DISEÑO ({Res_X['Control']})", # <--- Dice cual ganó
                "DISTRIBUCIÓN EJE X",
                "--- EJE Y (Vertical) ---",
                f"Vuelo Crítico ({Res_Y['Lado']})",
                "Momento Último (Mu)",
                "As Requerido (Por Cálculo)",   # <--- NUEVO
                "As Mínimo (Norma)",            # <--- NUEVO
                f"AS DISEÑO ({Res_Y['Control']})", # <--- Dice cual ganó
                "DISTRIBUCIÓN EJE Y"
            ],
            "Resultado": [
                f"{Pu:.2f}", f"{qu:.2f}",
                "", 
                f"{Res_X['Vuelo']:.2f}",
                f"{Res_X['Mu']:.2f}",
                f"{Res_X['As_Calc']:.2f}",     # Valor Calc
                f"{Res_X['As_Min']:.2f}",      # Valor Min
                f"{Res_X['As_Final']:.2f}",    # Valor Final
                Res_X['Detalle'],
                "", 
                f"{Res_Y['Vuelo']:.2f}",
                f"{Res_Y['Mu']:.2f}",
                f"{Res_Y['As_Calc']:.2f}",     # Valor Calc
                f"{Res_Y['As_Min']:.2f}",      # Valor Min
                f"{Res_Y['As_Final']:.2f}",    # Valor Final
                Res_Y['Detalle']
            ],
            "Unidad": [
                "Ton", "Ton/m2",
                "", "m", "Ton.m/m", "cm2/m", "cm2/m", "cm2/m", "-",
                "", "m", "Ton.m/m", "cm2/m", "cm2/m", "cm2/m", "-"
            ]
        }
        
        df = pd.DataFrame(data)
        return {"tabla": df, "parametros": {}}

# --- INPUTS (Asegúrate que coinciden con tu imagen/geotecnia) ---
Inputs_Imagen = {'Ix': 1.00, 'Dx': 0.70, 'Iy': 2.45, 'Dy': 2.45}
ConfigBarra = {'nombre': '3/4"', 'diam_cm': 1.905, 'area_cm2': 2.85}
MatDiseno = {'Fc': 210, 'Fy': 4200}

concreto = CalculadoraConcretoDetallada(resultados_geo, MatDiseno, ConfigBarra, Inputs_Imagen)
resultados_acero = concreto.Ejecutar()
library(reticulate)
library(flextable)
library(dplyr)

py_run_string("export_acero = resultados_acero['tabla'].to_dict(orient='list')")
raw_acero <- py$export_acero

df_acero <- data.frame(
  Concepto  = unlist(raw_acero$Concepto),
  Resultado = unlist(raw_acero$Resultado),
  Unidad    = unlist(raw_acero$Unidad),
  stringsAsFactors = FALSE
)

flextable(df_acero) %>%
  set_caption("Tabla 3: Diseño de Acero (Comparativo Cálculo vs Mínimo)") %>%
  theme_box() %>%
  autofit() %>%
  # 1. Separadores de Sección
  bg(i = ~ grepl("---", Concepto), bg = "#404040") %>%
  color(i = ~ grepl("---", Concepto), color = "white") %>%
  bold(i = ~ grepl("---", Concepto)) %>%
  
  # 2. Resaltar COMPARATIVO
  # Gris suave para los inputs de la decisión
  bg(i = ~ grepl("As Requerido|As Mínimo", Concepto), bg = "#F9F9F9") %>%
  # VERDE para el Ganador (As Diseño)
  bg(i = ~ grepl("AS DISEÑO", Concepto), bg = "#E2EFDA") %>%
  bold(i = ~ grepl("AS DISEÑO", Concepto)) %>%
  
  # 3. Resaltar RECETA FINAL (Amarillo)
  bg(i = ~ grepl("DISTRIBUCIÓN", Concepto), bg = "#FFFF00") %>%
  bold(i = ~ grepl("DISTRIBUCIÓN", Concepto)) %>%
  
  align(j = "Resultado", align = "center", part = "all")
Table 3

Concepto

Resultado

Unidad

Carga Factorada (Pu)

168.14

Ton

Presión Diseño (qu)

4.97

Ton/m2

--- EJE X (Horizontal) ---

Vuelo Crítico (Izquierda/Arriba)

1.00

m

Momento Último (Mu)

2.49

Ton.m/m

As Requerido (Por Cálculo)

1.59

cm2/m

As Mínimo (Norma)

9.00

cm2/m

AS DISEÑO (MÍNIMO (Temp))

9.00

cm2/m

DISTRIBUCIÓN EJE X

15 φ 3/4" @ 0.31 m

-

--- EJE Y (Vertical) ---

Vuelo Crítico (Derecha/Abajo)

2.45

m

Momento Último (Mu)

14.92

Ton.m/m

As Requerido (Por Cálculo)

9.77

cm2/m

As Mínimo (Norma)

9.00

cm2/m

AS DISEÑO (CÁLCULO (Flexión))

9.77

cm2/m

DISTRIBUCIÓN EJE Y

27 φ 3/4" @ 0.29 m

-

class VerificacionCortante:
    def __init__(self, geo_output, mat, acero_config):
        params = geo_output['parametros']
        
        # Geometría
        self.Lx_Zap = params['Lx_Zap']
        self.Ly_Zap = params['Ly_Zap']
        self.Lx_Cis = params['Lx_Cis']
        self.Ly_Cis = params['Ly_Cis']
        self.H_Zap  = params['H_Zap']
        
        # Cargas
        self.Total_CM = params['Total_CM']
        self.Total_CV = params['Total_CV']
        
        self.Mat = mat
        # d promedio (restamos 1 diametro aprox)
        self.d_cm = (self.H_Zap * 100) - 7.5 - acero_config['diam_cm']
        self.d_m  = self.d_cm / 100

    def _capacidad_concreto(self, b_cm, d_cm, tipo="Viga"):
        phi = 0.85
        fc = self.Mat['Fc']
        
        if tipo == "Viga":
            Vc = 0.53 * np.sqrt(fc) * b_cm * d_cm
        elif tipo == "Punzonamiento":
            Vc = 1.06 * np.sqrt(fc) * b_cm * d_cm
            
        return (phi * Vc) / 1000 # Ton

    def Ejecutar(self):
        # 1. Cargas
        Pu = (1.4 * self.Total_CM) + (1.7 * self.Total_CV)
        Area_Zap = self.Lx_Zap * self.Ly_Zap
        qu = Pu / Area_Zap 
        
        Resultados = []
        
        # ==========================================
        # 1. CORTANTE 1-DIRECCIÓN (VIGA)
        # ==========================================
        # Calculamos vuelos teóricos promedio
        Vuelo_X = (self.Lx_Zap - self.Lx_Cis) / 2
        Vuelo_Y = (self.Ly_Zap - self.Ly_Cis) / 2
        
        # Determinamos cuál es el crítico para reportarlo
        if Vuelo_X > Vuelo_Y:
            Eje_Critico = "EJE X (Transversal)"
            Vuelo_Max = Vuelo_X
            L_Corte = self.Ly_Zap # El ancho que resiste es el opuesto
        else:
            Eje_Critico = "EJE Y (Longitudinal)"
            Vuelo_Max = Vuelo_Y
            L_Corte = self.Lx_Zap
            
        # Análisis
        L_Tributaria = Vuelo_Max - self.d_m
        
        if L_Tributaria > 0:
            # Vu = qu * Area_Trib (Ancho total de la zapata)
            # OJO: Se verifica en todo el ancho L_Corte
            Vu_Viga = qu * L_Tributaria * L_Corte
            
            # Resistencia en todo el ancho
            Phi_Vc_Viga = self._capacidad_concreto(L_Corte*100, self.d_cm, "Viga")
            
            Ratio_Viga = Vu_Viga / Phi_Vc_Viga
            Status_Viga = "OK" if Ratio_Viga <= 1.0 else "FALLA"
        else:
            Vu_Viga = 0
            Phi_Vc_Viga = 0
            Ratio_Viga = 0
            Status_Viga = "N/A (d > Vuelo)"

        Resultados.append({
            "Control": f"Cortante por Flexión",
            "Detalle": f"Crítico: {Eje_Critico}", # <--- NUEVO CAMPO
            "Demanda_Vu": Vu_Viga,
            "Capacidad_PhiVc": Phi_Vc_Viga,
            "Ratio": Ratio_Viga,
            "Estado": Status_Viga
        })

        # ==========================================
        # 2. PUNZONAMIENTO (2-DIRECCIONES)
        # ==========================================
        b1 = self.Lx_Cis + self.d_m
        b2 = self.Ly_Cis + self.d_m
        Perimetro_Bo = 2 * (b1 + b2)
        Area_Critica = b1 * b2
        
        if Area_Critica < Area_Zap:
            Fuerza_Alivio = qu * Area_Critica
            Vu_Punch = Pu - Fuerza_Alivio
            Phi_Vc_Punch = self._capacidad_concreto(Perimetro_Bo*100, self.d_cm, "Punzonamiento")
            
            Ratio_Punch = Vu_Punch / Phi_Vc_Punch
            Status_Punch = "OK" if Ratio_Punch <= 1.0 else "FALLA"
        else:
            Vu_Punch = 0
            Phi_Vc_Punch = 0
            Ratio_Punch = 0
            Status_Punch = "N/A"

        Resultados.append({
            "Control": "Punzonamiento",
            "Detalle": f"Perímetro Bo = {Perimetro_Bo:.2f} m", # <--- NUEVO CAMPO
            "Demanda_Vu": Vu_Punch,
            "Capacidad_PhiVc": Phi_Vc_Punch,
            "Ratio": Ratio_Punch,
            "Estado": Status_Punch
        })
        
        df = pd.DataFrame(Resultados)
        return {"tabla": df}

# Ejecución
cortante = VerificacionCortante(resultados_geo, MatDiseno, ConfigBarra)
resultados_cortante = cortante.Ejecutar()
library(reticulate)
library(flextable)
library(dplyr)

# 1. Extracción Segura
py_run_string("export_cort = resultados_cortante['tabla'].to_dict(orient='list')")
raw_cort <- py$export_cort

# 2. Reconstrucción
df_cort <- data.frame(
  Control     = unlist(raw_cort$Control),
  Detalle     = unlist(raw_cort$Detalle), # <--- Columna Nueva
  Vu          = as.numeric(unlist(raw_cort$Demanda_Vu)),
  PhiVc       = as.numeric(unlist(raw_cort$Capacidad_PhiVc)),
  Ratio       = as.numeric(unlist(raw_cort$Ratio)),
  Estado      = unlist(raw_cort$Estado),
  stringsAsFactors = FALSE
)

# 3. Renderizado
ft <- flextable(df_cort) %>%
  set_caption("Tabla 4: Verificación de Cortante (ACI 318)") %>%
  set_header_labels(
    Control = "Tipo de Verificación",
    Detalle = "Ubicación / Detalle",
    Vu = "Vu (Ton)",
    PhiVc = "φVc (Ton)",
    Ratio = "Ratio"
  ) %>%
  theme_box() %>%
  autofit() %>%
  colformat_double(j = c("Vu", "PhiVc", "Ratio"), digits = 2) %>%
  
  # Resaltar la ubicación crítica en itálica
  italic(j = "Detalle") %>%
  
  # Semáforo
  bg(i = ~ Ratio <= 1.0, j = "Estado", bg = "#C6EFCE") %>%
  color(i = ~ Ratio <= 1.0, j = "Estado", color = "#006100") %>%
  bg(i = ~ Ratio > 1.0, j = "Estado", bg = "#FFC7CE") %>%
  color(i = ~ Ratio > 1.0, j = "Estado", color = "#9C0006") %>%
  bold(j = "Estado") %>%
  
  align(j = c("Vu", "PhiVc", "Ratio", "Estado"), align = "center", part = "all")

ft
Table 4

Tipo de Verificación

Ubicación / Detalle

Vu (Ton)

φVc (Ton)

Ratio

Estado

Cortante por Flexión

Crítico: EJE X (Transversal)

38.03

117.93

0.32

OK

Punzonamiento

Perímetro Bo = 15.02 m

98.00

796.32

0.12

OK