From be2c83d26b1c2fbf9479eaef89a7c2015e741529 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Wed, 11 Dec 2024 15:36:29 +0100 Subject: [PATCH 01/17] Introduce draft of NotchApproxBinner Signed-off-by: Johannes Mueller --- .../materiallaws/notch_approximation_law.py | 71 ++++++ .../test_notch_approximation_law.py | 209 +++++++++++++++++- 2 files changed, 274 insertions(+), 6 deletions(-) diff --git a/src/pylife/materiallaws/notch_approximation_law.py b/src/pylife/materiallaws/notch_approximation_law.py index 2978e293..8069f70f 100644 --- a/src/pylife/materiallaws/notch_approximation_law.py +++ b/src/pylife/materiallaws/notch_approximation_law.py @@ -198,6 +198,19 @@ def load(self, stress, *, rtol=1e-4, tol=1e-4): ) return load + def primary(self, load): + load = np.asarray(load) + stress = self.stress(load) + strain = self.strain(stress, None) + return np.stack([stress, strain], axis=len(load.shape)) + + def secondary(self, delta_load): + delta_load = np.asarray(delta_load) + delta_stress = self.stress_secondary_branch(delta_load) + delta_strain = self.strain_secondary_branch(delta_stress, None) + return np.stack([delta_stress, delta_strain], axis=len(delta_load.shape)) + + def stress_secondary_branch(self, delta_load, *, rtol=1e-4, tol=1e-4): """Calculate the stress on secondary branches in the stress-strain diagram at a given elastic-plastic stress (load), from a FE computation. @@ -906,3 +919,61 @@ def _create_bins_multiple_assessment_points(self): self._lut_secondary_branch.delta_strain \ = self._notch_approximation_law.strain_secondary_branch( self._lut_secondary_branch.delta_stress, self._lut_secondary_branch.delta_load) + + + +class NotchApproxBinner: + + def __init__(self, notch_approximation_law, number_of_bins=100): + self._n_bins = number_of_bins + self._notch_approximation_law = notch_approximation_law + self.ramberg_osgood_relation = notch_approximation_law.ramberg_osgood_relation + self._max_load_rep = None + + def initialize(self, max_load): + max_load = np.asarray(max_load) + self._max_load_rep, _ = self._rep_abs_and_sign(max_load) + + load = self._param_for_lut(self._n_bins, max_load) + self._lut_primary = self._notch_approximation_law.primary(load) + + delta_load = self._param_for_lut(2 * self._n_bins, 2.0*max_load) + self._lut_secondary = self._notch_approximation_law.secondary(delta_load) + + return self + + def primary(self, load): + self._raise_if_uninitialized() + load_rep, sign = self._rep_abs_and_sign(load) + + if load_rep > self._max_load_rep: + msg = f"Requested load `{load_rep}`, higher than initialized maximum load `{self._max_load_rep}`" + raise ValueError(msg) + + idx = int(np.ceil(load_rep / self._max_load_rep * self._n_bins)) - 1 + return sign * self._lut_primary[idx, :] + + def secondary(self, delta_load): + self._raise_if_uninitialized() + delta_load_rep, sign = self._rep_abs_and_sign(delta_load) + + if delta_load_rep > 2.0 * self._max_load_rep: + msg = f"Requested load `{delta_load_rep}`, higher than initialized maximum delta load `{2.0*self._max_load_rep}`" + raise ValueError(msg) + + idx = int(np.ceil(delta_load_rep / (2.0*self._max_load_rep) * 2*self._n_bins)) - 1 + return sign * self._lut_secondary[idx, :] + + def _raise_if_uninitialized(self): + if self._max_load_rep is None: + raise RuntimeError("NotchApproxBinner not initialized.") + + def _param_for_lut(self, number_of_bins, max_val): + scale = np.linspace(0.0, 1.0, number_of_bins + 1)[1:] + max_val, scale_m = np.meshgrid(max_val, scale) + return (max_val * scale_m) + + def _rep_abs_and_sign(self, value): + value = np.asarray(value) + value_rep = value if len(value.shape) == 0 else value[0] + return np.abs(value_rep), np.sign(value_rep) diff --git a/tests/materiallaws/test_notch_approximation_law.py b/tests/materiallaws/test_notch_approximation_law.py index 982b0ac2..2b1ede8c 100644 --- a/tests/materiallaws/test_notch_approximation_law.py +++ b/tests/materiallaws/test_notch_approximation_law.py @@ -22,7 +22,7 @@ import pandas as pd import pylife.strength.damage_parameter -from pylife.materiallaws.notch_approximation_law import ExtendedNeuber +from pylife.materiallaws.notch_approximation_law import ExtendedNeuber, NotchApproxBinner from .data import * def test_extended_neuber_example_1(): @@ -32,7 +32,7 @@ def test_extended_neuber_example_1(): K = 1184 # [MPa] n = 0.187 # [-] K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - + L = pd.Series([100, -200, 100, -250, 200, 0, 200, -200]) c = 1.4 gamma_L = (250+6.6)/250 @@ -171,10 +171,11 @@ def test_derivatives(stress, load): assert np.isclose(numeric_derivative, derivative) + @pytest.mark.parametrize('E, K, n, L', [ - (260e3, 1184, 0.187, pd.Series([100, -200, 100, -250, 200, 100, 200, -200])), - (100e3, 1500, 0.4, pd.Series([-100, 100, -200])), - (200e3, 1000, 0.2, pd.Series([100, 10])), + (260e3, 1184, 0.187, np.array([100, -200, 100, -250, 200, 100, 200, -200])), + (100e3, 1500, 0.4, np.array([-100, 100, -200])), + (200e3, 1000, 0.2, np.array([100, 10])), ]) def test_load(E, K, n, L): c = 1.4 @@ -184,7 +185,7 @@ def test_load(E, K, n, L): # initialize notch approximation law and damage parameter notch_approximation_law = ExtendedNeuber(E, K, n, K_p=3.5) - # The "load" method is the inverse operation of "stress", + # The "load" method is the inverse operation of "stress", # i.e., ``L = load(stress(L))`` and ``S = stress(load(stress))``. stress = notch_approximation_law.stress(L) load = notch_approximation_law.load(stress) @@ -193,3 +194,199 @@ def test_load(E, K, n, L): np.testing.assert_allclose(L, load, rtol=1e-3) np.testing.assert_allclose(stress, stress2, rtol=1e-3) + +@pytest.mark.parametrize("L, expected", [ + (150.0, [148.5, 7.36e-4]), + (175.0, [171.7, 8.67e-4]), + (200.0, [193.7, 1.00e-3]) +]) +def test_load_primary_scalar(L, expected): + notch_approximation_law = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + result = notch_approximation_law.primary(L) + + assert result.shape == (2, ) + np.testing.assert_allclose(result, expected, rtol=1e-2) + + +def test_load_primary_vectorized(): + notch_approximation_law = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + + result = notch_approximation_law.primary([150.0, 175.0, 200.0]) + expected = [[148.4, 7.36e-4], [171.7, 8.67e-4], [193.7, 1.00e-3]] + + assert result.shape == (3, 2) + np.testing.assert_allclose(result, expected, rtol=1e-2) + + +@pytest.mark.parametrize("L, expected", [ + (100.0, [100.0, 4.88e-4]), + (400.0, [386.0, 1.99e-3]), + (600.0, [533.0, 3.28e-3]) +]) +def test_load_secondary_scalar(L, expected): + notch_approximation_law = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + result = notch_approximation_law.secondary(L) + + assert result.shape == (2, ) + np.testing.assert_allclose(result, expected, rtol=1e-2) + + +def test_load_secondary_vectorized(): + notch_approximation_law = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + + result = notch_approximation_law.secondary([100.0, 400.0, 600.0]) + expected = [[100.0, 4.88e-4], [386.0, 1.99e-3], [533.0, 3.28e-3]] + + assert result.shape == (3, 2) + np.testing.assert_allclose(result, expected, rtol=1e-2) + + +def test_binner_uninitialized(): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned) + + with pytest.raises(RuntimeError, match="NotchApproxBinner not initialized."): + binned.primary(100.0) + + with pytest.raises(RuntimeError, match="NotchApproxBinner not initialized."): + binned.secondary(100.0) + + +@pytest.mark.parametrize("L, expected", [ + (120.0, [148.0, 7.36e-4]), + (160.0, [193.7, 1.00e-3]), + (200.0, [193.7, 1.00e-3]), +]) +def test_binner_initialized_five_points_primary_scalar(L, expected): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned, number_of_bins=4).initialize(max_load=200.0) + + result = binned.primary(L) + + np.testing.assert_allclose(result, np.array([expected]), rtol=1e-2) + + +@pytest.mark.parametrize("L, expected", [ + (-120.0, [-148.0, -7.36e-4]), + (-160.0, [-193.7, -1.00e-3]), + (-200.0, [-193.7, -1.00e-3]), +]) +def test_binner_initialized_five_points_primary_scalar_symmetry(L, expected): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned, number_of_bins=4).initialize(max_load=200.0) + + result = binned.primary(L) + + np.testing.assert_allclose(result, np.array([expected]), rtol=1e-2) + + +@pytest.mark.parametrize("L, expected", [ + (120.0, [124.0, 6.10e-4]), + (160.0, [171.7, 8.67e-4]), + (200.0, [193.7, 1.00e-3]) +]) +def test_binner_initialized_nine_points_primary_scalar(L, expected): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned, number_of_bins=8).initialize(max_load=200.0) + + result = binned.primary(L) + + np.testing.assert_allclose(result, np.array([expected]), rtol=1e-2) + + +def test_binner_initialized_nine_points_primary_out_of_scale(): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned, number_of_bins=8).initialize(max_load=200.0) + + with pytest.raises( + ValueError, + match="Requested load `400.0`, higher than initialized maximum load `200.0`", + ): + binned.primary(400.0) + + +@pytest.mark.parametrize("L, expected", [ + (120.0, [[148.0, 7.36e-4], [182.9, 9.34e-4], [214.3, 1.15e-3]]), + (160.0, [[193.7, 1.00e-3], [233.3, 1.30e-3], [266.7, 1.64e-3]]), + (200.0, [[193.7, 1.00e-3], [233.3, 1.30e-3], [266.7, 1.64e-3]]) +]) +def test_binner_initialized_five_points_primary_vectorized(L, expected): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned, number_of_bins=4).initialize( + max_load=[200.0, 250.0, 300.0] + ) + + result = binned.primary([L, 1.25*L, 1.5*L]) + + assert result.shape == (3, 2) + + np.testing.assert_allclose(result, expected, rtol=1e-2) + + +@pytest.mark.parametrize("L, expected", [ + (20.0, [297.0, 1.48e-3]), + (340.0, [533.0, 3.28e-3]), + (600.0, [533.0, 3.28e-3]) +]) +def test_binner_initialized_one_bin_secondary_scalar(L, expected): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned, number_of_bins=1).initialize(max_load=300.0) + + result = binned.secondary(L) + + np.testing.assert_allclose(result, np.array([expected]), rtol=1e-2) + + +@pytest.mark.parametrize("L, expected", [ + (-20.0, [-297.0, -1.48e-3]), + (-340.0, [-533.0, -3.28e-3]), + (-600.0, [-533.0, -3.28e-3]) +]) +def test_binner_initialized_one_bin_secondary_scalar_symmetry(L, expected): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned, number_of_bins=1).initialize(max_load=300.0) + + result = binned.secondary(L) + + np.testing.assert_allclose(result, np.array([expected]), rtol=1e-2) + + +@pytest.mark.parametrize("L, expected", [ + (20.0, [100.0, 4.88e-4]), + (400.0, [386.0, 1.99e-3]), + (600.0, [533.0, 3.28e-3]) +]) +def test_binner_initialized_three_points_secondary_scalar(L, expected): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned, number_of_bins=3).initialize(max_load=300.0) + + result = binned.secondary(L) + + np.testing.assert_allclose(result, np.array([expected]), rtol=1e-2) + + +def test_binner_initialized_three_points_secondary_out_of_scale(): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned, number_of_bins=3).initialize(max_load=300.0) + + with pytest.raises( + ValueError, + match="Requested load `700.0`, higher than initialized maximum delta load `600.0`", + ): + binned.secondary(700.0) + + +@pytest.mark.parametrize("L, expected", [ + (20.0, [[100.0, 4.88e-4], [133.3, 6.47e-4], [200.0, 9.74e-4]]), + (400.0, [[386.0, 1.99e-3], [490.0, 2.81e-3], [638.2, 4.90e-3]]), + (600.0, [[533.0, 3.28e-3], [638.2, 4.90e-03], [784.8, 9.26e-3]]) +]) +def test_binner_initialized_three_points_secondary_vectorized(L, expected): + unbinned = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) + binned = NotchApproxBinner(unbinned, number_of_bins=3).initialize(max_load=[300.0, 400.0, 600.0]) + + result = binned.secondary(([L, 1.25*L, 1.5*L])) + + assert result.shape == (3, 2) + + np.testing.assert_allclose(result, expected, rtol=1e-2) From 824bc115cb9b8aadc8b5a7dae287428c48ddc231 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Wed, 11 Dec 2024 16:40:59 +0100 Subject: [PATCH 02/17] Adjust Rainflow HCM to NotchApproxBinner Signed-off-by: Johannes Mueller --- src/pylife/stress/rainflow/fkm_nonlinear.py | 16 +-- tests/stress/rainflow/test_fkm_nonlinear.py | 137 ++++++++++---------- 2 files changed, 75 insertions(+), 78 deletions(-) diff --git a/src/pylife/stress/rainflow/fkm_nonlinear.py b/src/pylife/stress/rainflow/fkm_nonlinear.py index 7c45fde6..22deabbf 100644 --- a/src/pylife/stress/rainflow/fkm_nonlinear.py +++ b/src/pylife/stress/rainflow/fkm_nonlinear.py @@ -318,21 +318,15 @@ def _update_residuals(self, record_vals, turning_point, load_turning_points_rep) def _collect_record(self, load_turning_points, num_turning_points, record): def primary(_prev, load): - sigma = self._notch_approximation_law.stress(load) - epsilon = self._notch_approximation_law.strain(sigma, load) - return np.array([load, sigma, epsilon]) + return self._notch_approximation_law.primary(load) def secondary(prev, load): prev_load = prev[LOAD] delta_L = load - prev_load - delta_sigma = self._notch_approximation_law.stress_secondary_branch(delta_L) - delta_epsilon = self._notch_approximation_law.strain_secondary_branch(delta_sigma, delta_L) + delta = self._notch_approximation_law.secondary(delta_L) - sigma = prev[STRESS] + delta_sigma - epsilon = prev[STRAIN] + delta_epsilon - - return np.array([load, sigma, epsilon]) + return prev[STRESS:STRAIN+1].T + delta def determine_prev_record(prev_idx): if prev_idx < 0: @@ -372,7 +366,9 @@ def determine_prev_record(prev_idx): return record_vals.set_index("turning_point", drop=True, append=True) def _process_deformation(self, deformation_func, result_buf, load, prev_record): - result_buf[:3] = deformation_func(prev_record, load) + result_buf[0] = load + res = deformation_func(prev_record, load).T + result_buf[1:3] = res old_load = self._last_record[LOAD, 0] diff --git a/tests/stress/rainflow/test_fkm_nonlinear.py b/tests/stress/rainflow/test_fkm_nonlinear.py index 325f023a..ab32aac9 100644 --- a/tests/stress/rainflow/test_fkm_nonlinear.py +++ b/tests/stress/rainflow/test_fkm_nonlinear.py @@ -25,8 +25,8 @@ from pylife.stress.rainflow.fkm_nonlinear import FKMNonlinearDetector import pylife.stress.rainflow.recorders as RFR -import pylife.materiallaws.notch_approximation_law -import pylife.materiallaws.notch_approximation_law_seegerbeste +import pylife.materiallaws.notch_approximation_law as NAL +from pylife.materiallaws.notch_approximation_law_seegerbeste import SeegerBeste @pytest.fixture(autouse=True) @@ -54,12 +54,13 @@ def setUp(self): K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) # initialize notch approximation law - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100) + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load + ) # first run detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber_binned) @@ -117,12 +118,13 @@ def setUp(self): K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) # initialize notch approximation law - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100) + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load + ) # first run detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber_binned) @@ -194,12 +196,13 @@ def setUp(self): K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) # initialize notch approximation law - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100) + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load + ) # first run detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber_binned) @@ -269,12 +272,13 @@ def setUp(self): K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) # initialize notch approximation law - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(abs(self.signal)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100) + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load + ) # first run detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber_binned) @@ -338,13 +342,14 @@ def setUp(self): np.testing.assert_allclose(max(abs(signal)), 1013) # initialize notch approximation law - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100) + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load + ) # first run detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber_binned) @@ -416,12 +421,12 @@ def test_edge_case_value_in_sample_tail_simple_signal(vals, expected_loads_min, n = 0.187 # [-] K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100 + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load ) detector = FKMNonlinearDetector( @@ -466,12 +471,12 @@ def test_edge_case_value_in_sample_tail(vals, expected_loads_min, expected_loads n = 0.187 # [-] K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100 + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load ) detector = FKMNonlinearDetector( @@ -512,12 +517,14 @@ def test_flush_edge_case_load(): n = 0.07 # [-] K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - maximum_absolute_load = max(abs(pd.concat([signal_1, signal_2]))) + maximum_absolute_load = pd.concat([signal_1, signal_2]).abs().groupby("node_id").max() - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100 + print(maximum_absolute_load) + + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load ) detector = FKMNonlinearDetector( @@ -570,11 +577,7 @@ def test_flush_edge_case_load(): def test_flush_edge_case_load_simple_signal(): - signal_1 = np.array([0.0, 143.0, -287.0, 143.0, -359.0, 287.0, 0.0, 287.0, -287.0]) - - mi_2 = pd.MultiIndex.from_product([range(9, 17), range(3)], names=["load_step", "node_id"]) - signal_2 = np.array([143.0, -287.0, 143.0, -359.0, 287.0, 0.0, 287.0, -287.0]) E = 206e3 # [MPa] Young's modulus @@ -583,12 +586,12 @@ def test_flush_edge_case_load_simple_signal(): n = 0.07 # [-] K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) maximum_absolute_load = max(abs(np.concatenate([signal_1, signal_2]))) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100 + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load ) detector = FKMNonlinearDetector( @@ -613,9 +616,7 @@ def test_flush_edge_case_load_simple_signal(): def test_flush_edge_case_S_simple_signal(): - signal_1 = np.array([0.0, 143.0, -287.0, 143.0, -359.0, 287.0, 0.0, 287.0, -287.0]) - signal_2 = np.array([143.0, -287.0, 143.0, -359.0, 287.0, 0.0, 287.0, -287.0]) E = 206e3 # [MPa] Young's modulus @@ -624,12 +625,12 @@ def test_flush_edge_case_S_simple_signal(): n = 0.07 # [-] K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) maximum_absolute_load = max(abs(np.concatenate([signal_1, signal_2]))) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100 + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load ) detector = FKMNonlinearDetector( @@ -672,12 +673,15 @@ def test_flush_edge_case_S(): n = 0.07 # [-] K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) + + maximum_absolute_load = pd.concat([signal_1, signal_2]).abs().groupby("node_id").max() - maximum_absolute_load = max(abs(pd.concat([signal_1, signal_2]))) + print(maximum_absolute_load) + print(max(abs(pd.concat([signal_1, signal_2])))) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100 + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load ) detector = FKMNonlinearDetector( @@ -714,11 +718,8 @@ def test_flush_edge_case_S(): S_min = detector.recorder.S_min S_max = detector.recorder.S_max - pd.testing.assert_series_equal(S_min, expected_S_min, check_index=False) - pd.testing.assert_series_equal(S_max, expected_S_max, check_index=False) - - pd.testing.assert_series_equal(S_min, expected_S_min) - pd.testing.assert_series_equal(S_max, expected_S_max) + pd.testing.assert_series_equal(S_min, expected_S_min, rtol=1e-1) + pd.testing.assert_series_equal(S_max, expected_S_max, rtol=1e-1) @pytest.mark.parametrize('vals, num', [ @@ -768,15 +769,15 @@ def test_edge_case_value_in_sample_tail_compare_simple(vals, num): n = 0.187 # [-] K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) maximum_absolute_load_simple = max(abs(vals)) maximum_absolute_load_multiple = signal.abs().groupby('node_id').max() print("single") - extended_neuber_binned_simple = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load_simple, 100 + extended_neuber_binned_simple = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load_simple ) detector_simple = FKMNonlinearDetector( recorder=RFR.FKMNonlinearRecorder(), @@ -785,8 +786,8 @@ def test_edge_case_value_in_sample_tail_compare_simple(vals, num): detector_simple.process(vals).process(vals) print("multiple") - extended_neuber_binned_multiple = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load_multiple, 100 + extended_neuber_binned_multiple = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load_multiple ) detector_multiindex = FKMNonlinearDetector( recorder=RFR.FKMNonlinearRecorder(), @@ -852,16 +853,16 @@ def test_hcm_first_second(vals, num): n = 0.187 # [-] K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) maximum_absolute_load_simple = max(abs(vals)) maximum_absolute_load_multiple = signal.abs().groupby('node_id').max() - extended_neuber_binned_simple = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load_simple, 100 + extended_neuber_binned_simple = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load_simple ) - extended_neuber_binned_multiple = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load_multiple, 100 + extended_neuber_binned_multiple = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load_multiple ) detector_simple = FKMNonlinearDetector( recorder=RFR.FKMNonlinearRecorder(), @@ -897,10 +898,8 @@ def detector_seeger_beste(): n = 0.187 # [-] K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - seeger_beste = pylife.materiallaws.notch_approximation_law_seegerbeste.SeegerBeste(E, K, n, K_p) - seeger_beste_binned = pylife.materiallaws.notch_approximation_law.Binned( - seeger_beste, 800, 100 - ) + seeger_beste = SeegerBeste(E, K, n, K_p) + seeger_beste_binned = NAL.NotchApproxBinner(seeger_beste).initialize(800) return FKMNonlinearDetector( recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=seeger_beste_binned @@ -1097,12 +1096,13 @@ def test_history_guideline_at_once(): K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) # initialize notch approximation law - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100) + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load + ) # first run detector = FKMNonlinearDetector(recorder=recorder, notch_approximation_law=extended_neuber_binned) @@ -1144,12 +1144,13 @@ def test_history_guideline_at_split(split_point): K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) # initialize notch approximation law - extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K, n, K_p) + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100) + extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( + maximum_absolute_load + ) # first run detector = FKMNonlinearDetector(recorder=recorder, notch_approximation_law=extended_neuber_binned) From b116ad2c7219539fcdcf1146014d93eb5676bbb2 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Thu, 12 Dec 2024 11:04:17 +0100 Subject: [PATCH 03/17] Apply NotchApproxBinner to Seeger Beste Signed-off-by: Johannes Mueller --- .../materiallaws/notch_approximation_law.py | 25 +++++----- .../notch_approximation_law_seegerbeste.py | 18 ++++--- .../test_notch_approximation_law.py | 8 +-- ...est_notch_approximation_law_seegerbeste.py | 50 ++++++++++++++++++- 4 files changed, 74 insertions(+), 27 deletions(-) diff --git a/src/pylife/materiallaws/notch_approximation_law.py b/src/pylife/materiallaws/notch_approximation_law.py index 8069f70f..9e015b89 100644 --- a/src/pylife/materiallaws/notch_approximation_law.py +++ b/src/pylife/materiallaws/notch_approximation_law.py @@ -79,6 +79,18 @@ def K(self, value): """Set the strain hardening coefficient""" self.K_prime = value + def primary(self, load): + load = np.asarray(load) + stress = self.stress(load) + strain = self.strain(stress, None) + return np.stack([stress, strain], axis=len(load.shape)) + + def secondary(self, delta_load): + delta_load = np.asarray(delta_load) + delta_stress = self.stress_secondary_branch(delta_load) + delta_strain = self.strain_secondary_branch(delta_stress, None) + return np.stack([delta_stress, delta_strain], axis=len(delta_load.shape)) + class ExtendedNeuber(NotchApproximationLawBase): r"""Implementation of the extended Neuber notch approximation material relation. @@ -198,19 +210,6 @@ def load(self, stress, *, rtol=1e-4, tol=1e-4): ) return load - def primary(self, load): - load = np.asarray(load) - stress = self.stress(load) - strain = self.strain(stress, None) - return np.stack([stress, strain], axis=len(load.shape)) - - def secondary(self, delta_load): - delta_load = np.asarray(delta_load) - delta_stress = self.stress_secondary_branch(delta_load) - delta_strain = self.strain_secondary_branch(delta_stress, None) - return np.stack([delta_stress, delta_strain], axis=len(delta_load.shape)) - - def stress_secondary_branch(self, delta_load, *, rtol=1e-4, tol=1e-4): """Calculate the stress on secondary branches in the stress-strain diagram at a given elastic-plastic stress (load), from a FE computation. diff --git a/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py b/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py index c8ab1370..8c78c18e 100644 --- a/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py +++ b/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py @@ -96,7 +96,7 @@ def stress(self, load, *, rtol=1e-4, tol=1e-4): # or (value, converged, zero_der) for vector-valued invocation # only for multiple points at once, if some points diverged - if len(stress) == 3 and sum(stress[1]) < len(stress[1]): + if len(stress) == 3 and not stress[1].all(): stress = self._stress_fix_not_converged_values(stress, load, x0, rtol, tol) return stress[0] @@ -189,6 +189,7 @@ def stress_secondary_branch(self, delta_load, *, rtol=1e-4, tol=1e-4): ''' # initial value as given by correction document to FKM nonlinear + delta_load = np.asarray(delta_load) x0 = delta_load * (1 - (1 - 1/self._K_p)/1000) # suppress the divergence warnings @@ -197,7 +198,7 @@ def stress_secondary_branch(self, delta_load, *, rtol=1e-4, tol=1e-4): delta_stress = optimize.newton( func=self._stress_secondary_implicit, - x0=np.asarray(x0), + x0=x0, args=([delta_load]), full_output=True, rtol=rtol, tol=tol, maxiter=50 @@ -208,7 +209,9 @@ def stress_secondary_branch(self, delta_load, *, rtol=1e-4, tol=1e-4): # or (value, converged, zero_der) for vector-valued invocation # only for multiple points at once, if some points diverged - if len(delta_stress) == 3 and sum(delta_stress[1]) < len(delta_stress[1]): + + multidim = len(x0.shape) > 1 and x0.shape[1] > 1 + if multidim and x0.shape[1] > 1 and not delta_stress[1].all(): delta_stress = self._stress_secondary_fix_not_converged_values(delta_stress, delta_load, x0, rtol, tol) return delta_stress[0] @@ -482,22 +485,21 @@ def _stress_secondary_fix_not_converged_values(self, delta_stress, delta_load, x '''For the values that did not converge in the previous vectorized call to optimize.newton, call optimize.newton again on the scalar value. This usually finds the correct solution.''' - indices_diverged = [index for index, is_converged in enumerate(delta_stress[1]) if not is_converged] + indices_diverged = np.where(~delta_stress[1].all(axis=1))[0] x0_array = np.asarray(x0) delta_load_array = np.asarray(delta_load) # recompute previously failed points individually for index_diverged in indices_diverged: - x0_diverged = x0_array[index_diverged] - delta_load_diverged = delta_load_array[index_diverged] + x0_diverged = x0_array[index_diverged, 0] + delta_load_diverged = delta_load_array[index_diverged, 0] result = optimize.newton( func=self._stress_secondary_implicit, - x0=x0_diverged, + x0=np.asarray(x0_diverged), args=([delta_load_diverged]), full_output=True, rtol=rtol, tol=tol, maxiter=50 ) - if result[1].converged: delta_stress[0][index_diverged] = result[0] return delta_stress diff --git a/tests/materiallaws/test_notch_approximation_law.py b/tests/materiallaws/test_notch_approximation_law.py index 2b1ede8c..e7eae318 100644 --- a/tests/materiallaws/test_notch_approximation_law.py +++ b/tests/materiallaws/test_notch_approximation_law.py @@ -200,7 +200,7 @@ def test_load(E, K, n, L): (175.0, [171.7, 8.67e-4]), (200.0, [193.7, 1.00e-3]) ]) -def test_load_primary_scalar(L, expected): +def test_primary_scalar(L, expected): notch_approximation_law = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) result = notch_approximation_law.primary(L) @@ -208,7 +208,7 @@ def test_load_primary_scalar(L, expected): np.testing.assert_allclose(result, expected, rtol=1e-2) -def test_load_primary_vectorized(): +def test_primary_vectorized(): notch_approximation_law = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) result = notch_approximation_law.primary([150.0, 175.0, 200.0]) @@ -223,7 +223,7 @@ def test_load_primary_vectorized(): (400.0, [386.0, 1.99e-3]), (600.0, [533.0, 3.28e-3]) ]) -def test_load_secondary_scalar(L, expected): +def test_secondary_scalar(L, expected): notch_approximation_law = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) result = notch_approximation_law.secondary(L) @@ -231,7 +231,7 @@ def test_load_secondary_scalar(L, expected): np.testing.assert_allclose(result, expected, rtol=1e-2) -def test_load_secondary_vectorized(): +def test_secondary_vectorized(): notch_approximation_law = ExtendedNeuber(E=206e3, K=1184., n=0.187, K_p=3.5) result = notch_approximation_law.secondary([100.0, 400.0, 600.0]) diff --git a/tests/materiallaws/test_notch_approximation_law_seegerbeste.py b/tests/materiallaws/test_notch_approximation_law_seegerbeste.py index f94d0fcc..4e76f65a 100644 --- a/tests/materiallaws/test_notch_approximation_law_seegerbeste.py +++ b/tests/materiallaws/test_notch_approximation_law_seegerbeste.py @@ -176,7 +176,7 @@ def test_seeger_beste_load(E, K, n, L): # initialize notch approximation law and damage parameter notch_approximation_law = SeegerBeste(E, K, n, K_p=3.5) - # The "load" method is the inverse operation of "stress", + # The "load" method is the inverse operation of "stress", # i.e., ``L = load(stress(L))`` and ``S = stress(load(stress))``. stress = notch_approximation_law.stress(L) load = notch_approximation_law.load(stress) @@ -200,7 +200,7 @@ def test_seeger_beste_load_secondary_branch(E, K, n, L): # initialize notch approximation law and damage parameter notch_approximation_law = SeegerBeste(E, K, n, K_p=3.5) - # The "load" method is the inverse operation of "stress", + # The "load" method is the inverse operation of "stress", # i.e., ``L = load(stress(L))`` and ``S = stress(load(stress))``. stress = notch_approximation_law.stress_secondary_branch(L) load = notch_approximation_law.load_secondary_branch(stress) @@ -209,3 +209,49 @@ def test_seeger_beste_load_secondary_branch(E, K, n, L): np.testing.assert_allclose(L, load, rtol=1e-3) np.testing.assert_allclose(stress, stress2, rtol=1e-3) + + +@pytest.mark.parametrize("L, expected", [ + (7.18, [7.18, 3.50e-5]), + (179.6, [172.9, 8.73e-4]), + (359.2, [283.7, 1.86e-3]) +]) # Values from FKM NL Guideline p. 135 +def test_primary_scalar(L, expected): + notch_approximation_law = SeegerBeste(E=206e3, K=1184., n=0.187, K_p=3.5) + result = notch_approximation_law.primary(L) + + assert result.shape == (2, ) + np.testing.assert_allclose(result, expected, rtol=1e-2) + + +def test_primary_vectorized(): + notch_approximation_law = SeegerBeste(E=206e3, K=1184., n=0.187, K_p=3.5) + + result = notch_approximation_law.primary([7.18, 179.6, 359.2]) + expected = [[7.18, 3.50e-5], [172.9, 8.73e-4], [283.7, 1.86e-3]] + + assert result.shape == (3, 2) + np.testing.assert_allclose(result, expected, rtol=1e-2) + + +@pytest.mark.parametrize("L, expected", [ + (359.2, [345.9, 1.75e-3]), + (362.8, [348.9, 1.77e-3]), + (718.5, [567.7, 3.72e-3]) +]) +def test_secondary_scalar(L, expected): + notch_approximation_law = SeegerBeste(E=206e3, K=1184., n=0.187, K_p=3.5) + result = notch_approximation_law.secondary(L) + + assert result.shape == (2, ) + np.testing.assert_allclose(result, expected, rtol=1e-2) + + +def test_secondary_vectorized(): + notch_approximation_law = SeegerBeste(E=206e3, K=1184., n=0.187, K_p=3.5) + + result = notch_approximation_law.secondary([359.2, 362.8, 718.5]) + expected = [[345.9, 1.75e-3], [348.9, 1.77e-3], [567.7, 3.72e-3]] + + assert result.shape == (3, 2) + np.testing.assert_allclose(result, expected, rtol=1e-2) From 88693bb20fc86e5b06a47161854b71fe7e0e0f7a Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Thu, 12 Dec 2024 11:04:46 +0100 Subject: [PATCH 04/17] Adjust other modules to NotchApproxBinner Signed-off-by: Johannes Mueller --- .../assessment_nonlinear_standard.py | 12 ++++++---- tests/strength/test_damage_calculator.py | 24 ++++++++++++------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py b/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py index 060a8249..9c27851e 100644 --- a/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py +++ b/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py @@ -398,8 +398,11 @@ def _compute_hcm_RAM(assessment_parameters, scaled_load_sequence, maximum_absolu extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K_prime, n_prime, K_p) # wrap the notch approximation law by a binning class, which precomputes the values - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100) + extended_neuber_binned = ( + pylife.materiallaws.notch_approximation_law.NotchApproxBinner( + extended_neuber + ).initialize(maximum_absolute_load) + ) # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() @@ -491,8 +494,9 @@ def _compute_hcm_RAJ(assessment_parameters, scaled_load_sequence, maximum_absolu seeger_beste = pylife.materiallaws.notch_approximation_law_seegerbeste.SeegerBeste(E, K_prime, n_prime, K_p) # wrap the notch approximation law by a binning class, which precomputes the values - seeger_beste_binned = pylife.materiallaws.notch_approximation_law.Binned( - seeger_beste, maximum_absolute_load, 100) + seeger_beste_binned = pylife.materiallaws.notch_approximation_law.NotchApproxBinner( + seeger_beste + ).initialize(maximum_absolute_load) # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() diff --git a/tests/strength/test_damage_calculator.py b/tests/strength/test_damage_calculator.py index 02c99624..96910856 100644 --- a/tests/strength/test_damage_calculator.py +++ b/tests/strength/test_damage_calculator.py @@ -94,8 +94,11 @@ def test_woehler_curve_P_RAM_collective_has_no_index(): # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(np.abs(load_sequence_list)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100) + extended_neuber_binned = ( + pylife.materiallaws.notch_approximation_law.NotchApproxBinner( + extended_neuber + ).initialize(maximum_absolute_load) + ) # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() @@ -200,8 +203,11 @@ def test_woehler_curve_P_RAM_collective_has_MultiIndex(): # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(np.abs(load_sequence_list)) - extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned( - extended_neuber, maximum_absolute_load, 100) + extended_neuber_binned = ( + pylife.materiallaws.notch_approximation_law.NotchApproxBinner( + extended_neuber + ).initialize(maximum_absolute_load) + ) # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() @@ -295,8 +301,9 @@ def test_woehler_curve_P_RAJ_has_no_index(): # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(np.abs(load_sequence_list)) - seeger_beste_binned = pylife.materiallaws.notch_approximation_law.Binned( - seeger_beste, maximum_absolute_load, 100) + seeger_beste_binned = pylife.materiallaws.notch_approximation_law.NotchApproxBinner( + seeger_beste + ).initialize(maximum_absolute_load) # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() @@ -402,8 +409,9 @@ def test_woehler_curve_P_RAJ_has_MultiIndex(): # wrap the notch approximation law by a binning class, which precomputes the values maximum_absolute_load = max(np.abs(load_sequence_list)) - seeger_beste_binned = pylife.materiallaws.notch_approximation_law.Binned( - seeger_beste, maximum_absolute_load, 100) + seeger_beste_binned = pylife.materiallaws.notch_approximation_law.NotchApproxBinner( + seeger_beste + ).initialize(maximum_absolute_load) # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() From 339dcf1d11306a53a172cec7112f83c189b21c84 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Thu, 12 Dec 2024 13:28:05 +0100 Subject: [PATCH 05/17] Adjust HCM rainflow counting unit tests to NotchApproxBinner Signed-off-by: Johannes Mueller --- .../reference-fkm-nonlinear/reference_first_second-1.json | 2 +- .../reference_process-process-1.json | 2 +- tests/stress/rainflow/test_fkm_nonlinear.py | 7 ++----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/stress/rainflow/reference-fkm-nonlinear/reference_first_second-1.json b/tests/stress/rainflow/reference-fkm-nonlinear/reference_first_second-1.json index c9b4b5d0..af849f5b 100644 --- a/tests/stress/rainflow/reference-fkm-nonlinear/reference_first_second-1.json +++ b/tests/stress/rainflow/reference-fkm-nonlinear/reference_first_second-1.json @@ -468,7 +468,7 @@ "(13, 2)":5.754102661, "(14, 0)":4.9721593167, "(14, 1)":3.8025932597, - "(14, 2)":2.5366373199, + "(14, 2)":2.53663732, "(15, 0)":0.0, "(15, 1)":0.0, "(15, 2)":0.0, diff --git a/tests/stress/rainflow/reference-fkm-nonlinear/reference_process-process-1.json b/tests/stress/rainflow/reference-fkm-nonlinear/reference_process-process-1.json index ef3af193..f8a9be9f 100644 --- a/tests/stress/rainflow/reference-fkm-nonlinear/reference_process-process-1.json +++ b/tests/stress/rainflow/reference-fkm-nonlinear/reference_process-process-1.json @@ -468,7 +468,7 @@ "(13, 2)":5.754102661, "(14, 0)":4.9721593167, "(14, 1)":3.8025932597, - "(14, 2)":2.5366373199, + "(14, 2)":2.53663732, "(15, 0)":0.0, "(15, 1)":0.0, "(15, 2)":0.0, diff --git a/tests/stress/rainflow/test_fkm_nonlinear.py b/tests/stress/rainflow/test_fkm_nonlinear.py index ab32aac9..aba64c5c 100644 --- a/tests/stress/rainflow/test_fkm_nonlinear.py +++ b/tests/stress/rainflow/test_fkm_nonlinear.py @@ -677,9 +677,6 @@ def test_flush_edge_case_S(): maximum_absolute_load = pd.concat([signal_1, signal_2]).abs().groupby("node_id").max() - print(maximum_absolute_load) - print(max(abs(pd.concat([signal_1, signal_2])))) - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( maximum_absolute_load ) @@ -694,9 +691,9 @@ def test_flush_edge_case_S(): expected_S_min = pd.Series( [ -49.096964, -57.761134, -11.552227, -96.749900, -115.522268, -21.660425, -9.610801e-11, - -1.143832e-10, -1.241333e-07, -96.749900, -115.522268, -21.660425, + -1.143832e-10, -1.549e-07, -96.749900, -115.522268, -21.660425, -96.749900, -115.522268, -21.660425, -121.298382, -144.402835, -27.436539, - -9.610801e-11, -1.143832e-10, -1.241333e-07, + -9.610801e-11, -1.143832e-10, -1.549e-07, ], index=pd.MultiIndex.from_product( [[1, 2, 6, 8, 10, 4, 14], range(3)], names=["load_step", "node_id"] From 7efe416716617b63c01dce80c33749cfe524012c Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 13 Dec 2024 08:34:20 +0100 Subject: [PATCH 06/17] Add docstrings to NotchApproxBinner Signed-off-by: Johannes Mueller --- .../materiallaws/notch_approximation_law.py | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/pylife/materiallaws/notch_approximation_law.py b/src/pylife/materiallaws/notch_approximation_law.py index 9e015b89..6a5d97d9 100644 --- a/src/pylife/materiallaws/notch_approximation_law.py +++ b/src/pylife/materiallaws/notch_approximation_law.py @@ -922,6 +922,31 @@ def _create_bins_multiple_assessment_points(self): class NotchApproxBinner: + """Binning for notch approximation laws, as described in FKM nonlinear 2.5.8.2, p.55. + The implicitly defined stress function of the notch approximation law is precomputed + for various loads at a fixed number of equispaced `bins`. The values are stored in two + look-up tables for the primary and secondary branches of the stress-strain hysteresis + curves. When stress and strain values are needed for a given load, the nearest value + of the corresponding bin is retrived. This is faster than invoking the nonlinear + root finding algorithm for every new load. + + There are two variants of the data structure. + + * First, for a single assessment point, the lookup-table contains one load, + strain and stress value in every bin. + * Second, for vectorized assessment of multiple nodes at once, the lookup-table + contains at every load bin an array with stress and strain values for every node. + The representative load, stored in the lookup table and used for the lookup + is the first element of the given load array. + + Parameters + ---------- + notch_approximation_law : NotchApproximationLawBase + The law for the notch approximation to be used. + + number_of_bins : int, optional + The number of bins in the lookup table, default 100 + """ def __init__(self, notch_approximation_law, number_of_bins=100): self._n_bins = number_of_bins @@ -930,6 +955,18 @@ def __init__(self, notch_approximation_law, number_of_bins=100): self._max_load_rep = None def initialize(self, max_load): + """Initialize with a maximum expected load. + + Parameters + ---------- + max_load : array_like + The state of the maximum nominal load that is expected. The first + element is chosen as representative to caclulate the lookup table. + + Returns + ------- + self + """ max_load = np.asarray(max_load) self._max_load_rep, _ = self._rep_abs_and_sign(max_load) @@ -942,6 +979,27 @@ def initialize(self, max_load): return self def primary(self, load): + """Lookup the stress strain of the primary branch. + + Parameters + ---------- + load : array-like + The load as argument for the stress strain laws. + If non-scalar, the first element will be used to look it up in the + lookup table. + + Returns + ------- + stress strain : ndarray + The resulting stress strain data. + + If the argument is scalar, the resulting array is of the strucuture + ``[<σ>, <ε>]`` + + If the argument is an 1D-array with length `n`the resulting array is of the + structure ``[[<σ1>, <σ2>, <σ3>, ... <σn>], [<ε1>, <ε2>, <ε3>, ... <εn>]]`` + + """ self._raise_if_uninitialized() load_rep, sign = self._rep_abs_and_sign(load) @@ -953,6 +1011,27 @@ def primary(self, load): return sign * self._lut_primary[idx, :] def secondary(self, delta_load): + """Lookup the stress strain of the secondary branch. + + Parameters + ---------- + load : array-like + The load as argument for the stress strain laws. + If non-scalar, the first element will be used to look it up in the + lookup table. + + Returns + ------- + stress strain : ndarray + The resulting stress strain data. + + If the argument is scalar, the resulting array is of the strucuture + ``[<σ>, <ε>]`` + + If the argument is an 1D-array with length `n`the resulting array is of the + structure ``[[<σ1>, <σ2>, <σ3>, ..., <σn>], [<ε1>, <ε2>, <ε3>, ..., <εn>]]`` + + """ self._raise_if_uninitialized() delta_load_rep, sign = self._rep_abs_and_sign(delta_load) From 7a93c986731e90608dfbd8d6ee8c09b6e5bf6dd2 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 13 Dec 2024 10:33:53 +0100 Subject: [PATCH 07/17] Drop Binned class for notch approximation and adjust tests accordingly Signed-off-by: Johannes Mueller --- .../materiallaws/notch_approximation_law.py | 446 ------------------ .../test_notch_approximation_law.py | 112 +---- ...est_notch_approximation_law_seegerbeste.py | 100 ++-- 3 files changed, 53 insertions(+), 605 deletions(-) diff --git a/src/pylife/materiallaws/notch_approximation_law.py b/src/pylife/materiallaws/notch_approximation_law.py index 6a5d97d9..37e67abd 100644 --- a/src/pylife/materiallaws/notch_approximation_law.py +++ b/src/pylife/materiallaws/notch_approximation_law.py @@ -474,452 +474,6 @@ def _d_load_secondary_implicit(self, delta_load, delta_stress): - delta_load/delta_stress * self.K_p * self._d_delta_e_star(delta_load) -class Binned: - """Binning for notch approximation laws, as described in FKM nonlinear 2.5.8.2, p.55. - The implicitly defined stress function of the notch approximation law is precomputed - for various loads at a fixed number of equispaced `bins`. The values are stored in two - look-up tables for the primary and secondary branches of the stress-strain hysteresis - curves. When stress and strain values are needed for a given load, the nearest value - of the corresponding bin is retrived. This is faster than invoking the nonlinear - root finding algorithm for every new load. - - There are two variants of the data structure. - - * First, for a single assessment point, the lookup-table contains one load, - strain and stress value in every bin. - * Second, for vectorized assessment of multiple nodes at once, the lookup-table - contains at every load bin a list with stress and strain values for every node. - The DataFrame has a multi-index over class_index and node_id. - - """ - - def __init__(self, notch_approximation_law, maximum_absolute_load, number_of_bins=100): - self._notch_approximation_law = notch_approximation_law - self._maximum_absolute_load = maximum_absolute_load - self._number_of_bins = number_of_bins - self._create_bins() - - @property - def ramberg_osgood_relation(self): - """The ramberg osgood relation object - """ - return self._notch_approximation_law.ramberg_osgood_relation - - def stress(self, load, *, rtol=1e-5, tol=1e-6): - """The stress of the primary path in the stress-strain diagram at a given load - by using the value of the look-up table. - - .. note:: - - The exact value would be computed by ``self._notch_approximation_law.stress(load)``. - - Parameters - ---------- - load : array-like float - The load, either a scalar value or a pandas DataFrame with RangeIndex (no named index) - rtol : float, optional - The relative tolerance to which the implicit formulation of the stress gets solved. - In this case for the `Binning` class, the parameter is not used. - tol : float, optional - The absolute tolerance to which the implicit formulation of the stress gets solved. - In this case for the `Binning` class, the parameter is not used. - - Returns - ------- - stress : array-like float - The resulting stress - """ - - - - # FIXME consolidate theese methods (duplicated code) - - load = np.asarray(load) - sign = np.sign(load) - - # if the assessment is performed for multiple points at once, i.e. load is a DataFrame with values for every node - if isinstance(self._lut_primary_branch.index, pd.MultiIndex): - - # the lut is a DataFrame with MultiIndex with levels class_index and node_id - - # find the corresponding class only for the first node, use the result for all nodes - first_node_id = self._lut_primary_branch.index.get_level_values("node_id")[0] - lut_for_first_node = self._lut_primary_branch.load[self._lut_primary_branch.index.get_level_values("node_id")==first_node_id] - first_abs_load = abs(load[0]) - - # get the class index of the corresponding bin/class - class_index = lut_for_first_node.searchsorted(first_abs_load) - - max_class_index = max(self._lut_primary_branch.index.get_level_values("class_index")) - - # raise error if requested load is higher than initialized maximum absolute load - if class_index+1 > max_class_index: - raise ValueError(f"Binned class is initialized with a maximum absolute load of {self._maximum_absolute_load}, "\ - f" but a higher absolute load value of {first_abs_load} is requested (in stress()).") - - # get stress from matching class, "+1", because the next higher class is used - stress = self._lut_primary_branch[self._lut_primary_branch.index.get_level_values("class_index") == class_index+1].stress - - # multiply with sign - return sign * stress.to_numpy() - - # if the assessment is performed for multiple points at once, but only one lookup-table is used - elif isinstance(load, pd.Series): - - load = load.fillna(0) - sign = sign.fillna(0) - - index = self._lut_primary_branch.load.searchsorted(np.abs(load.values))-1 # "-1", transform to zero-based indices - - # raise error if requested load is higher than initialized maximum absolute load - if np.any(index+1 >= len(self._lut_primary_branch)): - raise ValueError(f"Binned class is initialized with a maximum absolute load of {self._maximum_absolute_load}, "\ - f" but a higher absolute load value of |{load.max()}| is requested (in stress()).") - - return sign.values.flatten() * self._lut_primary_branch.iloc[(index+1).flatten()].stress.reset_index(drop=True) # "+1", because the next higher class is used - - # if the assessment is done only for one value, i.e. load is a scalar - else: - - index = self._lut_primary_branch.load.searchsorted(np.abs(load))-1 # "-1", transform to zero-based indices - - # raise error if requested load is higher than initialized maximum absolute load - if np.any(index+1 >= len(self._lut_primary_branch)): - raise ValueError(f"Binned class is initialized with a maximum absolute load of {self._maximum_absolute_load}, "\ - f" but a higher absolute load value of |{load}| is requested (in stress()).") - - return sign * self._lut_primary_branch.iloc[index+1].stress # "+1", because the next higher class is used - - def strain(self, stress, load): - """Get the strain of the primary path in the stress-strain diagram at a given stress and load - by using the value of the look-up table. - - This method performs the task for for multiple points at once, - i.e. delta_load is a DataFrame with values for every node. - - Parameters - ---------- - load : array-like float - The load - - Returns - ------- - strain : array-like float - The resulting strain - """ - load = np.asarray(load) - sign = np.sign(load) - - # if the assessment is performed for multiple points at once, i.e. load is a DataFrame with values for every node - if isinstance(self._lut_primary_branch.index, pd.MultiIndex): - - # the lut is a DataFrame with MultiIndex with levels class_index and node_id - - # find the corresponding class only for the first node, use the result for all nodes - first_node_id = self._lut_primary_branch.index.get_level_values("node_id")[0] - lut_for_first_node = self._lut_primary_branch.load[self._lut_primary_branch.index.get_level_values("node_id")==first_node_id] - first_abs_load = abs(load[0]) - - # get the class index of the corresponding bin/class - class_index = lut_for_first_node.searchsorted(first_abs_load) - - max_class_index = max(self._lut_primary_branch.index.get_level_values("class_index")) - - # raise error if requested load is higher than initialized maximum absolute load - if class_index+1 > max_class_index: - raise ValueError(f"Binned class is initialized with a maximum absolute load of {self._maximum_absolute_load}, "\ - f" but a higher absolute load value of {first_abs_load} is requested (in strain()).") - - # get strain from matching class, "+1", because the next higher class is used - strain = self._lut_primary_branch[self._lut_primary_branch.index.get_level_values("class_index") == class_index+1].strain - - # multiply with sign - return sign * strain.to_numpy() - - # if the assessment is performed for multiple points at once, but only one lookup-table is used - elif isinstance(load, pd.Series): - - load = load.fillna(0) - sign = sign.fillna(0) - - index = self._lut_primary_branch.load.searchsorted(np.abs(load.values))-1 # "-1", transform to zero-based indices - - # raise error if requested load is higher than initialized maximum absolute load - if np.any(index+1 >= len(self._lut_primary_branch)): - raise ValueError(f"Binned class is initialized with a maximum absolute load of {self._maximum_absolute_load}, "\ - f" but a higher absolute load value of |{load.max()}| is requested (in strain()).") - - return sign.values.flatten() * self._lut_primary_branch.iloc[(index+1).flatten()].strain.reset_index(drop=True) # "+1", because the next higher class is used - - # if the assessment is done only for one value, i.e. load is a scalar - else: - - index = self._lut_primary_branch.load.searchsorted(np.abs(load))-1 # "-1", transform to zero-based indices - - # raise error if requested load is higher than initialized maximum absolute load - if np.any(index+1 >= len(self._lut_primary_branch)): - raise ValueError(f"Binned class is initialized with a maximum absolute load of {self._maximum_absolute_load}, "\ - f" but a higher absolute load value of |{load}| is requested (in strain()).") - - return sign * self._lut_primary_branch.iloc[index+1].strain # "+1", because the next higher class is used - - def stress_secondary_branch(self, delta_load, *, rtol=1e-5, tol=1e-6): - """Get the stress on secondary branches in the stress-strain diagram at a given load - by using the value of the look-up table (lut). - - This method performs the task for for multiple points at once, - i.e. delta_load is a DataFrame with values for every node. - - Parameters - ---------- - delta_load : array-like float - The load increment of the hysteresis - rtol : float, optional - The relative tolerance to which the implicit formulation of the stress gets solved. - In this case for the `Binning` class, the parameter is not used. - tol : float, optional - The absolute tolerance to which the implicit formulation of the stress gets solved. - In this case for the `Binning` class, the parameter is not used. - - Returns - ------- - delta_stress : array-like float - The resulting stress increment within the hysteresis - """ - - delta_load = np.asarray(delta_load) - sign = np.sign(delta_load) - - # if the assessment is performed for multiple points at once, i.e. load is a DataFrame with values for every node - if isinstance(self._lut_primary_branch.index, pd.MultiIndex): - - # the lut is a DataFrame with MultiIndex with levels class_index and node_id - - # find the corresponding class only for the first node, use the result for all nodes - first_node_id = self._lut_primary_branch.index.get_level_values("node_id")[0] - lut_for_first_node = self._lut_secondary_branch.delta_load[self._lut_secondary_branch.index.get_level_values("node_id")==first_node_id] - first_abs_load = abs(delta_load[0]) - - # get the class index of the corresponding bin/class - class_index = lut_for_first_node.searchsorted(first_abs_load) - - max_class_index = max(self._lut_secondary_branch.index.get_level_values("class_index")) - - # raise error if requested load is higher than initialized maximum absolute load - if class_index+1 > max_class_index: - raise ValueError(f"Binned class is initialized with a maximum absolute delta_load load of {2*self._maximum_absolute_load}, "\ - f" but a higher absolute delta_load value of {first_abs_load} is requested (in stress_secondary_branch()).") - - # get stress from matching class, "+1", because the next higher class is used - delta_stress = self._lut_secondary_branch[self._lut_secondary_branch.index.get_level_values("class_index") == class_index+1].delta_stress - - # multiply with sign - return sign * delta_stress.to_numpy() - - # if the assessment is performed for multiple points at once, but only one lookup-table is used - elif isinstance(delta_load, pd.Series): - - delta_load = delta_load.fillna(0) - sign = sign.fillna(0) - - index = self._lut_secondary_branch.delta_load.searchsorted(np.abs(delta_load.values))-1 # "-1", transform to zero-based indices - - # raise error if requested load is higher than initialized maximum absolute load - if np.any(index+1 >= len(self._lut_secondary_branch)): - raise ValueError(f"Binned class is initialized with a maximum absolute load of {self._maximum_absolute_load}, "\ - f" but a higher absolute load value of |{delta_load.max()}| is requested (in stress_secondary_branch()).") - - return sign.values.flatten() * self._lut_secondary_branch.iloc[(index+1).flatten()].delta_stress.reset_index(drop=True) # "+1", because the next higher class is used - - # if the assessment is done only for one value, i.e. load is a scalar - else: - - index = self._lut_secondary_branch.delta_load.searchsorted(np.abs(delta_load))-1 # "-1", transform to zero-based indices - - # raise error if requested load is higher than initialized maximum absolute load - if np.any(index+1 >= len(self._lut_secondary_branch)): - raise ValueError(f"Binned class is initialized for a maximum absolute delta_load of {2*self._maximum_absolute_load}, "\ - f" but a higher absolute delta_load value of |{delta_load}| is requested (in stress_secondary_branch()).") - - return sign * np.asarray(self._lut_secondary_branch.iloc[index+1].delta_stress) - - def strain_secondary_branch(self, delta_stress, delta_load): - """Get the strain on secondary branches in the stress-strain diagram at a given stress and load - by using the value of the look-up table (lut). - The lut is a DataFrame with MultiIndex with levels class_index and node_id. - - This method performs the task for for multiple points at once, - i.e. delta_load is a DataFrame with values for every node. - - Parameters - ---------- - delta_load : array-like float - The load increment - - Returns - ------- - strain : array-like float - The resulting strain - """ - #return self._notch_approximation_law.strain_secondary_branch(delta_stress, delta_load) - - delta_load = np.asarray(delta_load) - sign = np.sign(delta_load) - - # if the assessment is performed for multiple points at once, i.e. load is a DataFrame with values for every node - if isinstance(self._lut_primary_branch.index, pd.MultiIndex): - - # the lut is a DataFrame with MultiIndex with levels class_index and node_id - - # find the corresponding class only for the first node, use the result for all nodes - first_node_id = self._lut_secondary_branch.index.get_level_values("node_id")[0] - lut_for_first_node = self._lut_secondary_branch.delta_load[self._lut_secondary_branch.index.get_level_values("node_id")==first_node_id] - first_abs_load = abs(delta_load[0]) - - # get the class index of the corresponding bin/class - class_index = lut_for_first_node.searchsorted(first_abs_load) - - max_class_index = max(self._lut_secondary_branch.index.get_level_values("class_index")) - - # raise error if requested load is higher than initialized maximum absolute load - if class_index+1 > max_class_index: - raise ValueError(f"Binned class is initialized with a maximum absolute delta_load of {2*self._maximum_absolute_load}, "\ - f" but a higher absolute delta_load value of {first_abs_load} is requested (in strain_secondary_branch()).") - - # get strain from matching class, "+1", because the next higher class is used - delta_strain = self._lut_secondary_branch[self._lut_secondary_branch.index.get_level_values("class_index") == class_index+1].delta_strain - - # multiply with sign - return sign * delta_strain.to_numpy() - - # if the assessment is performed for multiple points at once, but only one lookup-table is used - elif isinstance(delta_load, pd.Series): - - delta_load = delta_load.fillna(0) - sign = sign.fillna(0) - - index = self._lut_secondary_branch.delta_load.searchsorted(np.abs(delta_load.values))-1 # "-1", transform to zero-based indices - - # raise error if requested load is higher than initialized maximum absolute load - if np.any(index+1 >= len(self._lut_secondary_branch)): - raise ValueError(f"Binned class is initialized with a maximum absolute load of {self._maximum_absolute_load}, "\ - f" but a higher absolute load value of |{delta_load.max()}| is requested (in strain_secondary_branch()).") - - return sign.values.flatten() * self._lut_secondary_branch.iloc[(index+1).flatten()].delta_strain.reset_index(drop=True) # "+1", because the next higher class is used - - # if the assessment is done only for one value, i.e. load is a scalar - else: - - index = self._lut_secondary_branch.delta_load.searchsorted(np.abs(delta_load))-1 # "-1", transform to zero-based indices - - # raise error if requested load is higher than initialized maximum absolute load - if np.any(index+1 >= len(self._lut_secondary_branch)): - raise ValueError(f"Binned class is initialized for a maximum absolute delta_load of {2*self._maximum_absolute_load}, "\ - f" but a higher absolute delta_load value of |{delta_load}| is requested (in strain_secondary_branch()).") - - return sign * np.asarray(self._lut_secondary_branch.iloc[index+1].delta_strain) # "-1", transform to zero-based indices - - def _create_bins(self): - """Initialize the lookup tables by precomputing the notch approximation law values. - """ - # for multiple assessment points at once use a Series with MultiIndex - if isinstance(self._maximum_absolute_load, pd.Series): - assert self._maximum_absolute_load.index.name == "node_id" - - self._create_bins_multiple_assessment_points() - - # for a single assessment point use the standard data structure - else: - self._create_bins_single_assessment_point() - - def _create_bins_single_assessment_point(self): - """Initialize the lookup tables by precomputing the notch approximation law values, - for the case of scalar variables, i.e., only a single assessment point.""" - - # create look-up table (lut) for the primary branch values, named PFAD in FKM nonlinear - self._lut_primary_branch = pd.DataFrame(0, - index=pd.Index(np.arange(1, self._number_of_bins+1), name="class_index"), - columns=["load", "strain", "stress"]) - - self._lut_primary_branch.load \ - = self._lut_primary_branch.index/self._number_of_bins * self._maximum_absolute_load - - self._lut_primary_branch.stress \ - = self._notch_approximation_law.stress(self._lut_primary_branch.load) - - self._lut_primary_branch.strain \ - = self._notch_approximation_law.strain( - self._lut_primary_branch.stress, self._lut_primary_branch.load) - - # create look-up table (lut) for the secondary branch values, named AST in FKM nonlinear - # Note that this time, we used twice the number of entries with the same bin width. - self._lut_secondary_branch = pd.DataFrame(0, - index=pd.Index(np.arange(1, 2*self._number_of_bins+1), name="class_index"), - columns=["delta_load", "delta_strain", "delta_stress"]) - - self._lut_secondary_branch.delta_load \ - = self._lut_secondary_branch.index/self._number_of_bins * self._maximum_absolute_load - - self._lut_secondary_branch.delta_stress \ - = self._notch_approximation_law.stress_secondary_branch(self._lut_secondary_branch.delta_load) - - self._lut_secondary_branch.delta_strain \ - = self._notch_approximation_law.strain_secondary_branch( - self._lut_secondary_branch.delta_stress, self._lut_secondary_branch.delta_load) - - def _create_bins_multiple_assessment_points(self): - """Initialize the lookup tables by precomputing the notch approximation law values, - for the case of vector-valued variables caused by an assessment on multiple points at once.""" - - self._maximum_absolute_load.name = "max_abs_load" - - # create look-up table (lut) for the primary branch values, named PFAD in FKM nonlinear - index = pd.MultiIndex.from_product([np.arange(1, self._number_of_bins+1), self._maximum_absolute_load.index], - names = ["class_index", "node_id"]) - - self._lut_primary_branch = pd.DataFrame(0, index=index, columns=["load", "strain", "stress"]) - - # create cartesian product of class index and max load - a = pd.DataFrame({"class_index": np.arange(1, self._number_of_bins+1)}) - class_index_with_max_load = a.merge(self._maximum_absolute_load, how='cross') - - # calculate load of each bin - load = class_index_with_max_load.class_index.astype(float)/self._number_of_bins * class_index_with_max_load.max_abs_load - load.index = index - self._lut_primary_branch.load = load - - self._lut_primary_branch.stress \ - = self._notch_approximation_law.stress(self._lut_primary_branch.load) - - self._lut_primary_branch.strain \ - = self._notch_approximation_law.strain( - self._lut_primary_branch.stress, self._lut_primary_branch.load) - - # ---------------- - # create look-up table (lut) for the secondary branch values, named AST in FKM nonlinear - # Note that this time, we used twice the number of entries with the same bin width. - index = pd.MultiIndex.from_product([np.arange(1, 2*self._number_of_bins+1), self._maximum_absolute_load.index], - names = ["class_index", "node_id"]) - - self._lut_secondary_branch = pd.DataFrame(0, index=index, columns=["delta_load", "delta_strain", "delta_stress"]) - - # create cartesian product of class index and max load - a = pd.DataFrame({"class_index": np.arange(1, 2*self._number_of_bins+1)}) - class_index_with_max_load = a.merge(self._maximum_absolute_load, how='cross') - - # calculate load of each bin - delta_load = class_index_with_max_load.class_index.astype(float)/self._number_of_bins * class_index_with_max_load.max_abs_load - delta_load.index = index - self._lut_secondary_branch.delta_load = delta_load - - self._lut_secondary_branch.delta_stress \ - = self._notch_approximation_law.stress_secondary_branch(self._lut_secondary_branch.delta_load) - - self._lut_secondary_branch.delta_strain \ - = self._notch_approximation_law.strain_secondary_branch( - self._lut_secondary_branch.delta_stress, self._lut_secondary_branch.delta_load) - - class NotchApproxBinner: """Binning for notch approximation laws, as described in FKM nonlinear 2.5.8.2, p.55. diff --git a/tests/materiallaws/test_notch_approximation_law.py b/tests/materiallaws/test_notch_approximation_law.py index e7eae318..cafffe0e 100644 --- a/tests/materiallaws/test_notch_approximation_law.py +++ b/tests/materiallaws/test_notch_approximation_law.py @@ -19,105 +19,45 @@ import pytest import numpy as np -import pandas as pd - -import pylife.strength.damage_parameter from pylife.materiallaws.notch_approximation_law import ExtendedNeuber, NotchApproxBinner from .data import * -def test_extended_neuber_example_1(): - """ example under 2.7.1, p.74 of FKM nonlinear "Akademisches Beispiel" """ - - E = 206e3 # [MPa] Young's modulus - K = 1184 # [MPa] - n = 0.187 # [-] - K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - - L = pd.Series([100, -200, 100, -250, 200, 0, 200, -200]) - c = 1.4 - gamma_L = (250+6.6)/250 - L = c * gamma_L * L - - # initialize notch approximation law and damage parameter - notch_approximation_law = ExtendedNeuber(E, K, n, K_p) - - assert notch_approximation_law.E == E - assert notch_approximation_law.K == K - assert notch_approximation_law.n == n - assert notch_approximation_law.K_p == K_p - - maximum_absolute_load = max(abs(L)) - - # the FKM example seems to round here, real value is 359.24 - assert np.isclose(maximum_absolute_load, 359.3, rtol=1e-3) - - binned_notch_approximation_law = pylife.materiallaws.notch_approximation_law.Binned( - notch_approximation_law, maximum_absolute_load, 100) - - # some rows of PFAD are given in the FKM nonlinear example on p.76 - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[0], \ - pd.Series([3.592, 0.0017e-2, 3.592]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[1], \ - pd.Series([7.185, 0.0035e-2, 7.185]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - # Note that the original FKM document has an error at this row (it is row 49, not 50) - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[48], \ - pd.Series([176.057, 8.71e-4, 172.639]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[99], \ - pd.Series([359.3, 0.0021, 299.78]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - - # matrix AST on page 162, chapter 3.4.1 - pd.testing.assert_frame_equal( - binned_notch_approximation_law._lut_secondary_branch, expected_matrix_AST_162, rtol=1e-3, atol=1e-5) +@pytest.mark.parametrize('load, strain, stress', [ + (3.592, 0.0017e-2, 3.592), + (7.185, 0.0035e-2, 7.185), + (176.057, 8.71e-4, 172.639), + (359.3, 0.0021, 299.78) +]) +def test_extended_neuber_example_1(load, strain, stress): + """ example under 2.7.1, p.74 of FKM nonlinear "Akademisches Beispiel" """ + notch_approximation_law = ExtendedNeuber(E=206e3, K=1184, n=0.187, K_p=3.5) - assert np.isclose(binned_notch_approximation_law._lut_primary_branch.load.max(), maximum_absolute_load) + assert np.isclose(notch_approximation_law.stress(load), stress, rtol=1e-1) + assert np.isclose(notch_approximation_law.strain(stress, None), strain, rtol=1e-1) + assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-1) -def test_extended_neuber_example_2(): +@pytest.mark.parametrize('load, strain, stress', [ + (10.13, 0.0049e-2, 10.130), + (20.26, 0.0098e-2, 20.260), + (1013.00, 0.6035e-2, 829.681), +]) +def test_extended_neuber_example_2(load, strain, stress): """ example under 2.7.2, p.78 of FKM nonlinear, "Welle mit V-Kerbe" """ + notch_approximation_law = ExtendedNeuber(E=206e3, K=2650.5, n=0.187, K_p=3.5) - E = 206e3 # [MPa] Young's modulus - K = 2650.5 # [MPa] - n = 0.187 # [-] - K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - - L = 1266.25 * pd.Series([0.3, -0.3, 0.5, -0.5, 0.6, -0.6, 0.3, -0.3, 0.7, -0.7, 0.2, -0.2, 0.6, -0.6, 0.8, -0.8, 0.8, -0.8]) - - # initialize notch approximation law and damage parameter - notch_approximation_law = ExtendedNeuber(E, K, n, K_p) - - maximum_absolute_load = max(abs(L)) - - # the FKM example seems to round here, real value is 359.24 - assert np.isclose(maximum_absolute_load, 1013, rtol=1e-3) - - binned_notch_approximation_law = pylife.materiallaws.notch_approximation_law.Binned( - notch_approximation_law, maximum_absolute_load) - - # some rows of PFAD are given in the FKM nonlinear example on p.79 - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[0], \ - pd.Series([10.13, 0.0049e-2, 10.130]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[1], \ - pd.Series([20.26, 0.0098e-2, 20.260]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - - # this row seems off in the FKM nonlinear example, error is as high as 5% - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[99], \ - pd.Series([1013.00, 0.6035e-2, 829.681]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - - # test binning directly - assert np.isclose(binned_notch_approximation_law.stress(10.13), 10.130) - assert np.isclose(binned_notch_approximation_law.strain(10.30, 10.13), 0.0049174e-2) + assert np.isclose(notch_approximation_law.stress(load), stress, rtol=1e-1) + assert np.isclose(notch_approximation_law.strain(stress, None), strain, rtol=1e-1) + assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-1) - assert np.isclose(binned_notch_approximation_law.stress_secondary_branch(10.13), 10.130) - assert np.isclose(binned_notch_approximation_law.strain_secondary_branch(10.30, 10.13), 0.0049174e-2) + assert np.isclose(notch_approximation_law.stress_secondary_branch(10.13), 10.130) + assert np.isclose(notch_approximation_law.strain_secondary_branch(10.30, 10.13), 0.0049174e-2, rtol=1e-1) # matrix AST on page 171, chapter 3.4.2 - pd.testing.assert_frame_equal( - binned_notch_approximation_law._lut_secondary_branch, expected_matrix_AST_171, rtol=1e-3, atol=1e-5) + # pd.testing.assert_frame_equal( + # binned_notch_approximation_law._lut_secondary_branch, expected_matrix_AST_171, rtol=1e-3, atol=1e-5) def test_extended_neuber_example_no_binning_vectorized(): diff --git a/tests/materiallaws/test_notch_approximation_law_seegerbeste.py b/tests/materiallaws/test_notch_approximation_law_seegerbeste.py index 4e76f65a..1b065e4a 100644 --- a/tests/materiallaws/test_notch_approximation_law_seegerbeste.py +++ b/tests/materiallaws/test_notch_approximation_law_seegerbeste.py @@ -26,91 +26,45 @@ from pylife.materiallaws.notch_approximation_law_seegerbeste import SeegerBeste from .data import * - -def test_seeger_beste_example_1(): +@pytest.mark.parametrize('load, strain, stress', [ + (3.592, 0.0017e-2, 3.592), + (7.185, 0.0035e-2, 7.185), + (176.057, 8.71e-4, 172.639), + (359.3, 0.0019, 283.86) +]) +def test_seeger_beste_example_1(load, strain, stress): """ example under 2.7.1, p.74 of FKM nonlinear "Akademisches Beispiel" """ + notch_approximation_law = SeegerBeste(E=206e3, K=1184, n=0.187, K_p=3.5) - E = 206e3 # [MPa] Young's modulus - K = 1184 # [MPa] - n = 0.187 # [-] - K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - - L = pd.Series([100, -200, 100, -250, 200, 0, 200, -200]) - c = 1.4 - gamma_L = (250+6.6)/250 - L = c * gamma_L * L - - # initialize notch approximation law and damage parameter - #notch_approximation_law = ExtendedNeuber(E, K, n, K_p) - notch_approximation_law = SeegerBeste(E, K, n, K_p) -# damage_parameter_type = pylife.strength.damage_parameter.P_RAM() - - maximum_absolute_load = max(abs(L)) -# print(maximum_absolute_load) - - # the FKM example seems to round here, real value is 359.24 - assert np.isclose(maximum_absolute_load, 359.3, rtol=1e-3) - - binned_notch_approximation_law = pylife.materiallaws.notch_approximation_law.Binned(notch_approximation_law, maximum_absolute_load) - - # some rows of PFAD are given in the FKM nonlinear example on p.76 - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[0], \ - pd.Series([3.59, 0.0017e-2, 3.59]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) + assert np.isclose(notch_approximation_law.stress(load), stress, rtol=1e-1) + assert np.isclose(notch_approximation_law.strain(stress, None), strain, rtol=1e-1) + assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-1) - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[1], \ - pd.Series([7.18, 0.0035e-2, 7.18]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - - # this row seems off in the FKM nonlinear example, error is as high as 5% - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[49], \ - #pd.Series([179.62, 8.73e-4, 172.93]), check_names=False, check_index=False, rtol=10e-2, atol=1e-5) - pd.Series([179.62, 8.73e-4, 172.93]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[99], \ - pd.Series([359.24, 0.001859, 283.86]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) # matrix AST on page 162, chapter 3.4.1 - pd.testing.assert_frame_equal( - binned_notch_approximation_law._lut_secondary_branch, expected_matrix_AST_162_seeger_beste, rtol=1e-3, atol=1e-5) + # pd.testing.assert_frame_equal( + # binned_notch_approximation_law._lut_secondary_branch, expected_matrix_AST_162_seeger_beste, rtol=1e-3, atol=1e-5) - assert np.isclose(binned_notch_approximation_law._lut_primary_branch.load.max(), maximum_absolute_load) + # assert np.isclose(binned_notch_approximation_law._lut_primary_branch.load.max(), maximum_absolute_load) -def test_seeger_beste_example_2(): +@pytest.mark.parametrize('load, strain, stress', [ + (10.13, 0.0049e-2, 10.130), + (20.26, 0.0098e-2, 20.260), + (1013.00, 0.53e-2, 784.89), +]) +def test_seeger_beste_example_2(load, strain, stress): """ example under 2.7.2, p.78 of FKM nonlinear, "Welle mit V-Kerbe" """ + notch_approximation_law = SeegerBeste(E=206e3, K=2650.5, n=0.187, K_p=3.5) - E = 206e3 # [MPa] Young's modulus - K = 2650.5 # [MPa] - n = 0.187 # [-] - K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) - - L = 1266.25 * pd.Series([0.3, -0.3, 0.5, -0.5, 0.6, -0.6, 0.3, -0.3, 0.7, -0.7, 0.2, -0.2, 0.6, -0.6, 0.8, -0.8, 0.8, -0.8]) - - # initialize notch approximation law and damage parameter - notch_approximation_law = SeegerBeste(E, K, n, K_p) - - maximum_absolute_load = max(abs(L)) - - # the FKM example seems to round here, real value is 359.24 - assert np.isclose(maximum_absolute_load, 1013, rtol=1e-3) - - binned_notch_approximation_law = pylife.materiallaws.notch_approximation_law.Binned( - notch_approximation_law, maximum_absolute_load) - - # some rows of PFAD are given in the FKM nonlinear example on p.79 - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[0], \ - pd.Series([10.13, 0.0049e-2, 10.130]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[1], \ - pd.Series([20.26, 0.01e-2, 20.260]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) - - # this row seems off in the FKM nonlinear example, error is as high as 5% - pd.testing.assert_series_equal(binned_notch_approximation_law._lut_primary_branch.iloc[99], \ - pd.Series([1013.00, 0.53e-2, 784.890]), check_names=False, check_index=False, rtol=1e-3, atol=1e-5) + assert np.isclose(notch_approximation_law.stress(load), stress, rtol=1e-1) + assert np.isclose(notch_approximation_law.strain(stress, None), strain, rtol=1e-1) + assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-1) - # matrix AST on page 171, chapter 3.4.2 - pd.testing.assert_frame_equal( - binned_notch_approximation_law._lut_secondary_branch, expected_matrix_AST_171_seeger_beste, rtol=2e-3, atol=1e-5) + # # matrix AST on page 171, chapter 3.4.2 + # pd.testing.assert_frame_equal( + # binned_notch_approximation_law._lut_secondary_branch, expected_matrix_AST_171_seeger_beste, rtol=2e-3, atol=1e-5) def test_seeger_beste_example_no_binning(): From d922aeb9719408db066f9e8983c2742751c58aae Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 13 Dec 2024 10:43:05 +0100 Subject: [PATCH 08/17] Drop assertions that checked for the LUTs of former Binned The LUTs are implementation details and should not be accessed directly by tests. Signed-off-by: Johannes Mueller --- .../assessment_nonlinear_standard.py | 2 - .../fkm_nonlinear/test_fkm_nonlinear.py | 52 ------------------- 2 files changed, 54 deletions(-) diff --git a/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py b/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py index 9c27851e..6f9592f7 100644 --- a/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py +++ b/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py @@ -630,7 +630,6 @@ def _compute_lifetimes_P_RAJ(assessment_parameters, result, scaled_load_sequence """ detector_1st, detector, seeger_beste_binned, recorder = _compute_hcm_RAJ(assessment_parameters, scaled_load_sequence, maximum_absolute_load) - result["seeger_beste_binned"] = seeger_beste_binned result, damage_calculator = _compute_damage_and_lifetimes_RAJ(assessment_parameters, recorder, component_woehler_curve_P_RAJ, result) @@ -650,7 +649,6 @@ def _compute_lifetimes_P_RAM(assessment_parameters, result, scaled_load_sequence * Do statistical assessment and store all results in a dict. """ detector_1st, detector, extended_neuber_binned, recorder = _compute_hcm_RAM(assessment_parameters, scaled_load_sequence, maximum_absolute_load) - result["extended_neuber_binned"] = extended_neuber_binned result, damage_calculator = _compute_damage_and_lifetimes_RAM(assessment_parameters, recorder, component_woehler_curve_P_RAM, result) diff --git a/tests/strength/fkm_nonlinear/test_fkm_nonlinear.py b/tests/strength/fkm_nonlinear/test_fkm_nonlinear.py index 1fe70d04..9edfe911 100644 --- a/tests/strength/fkm_nonlinear/test_fkm_nonlinear.py +++ b/tests/strength/fkm_nonlinear/test_fkm_nonlinear.py @@ -511,19 +511,6 @@ def test_assessment_multiple_points_p_ram(): # assert that the results are equal for i in range(3): - # assert that the lookup table in the notch approximation law is the same - # lut for primary branch - assert np.allclose( - result_multiple["extended_neuber_binned"]._lut_primary_branch[ - result_multiple["extended_neuber_binned"]._lut_primary_branch.index.get_level_values("node_id")==i].values, - result[i]["extended_neuber_binned"]._lut_primary_branch.values) - - # lut for secondary branch - assert np.allclose( - result_multiple["extended_neuber_binned"]._lut_secondary_branch[ - result_multiple["extended_neuber_binned"]._lut_secondary_branch.index.get_level_values("node_id")==i].values, - result[i]["extended_neuber_binned"]._lut_secondary_branch.values) - assert result[i]["P_RAM_is_life_infinite"] == result_multiple["P_RAM_is_life_infinite"][i] assert np.isclose(result[i]["P_RAM_lifetime_n_cycles"], result_multiple["P_RAM_lifetime_n_cycles"][i]) assert np.isclose(result[i]["P_RAM_lifetime_n_times_load_sequence"], result_multiple["P_RAM_lifetime_n_times_load_sequence"][i]) @@ -585,19 +572,6 @@ def test_assessment_multiple_points_p_raj(): # assert that the results are equal for i in range(3): - # assert that the lookup tabe in the notch approximation law is the same - # lut for primary branch - assert np.allclose( - result_multiple["seeger_beste_binned"]._lut_primary_branch[ - result_multiple["seeger_beste_binned"]._lut_primary_branch.index.get_level_values("node_id")==i].values, - result[i]["seeger_beste_binned"]._lut_primary_branch.values) - - # lut for secondary branch - assert np.allclose( - result_multiple["seeger_beste_binned"]._lut_secondary_branch[ - result_multiple["seeger_beste_binned"]._lut_secondary_branch.index.get_level_values("node_id")==i].values, - result[i]["seeger_beste_binned"]._lut_secondary_branch.values, rtol=1e-3) - assert result[i]["P_RAJ_is_life_infinite"] == result_multiple["P_RAJ_is_life_infinite"][i] assert np.isclose(result[i]["P_RAJ_lifetime_n_cycles"], result_multiple["P_RAJ_lifetime_n_cycles"][i]) assert np.isclose(result[i]["P_RAJ_lifetime_n_times_load_sequence"], result_multiple["P_RAJ_lifetime_n_times_load_sequence"][i]) @@ -666,19 +640,6 @@ def test_assessment_multiple_points_p_ram_with_spatially_varying_gradient(): # assert that the results are equal for i in range(3): - # assert that the lookup table in the notch approximation law is the same - # lut for primary branch - assert np.allclose( - result_multiple["extended_neuber_binned"]._lut_primary_branch[ - result_multiple["extended_neuber_binned"]._lut_primary_branch.index.get_level_values("node_id")==i].values, - result[i]["extended_neuber_binned"]._lut_primary_branch.values) - - # lut for secondary branch - assert np.allclose( - result_multiple["extended_neuber_binned"]._lut_secondary_branch[ - result_multiple["extended_neuber_binned"]._lut_secondary_branch.index.get_level_values("node_id")==i].values, - result[i]["extended_neuber_binned"]._lut_secondary_branch.values) - assert result[i]["P_RAM_is_life_infinite"] == result_multiple["P_RAM_is_life_infinite"][i] assert np.isclose(result[i]["P_RAM_lifetime_n_cycles"], result_multiple["P_RAM_lifetime_n_cycles"][i]) assert np.isclose(result[i]["P_RAM_lifetime_n_times_load_sequence"], result_multiple["P_RAM_lifetime_n_times_load_sequence"][i]) @@ -746,19 +707,6 @@ def test_assessment_multiple_points_p_raj_with_spatially_varying_gradient(): # assert that the results are equal for i in range(3): - # assert that the lookup tabe in the notch approximation law is the same - # lut for primary branch - assert np.allclose( - result_multiple["seeger_beste_binned"]._lut_primary_branch[ - result_multiple["seeger_beste_binned"]._lut_primary_branch.index.get_level_values("node_id")==i].values, - result[i]["seeger_beste_binned"]._lut_primary_branch.values) - - # lut for secondary branch - assert np.allclose( - result_multiple["seeger_beste_binned"]._lut_secondary_branch[ - result_multiple["seeger_beste_binned"]._lut_secondary_branch.index.get_level_values("node_id")==i].values, - result[i]["seeger_beste_binned"]._lut_secondary_branch.values, rtol=1e-3) - assert result[i]["P_RAJ_is_life_infinite"] == result_multiple["P_RAJ_is_life_infinite"][i] assert np.isclose(result[i]["P_RAJ_lifetime_n_cycles"], result_multiple["P_RAJ_lifetime_n_cycles"][i]) assert np.isclose(result[i]["P_RAJ_lifetime_n_times_load_sequence"], result_multiple["P_RAJ_lifetime_n_times_load_sequence"][i]) From 6735016853e31d21a674def58ede4adf9127fb9d Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 13 Dec 2024 11:15:55 +0100 Subject: [PATCH 09/17] Add tests for secondary branch notch approximation Signed-off-by: Johannes Mueller --- tests/materiallaws/data.py | 10 ++--- .../test_notch_approximation_law.py | 40 +++++++++++++++---- ...est_notch_approximation_law_seegerbeste.py | 35 ++++++++++++---- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/tests/materiallaws/data.py b/tests/materiallaws/data.py index 9b7af37a..ffe39c47 100644 --- a/tests/materiallaws/data.py +++ b/tests/materiallaws/data.py @@ -417,8 +417,8 @@ ] ) -# matrix AST on page 162, chapter 3.4.1 -expected_matrix_AST_162_seeger_beste = pd.DataFrame( +# matrix AST on page 180, chapter 3.5.1 +expected_matrix_AST_180_seeger_beste = pd.DataFrame( index=pd.Index(np.arange(1, 200+1), name="class_index"), columns=["delta_load", "delta_strain", "delta_stress"], data=[ @@ -625,8 +625,8 @@ ] ) -# matrix AST on page 171, chapter 3.4.2 -expected_matrix_AST_171_seeger_beste = pd.DataFrame( +# matrix AST on page 189, chapter 3.5.2 +expected_matrix_AST_189_seeger_beste = pd.DataFrame( index=pd.Index(np.arange(1, 200+1), name="class_index"), columns=["delta_load", "delta_strain", "delta_stress"], data=[ @@ -831,4 +831,4 @@ [2015.87, 0.010537, 1565.324], [2026.0, 0.010604, 1569.781] ] -) \ No newline at end of file +) diff --git a/tests/materiallaws/test_notch_approximation_law.py b/tests/materiallaws/test_notch_approximation_law.py index cafffe0e..793f448b 100644 --- a/tests/materiallaws/test_notch_approximation_law.py +++ b/tests/materiallaws/test_notch_approximation_law.py @@ -29,13 +29,28 @@ (176.057, 8.71e-4, 172.639), (359.3, 0.0021, 299.78) ]) -def test_extended_neuber_example_1(load, strain, stress): +def test_extended_neuber_primary_example_1(load, strain, stress): """ example under 2.7.1, p.74 of FKM nonlinear "Akademisches Beispiel" """ notch_approximation_law = ExtendedNeuber(E=206e3, K=1184, n=0.187, K_p=3.5) - assert np.isclose(notch_approximation_law.stress(load), stress, rtol=1e-1) + assert np.isclose(notch_approximation_law.stress(load), stress, rtol=1e-3) assert np.isclose(notch_approximation_law.strain(stress, None), strain, rtol=1e-1) - assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-1) + assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-3) + + +@pytest.mark.parametrize('delta_load, delta_strain, delta_stress', [ + (100.587, 0.0488e-2, 100.578), + (201.174, 0.0978e-2, 200.794), + (402.349, 0.2019e-2, 389.430), + (700.518, 0.4051e-2, 590.289) +]) # values from matrix AST in FKM guideline nonlinear on page 162, chapter 3.4.1 +def test_extended_neuber_secondary_example_1(delta_load, delta_strain, delta_stress): + """ example under 2.7.1, p.74 of FKM nonlinear "Akademisches Beispiel" """ + notch_approximation_law = ExtendedNeuber(E=206e3, K=1184, n=0.187, K_p=3.5) + + assert np.isclose(notch_approximation_law.stress_secondary_branch(delta_load), delta_stress, rtol=1e-3) + assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress, None), delta_strain, rtol=1e-3) + assert np.isclose(notch_approximation_law.load_secondary_branch(delta_stress), delta_load, rtol=1e-3) @pytest.mark.parametrize('load, strain, stress', [ @@ -43,7 +58,7 @@ def test_extended_neuber_example_1(load, strain, stress): (20.26, 0.0098e-2, 20.260), (1013.00, 0.6035e-2, 829.681), ]) -def test_extended_neuber_example_2(load, strain, stress): +def test_extended_neuber_primary_example_2(load, strain, stress): """ example under 2.7.2, p.78 of FKM nonlinear, "Welle mit V-Kerbe" """ notch_approximation_law = ExtendedNeuber(E=206e3, K=2650.5, n=0.187, K_p=3.5) @@ -54,10 +69,21 @@ def test_extended_neuber_example_2(load, strain, stress): assert np.isclose(notch_approximation_law.stress_secondary_branch(10.13), 10.130) assert np.isclose(notch_approximation_law.strain_secondary_branch(10.30, 10.13), 0.0049174e-2, rtol=1e-1) - # matrix AST on page 171, chapter 3.4.2 - # pd.testing.assert_frame_equal( - # binned_notch_approximation_law._lut_secondary_branch, expected_matrix_AST_171, rtol=1e-3, atol=1e-5) +@pytest.mark.parametrize('delta_load, delta_strain, delta_stress', [ + (101.30, 0.0492e-2, 101.300), + (506.50, 0.2462e-2, 505.784), + (1002.87, 0.4990e-2, 978.725), + (1499.24, 0.8017e-2, 1362.884), + (2005.74, 1.1897e-2, 1649.573) +]) # values from matrix AST in FKM guideline nonlinear on page 171, chapter 3.4.2 +def test_extended_neuber_secondary_example_2(delta_load, delta_strain, delta_stress): + """ example under 2.7.1, p.74 of FKM nonlinear "Akademisches Beispiel" """ + notch_approximation_law = ExtendedNeuber(E=206e3, K=2650.5, n=0.187, K_p=3.5) + + assert np.isclose(notch_approximation_law.stress_secondary_branch(delta_load), delta_stress, rtol=1e-3) + assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress, None), delta_strain, rtol=1e-3) + assert np.isclose(notch_approximation_law.load_secondary_branch(delta_stress), delta_load, rtol=1e-3) def test_extended_neuber_example_no_binning_vectorized(): diff --git a/tests/materiallaws/test_notch_approximation_law_seegerbeste.py b/tests/materiallaws/test_notch_approximation_law_seegerbeste.py index 1b065e4a..b253f725 100644 --- a/tests/materiallaws/test_notch_approximation_law_seegerbeste.py +++ b/tests/materiallaws/test_notch_approximation_law_seegerbeste.py @@ -32,7 +32,7 @@ (176.057, 8.71e-4, 172.639), (359.3, 0.0019, 283.86) ]) -def test_seeger_beste_example_1(load, strain, stress): +def test_seeger_beste_primary_example_1(load, strain, stress): """ example under 2.7.1, p.74 of FKM nonlinear "Akademisches Beispiel" """ notch_approximation_law = SeegerBeste(E=206e3, K=1184, n=0.187, K_p=3.5) @@ -41,11 +41,19 @@ def test_seeger_beste_example_1(load, strain, stress): assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-1) - # matrix AST on page 162, chapter 3.4.1 - # pd.testing.assert_frame_equal( - # binned_notch_approximation_law._lut_secondary_branch, expected_matrix_AST_162_seeger_beste, rtol=1e-3, atol=1e-5) +@pytest.mark.parametrize('delta_load, delta_strain, delta_stress', [ + (100.59, 0.000488, 100.568), + (301.76, 0.001466, 295.844), + (499.34, 0.002456, 449.183), + (700.52, 0.003605, 559.407), +]) # values from matrix AST in FKM guideline nonlinear on page 180, chapter 3.5.1 +def test_seeger_beste_secondary_example_1(delta_load, delta_strain, delta_stress): + """ example under 2.7.1, p.74 of FKM nonlinear "Akademisches Beispiel" """ + notch_approximation_law = SeegerBeste(E=206e3, K=1184, n=0.187, K_p=3.5) - # assert np.isclose(binned_notch_approximation_law._lut_primary_branch.load.max(), maximum_absolute_load) + assert np.isclose(notch_approximation_law.stress_secondary_branch(delta_load), delta_stress, rtol=1e-3) + assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress, None), delta_strain, rtol=1e-3) + assert np.isclose(notch_approximation_law.load_secondary_branch(delta_stress), delta_load, rtol=1e-3) @pytest.mark.parametrize('load, strain, stress', [ @@ -62,9 +70,20 @@ def test_seeger_beste_example_2(load, strain, stress): assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-1) - # # matrix AST on page 171, chapter 3.4.2 - # pd.testing.assert_frame_equal( - # binned_notch_approximation_law._lut_secondary_branch, expected_matrix_AST_171_seeger_beste, rtol=2e-3, atol=1e-5) +@pytest.mark.parametrize('delta_load, delta_strain, delta_stress', [ + (101.3, 0.000492, 101.3), + (496.37, 0.00241, 495.095), + (1002.87, 0.004879, 960.634), + (1499.24, 0.007441, 1304.533), + (2005.74, 0.010471, 1560.847), +]) # values from matrix AST in FKM guideline nonlinear on page 189, chapter 3.5.2 +def test_seeger_beste_secondary_example_2(delta_load, delta_strain, delta_stress): + """ example under 2.7.1, p.74 of FKM nonlinear "Akademisches Beispiel" """ + notch_approximation_law = SeegerBeste(E=206e3, K=2650.5, n=0.187, K_p=3.5) + + assert np.isclose(notch_approximation_law.stress_secondary_branch(delta_load), delta_stress, rtol=1e-3) + assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress, None), delta_strain, rtol=1e-3) + assert np.isclose(notch_approximation_law.load_secondary_branch(delta_stress), delta_load, rtol=1e-3) def test_seeger_beste_example_no_binning(): From fc48415f1d3e98d2f64c9b625e802bab5fcb22d9 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 13 Dec 2024 11:22:05 +0100 Subject: [PATCH 10/17] Drop `load` argument of notch approximation strain methods Signed-off-by: Johannes Mueller --- src/pylife/materiallaws/notch_approximation_law.py | 8 ++++---- .../notch_approximation_law_seegerbeste.py | 4 ++-- tests/materiallaws/test_notch_approximation_law.py | 11 ++++------- .../test_notch_approximation_law_seegerbeste.py | 8 ++++---- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/pylife/materiallaws/notch_approximation_law.py b/src/pylife/materiallaws/notch_approximation_law.py index 37e67abd..be9d5627 100644 --- a/src/pylife/materiallaws/notch_approximation_law.py +++ b/src/pylife/materiallaws/notch_approximation_law.py @@ -82,13 +82,13 @@ def K(self, value): def primary(self, load): load = np.asarray(load) stress = self.stress(load) - strain = self.strain(stress, None) + strain = self.strain(stress) return np.stack([stress, strain], axis=len(load.shape)) def secondary(self, delta_load): delta_load = np.asarray(delta_load) delta_stress = self.stress_secondary_branch(delta_load) - delta_strain = self.strain_secondary_branch(delta_stress, None) + delta_strain = self.strain_secondary_branch(delta_stress) return np.stack([delta_stress, delta_strain], axis=len(delta_load.shape)) @@ -151,7 +151,7 @@ def stress(self, load, *, rtol=1e-4, tol=1e-4): ) return stress - def strain(self, stress, load): + def strain(self, stress): """Calculate the strain of the primary path in the stress-strain diagram at a given stress and load. The formula is given by eq. 2.5-42 of FKM nonlinear. load / stress * self._K_p * e_star @@ -240,7 +240,7 @@ def stress_secondary_branch(self, delta_load, *, rtol=1e-4, tol=1e-4): ) return delta_stress - def strain_secondary_branch(self, delta_stress, delta_load): + def strain_secondary_branch(self, delta_stress): """Calculate the strain on secondary branches in the stress-strain diagram at a given stress and load. The formula is given by eq. 2.5-46 of FKM nonlinear. diff --git a/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py b/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py index 8c78c18e..cc2f0985 100644 --- a/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py +++ b/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py @@ -101,7 +101,7 @@ def stress(self, load, *, rtol=1e-4, tol=1e-4): return stress[0] - def strain(self, stress, load): + def strain(self, stress): '''Calculate the strain of the primary path in the stress-strain diagram at a given stress and load. The formula is given by eq. 2.8-39 of FKM nonlinear. load / stress * self._K_p * e_star @@ -216,7 +216,7 @@ def stress_secondary_branch(self, delta_load, *, rtol=1e-4, tol=1e-4): return delta_stress[0] - def strain_secondary_branch(self, delta_stress, delta_load): + def strain_secondary_branch(self, delta_stress): '''Calculate the strain on secondary branches in the stress-strain diagram at a given stress and load. The formula is given by eq. 2.8-43 of FKM nonlinear. diff --git a/tests/materiallaws/test_notch_approximation_law.py b/tests/materiallaws/test_notch_approximation_law.py index 793f448b..4d8f3e08 100644 --- a/tests/materiallaws/test_notch_approximation_law.py +++ b/tests/materiallaws/test_notch_approximation_law.py @@ -34,7 +34,7 @@ def test_extended_neuber_primary_example_1(load, strain, stress): notch_approximation_law = ExtendedNeuber(E=206e3, K=1184, n=0.187, K_p=3.5) assert np.isclose(notch_approximation_law.stress(load), stress, rtol=1e-3) - assert np.isclose(notch_approximation_law.strain(stress, None), strain, rtol=1e-1) + assert np.isclose(notch_approximation_law.strain(stress), strain, rtol=1e-1) assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-3) @@ -49,7 +49,7 @@ def test_extended_neuber_secondary_example_1(delta_load, delta_strain, delta_str notch_approximation_law = ExtendedNeuber(E=206e3, K=1184, n=0.187, K_p=3.5) assert np.isclose(notch_approximation_law.stress_secondary_branch(delta_load), delta_stress, rtol=1e-3) - assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress, None), delta_strain, rtol=1e-3) + assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress), delta_strain, rtol=1e-3) assert np.isclose(notch_approximation_law.load_secondary_branch(delta_stress), delta_load, rtol=1e-3) @@ -63,12 +63,9 @@ def test_extended_neuber_primary_example_2(load, strain, stress): notch_approximation_law = ExtendedNeuber(E=206e3, K=2650.5, n=0.187, K_p=3.5) assert np.isclose(notch_approximation_law.stress(load), stress, rtol=1e-1) - assert np.isclose(notch_approximation_law.strain(stress, None), strain, rtol=1e-1) + assert np.isclose(notch_approximation_law.strain(stress), strain, rtol=1e-1) assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-1) - assert np.isclose(notch_approximation_law.stress_secondary_branch(10.13), 10.130) - assert np.isclose(notch_approximation_law.strain_secondary_branch(10.30, 10.13), 0.0049174e-2, rtol=1e-1) - @pytest.mark.parametrize('delta_load, delta_strain, delta_stress', [ (101.30, 0.0492e-2, 101.300), @@ -82,7 +79,7 @@ def test_extended_neuber_secondary_example_2(delta_load, delta_strain, delta_str notch_approximation_law = ExtendedNeuber(E=206e3, K=2650.5, n=0.187, K_p=3.5) assert np.isclose(notch_approximation_law.stress_secondary_branch(delta_load), delta_stress, rtol=1e-3) - assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress, None), delta_strain, rtol=1e-3) + assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress), delta_strain, rtol=1e-3) assert np.isclose(notch_approximation_law.load_secondary_branch(delta_stress), delta_load, rtol=1e-3) diff --git a/tests/materiallaws/test_notch_approximation_law_seegerbeste.py b/tests/materiallaws/test_notch_approximation_law_seegerbeste.py index b253f725..b6b2795b 100644 --- a/tests/materiallaws/test_notch_approximation_law_seegerbeste.py +++ b/tests/materiallaws/test_notch_approximation_law_seegerbeste.py @@ -37,7 +37,7 @@ def test_seeger_beste_primary_example_1(load, strain, stress): notch_approximation_law = SeegerBeste(E=206e3, K=1184, n=0.187, K_p=3.5) assert np.isclose(notch_approximation_law.stress(load), stress, rtol=1e-1) - assert np.isclose(notch_approximation_law.strain(stress, None), strain, rtol=1e-1) + assert np.isclose(notch_approximation_law.strain(stress), strain, rtol=1e-1) assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-1) @@ -52,7 +52,7 @@ def test_seeger_beste_secondary_example_1(delta_load, delta_strain, delta_stress notch_approximation_law = SeegerBeste(E=206e3, K=1184, n=0.187, K_p=3.5) assert np.isclose(notch_approximation_law.stress_secondary_branch(delta_load), delta_stress, rtol=1e-3) - assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress, None), delta_strain, rtol=1e-3) + assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress), delta_strain, rtol=1e-3) assert np.isclose(notch_approximation_law.load_secondary_branch(delta_stress), delta_load, rtol=1e-3) @@ -66,7 +66,7 @@ def test_seeger_beste_example_2(load, strain, stress): notch_approximation_law = SeegerBeste(E=206e3, K=2650.5, n=0.187, K_p=3.5) assert np.isclose(notch_approximation_law.stress(load), stress, rtol=1e-1) - assert np.isclose(notch_approximation_law.strain(stress, None), strain, rtol=1e-1) + assert np.isclose(notch_approximation_law.strain(stress), strain, rtol=1e-1) assert np.isclose(notch_approximation_law.load(stress), load, rtol=1e-1) @@ -82,7 +82,7 @@ def test_seeger_beste_secondary_example_2(delta_load, delta_strain, delta_stress notch_approximation_law = SeegerBeste(E=206e3, K=2650.5, n=0.187, K_p=3.5) assert np.isclose(notch_approximation_law.stress_secondary_branch(delta_load), delta_stress, rtol=1e-3) - assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress, None), delta_strain, rtol=1e-3) + assert np.isclose(notch_approximation_law.strain_secondary_branch(delta_stress), delta_strain, rtol=1e-3) assert np.isclose(notch_approximation_law.load_secondary_branch(delta_stress), delta_load, rtol=1e-3) From 190e491d87211d29ac4457759bec61ef7a3981b8 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 13 Dec 2024 13:28:15 +0100 Subject: [PATCH 11/17] Let FKMNonLinearDetector initialize NotchApproxBinner Signed-off-by: Johannes Mueller --- .../assessment_nonlinear_standard.py | 4 +- src/pylife/stress/rainflow/fkm_nonlinear.py | 19 +- tests/strength/test_damage_calculator.py | 12 +- tests/stress/rainflow/test_fkm_nonlinear.py | 172 +++++------------- 4 files changed, 70 insertions(+), 137 deletions(-) diff --git a/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py b/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py index 6f9592f7..a59f4c03 100644 --- a/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py +++ b/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py @@ -409,7 +409,7 @@ def _compute_hcm_RAM(assessment_parameters, scaled_load_sequence, maximum_absolu # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=extended_neuber_binned) + recorder=recorder, notch_approximation_law=extended_neuber_binned, binner=None) # perform HCM algorithm, first run detector.process_hcm_first(scaled_load_sequence) @@ -503,7 +503,7 @@ def _compute_hcm_RAJ(assessment_parameters, scaled_load_sequence, maximum_absolu # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=seeger_beste_binned) + recorder=recorder, notch_approximation_law=seeger_beste_binned, binner=None) detector_1st = copy.deepcopy(detector) # perform HCM algorithm, first run diff --git a/src/pylife/stress/rainflow/fkm_nonlinear.py b/src/pylife/stress/rainflow/fkm_nonlinear.py index 22deabbf..db61d1dc 100644 --- a/src/pylife/stress/rainflow/fkm_nonlinear.py +++ b/src/pylife/stress/rainflow/fkm_nonlinear.py @@ -21,6 +21,7 @@ import pandas as pd import pylife.stress.rainflow.general as RFG +import pylife.materiallaws.notch_approximation_law as NAL INDEX = 0 LOAD_TYPE = 1 @@ -86,9 +87,15 @@ class FKMNonlinearDetector(RFG.AbstractDetector): """ - def __init__(self, recorder, notch_approximation_law): + def __init__(self, recorder, notch_approximation_law, binner=NAL.NotchApproxBinner): super().__init__(recorder) - self._notch_approximation_law = notch_approximation_law + + if binner is not None: + self._binner = binner(notch_approximation_law) + self._notch_approximation_law = self._binner + else: + self._binner = None + self._notch_approximation_law = notch_approximation_law if notch_approximation_law is not None: self._ramberg_osgood_relation = self._notch_approximation_law.ramberg_osgood_relation @@ -252,6 +259,14 @@ def _determine_load_turning_points(self, samples, flush): else: rep_samples = np.asarray(samples) + if self._binner is not None: + if have_multi_index: + load_max_idx = samples.groupby("load_step").first().abs().idxmax() + load_max = samples.xs(load_max_idx, level="load_step").abs() + else: + load_max = np.abs(samples).max() + self._binner.initialize(load_max) + loads_indices, load_turning_points = self._new_turns(rep_samples, flush) self._group_size = len(samples) // len(rep_samples) diff --git a/tests/strength/test_damage_calculator.py b/tests/strength/test_damage_calculator.py index 96910856..c7c620c5 100644 --- a/tests/strength/test_damage_calculator.py +++ b/tests/strength/test_damage_calculator.py @@ -105,7 +105,8 @@ def test_woehler_curve_P_RAM_collective_has_no_index(): # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=extended_neuber_binned) + recorder=recorder, notch_approximation_law=extended_neuber_binned, binner=None + ) # perform HCM algorithm, first run detector.process_hcm_first(load_sequence_list) @@ -214,7 +215,8 @@ def test_woehler_curve_P_RAM_collective_has_MultiIndex(): # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=extended_neuber_binned) + recorder=recorder, notch_approximation_law=extended_neuber_binned, binner=None + ) # perform HCM algorithm, first run detector.process_hcm_first(load_sequence_list) @@ -310,7 +312,8 @@ def test_woehler_curve_P_RAJ_has_no_index(): # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=seeger_beste_binned) + recorder=recorder, notch_approximation_law=seeger_beste_binned, binner=None + ) # perform HCM algorithm, first run detector.process_hcm_first(load_sequence_list) @@ -418,7 +421,8 @@ def test_woehler_curve_P_RAJ_has_MultiIndex(): # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=seeger_beste_binned) + recorder=recorder, notch_approximation_law=seeger_beste_binned, binner=None + ) # perform HCM algorithm, first run detector.process_hcm_first(load_sequence_list) diff --git a/tests/stress/rainflow/test_fkm_nonlinear.py b/tests/stress/rainflow/test_fkm_nonlinear.py index aba64c5c..c8b4fcb3 100644 --- a/tests/stress/rainflow/test_fkm_nonlinear.py +++ b/tests/stress/rainflow/test_fkm_nonlinear.py @@ -56,14 +56,8 @@ def setUp(self): # initialize notch approximation law extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - # first run - detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber_binned) + detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber) detector.process(signal) # second run @@ -103,6 +97,31 @@ def test_interpolation(self): pd.testing.assert_frame_equal(df, expected, rtol=1e-1) +def test_no_binning_class(): + signal = np.array([100, 0, 80, 20, 60, 40]) + + E = 206e3 # [MPa] Young's modulus + K = 2650 # 1184 [MPa] + n = 0.187 # [-] + K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) + + # initialize notch approximation law + extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) + + detector = FKMNonlinearDetector( + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber, binner=None + ) + detector.process_hcm_first(signal).process_hcm_second(signal) + + recorder = detector.recorder + np.testing.assert_array_equal(recorder.loads_min, np.array([40., 20., 0.])) + np.testing.assert_array_equal(recorder.loads_max, np.array([60., 80., 100.])) + np.testing.assert_allclose(recorder.S_min, np.array([39.997581, 19.997582, -0.002388]), rtol=1e-3, atol=1e-6) + np.testing.assert_allclose(recorder.S_max, np.array([59.997581, 79.997574, 99.997488]), rtol=1e-3, atol=1e-6) + np.testing.assert_allclose(recorder.epsilon_min, np.array([1.941866e-04, 9.709922e-05, 1.169416e-08]), rtol=1e-3, atol=1e-6) + np.testing.assert_allclose(recorder.epsilon_max, np.array([0.000291, 0.000388, 0.000485]), rtol=1e-3, atol=1e-6) + + class TestFKMMemory1Inner(unittest.TestCase): """Example given in FKM nonlinear 3.2.1, p.147 """ @@ -120,14 +139,8 @@ def setUp(self): # initialize notch approximation law extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - # first run - detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber_binned) + detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber) detector.process_hcm_first(signal) # second run @@ -198,14 +211,8 @@ def setUp(self): # initialize notch approximation law extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - # first run - detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber_binned) + detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber) detector.process_hcm_first(signal) # second run @@ -274,14 +281,8 @@ def setUp(self): # initialize notch approximation law extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(abs(self.signal)) - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - # first run - detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber_binned) + detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber) detector.process_hcm_first(self.signal) self._detector_1st = copy.deepcopy(detector) @@ -344,15 +345,8 @@ def setUp(self): # initialize notch approximation law extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(abs(signal)) - - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - # first run - detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber_binned) + detector = FKMNonlinearDetector(recorder=self._recorder, notch_approximation_law=extended_neuber) detector.process_hcm_first(signal) # second run @@ -423,15 +417,8 @@ def test_edge_case_value_in_sample_tail_simple_signal(vals, expected_loads_min, extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - maximum_absolute_load = max(abs(signal)) - - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - detector = FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), - notch_approximation_law=extended_neuber_binned + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber ) detector.process(signal).process(signal) @@ -473,15 +460,8 @@ def test_edge_case_value_in_sample_tail(vals, expected_loads_min, expected_loads extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - maximum_absolute_load = max(abs(signal)) - - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - detector = FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), - notch_approximation_law=extended_neuber_binned + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber ) detector.process(signal).process(signal) @@ -519,17 +499,8 @@ def test_flush_edge_case_load(): extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - maximum_absolute_load = pd.concat([signal_1, signal_2]).abs().groupby("node_id").max() - - print(maximum_absolute_load) - - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - detector = FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), - notch_approximation_law=extended_neuber_binned + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber ) detector.process(signal_1, flush=True).process(signal_2, flush=True) @@ -588,15 +559,8 @@ def test_flush_edge_case_load_simple_signal(): extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - maximum_absolute_load = max(abs(np.concatenate([signal_1, signal_2]))) - - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - detector = FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), - notch_approximation_law=extended_neuber_binned + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber ) detector.process(signal_1, flush=True).process(signal_2, flush=True) @@ -627,15 +591,8 @@ def test_flush_edge_case_S_simple_signal(): extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - maximum_absolute_load = max(abs(np.concatenate([signal_1, signal_2]))) - - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - detector = FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), - notch_approximation_law=extended_neuber_binned + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber ) detector.process(signal_1, flush=True).process(signal_2, flush=True) @@ -675,15 +632,8 @@ def test_flush_edge_case_S(): extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - maximum_absolute_load = pd.concat([signal_1, signal_2]).abs().groupby("node_id").max() - - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - detector = FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), - notch_approximation_law=extended_neuber_binned + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber ) detector.process(signal_1, flush=True).process(signal_2, flush=True) @@ -768,27 +718,16 @@ def test_edge_case_value_in_sample_tail_compare_simple(vals, num): extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - maximum_absolute_load_simple = max(abs(vals)) - maximum_absolute_load_multiple = signal.abs().groupby('node_id').max() - print("single") - extended_neuber_binned_simple = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load_simple - ) detector_simple = FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), - notch_approximation_law=extended_neuber_binned_simple + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber ) detector_simple.process(vals).process(vals) print("multiple") - extended_neuber_binned_multiple = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load_multiple - ) detector_multiindex = FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), - notch_approximation_law=extended_neuber_binned_multiple + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber ) detector_multiindex.process(signal).process(signal) @@ -852,24 +791,13 @@ def test_hcm_first_second(vals, num): extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - maximum_absolute_load_simple = max(abs(vals)) - maximum_absolute_load_multiple = signal.abs().groupby('node_id').max() - - extended_neuber_binned_simple = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load_simple - ) - extended_neuber_binned_multiple = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load_multiple - ) detector_simple = FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), - notch_approximation_law=extended_neuber_binned_simple + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber ) detector_simple.process_hcm_first(vals).process_hcm_second(vals) detector_multiindex = FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), - notch_approximation_law=extended_neuber_binned_multiple + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=extended_neuber ) detector_multiindex.process_hcm_first(signal).process_hcm_second(signal) @@ -896,10 +824,8 @@ def detector_seeger_beste(): K_p = 3.5 # [-] (de: Traglastformzahl) K_p = F_plastic / F_yield (3.1.1) seeger_beste = SeegerBeste(E, K, n, K_p) - seeger_beste_binned = NAL.NotchApproxBinner(seeger_beste).initialize(800) - return FKMNonlinearDetector( - recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=seeger_beste_binned + recorder=RFR.FKMNonlinearRecorder(), notch_approximation_law=seeger_beste ) @@ -1095,14 +1021,8 @@ def test_history_guideline_at_once(): # initialize notch approximation law extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - # first run - detector = FKMNonlinearDetector(recorder=recorder, notch_approximation_law=extended_neuber_binned) + detector = FKMNonlinearDetector(recorder=recorder, notch_approximation_law=extended_neuber) detector.process(signal) @@ -1143,14 +1063,8 @@ def test_history_guideline_at_split(split_point): # initialize notch approximation law extended_neuber = NAL.ExtendedNeuber(E, K, n, K_p) - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(abs(signal)) - extended_neuber_binned = NAL.NotchApproxBinner(extended_neuber).initialize( - maximum_absolute_load - ) - # first run - detector = FKMNonlinearDetector(recorder=recorder, notch_approximation_law=extended_neuber_binned) + detector = FKMNonlinearDetector(recorder=recorder, notch_approximation_law=extended_neuber) detector.process(signal[:split_point]).process(signal[split_point:]) From 603a28f2db01cdc6b1f3d4d2c17c297c93837e0e Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 13 Dec 2024 13:59:27 +0100 Subject: [PATCH 12/17] Use automatic binning of FKMNonLinearDetector Signed-off-by: Johannes Mueller --- .../notch_approximation_law_seegerbeste.py | 11 +++--- .../assessment_nonlinear_standard.py | 24 ++++-------- tests/strength/test_damage_calculator.py | 37 ++----------------- 3 files changed, 17 insertions(+), 55 deletions(-) diff --git a/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py b/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py index cc2f0985..6551947e 100644 --- a/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py +++ b/src/pylife/materiallaws/notch_approximation_law_seegerbeste.py @@ -77,7 +77,7 @@ def stress(self, load, *, rtol=1e-4, tol=1e-4): The resulting stress ''' # initial value as given by correction document to FKM nonlinear - x0 = load * (1 - (1 - 1/self._K_p)/1000) + x0 = np.asarray(load * (1 - (1 - 1/self._K_p)/1000)) # suppress the divergence warnings with warnings.catch_warnings(): @@ -96,7 +96,8 @@ def stress(self, load, *, rtol=1e-4, tol=1e-4): # or (value, converged, zero_der) for vector-valued invocation # only for multiple points at once, if some points diverged - if len(stress) == 3 and not stress[1].all(): + multidim = len(x0.shape) > 1 and x0.shape[1] > 1 + if multidim and not stress[1].all(): stress = self._stress_fix_not_converged_values(stress, load, x0, rtol, tol) return stress[0] @@ -461,7 +462,7 @@ def _stress_fix_not_converged_values(self, stress, load, x0, rtol, tol): '''For the values that did not converge in the previous vectorized call to optimize.newton, call optimize.newton again on the scalar value. This usually finds the correct solution.''' - indices_diverged = [index for index, is_converged in enumerate(stress[1]) if not is_converged] + indices_diverged = np.where(~stress[1].all(axis=1))[0] x0_array = np.asarray(x0) load_array = np.asarray(load) @@ -471,13 +472,13 @@ def _stress_fix_not_converged_values(self, stress, load, x0, rtol, tol): load_diverged = load_array[index_diverged] result = optimize.newton( func=self._stress_implicit, - x0=x0_diverged, + x0=np.asarray(x0_diverged), args=([load_diverged]), full_output=True, rtol=rtol, tol=tol, maxiter=50 ) - if result[1].converged: + if result.converged.all(): stress[0][index_diverged] = result[0] return stress diff --git a/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py b/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py index a59f4c03..393f0638 100644 --- a/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py +++ b/src/pylife/strength/fkm_nonlinear/assessment_nonlinear_standard.py @@ -389,7 +389,7 @@ def _compute_component_woehler_curves(assessment_parameters): return assessment_parameters, component_woehler_curve_P_RAM, component_woehler_curve_P_RAJ -def _compute_hcm_RAM(assessment_parameters, scaled_load_sequence, maximum_absolute_load): +def _compute_hcm_RAM(assessment_parameters, scaled_load_sequence, maximum_absolute_): """Perform the HCM rainflow counting with the extended Neuber notch approximation. The HCM algorithm is executed twice, as described in the FKM nonlinear guideline.""" @@ -397,19 +397,13 @@ def _compute_hcm_RAM(assessment_parameters, scaled_load_sequence, maximum_absolu E, K_prime, n_prime, K_p = assessment_parameters[["E", "K_prime", "n_prime", "K_p"]] extended_neuber = pylife.materiallaws.notch_approximation_law.ExtendedNeuber(E, K_prime, n_prime, K_p) - # wrap the notch approximation law by a binning class, which precomputes the values - extended_neuber_binned = ( - pylife.materiallaws.notch_approximation_law.NotchApproxBinner( - extended_neuber - ).initialize(maximum_absolute_load) - ) - # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=extended_neuber_binned, binner=None) + recorder=recorder, notch_approximation_law=extended_neuber + ) # perform HCM algorithm, first run detector.process_hcm_first(scaled_load_sequence) @@ -418,7 +412,7 @@ def _compute_hcm_RAM(assessment_parameters, scaled_load_sequence, maximum_absolu # perform HCM algorithm, second run detector.process_hcm_second(scaled_load_sequence) - return detector_1st, detector, extended_neuber_binned, recorder + return detector_1st, detector, extended_neuber, recorder def _compute_damage_and_lifetimes_RAM(assessment_parameters, recorder, component_woehler_curve_P_RAM, result): @@ -493,17 +487,13 @@ def _compute_hcm_RAJ(assessment_parameters, scaled_load_sequence, maximum_absolu E, K_prime, n_prime, K_p = assessment_parameters[["E", "K_prime", "n_prime", "K_p"]] seeger_beste = pylife.materiallaws.notch_approximation_law_seegerbeste.SeegerBeste(E, K_prime, n_prime, K_p) - # wrap the notch approximation law by a binning class, which precomputes the values - seeger_beste_binned = pylife.materiallaws.notch_approximation_law.NotchApproxBinner( - seeger_beste - ).initialize(maximum_absolute_load) - # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=seeger_beste_binned, binner=None) + recorder=recorder, notch_approximation_law=seeger_beste + ) detector_1st = copy.deepcopy(detector) # perform HCM algorithm, first run @@ -512,7 +502,7 @@ def _compute_hcm_RAJ(assessment_parameters, scaled_load_sequence, maximum_absolu # perform HCM algorithm, second run detector.process_hcm_second(scaled_load_sequence) - return detector_1st, detector, seeger_beste_binned, recorder + return detector_1st, detector, seeger_beste, recorder def _compute_damage_and_lifetimes_RAJ(assessment_parameters, recorder, component_woehler_curve_P_RAJ, result): diff --git a/tests/strength/test_damage_calculator.py b/tests/strength/test_damage_calculator.py index c7c620c5..8fcb29e8 100644 --- a/tests/strength/test_damage_calculator.py +++ b/tests/strength/test_damage_calculator.py @@ -92,20 +92,12 @@ def test_woehler_curve_P_RAM_collective_has_no_index(): load_sequence_list = pd.Series([143.696, -287.392, 143.696, -359.240, 287.392, 0.000, 287.392, -287.392]) - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(np.abs(load_sequence_list)) - extended_neuber_binned = ( - pylife.materiallaws.notch_approximation_law.NotchApproxBinner( - extended_neuber - ).initialize(maximum_absolute_load) - ) - # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=extended_neuber_binned, binner=None + recorder=recorder, notch_approximation_law=extended_neuber ) # perform HCM algorithm, first run @@ -202,20 +194,12 @@ def test_woehler_curve_P_RAM_collective_has_MultiIndex(): load_sequence_list = pd.Series([143.696, -287.392, 143.696, -359.240, 287.392, 0.000, 287.392, -287.392]) - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(np.abs(load_sequence_list)) - extended_neuber_binned = ( - pylife.materiallaws.notch_approximation_law.NotchApproxBinner( - extended_neuber - ).initialize(maximum_absolute_load) - ) - # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=extended_neuber_binned, binner=None + recorder=recorder, notch_approximation_law=extended_neuber ) # perform HCM algorithm, first run @@ -300,19 +284,12 @@ def test_woehler_curve_P_RAJ_has_no_index(): seeger_beste = pylife.materiallaws.notch_approximation_law_seegerbeste.SeegerBeste(E, K_prime, n_prime, K_p) load_sequence_list = pd.Series([143.696, -287.392, 143.696, -359.240, 287.392, 0.000, 287.392, -287.392]) - - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(np.abs(load_sequence_list)) - seeger_beste_binned = pylife.materiallaws.notch_approximation_law.NotchApproxBinner( - seeger_beste - ).initialize(maximum_absolute_load) - # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=seeger_beste_binned, binner=None + recorder=recorder, notch_approximation_law=seeger_beste ) # perform HCM algorithm, first run @@ -410,18 +387,12 @@ def test_woehler_curve_P_RAJ_has_MultiIndex(): load_sequence_list = pd.Series([143.696, -287.392, 143.696, -359.240, 287.392, 0.000, 287.392, -287.392]) - # wrap the notch approximation law by a binning class, which precomputes the values - maximum_absolute_load = max(np.abs(load_sequence_list)) - seeger_beste_binned = pylife.materiallaws.notch_approximation_law.NotchApproxBinner( - seeger_beste - ).initialize(maximum_absolute_load) - # create recorder object recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder() # create detector object detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector( - recorder=recorder, notch_approximation_law=seeger_beste_binned, binner=None + recorder=recorder, notch_approximation_law=seeger_beste ) # perform HCM algorithm, first run From 980b022dc1bf3be72e91ab37b296bc0eddd62a95 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Mon, 16 Dec 2024 09:52:25 +0100 Subject: [PATCH 13/17] Adjust jupyter notebook Signed-off-by: Johannes Mueller --- demos/fkm_nonlinear/fkm_nonlinear_full.ipynb | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/demos/fkm_nonlinear/fkm_nonlinear_full.ipynb b/demos/fkm_nonlinear/fkm_nonlinear_full.ipynb index 5f0d80c2..04144c7e 100644 --- a/demos/fkm_nonlinear/fkm_nonlinear_full.ipynb +++ b/demos/fkm_nonlinear/fkm_nonlinear_full.ipynb @@ -215,7 +215,7 @@ "plt.rcParams.update({'font.size': 22})\n", "plt.plot(range(len(scaled_load_sequence)), load_sequence, \"o-\", c=\"g\", lw=3)\n", "plt.grid()\n", - "plt.ylabel(\"$\\sigma_V$ [MPa]\")" + "plt.ylabel(r\"$\\sigma_V$ [MPa]\")" ] }, { @@ -559,18 +559,13 @@ "load_sequence_list = scaled_load_sequence\n", "print(load_sequence_list)\n", "\n", - "# wrap the notch approximation law by a binning class, which precomputes the values\n", - "maximum_absolute_load = max(np.abs(load_sequence_list))\n", - "print(f\"maximum_absolute_load: {maximum_absolute_load}\")\n", - "extended_neuber_binned = pylife.materiallaws.notch_approximation_law.Binned(\n", - " extended_neuber, maximum_absolute_load, 100)\n", - "\n", "# create recorder object\n", "recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder()\n", "\n", "# create detector object\n", "detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector(\n", - " recorder=recorder, notch_approximation_law=extended_neuber_binned)\n", + " recorder=recorder, notch_approximation_law=extended_neuber\n", + ")\n", "\n", "# perform HCM algorithm, first run\n", "detector.process(load_sequence_list, flush=False)\n", @@ -759,18 +754,13 @@ "load_sequence_list = scaled_load_sequence\n", "print(load_sequence_list)\n", "\n", - "# wrap the notch approximation law by a binning class, which precomputes the values\n", - "maximum_absolute_load = max(np.abs(load_sequence_list))\n", - "print(f\"maximum_absolute_load: {maximum_absolute_load}\")\n", - "seeger_beste_binned = pylife.materiallaws.notch_approximation_law.Binned(\n", - " seeger_beste, maximum_absolute_load, 100)\n", - "\n", "# create recorder object\n", "recorder = pylife.stress.rainflow.recorders.FKMNonlinearRecorder()\n", "\n", "# create detector object\n", "detector = pylife.stress.rainflow.fkm_nonlinear.FKMNonlinearDetector(\n", - " recorder=recorder, notch_approximation_law=seeger_beste_binned)\n", + " recorder=recorder, notch_approximation_law=seeger_beste\n", + ")\n", "\n", "# perform HCM algorithm, first run\n", "detector.process(load_sequence_list, flush=False)\n", From 06e565c9cd401d16ac1ab57bd3bf85cd669d559a Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Mon, 16 Dec 2024 10:14:28 +0100 Subject: [PATCH 14/17] Don't regenerate the test signal while collecting benchmark tests Signed-off-by: Johannes Mueller --- benchmarks/generate_time_signal.py | 34 ++++++++++++++++-------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/benchmarks/generate_time_signal.py b/benchmarks/generate_time_signal.py index 1986ce28..05777e56 100644 --- a/benchmarks/generate_time_signal.py +++ b/benchmarks/generate_time_signal.py @@ -2,21 +2,23 @@ import numpy as np import pylife.stress.timesignal as TS -np.random.seed(23424711) -load_signal = TS.TimeSignalGenerator( - 10, - { - 'number': 50_000, - 'amplitude_median': 50.0, - 'amplitude_std_dev': 0.5, - 'frequency_median': 4, - 'frequency_std_dev': 3, - 'offset_median': 0, - 'offset_std_dev': 0.4, - }, - None, - None, -).query(1_000_000) +if __name__ == "__main__": + np.random.seed(23424711) -np.savetxt('load_signal.csv', load_signal) + load_signal = TS.TimeSignalGenerator( + 10, + { + 'number': 50_000, + 'amplitude_median': 50.0, + 'amplitude_std_dev': 0.5, + 'frequency_median': 4, + 'frequency_std_dev': 3, + 'offset_median': 0, + 'offset_std_dev': 0.4, + }, + None, + None, + ).query(1_000_000) + + np.savetxt('load_signal.csv', load_signal) From 7de23feda65bdccab9de8fd39499413328747329 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Mon, 16 Dec 2024 12:55:28 +0100 Subject: [PATCH 15/17] Add docstrings for .primary() and .secondary() Signed-off-by: Johannes Mueller --- .../materiallaws/notch_approximation_law.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/pylife/materiallaws/notch_approximation_law.py b/src/pylife/materiallaws/notch_approximation_law.py index be9d5627..e1ba8295 100644 --- a/src/pylife/materiallaws/notch_approximation_law.py +++ b/src/pylife/materiallaws/notch_approximation_law.py @@ -80,12 +80,50 @@ def K(self, value): self.K_prime = value def primary(self, load): + """Calculate stress and strain for primary branch. + + Parameters + ---------- + load : array-like + The load for which the stress and strain are to be calculated + + Returns + ------- + stress strain : ndarray + The resulting stress strain data. + + If the argument is scalar, the resulting array is of the strucuture + ``[<σ>, <ε>]`` + + If the argument is an 1D-array with length `n`the resulting array is of the + structure ``[[<σ1>, <σ2>, <σ3>, ... <σn>], [<ε1>, <ε2>, <ε3>, ... <εn>]]`` + + """ load = np.asarray(load) stress = self.stress(load) strain = self.strain(stress) return np.stack([stress, strain], axis=len(load.shape)) def secondary(self, delta_load): + """Calculate stress and strain for secondary branch. + + Parameters + ---------- + load : array-like + The load for which the stress and strain are to be calculated + + Returns + ------- + stress strain : ndarray + The resulting stress strain data. + + If the argument is scalar, the resulting array is of the strucuture + ``[<σ>, <ε>]`` + + If the argument is an 1D-array with length `n`the resulting array is of the + structure ``[[<σ1>, <σ2>, <σ3>, ... <σn>], [<ε1>, <ε2>, <ε3>, ... <εn>]]`` + + """ delta_load = np.asarray(delta_load) delta_stress = self.stress_secondary_branch(delta_load) delta_strain = self.strain_secondary_branch(delta_stress) From 3d8b326da3f503105a79058d7f50ab40f1351e07 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Tue, 17 Dec 2024 09:27:35 +0100 Subject: [PATCH 16/17] Add abstract base class methods to NotchApproximationLawBase Signed-off-by: Johannes Mueller --- .../materiallaws/notch_approximation_law.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/pylife/materiallaws/notch_approximation_law.py b/src/pylife/materiallaws/notch_approximation_law.py index e1ba8295..d067cd23 100644 --- a/src/pylife/materiallaws/notch_approximation_law.py +++ b/src/pylife/materiallaws/notch_approximation_law.py @@ -17,13 +17,15 @@ __author__ = ["Benjamin Maier"] __maintainer__ = __author__ +from abc import ABC, abstractmethod + import numpy as np from scipy import optimize import pandas as pd import pylife.materiallaws.rambgood -class NotchApproximationLawBase: +class NotchApproximationLawBase(ABC): """This is a base class for any notch approximation law, e.g., the extended Neuber and the Seeger-Beste laws. It initializes the internal variables used by the derived classes and provides getters and setters. @@ -79,6 +81,30 @@ def K(self, value): """Set the strain hardening coefficient""" self.K_prime = value + @abstractmethod + def load(self, load, *, rtol=1e-4, tol=1e-4): + ... + + @abstractmethod + def stress(self, load, *, rtol=1e-4, tol=1e-4): + ... + + @abstractmethod + def strain(self, load): + ... + + @abstractmethod + def load_secondary_branch(self, load, *, rtol=1e-4, tol=1e-4): + ... + + @abstractmethod + def stress_secondary_branch(self, load, *, rtol=1e-4, tol=1e-4): + ... + + @abstractmethod + def strain_secondary_branch(self, load): + ... + def primary(self, load): """Calculate stress and strain for primary branch. From c2dd7b9551d4b829c7f89c822500c4db9f351629 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Tue, 17 Dec 2024 09:30:39 +0100 Subject: [PATCH 17/17] Drop obsolete file Signed-off-by: Johannes Mueller --- .../notch_approximation_law_base.py | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 src/pylife/materiallaws/notch_approximation_law_base.py diff --git a/src/pylife/materiallaws/notch_approximation_law_base.py b/src/pylife/materiallaws/notch_approximation_law_base.py deleted file mode 100644 index c7f35054..00000000 --- a/src/pylife/materiallaws/notch_approximation_law_base.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (c) 2019-2023 - for information on the respective copyright owner -# see the NOTICE file and/or the repository -# https://github.com/boschresearch/pylife -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -__author__ = ["Benjamin Maier"] -__maintainer__ = __author__ - -import pylife.materiallaws.rambgood - -class NotchApproximationLawBase: - """This is a base class for any notch approximation law, e.g., the extended Neuber and the Seeger-Beste laws. - - It initializes the internal variables used by the derived classes and provides getters and setters. - """ - - def __init__(self, E, K, n, K_p=None): - self._E = E - self._K = K - self._n = n - self._K_p = K_p - - self._ramberg_osgood_relation = pylife.materiallaws.rambgood.RambergOsgood(E, K, n) - - @property - def E(self): - '''Get Young's Modulus''' - return self._E - - @property - def K(self): - '''Get the strength coefficient''' - return self._K - - @property - def n(self): - '''Get the strain hardening coefficient''' - return self._n - - @property - def K_p(self): - '''Get the shape factor (de: Traglastformzahl)''' - return self._K_p - - @property - def ramberg_osgood_relation(self): - '''Get the ramberg osgood relation object - ''' - return self._ramberg_osgood_relation - - @K_p.setter - def K_p(self, value): - """Set the shape factor value K_p (de: Traglastformzahl)""" - self._K_p = value - - @K.setter - def K_prime(self, value): - """Set the strain hardening coefficient""" - self._K = value - self._ramberg_osgood_relation = pylife.materiallaws.rambgood.RambergOsgood(self._E, self._K, self._n) - - @K.setter - def K(self, value): - """Set the strain hardening coefficient""" - self.K_prime = value -