Simulación de Monitoreo de Salud Estructural - Predicción de Fatiga y Vida Útil

Este artículo explora los métodos y modelos para predecir la fatiga y la vida útil de las estructuras bajo cargas cíclicas, un componente crucial en el Monitoreo de Salud Estructural (SHM).

  1. Modelos de Acumulación de Daño por Fatiga

1.1 Conceptos Fundamentales de la Fatiga La fatiga es un fenómeno de fallo en materiales o estructuras que ocurre bajo cargas cíclicas, debido al desarrollo progresivo de defectos a nivel microscópico. La acumulación de daño por fatiga es un proceso intrincado que involucra cambios en la microestructura del material, la evolución de campos de esfuerzo y deformación, y la influencia de factores ambientales. Características clave del daño por fatiga:

  • Ocurre bajo cargas cíclicas.
  • Puede llevar a la falla a niveles de esfuerzo muy por debajo del límite de fluencia del material.
  • Es un proceso de acumulación gradual.
  • Finalmente, puede resultar en una falla repentina de la estructura.

1.2 Modelos de Acumulación de Daño 1.2.1 Regla de Daño Lineal de Miner La regla de daño lineal de Miner es el modelo más comúnmente empleado. Asume que el daño se acumula linealmente y es independiente del orden de aplicación de las cargas. Ecuación matemática: $$ \sum_{i=1}^{k} \frac{n_i}{N_i} = D $$ Donde ( n_i ) es el número de ciclos bajo el nivel de esfuerzo ( S_i ), ( N_i ) es la vida útil a la fatiga para el nivel de esfuerzo ( S_i ), y ( D ) es el valor de daño acumulado. Cuando ( D = 1 ), el material o la estructura falla por fatiga. Ventajas:

  • Cálculo sencillo.
  • Amplia aplicabilidad. Desventajas:
  • Ignora la influencia del orden de carga.
  • No considera el efecto beneficioso de las sobrecargas en la vida útil posterior a la fatiga.
  • Asume acumulación lineal de daño, lo cual puede diferir de la realidad.

1.2.2 Modelo de Corten-Dolan El modelo de Corten-Dolan toma en cuenta el orden de la carga, particularmente el efecto de las sobrecargas en la extensión de la vida útil subsiguiente a la fatiga. Ecuación matemática: $$ D = \sum_{i=1}^{k} \left( \frac{n_i}{N_i} \right)^{m} $$ Donde ( m ) es una constante del material, típicamente entre 1.5 y 2.0.

1.2.3 Modelos de Mecánica del Daño Estos modelos se basan en la teoría de la mecánica del daño de medios continuos, introduciendo una varible de daño para describir el estado de daño del material. Ecuación de evolución del daño comúnmente utilizada: $$ \frac{dD}{dN} = C(1-D)^k \left( \frac{\sigma}{\sigma_0} \right)^m $$ Donde ( D ) es la variable de daño, ( C, k, m ) son constantes del material, y ( \sigma ) es la amplitud de esfuerzo, ( \sigma_0 ) es el esfuerzo de referencia.

1.3 Implementación en Python para la Acumulación de Daño por Fatiga

import numpy as np
import matplotlib.pyplot as plt

