-
Notifications
You must be signed in to change notification settings - Fork 6
Material Laws - S-N curves #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,3 +3,252 @@ | |
| Provides implementations of Wöhler (S-N) curve models along with methods for converting | ||
| between stress amplitude and fatigue life in both directions. | ||
| """ | ||
|
|
||
| import warnings | ||
| from abc import ABC, abstractmethod | ||
|
|
||
| import numpy as np | ||
| from numpy.typing import ArrayLike | ||
|
|
||
|
|
||
| class SN_Curve(ABC): | ||
| """Abstract base class for stress-life (S-N) curve models.""" | ||
|
|
||
| @abstractmethod | ||
| def stress_amp(self, life: ArrayLike) -> ArrayLike: | ||
| """Calculate stress amplitude from fatigue life. | ||
|
|
||
| Parameters: | ||
| life : ArrayLike | ||
| The fatigue life (N) in cycles. | ||
|
|
||
| Returns: | ||
| ArrayLike | ||
| The calculated stress amplitude (σ_a) in MPa. | ||
| """ | ||
| pass | ||
|
|
||
| @abstractmethod | ||
| def life(self, stress_amp: ArrayLike) -> ArrayLike: | ||
| """Calculate fatigue life from stress amplitude. | ||
|
|
||
| Parameters: | ||
| stress_amp : ArrayLike | ||
| The stress amplitude (σ_a) in MPa. | ||
|
|
||
| Returns: | ||
| ArrayLike | ||
| The calculated fatigue life (N) in cycles. | ||
| """ | ||
| pass | ||
|
|
||
|
|
||
| class WholerPowerLaw(SN_Curve): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Classmethod for construction via fatigue limit, log curve slope should implemented |
||
| """Wöhler (S-N) curve model using a power law relationship.""" | ||
|
|
||
| def __init__(self, SN_C: float, SN_w: float): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SN_C, SN_w are compared to rest of the library shortcuts, which is not desirable
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should be thought out, if we might want to scale this to multiple dimensions user might want to assess multiple welds for example, utilizing different coefficients
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this would mean that type of both coefs would be |
||
| """Initialize the Wöhler power law model. | ||
|
|
||
| Parameters: | ||
| SN_C : float | ||
| Material constant representing the power law coefficient (MPa^SN_w). | ||
| SN_w : float | ||
| Material constant representing the power law exponent. | ||
|
|
||
| Raises: | ||
| ValueError | ||
| If any parameter is not positive. | ||
| """ | ||
| if SN_C <= 0: | ||
| raise ValueError(f"SN_C must be positive, got {SN_C}") | ||
| if SN_w <= 0: | ||
| raise ValueError(f"SN_w must be positive, got {SN_w}") | ||
|
|
||
| self.SN_C = SN_C | ||
| self.SN_w = SN_w | ||
|
|
||
| def stress_amp(self, life: ArrayLike) -> ArrayLike: | ||
| """Calculate stress amplitude from fatigue life using the Wöhler power law. | ||
|
|
||
| Parameters: | ||
| life : ArrayLike | ||
| The fatigue life (N) in cycles. Must be positive. | ||
|
|
||
| Returns: | ||
| ArrayLike | ||
| The calculated stress amplitude (σ_a) in MPa. | ||
|
|
||
| Raises: | ||
| ValueError | ||
| If life contains non-positive values. | ||
| """ | ||
| life_array = np.asarray(life) | ||
| if np.any(life_array <= 0): | ||
| raise ValueError("life must contain only positive values") | ||
|
|
||
| return np.power((self.SN_C) / life_array, 1 / self.SN_w) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unless you need to use additional arguments of |
||
|
|
||
| def life(self, stress_amp: ArrayLike) -> ArrayLike: | ||
| """Calculate fatigue life from stress amplitude using the Wöhler power law. | ||
|
|
||
| Parameters: | ||
| stress_amp : ArrayLike | ||
| The stress amplitude (σ_a) in MPa. Must be positive. | ||
|
|
||
| Returns: | ||
| ArrayLike | ||
| The calculated fatigue life (N) in cycles. | ||
|
|
||
| Raises: | ||
| ValueError | ||
| If stress_amp contains non-positive values. | ||
| """ | ||
| stress_amp_array = np.asarray(stress_amp) | ||
| if np.any(stress_amp_array <= 0): | ||
| raise ValueError("stress_amp must contain only positive values") | ||
|
|
||
| return self.SN_C / np.power(stress_amp, self.SN_w) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use |
||
|
|
||
|
|
||
| class WohlerKohoutVechet(SN_Curve): | ||
| """Wöhler S-N curve model using the Kohout-Věchet method. | ||
|
|
||
| This model uses a more sophisticated relationship that accounts for | ||
| the asymptotic behavior of S-N curves at high cycle counts. | ||
| """ | ||
|
|
||
| def __init__(self, A: float, B: float, C: float, beta: float): | ||
Vybornak2 marked this conversation as resolved.
Show resolved
Hide resolved
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. scalability to multiple dims should be considered |
||
| """Initialize the Kohout-Věchet S-N curve model. | ||
|
|
||
| Parameters: | ||
| A : float | ||
| Material constant representing the stress amplitude scaling factor. | ||
| B : float | ||
| Material constant representing the life offset parameter. | ||
| C : float | ||
| Material constant representing the asymptotic life parameter. | ||
| beta : float | ||
| Material constant representing the power law exponent. | ||
|
|
||
| Raises: | ||
| ValueError | ||
| If A, B, C parameters are not positive or if beta is not negative. | ||
| """ | ||
| if A <= 0: | ||
| raise ValueError(f"A must be positive, got {A}") | ||
| if B <= 0: | ||
| raise ValueError(f"B must be positive, got {B}") | ||
| if C <= 0: | ||
| raise ValueError(f"C must be positive, got {C}") | ||
| if beta >= 0: | ||
| raise ValueError(f"beta must be negative, got {beta}") | ||
|
|
||
| self.A = A | ||
| self.B = B | ||
| self.C = C | ||
| self.beta = beta | ||
|
|
||
| def stress_amp(self, life: ArrayLike) -> ArrayLike: | ||
| """Calculate stress amplitude from fatigue life using Kohout-Věchet method. | ||
|
|
||
| Uses the forward relationship: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. math block should be used as part of documentation? |
||
| σ_a = A * (C * (N + B) / (N + C))^β | ||
|
|
||
| Parameters: | ||
| life : ArrayLike | ||
| The fatigue life (N) in cycles. Must be positive. | ||
|
|
||
| Returns: | ||
| ArrayLike | ||
| The calculated stress amplitude (σ_a) in MPa. | ||
|
|
||
| Raises: | ||
| ValueError | ||
| If life contains non-positive values. | ||
| """ | ||
| life_array = np.asarray(life) | ||
| if np.any(life_array <= 0): | ||
| raise ValueError("life must contain only positive values") | ||
|
|
||
| # Calculate stress amplitude | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this comment necessary? :D |
||
| stress_amp = self.A * np.power( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use |
||
| (self.C * (life_array + self.B)) / (life_array + self.C), self.beta | ||
| ) | ||
|
|
||
| return stress_amp | ||
|
|
||
| def life(self, stress_amp: ArrayLike, max_iterations: int = 100) -> ArrayLike: | ||
| """Calculate fatigue life from stress amplitude using Newton solver. | ||
|
|
||
| Uses a vectorized Newton solver to find the inverse of: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Math formula block should be used |
||
| σ_a = A * (C * (N + B) / (N + C))^β | ||
|
|
||
| Parameters: | ||
| stress_amp : ArrayLike | ||
| The stress amplitude (σ_a) in MPa. Must be positive. | ||
| max_iterations : int, optional | ||
| Maximum number of Newton iterations. Default is 100. | ||
|
|
||
| Returns: | ||
| ArrayLike | ||
| The calculated fatigue life (N) in cycles. | ||
|
|
||
| Raises: | ||
| ValueError | ||
| If stress_amp contains non-positive values. | ||
| """ | ||
| stress_amp_array = np.asarray(stress_amp) | ||
| if np.any(stress_amp_array <= 0): | ||
| raise ValueError("stress_amp must contain only positive values") | ||
|
|
||
| # Initialize solution array with starting guess | ||
| N = np.full_like(stress_amp_array, 1e5, dtype=np.float64) | ||
|
|
||
| # Newton solver parameters | ||
| tolerance = 1e-6 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tolerance should be potentially exposed as keyword param |
||
|
|
||
| # Pre-calculate constants for efficiency | ||
| derivative_constant = self.A * self.beta * np.power(self.C, self.beta) | ||
|
|
||
| converged = False | ||
| for _ in range(max_iterations): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For better understanding I would recommend recording iterations, and add logging that would be enabled via kw param this would allow user to debug the process
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this could be potentially be implemented in second iteration - issue could be created
|
||
| # Calculate function value f(N) = A * (C*(N+B)/(N+C))^β - σ_a | ||
| f_N = ( | ||
| self.A * np.power((self.C * (N + self.B)) / (N + self.C), self.beta) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use |
||
| - stress_amp_array | ||
| ) | ||
|
|
||
| # Calculate derivative f'(N) = A*β*C^β*(N+B)^(β-1)*(C-B)/(N+C)^(β+1) | ||
| f_prime_N = derivative_constant * ( | ||
| (np.power(N + self.B, self.beta - 1) * (self.C - self.B)) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. use ** instead of np.power |
||
| / np.power(N + self.C, self.beta + 1) | ||
| ) | ||
|
|
||
| # Avoid division by zero | ||
| f_prime_N = np.where(np.abs(f_prime_N) < 1e-15, 1e-15, f_prime_N) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could these hardcoded values lead to bug? |
||
|
|
||
| # Newton update | ||
| N_new = N - f_N / f_prime_N | ||
|
|
||
| # Clamp negative values to small positive number | ||
| N_new = np.maximum(N_new, 1.0) | ||
|
|
||
| # Check convergence | ||
| relative_change = np.abs((N_new - N) / np.maximum(N, 1e-15)) | ||
| if np.all(relative_change < tolerance): | ||
| converged = True | ||
| break | ||
|
|
||
| N = N_new | ||
|
|
||
| # Issue warning if Newton solver did not converge | ||
| if not converged: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| warnings.warn( | ||
| f"Newton solver did not converge after {max_iterations} " | ||
| f"iterations. Results may be inaccurate. Consider adjusting " | ||
| f"tolerance or max_iterations.", | ||
| RuntimeWarning, | ||
| stacklevel=2, | ||
| ) | ||
|
|
||
| return N | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
n_cycles could be more understandable to broader audience?
with variable names adjustment wait until confirmation please