Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions examples/A617-fatigue.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
Strain range,R,Temp (C),Heat/Lot ID,Cycles
0.036450961,-1,850,Yukawa,75.21741756
0.014954006,-1,850,Yukawa,138.3973613
0.026822562,-1,850,Yukawa,129.7052657
0.017588035,-1,850,Yukawa,205.2729761
0.014231616,-1,850,Yukawa,376.3218742
0.009863814,-1,850,Yukawa,341.5998648
0.009867647,-1,850,Yukawa,280.3663584
0.009016596,-1,850,Yukawa,734.7955132
0.008029606,-1,850,Yukawa,929.6364334
0.006958316,-1,850,Yukawa,1235.864882
0.006635537,-1,850,Yukawa,1206.028962
0.004950047,-1,850,Yukawa,1979.326069
0.00511943,-1,850,Yukawa,2323.507551
0.004174646,-1,850,Yukawa,2631.712654
0.003951646,-1,850,Yukawa,3496.960304
0.004225379,-1,850,Yukawa,5657.831618
0.003687669,-1,850,Yukawa,6485.570953
0.003001571,-1,850,Yukawa,18773.86086
0.002978575,-1,850,Yukawa,29645.78296
0.002976696,-1,850,Yukawa,40867.1717
0.002295122,-1,850,Yukawa,108535.6949
0.002137544,-1,850,Yukawa,554004.1577
0.003020836,-1,850,Yukawa,725554.5398
0.028706352,-1,850,INL,132.8995194
0.029490534,-1,850,INL,148.4971176
0.019740827,-1,850,INL,205.1459222
0.019327614,-1,850,INL,304.5747656
0.009647737,-1,850,INL,841.3761172
0.005869328,-1,850,INL,1507.156003
0.005827332,-1,850,INL,1836.393784
0.003951646,-1,850,INL,3496.960304
0.003922324,-1,850,INL,4880.678088
0.002945171,-1,850,INL,9174.994437
0.002964452,-1,850,INL,10509.25301
0.00985089,-1,850,Totemeier,665.3727018
0.009845151,-1,850,Totemeier,894.8549879
0.002958412,-1,850,Totemeier,29646.86268
0.023283702,-1,950,Yukawa,183.7683065
0.020655176,-1,950,Yukawa,192.9355521
0.0201126,-1,950,Yukawa,163.504856
0.0201126,-1,950,Yukawa,293.2448839
0.015514988,-1,950,Yukawa,349.4140633
0.010133987,-1,950,Yukawa,258.381941
0.010133987,-1,950,Yukawa,441.3877705
0.010133987,-1,950,Yukawa,608.6325567
0.007169541,-1,950,Yukawa,645.2457587
0.006152014,-1,950,Yukawa,684.0614828
0.006111209,-1,950,Yukawa,491.2832121
0.006234443,-1,950,Yukawa,1000
0.006276071,-1,950,Yukawa,1976.89265
0.005174553,-1,950,Yukawa,1595.731547
0.006360162,-1,950,Yukawa,3580.23229
0.004323527,-1,950,Yukawa,2157.933489
0.004209955,-1,950,Yukawa,2521.687885
0.00429485,-1,950,Yukawa,3908.104549
0.004238065,-1,950,Yukawa,7954.891298
0.003038498,-1,950,Yukawa,4392.442966
0.002024689,-1,950,Yukawa,7954.891298
0.002423221,-1,950,Yukawa,12090.72745
0.003058786,-1,950,Yukawa,8270.800945
0.001894334,-1,950,Yukawa,92506.73683
0.001647263,-1,950,Yukawa,714698.0902
0.020382082,-1,950,INL,392.7175758
0.019979197,-1,950,INL,356.2845912
0.019979197,-1,950,INL,296.113886
0.010066771,-1,950,INL,651.5586104
0.01,-1,950,INL,677.4337158
0.010066771,-1,950,INL,864.1219815
0.010066771,-1,950,INL,907.2285356
0.00603041,-1,950,INL,1433.666717
0.006070675,-1,950,INL,1741.866229
0.003938907,-1,950,INL,3004.701048
0.003058786,-1,950,INL,5825.426077
0.002978437,-1,950,INL,6941.249131
0.003038498,-1,950,INL,9028.228389
0.002521936,-1,950,INL,16835.09784
0.002038208,-1,950,INL,89843.85372
0.010201652,-1,950,Totemeier,510.7933601
0.01,-1,950,Totemeier,690.7540934
0.003018344,-1,950,Totemeier,13197.97798
32 changes: 32 additions & 0 deletions examples/fatigue-demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pandas as pd
from pycreep import fatigue
import numpy as np