class FatigueDamageModel:
    """
    Clase para modelar la acumulación de daño por fatiga.
    Soporta diferentes modelos como Miner, Corten-Dolan y Mecánica del Daño.
    """
    def __init__(self, model_type='miner'):
        """
        Inicializa el modelo de daño por fatiga.
        Args:
            model_type (str): Tipo de modelo a utilizar ('miner', 'corten-dolan', 'damage-mechanics').
        """
        self.model_type = model_type
        self.current_damage = 0.0
        # Parámetros de ejemplo para la curva S-N (pueden ser específicos del material)
        self.sigma_f = 500e6  # Coeficiente de resistencia a la fatiga
        self.b = -0.12        # Exponente de resistencia a la fatiga

    def _get_sn_life(self, stress):
        """
        Calcula la vida útil a la fatiga para un nivel de esfuerzo dado usando una curva S-N simplificada.
        Args:
            stress (float): Nivel de esfuerzo.
        Returns:
            float: Vida útil en ciclos. Devuelve infinito si el esfuerzo es muy bajo.
        """
        # Evitar división por cero o resultados inválidos con esfuerzos muy bajos
        if stress <= 1e5: # Umbral de esfuerzo bajo
            return float('inf')
        try:
            # La fórmula N = (sigma / sigma_f)^(1/b) se usa comúnmente
            # Asegurarse de que el esfuerzo no sea negativo y sigma_f sea positivo
            if stress < 0 or self.sigma_f <= 0:
                 return float('inf')
            # El exponente 1/b puede ser muy grande si b es pequeño
            exponent = 1.0 / self.b
            # Calcular la vida útil, manejar posibles overflow o underflow
            life = (stress / self.sigma_f) ** exponent
            # Limitar la vida útil a un valor máximo razonable para evitar números extremadamente grandes
            return min(life, 1e15) # Límite superior arbitrario
        except (ValueError, OverflowError):
            return float('inf') # Retornar infinito en caso de error numérico

    def calculate_damage(self, stress_levels, cycles):
        """
        Calcula el daño por fatiga acumulado para un conjunto de niveles de esfuerzo y ciclos.
        Args:
            stress_levels (list): Lista de niveles de esfuerzo.
            cycles (list): Lista de número de ciclos correspondientes a cada nivel de esfuerzo.
        Returns:
            float: Daño acumulado.
        """
        if len(stress_levels) != len(cycles):
            raise ValueError("Las listas de niveles de esfuerzo y ciclos deben tener la misma longitud.")

        if self.model_type == 'miner':
            return self._apply_miner_rule(stress_levels, cycles)
        elif self.model_type == 'corten-dolan':
            return self._apply_corten_dolan_rule(stress_levels, cycles)
        elif self.model_type == 'damage-mechanics':
            # Implementación simplificada de la mecánica del daño
            # Nota: Requiere una simulación paso a paso para ser precisa
            print("Advertencia: La mecánica del daño se implementa aquí de forma simplificada. Se recomienda una simulación detallada.")
            return self._apply_simplified_damage_mechanics(stress_levels, cycles)
        else:
            raise ValueError(f"Tipo de modelo desconocido: {self.model_type}")

    def _apply_miner_rule(self, stress_levels, cycles):
        """Aplica la regla de daño lineal de Miner."""
        total_damage = 0.0
        for stress, n in zip(stress_levels, cycles):
            N = self._get_sn_life(stress)
            if N > 0 and np.isfinite(N): # Asegurar que N sea finito y positivo
                total_damage += n / N
        return total_damage

    def _apply_corten_dolan_rule(self, stress_levels, cycles, m=1.75):
        """Aplica el modelo de Corten-Dolan."""
        total_damage = 0.0
        for stress, n in zip(stress_levels, cycles):
            N = self._get_sn_life(stress)
            if N > 0 and np.isfinite(N):
                total_damage += (n / N) ** m
        return total_damage

    def _apply_simplified_damage_mechanics(self, stress_levels, cycles, C=1e-10, k=2.0, sigma0=300e6):
        """
        Implementación simplificada del modelo de mecánica del daño.
        En una aplicación real, esto requeriría una integración paso a paso de dD/dN.
        Aquí, se aproxima el daño total asumiendo un daño constante por ciclo.
        """
        total_damage = 0.0
        for stress, n in zip(stress_levels, cycles):
            # Calcular el daño incremental por ciclo (aproximado)
            # Usamos un valor de D inicial bajo (ej. 0.01) para el cálculo de dD/dN
            initial_D = 0.01
            # Asegurar que sigma0 no sea cero
            if sigma0 <= 0:
                 raise ValueError("sigma0 debe ser positivo para la mecánica del daño.")
            
            stress_ratio = stress / sigma0
            # Evitar resultados inválidos si sigma_ratio es negativo o cero
            if stress_ratio <= 0:
                damage_increment_per_cycle = 0.0
            else:
                # El cálculo directo de dD/dN puede ser inestable si (1-D) es cercano a cero.
                # Una simulación paso a paso es más robusta. Aquí se usa una aproximación.
                try:
                    # Simplificación: calcular dD/dN con D inicial y asumir que se mantiene
                    dD_dN_approx = C * (1 - initial_D)**k * (stress_ratio)**self.b # Usando self.b como exponente
                    if np.isfinite(dD_dN_approx) and dD_dN_approx >= 0:
                         damage_increment_per_cycle = dD_dN_approx
                    else:
                         damage_increment_per_cycle = 0.0 # Si el cálculo falla, asumir 0 daño
                except (ValueError, OverflowError):
                     damage_increment_per_cycle = 0.0 # Manejar errores numéricos

            # Acumular daño total para este nivel de esfuerzo
            total_damage += n * damage_increment_per_cycle
            
            # Acumular daño directamente si se prefiere un enfoque más simple
            # total_damage += n * self.damage_evolution_rate(stress, initial_D, C, k, sigma0)
            
        # Limitar el daño total a 1.0 ya que no puede exceder 1 (fallo)
        return min(total_damage, 1.0)

    def plot_damage_accumulation(self, stress_levels, max_cycles):
        """
        Grafica la acumulación de daño a lo largo del tiempo para un nivel de esfuerzo dado.
        Args:
            stress_levels (list): Lista de niveles de esfuerzo aplicados secuencialmente.
            max_cycles (list): Lista de número de ciclos para cada nivel de esfuerzo.
        """
        if len(stress_levels) != len(max_cycles):
            raise ValueError("Las listas de niveles de esfuerzo y ciclos máximos deben tener la misma longitud.")

        all_stresses = []
        all_cycles = []
        cumulative_stresses = []
        cumulative_cycles = []
        
        # Generar puntos para la curva de acumulación de daño
        for stress, n_max in zip(stress_levels, max_cycles):
            current_stress_level = [stress]
            for i in range(1, n_max + 1):
                cumulative_stresses.append(stress)
                cumulative_cycles.append(i)
                
                # Calcular el daño acumulado hasta este punto
                current_damage = self.calculate_damage(cumulative_stresses, cumulative_cycles)
                all_stresses.append(current_damage)
                all_cycles.append(sum(cumulative_cycles)) # Ciclos totales hasta este punto

        plt.figure(figsize=(10, 6))
        plt.plot(all_cycles, all_stresses, label=f'Modelo: {self.model_type}')
        plt.axhline(y=1.0, color='r', linestyle='--', label='Umbral de Falla (D=1)')
        plt.xlabel('Ciclos Totales')
        plt.ylabel('Daño Acumulado')
        plt.title(f'Acumulación de Daño por Fatiga ({self.model_type})')
        plt.legend()
        plt.grid(True)
        plt.ylim(0, 1.2) # Asegurar que el eje Y muestre hasta 1.0
        plt.savefig(f'curva_acumulacion_dano_{self.model_type}.png')
        plt.show()


# --- Ejemplo de Uso ---
if __name__ == "__main__":
    # Parámetros de ejemplo para la simulación
    # Carga representativa: 10000 ciclos a 200 MPa, luego 20000 ciclos a 180 MPa
    stress_scenario_1 = [200e6, 180e6]
    cycles_scenario_1 = [10000, 20000]

    # Escenario de carga diferente: 5000 ciclos a 220 MPa, 15000 a 190 MPa, 30000 a 170 MPa
    stress_scenario_2 = [220e6, 190e6, 170e6]
    cycles_scenario_2 = [5000, 15000, 30000]

    # 1. Modelo de Miner
    miner_model = FatigueDamageModel(model_type='miner')
    damage_miner_1 = miner_model.calculate_damage(stress_scenario_1, cycles_scenario_1)
    damage_miner_2 = miner_model.calculate_damage(stress_scenario_2, cycles_scenario_2)
    print(f"Miner - Escenario 1 Daño Acumulado: {damage_miner_1:.4f}")
    print(f"Miner - Escenario 2 Daño Acumulado: {damage_miner_2:.4f}")
    miner_model.plot_damage_accumulation(stress_scenario_1, cycles_scenario_1)

    # 2. Modelo de Corten-Dolan
    corten_dolan_model = FatigueDamageModel(model_type='corten-dolan')
    damage_corten_1 = corten_dolan_model.calculate_damage(stress_scenario_1, cycles_scenario_1)
    damage_corten_2 = corten_dolan_model.calculate_damage(stress_scenario_2, cycles_scenario_2)
    print(f"Corten-Dolan - Escenario 1 Daño Acumulado: {damage_corten_1:.4f}")
    print(f"Corten-Dolan - Escenario 2 Daño Acumulado: {damage_corten_2:.4f}")
    corten_dolan_model.plot_damage_accumulation(stress_scenario_1, cycles_scenario_1)

    # 3. Modelo de Mecánica del Daño (Simplificado)
    damage_mechanics_model = FatigueDamageModel(model_type='damage-mechanics')
    # Nota: La implementación simplificada puede no ser precisa para la comparación directa.
    # Se usa un conjunto de cargas para ilustrar.
    damage_dm_1 = damage_mechanics_model.calculate_damage(stress_scenario_1, cycles_scenario_1)
    damage_dm_2 = damage_mechanics_model.calculate_damage(stress_scenario_2, cycles_scenario_2)
    print(f"Mecánica del Daño (Simplificado) - Escenario 1 Daño Acumulado: {damage_dm_1:.4f}")
    print(f"Mecánica del Daño (Simplificado) - Escenario 2 Daño Acumulado: {damage_dm_2:.4f}")
    # Graficar para el modelo de mecánica del daño requiere una simulación paso a paso más elaborada.
    # Aquí omitimos la gráfica para la versión simplificada para evitar confusiones.

  1. Método de Esfuerzo-Vida (Curva S-N)

