import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import scipy.stats as stats
from scipy.special import erfinv
from abc import ABC, abstractmethod
from scipy.optimize import differential_evolution
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from scipy import optimize # to be deleted later on
from .utils import median_rank
import streamlit as st
[docs]
class ProbModel(ABC):
[docs]
@abstractmethod
def log_likelihood(self,params:list, temp:np.ndarray, sigma_values:np.ndarray, strain:np.ndarray=np.array([])):
pass
[docs]
def minimize(self, bounds, args)->np.ndarray:
result_regression = differential_evolution(
self.log_likelihood,
args=args,
bounds=bounds,
)
return result_regression.x
[docs]
@abstractmethod
def predict(self):
pass
[docs]
def power_st_description(self, cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values):
st.header("📋 Mathematical Formulation")
# CDF Section
st.subheader("1. Cumulative Distribution Function (CDF)")
st.markdown("""
The CDF of this model is given by:
""")
st.latex(cdf)
# PDF Section
st.subheader("2. Probability Density Function (PDF)")
st.markdown("""
The PDF of this model can be expressed as:
""")
st.latex(pdf)
# Rearranged equation
st.subheader("3. Rearranged CDF")
st.markdown("""
Equation (1) can be rearranged as:
""")
st.latex(re_cdf)
# Arrhenius equation
st.subheader("4. Power Law Relationship")
st.markdown("""
Using Power law equation in Equation (3):
""")
st.latex(ar_cdf)
# Final fatigue life prediction model
st.subheader("5. Fatigue Life Prediction Model")
st.markdown("""
The complete fatigue life prediction model can be presented as:
""")
st.latex(fatigue_cdf)
st.subheader("Variable Values")
st.markdown("In the case of data given, values of the variables are")
st.markdown(variable_values)
[docs]
def st_description(self, cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values):
st.header("📋 Mathematical Formulation")
# CDF Section
st.subheader("1. Cumulative Distribution Function (CDF)")
st.markdown("""
The CDF of this model is given by:
""")
st.latex(cdf)
# PDF Section
st.subheader("2. Probability Density Function (PDF)")
st.markdown("""
The PDF of this model can be expressed as:
""")
st.latex(pdf)
# Rearranged equation
st.subheader("3. Rearranged CDF")
st.markdown("""
Equation (1) can be rearranged as:
""")
st.latex(re_cdf)
# Arrhenius equation
st.subheader("4. Arrhenius Relationship")
st.markdown("""
Using Arrhenius equation in Equation (3):
""")
st.latex(ar_cdf)
# Final fatigue life prediction model
st.subheader("5. Fatigue Life Prediction Model")
st.markdown("""
The complete fatigue life prediction model can be presented as:
""")
st.latex(fatigue_cdf)
st.subheader("Variable Values")
st.markdown("In the case of data given, values of the variables are")
st.markdown(variable_values)
[docs]
class WeibullModel(ProbModel):
def __init__(self,X_values:np.ndarray, Y_values:np.ndarray,X_values2:np.ndarray=np.array([]), power_law:bool=False)->None:
"""Initializes Weibull Probabilistic Model
Args:
X_values (np.ndarray): First accelerating variable (Temperature)
Y_values (np.ndarray): Values to be modelled
X_values2 (np.ndarray, optional): Second accelerating variable (Strain). Defaults to np.array([]).
power_law (bool, optional):Whether to use power law or not. Defaults to False, which means that we will use Arrhenius equation instead of power law.
"""
self.X_values = X_values
self.X_values2 = X_values2
self.Y_values = Y_values
self.name = "Weibull Model"
self.tab_name = "Weibull"
self.power_law = power_law
if power_law:
self.name = "Weibull Model With Power Law"
self.tab_name = "Weibull (Power)"
# Figure out the parameters
self.two_var = False
if len(X_values2):
# self.init_params = [2.0,-1, np.log(np.mean(self.Y_values)), -3.0]
self.bounds = [(1e-6, 20), (-20, 20),(-20, 20), (-20, 20)]
self.name = "Weibull Model With Two variables"
self.two_var = True
self.shape, self.intercept, self.slope, self.v = self.minimize(self.bounds, args=(self.X_values, self.Y_values, self.X_values2))
else:
# init_params = [2.0, np.log(np.mean(self.Y_values)), 0.0]
bounds = [(1e-6, 30), (-10, 10), (-10, 10)]
self.shape, self.intercept, self.slope = self.minimize( bounds, args=(self.X_values, self.Y_values))
[docs]
def log_likelihood(self,params:list, temp:np.ndarray, sigma_values:np.ndarray, strain:np.ndarray=np.array([])):
"""Log likelihood
Args:
params (list): parameters that are to be determined
temp (np.ndarray)
sigma_values (np.ndarray)
strain(np.ndarray): Defaults to np.array([]).
Returns:
Gives a negative sum of log likelihood for given data
"""
shape = params[0]
u = params[1]
w = params[2]
if shape <= 0:
return np.inf
if self.two_var:
v = params[3]
scale = np.exp(u + w * temp + v * strain)
else:
scale = np.exp(u + w * temp)
return -np.sum(stats.weibull_min.logpdf(sigma_values, c=shape, scale=scale))
[docs]
def predict(self,cdf:float, temperature_values:np.ndarray, strain_values:np.ndarray=np.array([]))->np.ndarray:
"""To predict values
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray): Defaults to np.array([])
Returns:
Predict values according to the trained model
"""
if self.power_law:
return np.exp(
(self.intercept + (self.slope * np.log(temperature_values))) +
((1 / self.shape) * np.log(np.log(1 / (1 - cdf))))
)
elif self.two_var:
return np.exp(
(self.intercept + (self.slope * 11604.53 / (temperature_values + 273.16)) + self.v * np.log(strain_values)) +
((1 / self.shape) * np.log(np.log(1 / (1 - cdf))))
) / 1000000
return np.exp(
(self.intercept + (self.slope * 11604.53 / (temperature_values + 273.16))) +
((1 / self.shape) * np.log(np.log(1 / (1 - cdf))))
)
[docs]
def two_var_predict(self, cdf:float, temperature_values:np.ndarray, strain_values:np.ndarray, params:np.ndarray) -> np.ndarray:
"""To predict values when there are two accelerating variables
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray)
params (np.ndarray): Parameters that will be needed to predict values \n\n `shape, intercept, slope, v = params`. \n
"""
shape, intercept, slope, v = params
return np.exp(
(intercept + (slope * 11604.53 / (temperature_values + 273.16)) + v * np.log(strain_values)) +
((1 / shape) * np.log(np.log(1 / (1 - cdf))))
) / 1000000
[docs]
@staticmethod
def estimate_params(data:np.ndarray, **kwargs):
"""Fit a weibull model on the `data`
Args:
data (np.ndarray)
Returns:
Gives shape and scale of the fitted weibull model
"""
shape, loc, scale = stats.weibull_min.fit(data, floc=0, **kwargs)
return shape, scale
@property
def st_description(self):
"""Description of this model to be used in the streamlit website"""
cdf = r"""
f_w = 1 - exp\left(-\left(\frac{\sigma_f}{\sigma_m}\right)^m\right) \quad \text{...(1)}
"""
pdf = r"""
F_w = \left(\frac{m}{\sigma_m}\right)\left(\frac{\sigma_f}{\sigma_m}\right)^{m-1}exp\left(-\left(\frac{\sigma_f}{\sigma_m}\right)^m\right) \quad \text{...(2)}
"""
re_cdf = r"""
\sigma_f = exp\left(ln(\sigma_m)+\frac{1}{m}ln\left(ln\left(\frac{1}{1-f_w}\right)\right)\right) \quad \text{...(3)}
"""
ar_cdf = r"""
\sigma_f = exp\left(\biggl\{U_t+\frac{W_t}{T}\biggl\}+\frac{1}{m}ln\left(ln\left(\frac{1}{1-f_w}\right)\right)\right) \quad \text{...(4)}
"""
fatigue_cdf = r"""
\sigma_f = exp\left(\biggl\{U_t+\frac{W_t}{T}+Vln(\epsilon)\biggl\}+\frac{1}{m}ln\left(ln\left(\frac{1}{1-f_w}\right)\right)\right) \quad \text{...(5)}
"""
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $U_t$ | {self.intercept:.6f} |
| $W_t$ | {self.slope:.6f} |
| $m$ | {self.shape:.6f} |
"""
if self.two_var:
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $U_t$ | {self.intercept:.6f} |
| $W_t$ | {self.slope:.6f} |
| $V_t$ | {self.v:.6f} |
| $m$ | {self.shape:.6f} |
"""
if self.power_law:
ar_cdf = r"""
\sigma_f = exp\left(\biggl\{U_t+W_t ln(T)\biggl\}+\frac{1}{m}ln\left(ln\left(\frac{1}{1-f_w}\right)\right)\right) \quad \text{...(4)}
"""
fatigue_cdf = r"""
\sigma_f = exp\left(\biggl\{U_t++W_t ln(T)+Vln(\epsilon)\biggl\}+\frac{1}{m}ln\left(ln\left(\frac{1}{1-f_w}\right)\right)\right) \quad \text{...(5)}
"""
super().power_st_description(cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values)
else:
super().st_description(cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values)
return ''
[docs]
class NormalModel(ProbModel):
def __init__(self, X_values:np.ndarray, Y_values:np.ndarray, X_values2:np.ndarray=np.array([])):
"""Initializes Normal Probabilistic Model
Args:
X_values (np.ndarray): First accelerating variable (Temperature)
Y_values (np.ndarray): Values to be modelled
X_values2 (np.ndarray, optional): Second accelerating variable (Strain). Defaults to np.array([]).
"""
self.X_values = X_values
self.X_values2 = X_values2
self.Y_values = Y_values
self.name = "Normal Model"
self.tab_name = "Normal"
self.two_var = False
if len(X_values2):
self.name = "Normal Model With Two variables"
self.two_var = True
self.bounds = [(1e-6, 100000),(-100000, 100000), (-100000, 100000), (-100000, 100000)]
self.sigma, self.intercept, self.slope, self.q = self.minimize(self.bounds, args=(self.X_values, self.Y_values, self.X_values2))
else:
self.bounds = [(1e-6, 30),(-300, 300), (-20, 20), (-300, 300)]
self.sigma, self.intercept, self.slope,_ = self.minimize(self.bounds, args=(self.X_values, self.Y_values))
[docs]
def log_likelihood(self,params:list, temp:np.ndarray, sigma_values:np.ndarray, strain:np.ndarray=np.array([])):
"""Log likelihood
Args:
params (list): parameters that are to be determined
temp (np.ndarray)
sigma_values (np.ndarray)
strain(np.ndarray): Defaults to np.array([]).
Returns:
Gives a negative sum of log likelihood for given data
"""
sigma = params[0]
k = params[1]
m = params[2]
if sigma <= 0:
return np.inf # Avoid invalid sigma
if self.two_var:
l = params[3]
mu = k + m * temp + l*strain
else:
mu = k + m * temp
log_likelihood = np.sum(stats.norm.logpdf(sigma_values, loc=mu, scale=sigma))
return -log_likelihood
[docs]
def predict(self,cdf:float, temperature_values:np.ndarray, strain_values:np.ndarray=np.array([])):
"""To predict values
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray): Defaults to np.array([])
Returns:
Predict values according to the trained model
"""
z = np.sqrt(2) * self.sigma * erfinv(2 * cdf - 1)
if self.two_var:
return self.intercept + (self.slope * 11604.53) / (temperature_values + 273.16) + self.q * np.log(strain_values) + z
return self.intercept + (self.slope * 11604.53) / (temperature_values + 273.16) + z
[docs]
def two_var_predict(self, cdf:float, temperature_values:np.ndarray, strain_values:np.ndarray, params:np.ndarray) -> np.ndarray:
"""To predict values when there are two accelerating variables
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray)
params (np.ndarray): Parameters that will be needed to predict values \n\n `sigma, k, m, l= params`. \n
"""
sigma, k, m, l = params
z = np.sqrt(2) * sigma * erfinv(2 * cdf - 1)
return np.exp(k + (m * 11604.53) / (temperature_values + 273.16) + l * np.log(strain_values) + z) / 1000000
[docs]
@staticmethod
def estimate_params(data:np.ndarray):
"""Fit a normal model on the `data`
Args:
data (np.ndarray)
Returns:
Gives shape and scale of the fitted normal model
"""
mu, sigma = np.mean(data), np.std(data, ddof=0)
return mu, sigma
@property
def st_description(self):
cdf = r"""
f_n = \frac{1}{2} + \frac{1}{2} \, \text{erf} \left( \frac{\sigma_f - \sigma_m}{\sqrt{2} \sigma_{sl}} \right) \quad \text{...(1)}
"""
pdf = r"""
F_n = \frac{1}{\sigma_{sl} \sqrt{2\pi}} \exp\left( -\left(\frac{\sigma_f - \sigma_m}{\sqrt{2}\sigma_{sl}}\right)^2 \right) \quad \text{...(2)}
"""
re_cdf = r"""
\sigma_f = \sigma_m + \sigma_{sl} \sqrt{2} \, \text{erf}^{-1}(2f_n - 1) \quad \text{...(3)}
"""
ar_cdf = r"""
\sigma_f = \left( P_t + \frac{R_t}{T} \right) + \left( \sigma_{sl} \sqrt{2} \, \text{erf}^{-1}(2f_n - 1) \right) \quad \text{...(4)}
"""
fatigue_cdf = r"""
\sigma_f = \left( P_t + Q_t \ln(\epsilon) + \frac{R_t}{T} \right) + \left( \sigma_{sl} \sqrt{2} \, \text{erf}^{-1}(2f_n - 1) \right) \quad \text{...(5)}
"""
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $P_t$ | {self.intercept} |
| $R_t$ | {self.slope} |
| $\sigma_{{sl}}$ | {self.slope} |
"""
if self.two_var:
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $P_t$ | {self.intercept} |
| $R_t$ | {self.slope} |
| $Q_t$ | {self.q} |
| $\sigma_{{sl}}$ | {self.slope} |
"""
super().st_description(cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values)
return ''
[docs]
class LognormalModel(ProbModel):
def __init__(self,X_values:np.ndarray, Y_values:np.ndarray,X_values2:np.ndarray=np.array([]), power_law:bool=False)->None:
"""Initializes LogNormal Probabilistic Model
Args:
X_values (np.ndarray): First accelerating variable (Temperature)
Y_values (np.ndarray): Values to be modelled
X_values2 (np.ndarray, optional): Second accelerating variable (Strain). Defaults to np.array([]).
power_law (bool, optional):Whether to use power law or not. Defaults to False, which means that we will use Arrhenius equation instead of power law.
"""
self.X_values = X_values
self.X_values2 = X_values2
self.Y_values = Y_values
self.name = "LogNormal Model"
self.tab_name = "LogNormal"
self.power_law = power_law
if power_law:
self.name = "LogNormal Model With Power Law"
self.tab_name = "LogNormal (Power)"
self.two_var = False
if len(X_values2):
self.name = "LogNormal Model With Two variables"
self.two_var = True
self.bounds = [(-20, 20),(-20, 20),(1e-6, 20), (-20, 20)]
self.k, self.m, self.sigma, self.l = self.minimize(self.bounds, args=(self.X_values, self.Y_values, self.X_values2))
else:
self.bounds = [(-20, 20),(-20, 20), (1e-10, 20)]
self.k, self.m, self.sigma = self.minimize(self.bounds, args=(self.X_values, self.Y_values))
[docs]
def log_likelihood(self,params:list, temp:np.ndarray, sigma_values:np.ndarray, strain:np.ndarray=np.array([])):
"""Log likelihood
Args:
params (list): parameters that are to be determined
temp (np.ndarray)
sigma_values (np.ndarray)
strain(np.ndarray): Defaults to np.array([]).
Returns:
Gives a negative sum of log likelihood for given data
"""
k = params[0]
m = params[1]
sigma = params[2]
if sigma <= 0:
return np.inf # Avoid invalid sigma
if self.two_var:
l = params[3]
mu = k + m * temp + l*strain
else:
mu = k + m * temp
log_likelihood = np.sum(stats.norm.logpdf(np.log(sigma_values), loc=mu, scale=sigma) - np.log(sigma_values))
return -log_likelihood
[docs]
def predict(self,cdf:float, temperature_values:np.ndarray, strain_values = np.array([])):
"""To predict values
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray): Defaults to np.array([])
Returns:
Predict values according to the trained model
"""
z = np.sqrt(2) * self.sigma * erfinv(2 * cdf - 1)
if self.power_law:
return np.exp(self.k + self.m * np.log(temperature_values) + z)
elif self.two_var:
return np.exp(self.k + (self.m * 11604.53) / (temperature_values + 273.16) + self.l * np.log(strain_values) + z) / 1000000
return np.exp(self.k + (self.m * 11604.53) / (temperature_values + 273.16) + z)
[docs]
def two_var_predict(self, cdf:float, temperature_values:np.ndarray, strain_values:np.ndarray, params:np.ndarray) -> np.ndarray:
"""To predict values when there are two accelerating variables
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray)
params (np.ndarray): Parameters that will be needed to predict values \n\n `k, m, sigma, l= params`. \n
"""
k, m, sigma, l = params
z = np.sqrt(2) * sigma * erfinv(2 * cdf - 1)
return np.exp(k + (m * 11604.53) / (temperature_values + 273.16) + l * np.log(strain_values) + z) / 1000000
[docs]
@staticmethod
def estimate_params(data:np.ndarray):
"""Fit a Lognormal model on the `data`
Args:
data (np.ndarray)
Returns:
Gives shape and scale of the fitted Lognormal model
"""
log_data = np.log(data)
mu, sigma = np.mean(log_data), np.std(log_data, ddof=0)
return mu, sigma
@property
def st_description(self):
cdf = r"""
f_{ln} = \frac{1}{2} + \frac{1}{2} \, \text{erf} \left[ \frac{\ln(\sigma_f) - \ln(\sigma_m)}{\sqrt{2} \sigma_{sl}} \right] \quad \text{...(1)}
"""
pdf = r"""
F_{ln} = \frac{1}{(\sigma_f) \sigma_{sl} \sqrt{2\pi}} \exp\left( -\left( \frac{\ln(\sigma_f) - \ln(\sigma_m)}{\sqrt{2} \sigma_{sl}} \right)^2 \right) \quad \text{...(2)}
"""
re_cdf = r"""
\sigma_f = \exp \left( \ln(\sigma_m) + \sqrt{2} \, \sigma_{sl} \, \text{erf}^{-1}(2f_{ln} - 1) \right) \quad \text{...(3)}
"""
ar_cdf = r"""
\sigma_f = \exp \left( \left\{ K_t + \frac{M_t}{T} \right\} + \left\{ \sqrt{2} \, \sigma_{sl} \, \text{erf}^{-1}(2f_{ln} - 1) \right\} \right) \quad \text{...(4)}
"""
fatigue_cdf = r"""
\sigma_f = \exp \left( \left\{ K_t + L \ln(\epsilon) + \frac{M_t}{T} \right\} + \left\{ \sqrt{2} \, \sigma_{sl} \, \text{erf}^{-1}(2f_{ln} - 1) \right\} \right) \quad \text{...(5)}
"""
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $K_t$ | {self.k:.6f} |
| $M_t$ | {self.m:.6f} |
| $\sigma{{sl}}$ | {self.sigma:.6f} |
"""
if self.two_var:
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $K_t$ | {self.k:.6f} |
| $M_t$ | {self.m:.6f} |
| $L_t$ | {self.l:.6f} |
| $\sigma{{sl}}$ | {self.sigma:.6f} |
"""
if self.power_law:
ar_cdf = r"""
\sigma_f = \exp \left( \left\{ K_t + M_t ln(T) \right\} + \left\{ \sqrt{2} \, \sigma_{sl} \, \text{erf}^{-1}(2f_{ln} - 1) \right\} \right) \quad \text{...(4)}
"""
fatigue_cdf = r"""
\sigma_f = \exp \left( \left\{ K_t + L \ln(\epsilon) + M_t ln(T) \right\} + \left\{ \sqrt{2} \, \sigma_{sl} \, \text{erf}^{-1}(2f_{ln} - 1) \right\} \right) \quad \text{...(5)}
"""
super().power_st_description(cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values)
else:
super().st_description(cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values)
return ''
[docs]
class WeibullModel3(ProbModel):
def __init__(self,X_values:np.ndarray, Y_values:np.ndarray):
"""Initializes Weibull Probabilistic Model with 3 parameters
Args:
X_values (np.ndarray): First accelerating variable (Temperature)
Y_values (np.ndarray): Values to be modelled
"""
self.X_values = X_values
self.Y_values = Y_values
self.name = "3-Parameter Weibull Model"
self.tab_name = "3-Parameter Weibull"
self.minimize()
[docs]
def log_likelihood(self, params:list,temp:np.ndarray, sigma_values:np.ndarray):
"""Log likelihood
Args:
params (list): parameters that are to be determined
temp (np.ndarray)
sigma_values (np.ndarray)
Returns:
Gives a negative sum of log likelihood for given data
"""
shape = params[0]
u = params[1]
w = params[2]
loc = params[3]
if shape <= 0 or np.any(self.Y_values - loc <= 0):
return np.inf
scale = np.exp(u + w * temp)
return -np.sum(stats.weibull_min.logpdf(sigma_values-loc, c=shape, scale=scale))
[docs]
def minimize(self):
init_params = [2.0, np.log(np.mean(self.Y_values)), 0.0, np.min(self.Y_values) * 0.9]
bounds = [(1e-6, None), (None, None), (None, None), (None, np.min(self.Y_values) - 1e-6)]
rng = np.random.default_rng(seed=212)
perm = rng.permutation(len(self.X_values))
result_regression = optimize.minimize(
self.log_likelihood,
init_params,
args=(self.X_values[perm], self.Y_values[perm]),
bounds=bounds,
method='L-BFGS-B'
)
self.shape, self.intercept, self.slope,self.delta = result_regression.x
[docs]
def predict(self,cdf:float, temperature_values:np.ndarray):
"""To predict values
Args:
cdf (float)
temperature_values (np.ndarray)
Returns:
Predict values according to the trained model
"""
return np.exp(
(self.intercept + (self.slope * 11604.53 / (temperature_values + 273.16))) +
((1 / self.shape) * np.log(np.log(1 / (1 - cdf))))
) + self.delta
[docs]
@staticmethod
def estimate_params(data:np.ndarray, **kwargs):
"""Fit a weibull 3-parameter model on the `data`
Args:
data (np.ndarray)
Returns:
Gives shape and scale of the fitted weibull 3-parameter model
"""
shape, loc, scale = stats.weibull_min.fit(data, **kwargs) # Lock location to 0 for typical Weibull fitting
return shape, scale, loc
@property
def st_description(self):
cdf = r"""
f_w = 1 - exp\left(-\left(\frac{\sigma_f - \delta}{\sigma_m}\right)^m\right) \quad \text{...(1)}
"""
pdf = r"""
F_w = \left(\frac{m}{\sigma_m}\right)\left(\frac{\sigma_f - \delta}{\sigma_m}\right)^{m-1}exp\left(-\left(\frac{\sigma_f - \delta}{\sigma_m}\right)^m\right) \quad \text{...(2)}
"""
re_cdf = r"""
\sigma_f = exp\left(ln(\sigma_m)+\frac{1}{m}ln\left(ln\left(\frac{1}{1-f_w}\right)\right)\right)+\delta \quad \text{...(3)}
"""
ar_cdf = r"""
\sigma_f = exp\left(\biggl\{U_t+\frac{W_t}{T}\biggl\}+\frac{1}{m}ln\left(ln\left(\frac{1}{1-f_w}\right)\right)\right)+\delta \quad \text{...(4)}
"""
fatigue_cdf = r"""
\sigma_f = exp\left(\biggl\{U_t+\frac{W_t}{T}+Vln(\epsilon)\biggl\}+\frac{1}{m}ln\left(ln\left(\frac{1}{1-f_w}\right)\right)\right)+\delta \quad \text{...(5)}
"""
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $U_t$ | {self.intercept:.6f} |
| $W_t$ | {self.slope:.6f} |
| $m$ | {self.shape:.6f} |
| $\delta$ | {self.delta:.6f} |
"""
super().st_description(cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values)
return ''
[docs]
class LognormalModel3(ProbModel):
def __init__(self, X_values:np.ndarray, Y_values:np.ndarray):
"""Initializes LogNormal Probabilistic Model with 3 parameters
Args:
X_values (np.ndarray): First accelerating variable (Temperature)
Y_values (np.ndarray): Values to be modelled
"""
self.X_values = X_values
self.Y_values = Y_values
self.name = "3-Parameter LogNormal Model"
self.tab_name = "3-Parameter LogNormal"
self.minimize()
[docs]
def log_likelihood(self, params:list,temp:np.ndarray, sigma_values:np.ndarray):
"""Log likelihood
Args:
params (list): parameters that are to be determined
temp (np.ndarray)
sigma_values (np.ndarray)
Returns:
Gives a negative sum of log likelihood for given data
"""
k,m, sigma,gamma = params
if sigma <= 0:
return np.inf # Avoid invalid sigma
mu = k + m * temp
log_likelihood = np.sum(stats.norm.logpdf(np.log(sigma_values-gamma), loc=mu, scale=sigma) - np.log(sigma_values))
return -log_likelihood
[docs]
def minimize(self):
init_params = [10, 1,1,1]
bounds = [(None, None),(None, None), (1e-10, None),(None, None)] # mu unbounded, sigma > 0
result_lognormal = optimize.minimize(
self.log_likelihood,
init_params,
args=(self.X_values, self.Y_values,),
method='L-BFGS-B',
bounds=bounds
)
self.k, self.m, self.sigma,self.gamma = result_lognormal.x
[docs]
def predict(self,cdf:float, temperature_values:np.ndarray):
"""To predict values
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray): Defaults to np.array([])
Returns:
Predict values according to the trained model
"""
z = np.sqrt(2) * self.sigma * erfinv(2 * cdf - 1)
return np.exp(self.k + (self.m * 11604.53) / (temperature_values + 273.16) + z) + self.gamma
[docs]
@staticmethod
def estimate_params(data:np.ndarray):
"""Fit a lognormal 3-parameter model on the `data`
Args:
data (np.ndarray)
Returns:
Gives shape and scale of the fitted lognormal 3-parameter model
"""
log_data = np.log(data)
mu, sigma = np.mean(log_data), np.std(log_data, ddof=0)
return mu, sigma
@property
def st_description(self):
cdf = r"""
f_{ln} = \frac{1}{2} + \frac{1}{2} \, \text{erf} \left[ \frac{\ln(\sigma_f-\gamma) - \ln(\sigma_m)}{\sqrt{2} \sigma_{sl}} \right] \quad \text{...(1)}
"""
pdf = r"""
F_{ln} = \frac{1}{(\sigma_f-\gamma) \sigma_{sl} \sqrt{2\pi}} \exp\left( -\left( \frac{\ln(\sigma_f-\gamma) - \ln(\sigma_m)}{\sqrt{2} \sigma_{sl}} \right)^2 \right) \quad \text{...(2)}
"""
re_cdf = r"""
\sigma_f = \exp \left( \ln(\sigma_m) + \sqrt{2} \, \sigma_{sl} \, \text{erf}^{-1}(2f_{ln} - 1) \right)+\gamma \quad \text{...(3)}
"""
ar_cdf = r"""
\sigma_f = \exp \left( \left\{ K_t + \frac{M_t}{T} \right\} + \left\{ \sqrt{2} \, \sigma_{sl} \, \text{erf}^{-1}(2f_{ln} - 1) \right\} \right)+\gamma \quad \text{...(4)}
"""
fatigue_cdf = r"""
\sigma_f = \exp \left( \left\{ K_t + L \ln(\epsilon) + \frac{M_t}{T} \right\} + \left\{ \sqrt{2} \, \sigma_{sl} \, \text{erf}^{-1}(2f_{ln} - 1) \right\} \right)+\gamma \quad \text{...(5)}
"""
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $K_t$ | {self.k:.6f} |
| $M_t$ | {self.m:.6f} |
| $\sigma_{{sl}}$ | {self.sigma:.6f} |
| $\gamma$ | {self.gamma:.6f} |
"""
super().st_description(cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values)
return ''
[docs]
class Gumbell(ProbModel):
def __init__(self,X_values:np.ndarray, Y_values:np.ndarray,X_values2:np.ndarray=np.array([])):
"""Initializes Gumbell Probabilistic Model
Args:
X_values (np.ndarray): First accelerating variable (Temperature)
Y_values (np.ndarray): Values to be modelled
X_values2 (np.ndarray, optional): Second accelerating variable (Strain). Defaults to np.array([]).
"""
self.X_values = X_values
self.X_values2 = X_values2
self.Y_values = Y_values
self.name = "Gumbell Model"
self.tab_name = "Gumbell"
self.two_var = False
if len(X_values2):
# self.init_params = [2.0,-1, np.log(np.mean(self.Y_values)), -3.0]
self.bounds = [(-100000, 100000),(-100000, 100000),(1e-6, 100000), (-100000, 100000)]
self.name = "Gumbell Model With Two variables"
self.two_var = True
self.intercept, self.slope,self.scale, self.v = self.minimize(self.bounds, args=(self.X_values, self.Y_values, self.X_values2))
else:
# init_params = [2.0, np.log(np.mean(self.Y_values)), 0.0]
bounds = [(-20, 400), (-20, 20), (1e-6, 30)]
self.intercept, self.slope,self.scale = self.minimize( bounds, args=(self.X_values, self.Y_values))
[docs]
def log_likelihood(self,params:list, temp:np.ndarray, sigma_values:np.ndarray, strain_values=np.array([])):
"""Log likelihood
Args:
params (list): parameters that are to be determined
temp (np.ndarray)
sigma_values (np.ndarray)
strain(np.ndarray): Defaults to np.array([]).
Returns:
Gives a negative sum of log likelihood for given data
"""
u = params[0] # Intercept
w = params[1] # Slope
scale = params[2] # Scale
if scale <= 0:
return np.inf
if self.two_var:
v = params[3]
loc = u + w * temp + v * strain_values
else:
loc = u + w * temp
z = (sigma_values - loc) / scale
z = np.clip(z, -700, 700) # -exp(-z) overflows around -745
logpdf = -z - np.exp(-z) - np.log(scale)
return -np.sum(logpdf)
[docs]
def predict(self,cdf:float, temperature_values:np.ndarray,strain_values=np.array([])):
"""To predict values
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray): Defaults to np.array([])
Returns:
Predict values according to the trained model
"""
inv_temp = 11604.53 / (temperature_values + 273.16)
if self.two_var:
return (self.intercept + self.slope * inv_temp + self.v*np.log(strain_values) - self.scale * np.log(-np.log(cdf))) / 1000000
return self.intercept + self.slope * inv_temp - self.scale * np.log(-np.log(cdf))
[docs]
def two_var_predict(self, cdf:float, temperature_values:np.ndarray, strain_values:np.ndarray, params:np.ndarray) -> np.ndarray:
"""To predict values when there are two accelerating variables
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray)
params (np.ndarray): Parameters that will be needed to predict values \n `u,w,scale,v= params`.
"""
u,w,scale,v = params
inv_temp = 11604.53 / (temperature_values + 273.16)
return (u + w * inv_temp + v*np.log(strain_values) - scale * np.log(-np.log(cdf)))/1000000
[docs]
@staticmethod
def estimate_params(data:np.ndarray, **kwargs):
"""Fit a gumbell model on the `data`
Args:
data (np.ndarray)
Returns:
Gives shape and scale of the fitted gumbell model
"""
pass
@property
def st_description(self):
cdf = r"""
f_{gb} = exp\left(-exp\left(-\frac{\sigma_f - \mu}{\sigma_m}\right)\right) \quad \text{...(1)}
"""
pdf = r"""
F_{gb} = \frac{1}{\sigma_m}\left(-exp\left(-\frac{\sigma_f - \mu}{\sigma_m} + e^{-z}\right)\right) \quad \text{...(2)}
"""
re_cdf = r"""
\sigma_f = -\sigma_mln(-ln(f_{gb})) + \mu \quad \text{...(3)}
"""
ar_cdf = r"""
\sigma_f = -\sigma_mln(-ln(f_{gb})) + \left( P_t + \frac{R_t}{T} \right) \quad \text{...(4)}
"""
fatigue_cdf = r"""
\sigma_f = -\sigma_mln(-ln(f_{gb})) + \left( P_t + Q_tln(\epsilon) +\frac{R_t}{T} \right) \quad \text{...(5)}
"""
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $U_t$ | {self.intercept:.6f} |
| $W_t$ | {self.slope:.6f} |
| $\sigma_m$ | {self.scale:.6f} |
"""
if self.two_var:
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $U_t$ | {self.intercept:.6f} |
| $W_t$ | {self.slope:.6f} |
| $V_t$ | {self.v:.6f} |
| $\sigma_m$ | {self.scale:.6f} |
"""
super().st_description(cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values)
return ''
[docs]
class Exponential(ProbModel):
def __init__(self,X_values:np.ndarray, Y_values:np.ndarray, X_values2:np.ndarray=np.array([])):
"""Initializes Exponential Probabilistic Model
Args:
X_values (np.ndarray): First accelerating variable (Temperature)
Y_values (np.ndarray): Values to be modelled
X_values2 (np.ndarray, optional): Second accelerating variable (Strain). Defaults to np.array([]).
"""
self.X_values = X_values
self.X_values2 = X_values2
self.Y_values = Y_values
self.name = "Exponential Model"
self.tab_name = "Exponential"
self.two_var = False
if len(X_values2):
self.bounds = [(-20, 20), (-20, 20), (-20, 20)]
self.name = "Exponential Model with two variables"
self.two_var = True
self.intercept, self.slope, self.v = self.minimize(self.bounds, args=(self.X_values, self.Y_values, self.X_values2))
else:
bounds = [(-10, 10), (-10, 10)]
self.intercept, self.slope = self.minimize(bounds, args=(self.X_values, self.Y_values))
[docs]
def log_likelihood(self,params:list, temp:np.ndarray, sigma_values:np.ndarray, strain_values = np.array([])):
"""Log likelihood
Args:
params (list): parameters that are to be determined
temp (np.ndarray)
sigma_values (np.ndarray)
strain(np.ndarray): Defaults to np.array([]).
Returns:
Gives a negative sum of log likelihood for given data
"""
u = params[0]
w = params[1]
if self.two_var:
v = params[2]
scale = np.exp(u + w * temp + v * strain_values)
else:
scale = np.exp(u + w * temp)
return -np.sum(stats.expon.logpdf(sigma_values, scale=scale))
[docs]
def predict(self,cdf:float, temperature_values:np.ndarray):
"""To predict values
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray): Defaults to np.array([])
Returns:
Predict values according to the trained model
"""
inv_temp_range = 11604.53 / (temperature_values + 273.16)
lambda_vals = np.exp(self.intercept + self.slope * inv_temp_range)
return -lambda_vals * np.log(1 - cdf)
[docs]
def two_var_predict(self, cdf:float, temperature_values:np.ndarray, strain_values:np.ndarray, params:np.ndarray) -> np.ndarray:
"""To predict values when there are two accelerating variables
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray)
params (np.ndarray): Parameters that will be needed to predict values \n\n `u,w,v= params`. \n
"""
u,w,v = params
inv_temp_range = 11604.53 / (temperature_values + 273.16)
lambda_vals = np.exp(u + w * inv_temp_range + v * np.log(strain_values))
return -lambda_vals * np.log(1 - cdf) / 1000000
[docs]
@staticmethod
def estimate_params(data:np.ndarray, **kwargs):
pass
@property
def st_description(self):
cdf = r"""
f_{exp} = 1 - \exp\left(- \frac{\sigma_f}{\sigma_m}\right) \quad \text{...(1)}
"""
pdf = r"""
F_{exp} = \frac{1}{\sigma_m}\exp\left(- \frac{\sigma_f}{\sigma_m}\right) \quad \text{...(2)}
"""
re_cdf = r"""
\sigma_f = -\sigma_m \ln(1-f_{exp}) \quad \text{...(3)}
"""
ar_cdf = r"""
\sigma_f = -\left( U_t +\frac{W_t}{T} \right) \ln(1-f_{exp}) \quad \text{...(4)}
"""
fatigue_cdf = r"""
\sigma_f = -\left( U_t + V_t\ln(\epsilon) +\frac{W_t}{T} \right) \ln(1-f_{exp}) \quad \text{...(5)}
"""
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $U_t$ | {self.intercept:.6f} |
| $W_t$ | {self.slope:.6f} |
"""
if self.two_var:
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $U_t$ | {self.intercept:.6f} |
| $W_t$ | {self.slope:.6f} |
| $V_t$ | {self.v:.6f} |
"""
super().st_description(cdf, pdf, re_cdf, ar_cdf, fatigue_cdf, variable_values)
return ''
[docs]
class Gamma(ProbModel):
def __init__(self,X_values:np.ndarray, Y_values:np.ndarray,X_values2:np.ndarray=np.array([])):
"""Initializes Gamma Probabilistic Model
Args:
X_values (np.ndarray): First accelerating variable (Temperature)
Y_values (np.ndarray): Values to be modelled
X_values2 (np.ndarray, optional): Second accelerating variable (Strain). Defaults to np.array([]).
"""
self.X_values = X_values
self.X_values2 = X_values2
self.Y_values = Y_values
self.name = "Gamma Model"
self.tab_name = "Gamma"
self.two_var = False
if len(X_values2):
self.two_var = True
self.bounds = [(1e-6, 20), (-20, 20),(-20, 20), (-20, 20)]
self.shape,self.intercept, self.slope, self.v = self.minimize(self.bounds, args=(self.X_values, self.Y_values, self.X_values2))
else:
self.bounds = [(1e-6, 1000), (-10, 10),(-10, 10)]
self.shape,self.intercept, self.slope= self.minimize(self.bounds, args=(self.X_values, self.Y_values))
[docs]
def log_likelihood(self,params:list, temp:np.ndarray, sigma_values:np.ndarray, strain_values:np.ndarray=np.array([])):
"""Log likelihood
Args:
params (list): parameters that are to be determined
temp (np.ndarray)
sigma_values (np.ndarray)
strain(np.ndarray): Defaults to np.array([]).
Returns:
Gives a negative sum of log likelihood for given data
"""
u = params[1]
w = params[2]
shape = params[0]
if self.two_var:
v = params[3]
scale = np.exp(u + w * temp + v * strain_values)
else:
scale = np.exp(u + w * temp)
return -np.sum(stats.gamma.logpdf(sigma_values, a=shape, scale=scale))
[docs]
def predict(self,cdf:float, temperature_values:np.ndarray, strain_values:np.ndarray=np.array([])):
"""To predict values
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray): Defaults to np.array([])
Returns:
Predict values according to the trained model
"""
inv_temp_range = 11604.53 / (temperature_values + 273.16)
if self.two_var:
scale_range = np.exp(self.intercept + self.slope * inv_temp_range + self.v * np.log(strain_values))
return stats.gamma.ppf(cdf, a=shape, scale=scale_range) / 1000000
scale_range = np.exp(self.intercept + self.slope * inv_temp_range)
return stats.gamma.ppf(cdf, a=self.shape, scale=scale_range)
[docs]
def two_var_predict(self, cdf:float, temperature_values:np.ndarray, strain_values:np.ndarray, params:np.ndarray) -> np.ndarray:
"""To predict values when there are two accelerating variables
Args:
cdf (float)
temperature_values (np.ndarray)
strain_values (np.ndarray)
params (np.ndarray): Parameters that will be needed to predict values \n\n `shape,u,w,v= params`. \n
"""
shape,u,w,v = params
inv_temp_range = 11604.53 / (temperature_values + 273.16)
scale_range = np.exp(u + w * inv_temp_range + v * np.log(strain_values))
return stats.gamma.ppf(cdf, a=shape, scale=scale_range) / 1000000
[docs]
@staticmethod
def estimate_params(data:np.ndarray, **kwargs):
"""Fit a gamma model on the `data`
Args:
data (np.ndarray)
Returns:
Gives shape and scale of the fitted gamma model
"""
return stats.gamma.fit(data, **kwargs)
@property
def st_description(self):
cdf = r"""
f_{gm}(x) = \frac{1}{\Gamma(m)} \, \gamma\left(m, \frac{\sigma_f}{\sigma_m}\right) \quad \text{...(1)}
"""
pdf = r"""
F_{gm}(x) = \frac{1}{\sigma_m^{\alpha}\Gamma(m)} \,\sigma_f^{\alpha-1}\,exp\left(-\frac{\sigma_f}{\sigma_m}\right) \quad \text{...(2)}
"""
st.header("📋 Mathematical Formulation")
# CDF Section
st.subheader("1. Cumulative Distribution Function (CDF)")
st.markdown("""
The CDF of this model is given by:
""")
st.latex(cdf)
# PDF Section
st.subheader("2. Probability Density Function (PDF)")
st.markdown("""
The PDF of this model can be expressed as:
""")
st.latex(pdf)
st.markdown("Equation (1) can't be written in terms of $\sigma_f$, so we have to do iteration to get $\sigma_f$")
st.subheader("Variable Values")
st.markdown("In the case of data given, values of the variables are")
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $U_t$ | {self.intercept:.6f} |
| $W_t$ | {self.slope:.6f} |
| $\sigma_m$ | {self.shape:.6f} |
"""
if self.two_var:
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $U_t$ | {self.intercept:.6f} |
| $W_t$ | {self.slope:.6f} |
| $V_t$ | {self.v:.6f} |
| $\sigma_m$ | {self.shape:.6f} |
"""
st.markdown(variable_values)
return ''
# Make a class of walker equation to have values to plot various things
[docs]
class WalkerEq():
def __init__(self, df:pd.DataFrame, isLinear:bool=True):
self.df = df
self.linear = isLinear # whether the relationship is linear or arrhenious
# Do the basic regressions
self.implement_paris_eq()
if isLinear:
self.linear_walker_regr()
else:
self.arrhenius_walker_regr()
self.implement_walker_eq()
self.regression_walker_eq()
[docs]
def implement_paris_eq(self):
"""Implementation of Paris–Erdoğan equation"""
self.df["da_dN"] = self.df.apply(
lambda row: row["c"] * ((row["Delta K"]) ** row["m"]),
axis = 1
)
self.df['Temperature,K'] = self.df.apply(
lambda row: row['Temperature, C'] + 273.15,
axis = 1
)
[docs]
@staticmethod
def lin_reg(X, Y):
# Compute means
X_mean = sum(X) / len(X)
Y_mean = sum(Y) / len(Y)
# Compute slope
numerator = sum((X[i] - X_mean) * (Y[i] - Y_mean) for i in range(len(X)))
denominator = sum((X[i] - X_mean) ** 2 for i in range(len(X)))
slope = numerator / denominator
# Compute intercept
intercept = Y_mean - slope * X_mean
return slope, intercept
[docs]
def implement_walker_eq(self):
self.df["y"] = self.df.apply(
lambda row: np.log(row["da_dN"]) - (row["M"] * np.log(row["Delta K"])),
axis = 1
)
self.df['x'] = self.df.apply(
lambda row: row["M"] * np.log(1 - row["R- Ratio"]),
axis = 1
)
[docs]
def linear_walker_regr(self):
features = ['Temperature,K']
target = 'm'
X = self.df[features]
y = self.df[target]
linear_model = LinearRegression()
linear_model.fit(X, y)
self.intercept = linear_model.intercept_
self.slope = linear_model.coef_[0]
# Predict full set
X_full = self.df[['Temperature,K']]
self.df['M'] = linear_model.predict(X_full) # predicted m
self.compute_M = lambda temp_C, intercept, slope: intercept + slope * (temp_C + 273.15)
[docs]
def arrhenius_walker_regr(self):
R = 8.314 # J/mol·K
X = 1 / self.df['Temperature,K'].values.reshape(-1, 1)
y_log = np.log(self.df['m'].values)
arrhenius_model = LinearRegression()
arrhenius_model.fit(X, y_log)
# Evaluate
lnA = arrhenius_model.intercept_
Ea_over_R = -arrhenius_model.coef_[0]
Ea = Ea_over_R * R
self.slope = lnA
self.intercept = Ea
# Predict full set
X_full = 1 / self.df['Temperature,K'].values.reshape(-1, 1)
log_m_pred = arrhenius_model.predict(X_full)
self.df['M'] = np.exp(log_m_pred)
self.compute_M = lambda temp_C, Ea, lnA: np.exp(lnA - Ea / (R * (temp_C + 273.15)))
[docs]
def regression_walker_eq(self):
self.df["y"] = self.df.apply(
lambda row: np.log(row["da_dN"]) - (row["M"] * np.log(row["Delta K"])),
axis = 1
)
self.df['x'] = self.df.apply(
lambda row: row["M"] * np.log(1 - row["R- Ratio"]),
axis = 1
)
slope, intercept = self.lin_reg(self.df['x'], self.df["y"])
self.slope_, self.intercept_= slope, intercept
self.C = np.exp(intercept)
self.gamma = slope
self.df['da/dN_calc'] = self.df.apply(
lambda row: self.C * ((row["Delta K"] * ((1 - row["R- Ratio"])** self.gamma)) ** row["M"]),
axis = 1
)
[docs]
def regression_plot(self, slope, intercept):
x_values = np.linspace(self.df['x'].min(), self.df['x'].max(), 100)
y_values = (
slope * x_values + intercept
)
fig, ax = plt.subplots(figsize=(10,6))
ax.plot(x_values, y_values, color="red", linestyle="-", linewidth=2, label="Regressed lines")
ax.scatter(self.df['x'], self.df['y'], color="darkblue", label="Data points")
ax.set_xlabel("X", fontsize=12, fontweight="bold")
ax.set_ylabel("Y", fontsize=12, fontweight="bold")
ax.set_title("Walker model - regression", fontsize=14, fontweight="bold")
ax.grid(True, linestyle="-", alpha=0.6)
ax.legend(fontsize=12)
st.pyplot(fig)
[docs]
def regression_dAdN_plot(self):
actual = self.df['da_dN']
predicted = self.df['da/dN_calc']
fig, ax = plt.subplots(figsize=(10,6))
ax.scatter(actual, predicted, color='blue', alpha=0.6, label='Predicted vs Actual da/dN')
# Reference line (perfect prediction)
ax.plot([actual.min(), actual.max()], [actual.min(), actual.max()], 'r--', label='Ideal Fit')
ax.set_xlabel('Actual da/dN', fontsize=12, fontweight='bold')
ax.set_ylabel('Predicted da/dN', fontsize=12, fontweight='bold')
ax.set_title('Model: Predicted vs Actual da/dN', fontsize=14, fontweight='bold')
ax.legend(fontsize=12)
ax.grid(True, linestyle='-', alpha=0.6)
st.pyplot(fig)
[docs]
def regression_dAdN_error_plot(self):
error = self.df["da/dN_calc"] - self.df["da_dN"]
fig, ax = plt.subplots(figsize=(10,6))
ax.hist(error, bins=20, color='teal', edgecolor='black', alpha=0.7)
ax.set_xlabel('Prediction Error (Calc - Actual)')
ax.set_ylabel('Frequency')
ax.set_title('Histogram of da/dN Prediction Errors')
ax.grid(True)
st.pyplot(fig)
[docs]
def plot_da_dN_vs_deltaK_equation(self):
delta_K_vals = np.linspace(5, 25, 200)
temp_C = st.selectbox("Temperature",(self.df['Temperature, C'].unique()),key=f"plot_da_dN_vs_deltaK_equation{self.linear}")
M = self.compute_M(temp_C, self.intercept, self.slope)
plot_all = st.toggle("Plot different K values",key=f"plot_da_dN_vs_deltaK_equation_toggle{self.linear}")
fig, ax = plt.subplots(figsize=(10,6))
if plot_all:
r_ratio_vals = np.linspace(0.05, 0.9, 10)
delta_K_range = np.linspace(5, 25, 200)
for r in r_ratio_vals:
da_dN_vals = []
for dk in delta_K_range:
M = self.compute_M(temp_C, self.intercept, self.slope)
base = dk * (max(1e-8, (1 - r)) ** self.gamma) # epsilon to prevent divide-by-zero
da_dN = self.C * (base ** M)
da_dN_vals.append(da_dN)
ax.plot(delta_K_range, da_dN_vals, label=f'R = {r:.2f}')
scatter_df = self.df[
(self.df['Temperature, C'] == temp_C) &
(np.isclose(self.df['R- Ratio'], r, rtol=1e-2))
]
# print(not scatter_df.empty)
if not scatter_df.empty:
ax.scatter(scatter_df['Delta K'], scatter_df['da_dN'], label=f'Actual R={r:.2f}', alpha=0.5)
ax.set_title(f'da/dN vs ΔK at T={temp_C}°C, for various R-ratios')
else:
da_dN_vals = []
rr = self.df['R- Ratio'].unique()
r_ratio = st.select_slider(
"Select a value for R",
options=[k for k in np.arange(min(rr),max(rr)+1,0.1)]
)
for dk in delta_K_vals:
da_dN = self.C * ((dk * (1 - r_ratio) ** self.gamma) ** M)
da_dN_vals.append(da_dN)
ax.plot(delta_K_vals, da_dN_vals, label='Predicted by Equation', color='darkorange')
scatter_df = self.df[
(self.df['Temperature, C'] == temp_C) &
(np.isclose(self.df['R- Ratio'], r_ratio, rtol=1e-2))
]
if not scatter_df.empty:
ax.scatter(scatter_df['Delta K'], scatter_df['da_dN'], label='Actual data', color='navy')
ax.set_title(f'da/dN vs ΔK at T={temp_C}°C, R={r_ratio}')
ax.plot(delta_K_vals, da_dN_vals, label='Predicted by Equation', color='darkorange')
ax.set_xlabel('ΔK')
ax.set_ylabel('da/dN')
ax.legend()
ax.grid(True)
st.pyplot(fig)
[docs]
def plot_da_dN_vs_r_ratio_equation(self):
r_ratio_vals = np.linspace(0.05, 1, 200)
delta_K_vals = [5, 10, 15, 20, 25]
fig, ax = plt.subplots(figsize=(10,6))
# option to select temperature
temp_C = st.selectbox("Temperature",(self.df['Temperature, C'].unique()),key=f"plot_da_dN_vs_r_ratio_equation{self.linear}")
M = self.compute_M(temp_C, self.intercept, self.slope)
# option to plot all of the values, or just to play with one
plot_all = st.toggle("Plot different K values",key=f"plot_da_dN_vs_r_ratio_equation_toggle{self.linear}")
if plot_all:
for dk in delta_K_vals:
da_dN_vals = []
for r in r_ratio_vals:
da_dN = self.C * ((dk * (1 - r) ** self.gamma) ** M)
da_dN_vals.append(da_dN)
ax.plot(r_ratio_vals, da_dN_vals, label='Predicted by Equation', color='darkorange')
scatter_df = self.df[
(self.df['Temperature, C'] == temp_C) &
(np.isclose(self.df['Delta K'], dk, rtol=1e-2))
]
if not scatter_df.empty:
plt.scatter(scatter_df['R- Ratio'], scatter_df['da_dN'], label=f'Actual ΔK={dk}', alpha=0.6)
ax.set_title(f'da/dN vs R at T={temp_C}°C')
else:
da_dN_vals = []
ks = self.df['Delta K'].unique()
dk = st.select_slider(
"Select a value for K",
options=[k for k in range(min(ks)-1,max(ks)+1,1)]
)
for r in r_ratio_vals:
da_dN = self.C * ((dk * (1 - r) ** self.gamma) ** M)
da_dN_vals.append(da_dN)
ax.plot(r_ratio_vals, da_dN_vals, label='Predicted by Equation', color='darkorange')
scatter_df = self.df[
(self.df['Temperature, C'] == temp_C) & (np.isclose(self.df['Delta K'],dk, rtol=1e-2))
]
if not scatter_df.empty:
ax.scatter(scatter_df['R- Ratio'], scatter_df['da_dN'], label=f'Actual ΔK={dk}')
ax.set_title(f'da/dN vs R at T={temp_C}°C, ΔK={dk}')
ax.set_xlabel('R-Ratio')
ax.set_ylabel('da/dN')
ax.legend()
ax.grid(True)
st.pyplot(fig)
[docs]
def plot_da_dN_vs_temperature_equation(self):
temp_vals = np.linspace(50, 300, 300)
da_dN_vals = []
plot_type = st.radio(
"Select Plot Type:",
("Different K ratios", "Different R ratios", "Custom"),
help="Choose the type of plot you want"
)
def da_dN(temp_C, dk, r_ratio, ax,doLabel=False):
M = self.compute_M(temp_C, self.intercept, self.slope)
scatter_df = self.df[
(np.isclose(self.df['Delta K'], dk, rtol=1e-2)) &
(np.isclose(self.df['R- Ratio'], r_ratio, rtol=1e-2))
]
if not scatter_df.empty:
if doLabel:
ax.scatter(scatter_df['Temperature, C'], scatter_df['da_dN'], label='Actual data')
else:
ax.scatter(scatter_df['Temperature, C'], scatter_df['da_dN'])
return self.C * ((dk * (1 - r_ratio) ** self.gamma) ** M)
# Start Plotting
fig, ax = plt.subplots(figsize=(10,6))
if plot_type=="Different R ratios":
ks = self.df['Delta K'].unique()
dk = st.select_slider(
"Select a value for K",
options=[k for k in range(min(ks)-1,max(ks)+1,1)],key=f"plot_da_dN_vs_temperature_equation_dk32{self.linear}"
)
r_ratio_vals = np.linspace(0.05, 0.9, 10)
for r in r_ratio_vals:
da_dN_vals = []
for temp_C in temp_vals:
da_dN_vals.append(da_dN(temp_C, dk, r,ax))
ax.plot(temp_vals, da_dN_vals, label=f'R = {r:.2f}')
ax.set_title(f'Fatigue Crack Growth Rate vs Temperature at ΔK={dk} MPa√m')
elif plot_type=="Different K ratios":
rr = self.df['R- Ratio'].unique()
r_ratio = st.select_slider(
"Select a value for R",
options=[k for k in np.arange(min(rr),max(rr)+1,0.1)],key=f"plot_da_dN_vs_temperature_equation_dk12{self.linear}"
)
delta_K_vals = [5, 10, 15, 20, 25]
for dk in delta_K_vals:
da_dN_vals = []
for temp_C in temp_vals:
da_dN_vals.append(da_dN(temp_C, dk, r_ratio,ax))
ax.plot(temp_vals, da_dN_vals, label=f'ΔK = {dk:.2f}')
ax.set_title(f'Fatigue Crack Growth Rate vs Temperature at R={r_ratio} MPa√m')
else:
ks = self.df['Delta K'].unique()
dk = st.select_slider(
"Select a value for K",
options=[k for k in range(min(ks)-1,max(ks)+1,1)],key=f"plot_da_dN_vs_temperature_equation_dk{self.linear}"
)
rr = self.df['R- Ratio'].unique()
r_ratio = st.select_slider(
"Select a value for R",
options=[k for k in np.arange(min(rr),max(rr)+1,0.1)],key=f"plot_da_dN_vs_temperature_equation_r{self.linear}"
)
count_scatter = 0
for temp_C in temp_vals:
if count_scatter!=0:
da_dN_vals.append(da_dN(temp_C, dk, r_ratio,ax))
else:
da_dN_vals.append(da_dN(temp_C, dk, r_ratio,ax, True))
count_scatter +=1
ax.set_title(f'Fatigue Crack Growth Rate vs Temperature at ΔK={dk} and R={r_ratio} MPa√m')
ax.plot(temp_vals, da_dN_vals, label='Predicted by Equation', color='darkorange')
ax.set_xlabel('Temperature, C')
ax.set_ylabel('da/dN')
ax.legend()
ax.grid(True)
st.pyplot(fig)
[docs]
def st_description(self):
st.header("📋 Mathematical Formulation")
st.markdown("### Model Equation")
st.write(
"The crack growth rate model is given by the following equation:"
)
st.latex(r"""
\frac{da}{dN} = C \left[ \left(1-r\right)^\gamma \, \Delta k \, \right] ^m
""")
st.markdown(
"""
where:
- $\\frac{da}{dN}$ is the crack growth rate,
- $C$ is a material constant,
- $\\gamma$ is a fitting parameter,
- $r$ is the stress ratio,
- $\\Delta k$ is the stress intensity factor range,
- $m$ is the crack growth exponent.
"""
)
st.markdown("### Linearization for Regression")
st.write(
"To determine the constants $C$ and $\\gamma$, we can transform the model into a linear form:"
)
st.latex(r"""
\ln\left(\frac{da}{dN}\right) - m \, \ln(\Delta k) = \gamma \, m \, \ln(1-r) + C
""")
st.write(
"This transformation allows us to apply **linear regression** to estimate $C$ and $\\gamma$."
)
st.markdown("### Temperature Dependence of $m$")
st.write(
"Since $m$ varies with temperature $T$, we can approximate it using either a **linear relation**:"
)
st.latex(r"""
m(T) = U + W \, T
""")
st.write("or an **Arrhenius-type relation**:")
st.latex(r"""
m(T) = A \, \exp\left( \frac{E_a \, R}{T} \right)
""")
st.write(
r"Here, $R = 8.314 \ \mathrm{J/(mol \cdot K)}$ is the universal gas constant, "
r"and $A$ and $E_a$ are determined via linear regression from the experimental data."
)
st.markdown("### Variable Values")
st.markdown("In the case of data given, values of the variables are")
if self.linear:
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $U_t$ | {self.intercept:.6f} |
| $W_t$ | {self.slope:.6f} |
| $C$ | {self.C:.6f} |
| $\gamma$ | {self.gamma:.6f} |
"""
else:
variable_values = f"""
| Variable | Values |
|----------|-------------|
| $E_a$ | {self.intercept:.6f} |
| $lnA$ | {self.slope:.6f} |
| $C$ | {self.C:.6f} |
| $\gamma$ | {self.gamma:.6f} |
"""
st.markdown(variable_values)