import matplotlib.pyplot as plt

if __name__ == "__main__":
data = pd.read_csv("A617-fatigue.csv")

fatigue_analysis = fatigue.LumpedTemperatureFatigueAnalysis(
fatigue.DiercksEquation(4), [850, 950], data
).analyze()

for T, inds in fatigue_analysis.temperature_groups.items():
(l,) = plt.loglog(
fatigue_analysis.cycles[inds],
fatigue_analysis.strain_range[inds],
"o",
label=f"T={T}C",
)
erange = np.logspace(
np.log10(fatigue_analysis.strain_range[inds].min()),
np.log10(fatigue_analysis.strain_range[inds].max()),
100,
)
pred = fatigue_analysis.predict(np.full_like(erange, T), erange)
plt.loglog(pred, erange, ls="--", color=l.get_color(), label="Prediction")

plt.xlabel("Cycles")
plt.ylabel("Strain Range")
plt.legend(loc="best")
plt.show()
167 changes: 167 additions & 0 deletions pycreep/fatigue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# pylint: disable=too-few-public-methods
"""Correlate fatigue data into fatigue curves"""

import numpy as np

from pycreep import methods, dataset


class FatigueAnalysis(dataset.DataSet):
"""
Superclass for analysis of fatigue data

Args:
data: dataset as a pandas dataframe

Keyword Args:
cycles_field (str): field in array giving cycles to use, default is
"Cycles"
temp_field (str): field in array giving temperature, default
is "Temp (C)"
strain_range_field (str): field in array giving strain range, default is
"Strain range"
r_ratio_field (str): field in array giving the R ratio, default is "R"
heat_field (str): field in array giving heat ID, default is
"Heat/Lot ID"
input_temp_units (str): temperature units, default is "C"
analysis_temp_units (str): temperature units for analysis,
default is "C"
"""

def __init__(
self,
data,
cycles_field="Cycles",
temp_field="Temp (C)",
strain_range_field="Strain range",
r_ratio_field="R",
heat_field="Heat/Lot ID",
input_temp_units="degC",
analysis_temp_units="degC",
):
super().__init__(data)

self.add_field_units("cycles", cycles_field, "", "")
self.add_field_units(
"temperature", temp_field, input_temp_units, analysis_temp_units
)
self.add_field_units("strain_range", strain_range_field, "", "")
self.add_field_units("r", r_ratio_field, "", "")
self.add_heat_field(heat_field)

self.analysis_temp_units = analysis_temp_units


class LumpedTemperatureFatigueAnalysis(FatigueAnalysis):
"""
Fatigue analysis binning data by temperature

Args:
method: method to use to correlate strain range to cycles
temperature_bins: list of temperature bins to use for analysis
data: dataset as a pandas dataframe

Keyword Args:
temperature_range (float): range of temperatures on either side of the bins
to collect, default 50
cycles_field (str): field in array giving cycles to use, default is
"Cycles"
temp_field (str): field in array giving temperature, default
is "Temp (C)"
strain_range_field (str): field in array giving strain range, default is
"Strain range"
r_ratio_field (str): field in array giving the R ratio, default is "R"
heat_field (str): filed in array giving heat ID, default is
"Heat/Lot ID"
input_temp_units (str): temperature units, default is "C"
analysis_temp_units (str): temperature units for analysis,
default is "C"
"""