2.1 Principios Básicos del Método Esfuerzo-Vida El método de esfuerzo-vida, también conocido como el método de la curva S-N, es el enfoque más tradicional para la predicción de la vida útil a la fatiga. Utiliza la curva S-N del material para estimar la vida útil de una estructura bajo esfuerzos cíclicos. Una curva S-N describe la relación entre la amplitud de esfuerzo (o esfuerzo máximo) y la vida útil a la fatiga. Típicamente, esta relación es lineal en un gráfico logarítmico-logarítmico.

2.2 Expresión Matemática de la Curva S-N En un gráfico logarítmico-logarítmico, la curva S-N se puede expresar como: $$ \log N = a - b \log S $$ O en forma exponencial: $$ N = \left( \frac{S}{\sigma_f} \right)^{1/b} $$ Donde:

  • \( N \) es la vida útil a la fatiga (número de ciclos).
  • \( S \) es la amplitud de esfuerzo.
  • \( \sigma_f \) es el coeficiente de resistencia a la fatiga.
  • \( b \) es el exponente de resistencia a la fatiga.
  • \( a \) es una constante (relacionada con \( \log(\sigma_f) \)).

2.3 Factores que Afectan la Curva S-N

  • Propiedades del Material: Las curvas S-N varían significativamente entre diferentes materiales.
  • Relación de Esfuerzo: La relación \( R = \sigma_{min} / \sigma_{max} \) tiene una influencia notable en la curva S-N.
  • Frecuencia de Carga: La frecuencia de aplicación de la carga puede afectar la vida útil a la fatiga.
  • Factores Ambientales: La temperatura y la corrosión, entre otros, pueden reducir la vida útil a la fatiga.
  • Estado Superficial: La rugosidad superficial y las tensiones residuales influyen en la vida útil.

2.4 Pasos de Aplicación del Método Esfuerzo-Vida

  1. Determinar el Espectro de Carga: Analizar las cargas cíclicas a las que está sujeta la estructura.
  2. Seleccionar la Curva S-N: Elegir la curva S-N apropiada basada en el tipo de material y la relación de esfuerzo.
  3. Calcular la Acumulación de Daño: Utilizar la regla de Miner u otro modelo de acumulación de daño.
  4. Predecir la Vida Útil a la Fatiga: Estimar la vida útil de la estructura basándose en la tasa de acumulación de daño.

2.5 Implementación en Python para el Método Esfuerzo-Vida

import numpy as np
import matplotlib.pyplot as plt

class SNCurveCalculator:
    """
    Calcula la vida útil a la fatiga basada en la curva S-N.
    """
    def __init__(self, fatigue_strength_coeff, fatigue_strength_exponent, endurance_limit=None):
        """
        Inicializa los parámetros de la curva S-N.
        Args:
            fatigue_strength_coeff (float): Coeficiente de resistencia a la fatiga (sigma_f').
            fatigue_strength_exponent (float): Exponente de resistencia a la fatiga (b). Debe ser negativo.
            endurance_limit (float, optional): Límite de fatiga (esfuerzo por debajo del cual no hay fatiga). Defaults to None.
        """
        if fatigue_strength_exponent >= 0:
            raise ValueError("El exponente de resistencia a la fatiga (b) debe ser negativo.")
        
        self.sigma_f_prime = fatigue_strength_coeff
        self.b = fatigue_strength_exponent
        self.endurance_limit = endurance_limit
        
        # Validación adicional para asegurar parámetros razonables
        if self.sigma_f_prime <= 0:
            raise ValueError("El coeficiente de resistencia a la fatiga (sigma_f') debe ser positivo.")
        if self.endurance_limit is not None and self.endurance_limit < 0:
             raise ValueError("El límite de fatiga no puede ser negativo.")

    def get_cycles_to_failure(self, stress_amplitude):
        """
        Calcula el número de ciclos hasta la falla para una amplitud de esfuerzo dada.
        Args:
            stress_amplitude (float): Amplitud de esfuerzo.
        Returns:
            float: Número de ciclos hasta la falla. Retorna infinito si está por debajo del límite de fatiga.
        """
        if stress_amplitude < 0:
             # Amplitud de esfuerzo negativa no es físicamente significativa en este contexto
             return float('inf') 
             
        if self.endurance_limit is not None and stress_amplitude <= self.endurance_limit:
            return float('inf')  # Vida útil infinita por debajo del límite de fatiga

        # Evitar cálculos con esfuerzos cercanos a cero si sigma_f_prime es muy grande
        if stress_amplitude < 1e3: # Umbral bajo para evitar inestabilidad numérica
             return float('inf')

        try:
            # N = (S / sigma_f')^(1/b)
            exponent = 1.0 / self.b
            # Asegurar que la base de la potencia sea positiva
            base = max(stress_amplitude / self.sigma_f_prime, 1e-15) # Evitar base negativa o cero
            cycles = base ** exponent
            # Limitar para evitar valores extremadamente grandes o infinitos si el cálculo falla
            return min(cycles, 1e15) 
        except (ValueError, OverflowError):
            # Si ocurre un error numérico (ej. división por cero, overflow)
            return float('inf')

    def plot_sn_curve(self, stress_range=None, num_points=100):
        """
        Grafica la curva S-N.
        Args:
            stress_range (tuple, optional): Rango de esfuerzos para graficar (min_stress, max_stress).
                                            Si es None, se usa un rango predeterminado.
            num_points (int): Número de puntos para generar la curva.
        """
        if stress_range is None:
            # Rango predeterminado: desde un poco por encima del límite de fatiga (si existe) hasta sigma_f'
            min_s = self.endurance_limit * 1.1 if self.endurance_limit else self.sigma_f_prime * 0.1
            max_s = self.sigma_f_prime * 1.1 # Un poco más allá para visualización
            # Asegurar que min_s sea razonable y menor que max_s
            min_s = max(min_s, 1e4) # Mínimo esfuerzo práctico
            max_s = max(max_s, min_s * 1.5) # Asegurar rango
            stress_range = (min_s, max_s)
        
        # Generar puntos de esfuerzo en escala logarítmica
        log_min_s = np.log10(stress_range[0])
        log_max_s = np.log10(stress_range[1])
        stresses = np.logspace(log_min_s, log_max_s, num_points)
        
        # Calcular la vida útil para cada punto de esfuerzo
        lives = [self.get_cycles_to_failure(s) for s in stresses]
        
        plt.figure(figsize=(10, 6))
        plt.loglog(stresses, lives, 'b-', linewidth=2, label='Curva S-N')
        
        if self.endurance_limit is not None:
            # Graficar la línea del límite de fatiga
            plt.axvline(x=self.endurance_limit, color='r', linestyle='--', label=f'Límite de Fatiga ({self.endurance_limit/1e6:.1f} MPa)')
            # Marcar el punto donde el esfuerzo es el límite de fatiga
            plt.plot(self.endurance_limit, max(lives), 'ro') # Marcador en el límite superior de vida

        plt.xlabel('Amplitud de Esfuerzo (Pa)')
        plt.ylabel('Vida Útil (Ciclos)')
        plt.title('Curva S-N de Fatiga')
        plt.grid(True, which='both', linestyle='--')
        plt.legend()
        plt.savefig('curva_SN.png')
        plt.show()

