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).
- 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.
- 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
- Determinar el Espectro de Carga: Analizar las cargas cíclicas a las que está sujeta la estructura.
- Seleccionar la Curva S-N: Elegir la curva S-N apropiada basada en el tipo de material y la relación de esfuerzo.
- Calcular la Acumulación de Daño: Utilizar la regla de Miner u otro modelo de acumulación de daño.
- 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()
- 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
- Determinar la Curva Deformación-Vida: Obtener los parámetros de la curva ε-N del material a través de pruebas experimentales.
- Calcular la Amplitud de Dfeormación: Determinar la amplitud de deformación en la estructura bajo las condiciones de carga.
- Predecir la Vida Útil a la Fatiga: Utilizar la ecuación de Manson-Coffin para predecir la vida útil.
- 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()
- 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:
- Análisis del Espectro de Carga: Recopilación y análisis de datos de carga vehicular.
- Análisis de Esfuerzos: Cálculo de las respuestas de esfuerzo en las partes críticas del puente.
- Selección de Curva S-N: Elección de la curva S-N apropiada según el material del puente.
- Cálculo de Daño Acumulado: Uso de la regla de Miner.
- 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:
- Análisis de Condiciones de Operación: Evaluación de los modos de operación y las condiciones de carga.
- Análisis por Elementos Finitos (FEA): Determinación de la distribución de esfuerzos y deformaciones.
- Análisis de Deformación-Vida: Aplicación del método ε-N para zonas de alta concentración de esfuerzo.
- 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:
- Análisis del Espectro de Carga de Vuelo: Recopilación y análisis de datos de carga de vuelo.
- Análisis de Tolerancia al Daño: Consideración de defectos iniciales y propagación de grietas.
- Predicción de Vida Útil a la Fatiga: Considerando múltiples condiciones de carga.
- Establecimiento de Factores de Seguridad: Implementación de factores de seguridad adecuados.
- 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.
- 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.
- 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.