def __init__(
self, method, temperature_bins, *args, temperature_range=50.0, **kwargs
):
super().__init__(*args, **kwargs)

self.method = method
self.temperature_bins = sorted(temperature_bins)
self.temperature_range = temperature_range
self.fields["temperature_groups"] = lambda self: {
Comment thread
reverendbedford marked this conversation as resolved.
T: np.where(
np.logical_and(
self.temperature < T + self.temperature_range,
self.temperature > T - self.temperature_range,
)
)[0]
for T in self.temperature_bins
}

for T, inds in self.temperature_groups.items():
if len(inds) == 0:
raise ValueError(f"No data found for temperature {T} C!")

def analyze(self):
"""
Analyze by fitting the methods to the data
"""
self.submodels = {
T: self.method(self.strain_range[inds], self.cycles[inds])
for T, inds in self.temperature_groups.items()
Comment thread
reverendbedford marked this conversation as resolved.
}
return self

def predict(self, temperature, erange):
"""
Predict the number of cycles to failure given the temperature and strain range

Args:
temperature (array like): temperature values to predict for
erange (array like): strain range values to predict for
"""
preds = np.zeros_like(erange)

for i, (T, de) in enumerate(zip(temperature, erange)):
mi = methods.find_nearest_index(self.temperature_bins, T)
preds[i] = self.submodels[self.temperature_bins[mi]].predict(de)

return preds


class DiercksEquation:
"""
Diercks method:

1/sqrt(log10(Nf)) = p(log10(strain_range))

Args:
order (int): polynomial order to use for the regression
"""

def __init__(self, order):
self.order = order

def __call__(self, strain_range, cycles):
lr = np.log10(strain_range)
lc = 1.0 / np.sqrt(np.log10(cycles))
return DiercksFit(np.polyfit(lr, lc, self.order))


class DiercksFit:
"""
Actual method to predict fatigue with a Diercks equation.

Args:
p (np.array): polynomial coefficients for the Diercks equation
"""

def __init__(self, p):
self.p = p

def predict(self, strain_range):
"""
Predict the number of cycles to failure given the strain range
Args:
strain_range (array like): strain range values to predict for
"""
A = np.polyval(self.p, np.log10(strain_range))
return 10 ** ((1 / A) ** 2)
36 changes: 36 additions & 0 deletions pycreep/methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
Mathematical helper functions used in multiple modules
"""

import bisect

import numpy as np
import numpy.linalg as la
import scipy.optimize as opt
Expand Down Expand Up @@ -120,3 +122,37 @@ def asme_tensile_analysis(T, R, order, Tref=21.0):
p = np.concatenate((p, [1.0])) # Add the constant term

return p, R2


def find_nearest_index(sorted_list, target):
"""
Finds the index of the element closest to the target in a sorted list.

Args:
sorted_list (list): A list of numbers sorted in ascending order.
target (int or float): The value to find the nearest element to.

Returns:
int: The index of the nearest element in the sorted list.
"""
# Find the insertion point for the target
# bisect_left returns an index where the target could be inserted
# to maintain sorted order, and all elements to its left are < target.
# bisect_right returns an index where the target could be inserted,
# and all elements to its left are <= target.
# For finding the *nearest* value, bisect_left is often preferred as a starting point.
idx = bisect.bisect_left(sorted_list, target)

if idx == 0: # Target is smaller than or equal to the first element
return 0
if idx == len(sorted_list): # Target is larger than the last element
return len(sorted_list) - 1

# Compare the element at 'idx' and the element before it ('idx-1')
# to find which is closer to the target.
before = sorted_list[idx - 1]
after = sorted_list[idx]

if abs(target - before) <= abs(target - after):
return idx - 1
return idx