class StressLifePredictor:
    """
    Realiza predicciones de vida útil a la fatiga usando el método S-N y la acumulación de daño.
    """
    def __init__(self, sn_calculator):
        """
        Inicializa el predictor con un objeto de cálculo de curva S-N.
        Args:
            sn_calculator (SNCurveCalculator): Objeto que define la curva S-N.
        """
        self.sn_calculator = sn_calculator
        # Instancia del modelo de daño de Miner para la acumulación
        self.damage_model = FatigueDamageModel(model_type='miner') 

    def predict_fatigue_life(self, load_spectrum):
        """
        Predice la vida útil a la fatiga para un espectro de carga dado.
        Args:
            load_spectrum (list): Lista de tuplas (stress_amplitude, num_cycles).
                                  Ej: [(200e6, 10000), (180e6, 20000)]
        Returns:
            float: Vida útil estimada en número de ciclos.
        """
        total_damage = 0.0
        total_cycles_applied = 0
        
        # Calcular el daño acumulado usando la regla de Miner
        stress_amplitudes = [s for s, c in load_spectrum]
        num_cycles = [c for s, c in load_spectrum]
        
        # Usar la instancia de FatigueDamageModel para calcular el daño total
        total_damage = self.damage_model.calculate_damage(stress_amplitudes, num_cycles)
        total_cycles_applied = sum(num_cycles) # Ciclos totales aplicados en el espectro

        if total_damage == 0 or not np.isfinite(total_damage):
             # Si no hay daño o el cálculo falló, determinar la vida útil
             # Si todos los esfuerzos están por debajo del límite de fatiga, la vida es infinita.
             # Si hay algún esfuerzo por encima, pero el daño calculado es cero (quizás por límites numéricos),
             # esto indica un problema o vida útil muy larga.
             if all(self.sn_calculator.get_cycles_to_failure(s) == float('inf') for s, c in load_spectrum):
                  return float('inf')
             else:
                  # Si se aplicaron ciclos pero el daño es cero, es una vida útil muy larga o un error.
                  # Se puede devolver infinito o un valor muy grande.
                  print("Advertencia: Daño calculado cero o no finito. La vida útil estimada puede ser infinita o imprecisa.")
                  return float('inf') 
        
        # Estimación de la vida útil restante si el daño es < 1
        # La vida útil total estimada se puede obtener si se asume que el espectro se repite.
        # Si el espectro se aplica una sola vez, la vida útil es 1 / total_damage (en unidades de espectro).
        # Para obtener ciclos totales, necesitamos normalizar.
        
        # Fórmula: Vida_Total = Ciclos_Aplicados / Daño_Acumulado_en_esos_ciclos
        # Esto asume que el espectro de carga se repite exactamente.
        estimated_life = total_cycles_applied / total_damage
        
        return estimated_life

    def calculate_total_damage(self, load_spectrum):
        """
        Calcula el daño total acumulado para un espectro de carga dado.
        Args:
            load_spectrum (list): Lista de tuplas (stress_amplitude, num_cycles).
        Returns:
            float: Daño acumulado.
        """
        stress_amplitudes = [s for s, c in load_spectrum]
        num_cycles = [c for s, c in load_spectrum]
        return self.damage_model.calculate_damage(stress_amplitudes, num_cycles)

# --- Ejemplo de Uso ---
if __name__ == "__main__":
    # Parámetros de ejemplo para la curva S-N (ej. Acero 45)
    # Estos valores son ilustrativos. Se deben obtener de datos experimentales o de referencia.
    example_sigma_f_prime = 500e6  # Coeficiente de resistencia a la fatiga (Pa)
    example_b = -0.12              # Exponente de resistencia a la fatiga
    example_endurance_limit = 200e6 # Límite de fatiga (Pa)

    # Crear un objeto para calcular la curva S-N
    sn_calculator = SNCurveCalculator(
        fatigue_strength_coeff=example_sigma_f_prime,
        fatigue_strength_exponent=example_b,
        endurance_limit=example_endurance_limit
    )

    # Graficar la curva S-N
    sn_calculator.plot_sn_curve()

    # Crear un predictor de vida útil a la fatiga
    stress_life_predictor = StressLifePredictor(sn_calculator)

    # Definir un espectro de carga de ejemplo
    # (Amplitud de Esfuerzo, Número de Ciclos)
    example_load_spectrum = [
        (250e6, 1000),   # 250 MPa, 1000 ciclos
        (220e6, 5000),   # 220 MPa, 5000 ciclos
        (200e6, 10000),  # 200 MPa, 10000 ciclos (cerca del límite de fatiga)
        (180e6, 50000),  # 180 MPa, 50000 ciclos (por debajo del límite de fatiga)
        (150e6, 100000)  # 150 MPa, 100000 ciclos (muy por debajo del límite de fatiga)
    ]

    # Calcular el daño total acumulado
    total_damage = stress_life_predictor.calculate_total_damage(example_load_spectrum)
    print(f"Daño Acumulado Total: {total_damage:.6f}")

    # Predecir la vida útil a la fatiga
    predicted_life = stress_life_predictor.predict_fatigue_life(example_load_spectrum)
    
    if predicted_life == float('inf'):
        print("Vida Útil a la Fatiga Predicha: Infinita (o extremadamente larga)")
    else:
        print(f"Vida Útil a la Fatiga Predicha: {predicted_life:,.0f} ciclos")

    # Visualización de la contribución al daño por nivel de esfuerzo
    stresses = [s for s, _ in example_load_spectrum]
    cycles = [c for _, c in example_load_spectrum]
    
    # Calcular la vida útil para cada nivel de esfuerzo en el espectro
    lives_at_stress = [sn_calculator.get_cycles_to_failure(s) for s in stresses]
    
    # Calcular la contribución al daño de cada segmento del espectro
    # Usando la regla de Miner: damage = n / N
    damage_contributions = []
    for i, (s, c) in enumerate(example_load_spectrum):
        N = lives_at_stress[i]
        if N > 0 and np.isfinite(N):
            damage_contributions.append(c / N)
        else:
            damage_contributions.append(0) # Si la vida útil es infinita, la contribución al daño es cero

    # Etiquetas para el gráfico (MPa)
    stress_labels = [f"{s / 1e6:.0f} MPa" for s in stresses]

    plt.figure(figsize=(10, 6))
    plt.bar(stress_labels, damage_contributions, color='skyblue')
    plt.axhline(y=total_damage, color='r', linestyle='--', label=f'Daño Total ({total_damage:.4f})')
    plt.xlabel('Amplitud de Esfuerzo')
    plt.ylabel('Contribución al Daño (Miner)')
    plt.title('Contribución al Daño por Segmento del Espectro de Carga')
    plt.xticks(rotation=45, ha='right')
    plt.legend()
    plt.grid(True, axis='y')
    plt.tight_layout()
    plt.savefig('contribucion_dano_por_esfuerzo.png')
    plt.show()

  1. Método de Deformación-Vida (Curva ε-N)

3.1 Principios Básicos del Método de Deformación-Vida El método de deformación-vida, basado en la curva ε-N, estima la vida útil a la fatiga basándose en la relación entre la deformación y la vida útil. Es particularmente adecuado para el régimen de baja-ciclo de fatiga (menos de ( 10^4 ) ciclos), donde las deformaciones plásticas son significativas. El núcleo de este método es la ecuación de Manson-Coffin, que descompone la amplitud de deformación total en sus componentes elástica y plástica.

3.2 Ecuación de Manson-Coffin La ecuación de Manson-Coffin se expresa matemáticamente como: $$ \frac{\Delta\varepsilon_t}{2} = \frac{\Delta\varepsilon_e}{2} + \frac{\Delta\varepsilon_p}{2} = \left(\frac{\sigma'_f}{E}\right) (2N_f)^b + (\varepsilon'_f) (2N_f)^c $$ Donde:

  • \( \Delta\varepsilon_t \) es la amplitud de deformación total.
  • \( \Delta\varepsilon_e \) es la amplitud de deformación elástica.
  • \( \Delta\varepsilon_p \) es la amplitud de deformación plástica.
  • \( \sigma'_f \) es el coeficiente de resistencia a la fatiga.
  • \( \varepsilon'_f \) es el coeficiente de ductilidad a la fatiga.
  • \( b \) es el exponente de la curva de fatiga (relacionado con la deformación elástica).
  • \( c \) es el exponente de la curva de ductilidad a la fatiga (relacionado con la deformación plástica).
  • \( E \) es el módulo de elasticidad.
  • \( N_f \) es la vida útil a la fatiga.

3.3 Pasos de Aplicación del Método de Deformación-Vida

  1. Determinar la Curva Deformación-Vida: Obtener los parámetros de la curva ε-N del material a través de pruebas experimentales.
  2. Calcular la Amplitud de Dfeormación: Determinar la amplitud de deformación en la estructura bajo las condiciones de carga.
  3. Predecir la Vida Útil a la Fatiga: Utilizar la ecuación de Manson-Coffin para predecir la vida útil.
  4. Considerar Efectos de Promedio: Corregir la predicción de vida útil si existen deformaciones promedio significativas.

3.4 Implementación en Python para el Método de Deformación-Vida

import numpy as np
import matplotlib.pyplot as plt

class StrainLifeCalculator:
    """
    Calcula la vida útil a la fatiga basándose en la relación Deformación-Vida (Ecuación de Manson-Coffin).
    """
    def __init__(self, fatigue_strength_coeff, fatigue_ductility_coeff, fatigue_strength_exponent, fatigue_ductility_exponent, elastic_modulus):
        """
        Inicializa los parámetros de la curva Deformación-Vida.
        Args:
            fatigue_strength_coeff (float): Coeficiente de resistencia a la fatiga (sigma_f').
            fatigue_ductility_coeff (float): Coeficiente de ductilidad a la fatiga (epsilon_f').
            fatigue_strength_exponent (float): Exponente de resistencia a la fatiga (b).
            fatigue_ductility_exponent (float): Exponente de ductilidad a la fatiga (c).
            elastic_modulus (float): Módulo de elasticidad del material (E).
        """
        self.sigma_f_prime = fatigue_strength_coeff
        self.epsilon_f_prime = fatigue_ductility_coeff
        self.b = fatigue_strength_exponent
        self.c = fatigue_ductility_exponent
        self.E = elastic_modulus

        # Validaciones básicas
        if not all(p > 0 for p in [self.sigma_f_prime, self.epsilon_f_prime, self.E]):
            raise ValueError("Coeficientes de fatiga y módulo de elasticidad deben ser positivos.")
        if not (-1 < self.b < 0):
            print(f"Advertencia: El exponente de resistencia a la fatiga (b={self.b}) está fuera del rango típico (-1, 0).")
        if not (-1 < self.c < 0):
            print(f"Advertencia: El exponente de ductilidad a la fatiga (c={self.c}) está fuera del rango típico (-1, 0).")

    def get_cycles_to_failure(self, total_strain_amplitude):
        """
        Calcula los ciclos hasta la falla para una amplitud de deformación total dada.
        Utiliza un método numérico (bisección) para resolver la ecuación de Manson-Coffin.
        Args:
            total_strain_amplitude (float): Amplitud de deformación total.
        Returns:
            float: Número de ciclos hasta la falla.
        """
        if total_strain_amplitude <= 0:
            return float('inf') # Deformación no positiva implica vida infinita

        # Función a resolver: F(N) = (Ecuación Manson-Coffin) - total_strain_amplitude = 0
        def equation_to_solve(N):
            # Evitar log(0) o potencias negativas de N si N es 0 o negativo
            if N <= 0: return float('inf')
            
            # Amplitud de deformación elástica
            # (sigma_f' / E) * (2*N)^b
            elastic_term = (self.sigma_f_prime / self.E) * (2.0 * N)**self.b
            
            # Amplitud de deformación plástica
            # epsilon_f' * (2*N)^c
            plastic_term = self.epsilon_f_prime * (2.0 * N)**self.c
            
            return elastic_term + plastic_term - total_strain_amplitude

        # Método de Bisección para encontrar la raíz (N)
        # Establecer un rango de búsqueda razonable para N
        lower_bound_N = 1.0       # Mínimo número de ciclos
        # Estimar un límite superior: si toda la deformación fuera plástica
        upper_bound_N_est = (total_strain_amplitude / self.epsilon_f_prime)**(1.0/self.c) if self.epsilon_f_prime > 0 else 1e7
        upper_bound_N = max(upper_bound_N_est, 1e7) # Usar un límite superior conservador
        upper_bound_N = min(upper_bound_N, 1e15) # Evitar valores excesivamente grandes

        # Asegurar que las condiciones iniciales de la bisección sean válidas
        if equation_to_solve(lower_bound_N) * equation_to_solve(upper_bound_N) >= 0:
             # Si los signos son iguales, puede que no haya raíz en el intervalo o sea un caso extremo.
             # Intentar ensanchar el intervalo o devolver infinito/error.
             # print(f"Advertencia: No se pudo asegurar una raíz para N con T.strain={total_strain_amplitude}. Bounds check failed.")
             # Podríamos intentar con un rango más amplio o devolver infinito si el esfuerzo es muy bajo.
             if equation_to_solve(lower_bound_N) > 0: # Si incluso al mínimo N, la deformación calculada es mayor que la aplicada
                  return lower_bound_N # La deformación aplicada es muy pequeña, vida útil muy larga.
             return float('inf') # Fallo al encontrar la raíz

        # Iteraciones de bisección
        for _ in range(100): # Número máximo de iteraciones para convergencia
            mid_N = (lower_bound_N + upper_bound_N) / 2.0
            if mid_N == lower_bound_N or mid_N == upper_bound_N: # Convergencia alcanzada
                 break
                 
            f_mid = equation_to_solve(mid_N)
            
            if f_mid == 0: # Raíz exacta encontrada
                return mid_N
            elif f_mid > 0: # La deformación calculada es mayor que la aplicada, necesitamos menos ciclos (N más pequeño)
                upper_bound_N = mid_N
            else: # La deformación calculada es menor que la aplicada, necesitamos más ciclos (N más grande)
                lower_bound_N = mid_N

        # Devolver el valor de N encontrado después de las iteraciones
        # Asegurar que el resultado sea un número finito y positivo
        result_N = (lower_bound_N + upper_bound_N) / 2.0
        return max(result_N, 1.0) if np.isfinite(result_N) else float('inf')

    def plot_strain_life_curve(self, strain_range=None, num_points=100):
        """
        Grafica la curva Deformación-Vida, separando las componentes elástica y plástica.
        Args:
            strain_range (tuple, optional): Rango de amplitudes de deformación (min_strain, max_strain).
            num_points (int): Número de puntos para generar la curva.
        """
        if strain_range is None:
            # Rango predeterminado: desde baja deformación hasta alta deformación
            min_eps = 1e-4
            max_eps = 5e-2 # 5% de deformación
            strain_range = (min_eps, max_eps)
        
        # Generar puntos de deformación en escala logarítmica
        log_min_eps = np.log10(strain_range[0])
        log_max_eps = np.log10(strain_range[1])
        strain_amplitudes = np.logspace(log_min_eps, log_max_eps, num_points)
        
        # Calcular la vida útil para cada amplitud de deformación
        lives = [self.get_cycles_to_failure(eps) for eps in strain_amplitudes]
        
        # Calcular componentes elástica y plástica para las vidas calculadas
        elastic_strains = []
        plastic_strains = []
        for i, N in enumerate(lives):
            if np.isfinite(N) and N > 0:
                eps_e = (self.sigma_f_prime / self.E) * (2.0 * N)**self.b
                eps_p = self.epsilon_f_prime * (2.0 * N)**self.c
                elastic_strains.append(eps_e)
                plastic_strains.append(eps_p)
            else:
                # Si la vida es infinita, las deformaciones correspondientes son cero o muy pequeñas.
                elastic_strains.append(0) 
                plastic_strains.append(0)

        plt.figure(figsize=(10, 6))
        # Graficar en ejes logarítmicos: Vida útil (X) vs. Deformación (Y)
        plt.loglog(lives, strain_amplitudes, 'b-', linewidth=2, label='Deformación Total')
        plt.loglog(lives, elastic_strains, 'r--', linewidth=1.5, label='Deformación Elástica')
        plt.loglog(lives, plastic_strains, 'g--', linewidth=1.5, label='Deformación Plástica')
        
        plt.xlabel('Vida Útil (Ciclos)')
        plt.ylabel('Amplitud de Deformación')
        plt.title('Curva Deformación-Vida (Manson-Coffin)')
        plt.grid(True, which='both', linestyle='--')
        plt.legend()
        plt.ylim(bottom=1e-5) # Asegurar límite inferior visible
        plt.savefig('curva_deformacion_vida.png')
        plt.show()

class StrainLifePredictor:
    """
    Realiza predicciones de vida útil a la fatiga usando el método Deformación-Vida.
    """
    def __init__(self, strain_life_calculator):
        """
        Inicializa el predictor con un objeto de cálculo de curva Deformación-Vida.
        Args:
            strain_life_calculator (StrainLifeCalculator): Objeto que define la curva Deformación-Vida.
        """
        self.strain_life_calculator = strain_life_calculator
        # Aquí se podría integrar un modelo de acumulación de daño basado en deformación si fuera necesario,
        # pero para la predicción simple, se calcula el daño total directamente.

    def predict_fatigue_life(self, strain_spectrum):
        """
        Predice la vida útil a la fatiga para un espectro de deformación dado.
        Args:
            strain_spectrum (list): Lista de tuplas (strain_amplitude, num_cycles).
                                    Ej: [(0.005, 100), (0.003, 1000)]
        Returns:
            float: Vida útil estimada en número de ciclos.
        """
        total_damage = 0.0
        total_cycles_applied = 0
        
        for strain_amp, n_cycles in strain_spectrum:
            # Calcular la vida útil para la amplitud de deformación dada
            life_at_strain = self.strain_life_calculator.get_cycles_to_failure(strain_amp)
            
            if np.isfinite(life_at_strain) and life_at_strain > 0:
                # Calcular el daño para este segmento (usando Miner implícitamente)
                damage_segment = n_cycles / life_at_strain
                total_damage += damage_segment
            # Si life_at_strain es infinito, la contribución al daño es cero.
            
            total_cycles_applied += n_cycles

        if total_damage == 0 or not np.isfinite(total_damage):
            # Si no hay daño o el cálculo falló, la vida útil es infinita o imprecisa.
            # Verificar si todas las deformaciones están en un régimen de vida infinita.
            if all(self.strain_life_calculator.get_cycles_to_failure(s) == float('inf') for s, c in strain_spectrum):
                 return float('inf')
            else:
                 print("Advertencia: Daño calculado cero o no finito. La vida útil estimada puede ser infinita o imprecisa.")
                 return float('inf')

        # Estimación de la vida útil total si el espectro se repite
        estimated_life = total_cycles_applied / total_damage
        
        return estimated_life

    def calculate_total_damage(self, strain_spectrum):
        """
        Calcula el daño total acumulado para un espectro de deformación dado.
        Args:
            strain_spectrum (list): Lista de tuplas (strain_amplitude, num_cycles).
        Returns:
            float: Daño acumulado.
        """
        total_damage = 0.0
        for strain_amp, n_cycles in strain_spectrum:
            life_at_strain = self.strain_life_calculator.get_cycles_to_failure(strain_amp)
            if np.isfinite(life_at_strain) and life_at_strain > 0:
                total_damage += n_cycles / life_at_strain
        return total_damage

# --- Ejemplo de Uso ---
if __name__ == "__main__":
    # Parámetros de ejemplo para la curva Deformación-Vida (ej. Acero 45)
    # Estos valores son ilustrativos.
    example_sigma_f_prime = 550e6   # Coeficiente de resistencia a la fatiga (Pa)
    example_epsilon_f_prime = 0.5   # Coeficiente de ductilidad a la fatiga
    example_b = -0.12               # Exponente de resistencia a la fatiga
    example_c = -0.55               # Exponente de ductilidad a la fatiga
    example_E = 210e9               # Módulo de elasticidad (Pa)

    # Crear un objeto para calcular la curva Deformación-Vida
    strain_life_calculator = StrainLifeCalculator(
        fatigue_strength_coeff=example_sigma_f_prime,
        fatigue_ductility_coeff=example_epsilon_f_prime,
        fatigue_strength_exponent=example_b,
        fatigue_ductility_exponent=example_c,
        elastic_modulus=example_E
    )

    # Graficar la curva Deformación-Vida
    strain_life_calculator.plot_strain_life_curve()

    # Crear un predictor de vida útil a la fatiga basado en deformación
    strain_life_predictor = StrainLifePredictor(strain_life_calculator)

    # Definir un espectro de deformación de ejemplo
    # (Amplitud de Deformación Total, Número de Ciclos)
    example_strain_spectrum = [
        (0.005, 100),    # 0.5% deformación, 100 ciclos
        (0.003, 1000),   # 0.3% deformación, 1000 ciclos
        (0.0015, 5000),  # 0.15% deformación, 5000 ciclos
        (0.0008, 20000)  # 0.08% deformación, 20000 ciclos
    ]

    # Calcular el daño total acumulado
    total_damage_strain = strain_life_predictor.calculate_total_damage(example_strain_spectrum)
    print(f"Daño Acumulado Total (Deformación): {total_damage_strain:.6f}")

    # Predecir la vida útil a la fatiga
    predicted_life_strain = strain_life_predictor.predict_fatigue_life(example_strain_spectrum)
    
    if predicted_life_strain == float('inf'):
        print("Vida Útil a la Fatiga Predicha (Deformación): Infinita (o extremadamente larga)")
    else:
        print(f"Vida Útil a la Fatiga Predicha (Deformación): {predicted_life_strain:,.0f} ciclos")

    # Visualización de la contribución al daño por nivel de deformación
    strains = [s for s, _ in example_strain_spectrum]
    cycles_strain = [c for _, c in example_strain_spectrum]
    
    # Calcular la vida útil para cada nivel de deformación en el espectro
    lives_at_strain = [strain_life_calculator.get_cycles_to_failure(s) for s in strains]
    
    # Calcular la contribución al daño de cada segmento
    damage_contributions_strain = []
    for i, (s, c) in enumerate(example_strain_spectrum):
        N = lives_at_strain[i]
        if np.isfinite(N) and N > 0:
            damage_contributions_strain.append(c / N)
        else:
            damage_contributions_strain.append(0) # Vida infinita = 0 daño

    # Etiquetas para el gráfico
    strain_labels = [f"{s:.4f}" for s in strains]

    plt.figure(figsize=(10, 6))
    plt.bar(strain_labels, damage_contributions_strain, color='lightgreen')
    plt.axhline(y=total_damage_strain, color='r', linestyle='--', label=f'Daño Total ({total_damage_strain:.4f})')
    plt.xlabel('Amplitud de Deformación')
    plt.ylabel('Contribución al Daño (Miner)')
    plt.title('Contribución al Daño por Segmento del Espectro de Deformación')
    plt.xticks(rotation=45, ha='right')
    plt.legend()
    plt.grid(True, axis='y')
    plt.tight_layout()
    plt.savefig('contribucion_dano_por_deformacion.png')
    plt.show()


  1. Aplicaciones de Ingeniería en la Predicción de Vida Útil a la Fatiga

4.1 Predicción de Vida Útil a la Fatiga en Puentes Los puentes experimentan esfuerzos cíclicos debido al tráfico vehicular, lo que puede provocar daño por fatiga. La predicción de la vida útil es esencial para la evaluación de seguridad y la planificación de mantenimiento. Pasos clave:

  1. Análisis del Espectro de Carga: Recopilación y análisis de datos de carga vehicular.
  2. Análisis de Esfuerzos: Cálculo de las respuestas de esfuerzo en las partes críticas del puente.
  3. Selección de Curva S-N: Elección de la curva S-N apropiada según el material del puente.
  4. Cálculo de Daño Acumulado: Uso de la regla de Miner.
  5. Predicción de Vida Útil: Estimación basada en la tasa de acumulación de daño.

4.2 Predicción de Vida Útil a la Fatiga en Estructuras Mecánicas Componentes mecánicos como motores, engranajes y rodamientos están sujetos a cargas cíclicas y son susceptibles a fallos por fatiga. Pasos clave:

  1. Análisis de Condiciones de Operación: Evaluación de los modos de operación y las condiciones de carga.
  2. Análisis por Elementos Finitos (FEA): Determinación de la distribución de esfuerzos y deformaciones.
  3. Análisis de Deformación-Vida: Aplicación del método ε-N para zonas de alta concentración de esfuerzo.
  4. Predicción de Vida Útil: Combinación de análisis para predecir la vida útil total.

4.3 Predicción de Vida Útil a la Fatiga en Estructuras Aeronáuticas Las estructuras aeronáuticas tienen requisitos extremadamente altos de vida útil a la fatiga, ya que un fallo puede tener consecuencias catastróficas. Pasos clave:

  1. Análisis del Espectro de Carga de Vuelo: Recopilación y análisis de datos de carga de vuelo.
  2. Análisis de Tolerancia al Daño: Consideración de defectos iniciales y propagación de grietas.
  3. Predicción de Vida Útil a la Fatiga: Considerando múltiples condiciones de carga.
  4. Establecimiento de Factores de Seguridad: Implementación de factores de seguridad adecuados.
  5. Problemas Comunes y Soluciones en la Predicción de Vida Útil a la Fatiga

5.1 Problemas Comunes

  • Incertidumbre del Espectro de Carga: Diferencias entre el espectro de carga real y el de diseño.
  • Variabilidad de las Propiedades del Material: Dispersión en las características de fatiga de los materiales.
  • Influencia de Factores Ambientales: La temperatura y la corrosión aceleran el daño por fatiga.
  • Efectos de Concentración de Esfuerzo: Las geometrías con cambios abruptos de sección causan concentraciones de esfuerzo localizadas.
  • Errores del Modelo Computacional: Discrepancias entre los modelos FEA y las estructuras reales.

5.2 Soluciones

  • Corrección del Espectro de Carga: Ajuste del espectro basado en datos de monitoreo real.
  • Análisis Estadístico: Uso de métodos estadísticos para manejar la variabilidad de las propiedades del material.
  • Consideración de Factores Ambientales: Incorporación de la influencia ambiental en los modelos de predicción.
  • Coeficientes de Concentración de Esfuerzo: Corrección de esfuerzos locales utilizando estos coeficientes.
  • Validación del Modelo: Verificación de la precisión de los modelos computacionales mediante pruebas experimentales.
  1. Desarrollos Futuros en la Predicción de Vida Útil a la Fatiga

6.1 Predicción de Vida Útil para Materiales Avanzados Con el desarrollo de nuevos materiales como compuestos y superaleaciones, se requieren métodos de predicción adaptados a sus comportamientos de fatiga únicos.

6.2 Predicción de Fatiga Multiaxial Las estructuras reales a menudo experimentan cargas multiaxiales. Es necesario desarrollar modelos predictivos que aborden estas condiciones complejas.

6.3 Monitoreo y Predicción Inteligente de Fatiga La integración de tecnología de sensores e inteligencia artificial permitirá el monitoreo en tiempo real y la predicción proactiva del estado de fatiga de las estructuras.

6.4 Aplicación de Gemelos Digitales El uso de gemelos digitales para crear modelos virtuales de estructuras, actualizados en tiempo real con datos de sensores, mejorará significativamente la precisión de las predicciones de vida útil a la fatiga.

  1. Resumen y Perspectivas La predicción de la fatiga y la vida útil es un área fundamental dentro del Monitoreo de Salud Estructural (SHM), vital para garantizar la operación segura y el mantenimiento adecuado de las estructuras. La selección apropiada de modelos de daño por fatiga y métodos de predicción permite estimar eficazmente la vida útil restante, proporcionando una base científica para el diseño, mantenimiento y gestión de estructuras. En el futuro, con los avances en computación, tecnología de sensores e IA, las predicciones de vida útil se orientarán hacia una mayor inteligencia y precisión. La aplicación de métodos de análisis multiescala y multifsico también aumentará la fiabilidad de estas predicciones. Mediante la mejora continua de los métodos de predicción, podemos asegurar mejor la seguridad operativa de las estructuras, extender su vida útil, reducir los costos de mantenimiento y contribuir al desarrollo sostenible de la ingeniería civil y mecánica.

Etiquetas: fatiga vida útil S-N curve Epsilon-N curve Manson-Coffin

Publicado el 7-27 04:53