From 543bbc99d46830905a58068151cbcc566e16a6f8 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 29 Sep 2023 14:52:37 +0200 Subject: [PATCH 1/4] Make wc_data and wc_int_overflow test data object a fixture ... an fix hereby detected broken test Signed-off-by: Johannes Mueller --- tests/materiallaws/test_woehlercurve.py | 112 +++++++++++++----------- 1 file changed, 61 insertions(+), 51 deletions(-) diff --git a/tests/materiallaws/test_woehlercurve.py b/tests/materiallaws/test_woehlercurve.py index f8617a00..0022a3d9 100644 --- a/tests/materiallaws/test_woehlercurve.py +++ b/tests/materiallaws/test_woehlercurve.py @@ -26,7 +26,9 @@ from pylife.materiallaws import WoehlerCurve -wc_data = pd.Series({ +@pytest.fixture +def wc_data(): + return pd.Series({ 'k_1': 7., 'TN': 1.75, 'ND': 1e6, @@ -34,7 +36,7 @@ }) -def test_woehler_accessor(): +def test_woehler_accessor(wc_data): wc = wc_data.drop('TN') for key in wc.index: @@ -129,7 +131,7 @@ def test_woehler_transform_probability_SD_0(): pd.testing.assert_series_equal(transformed_back, wc_50) -def test_woehler_basquin_cycles_50_single_load_single_wc(): +def test_woehler_basquin_cycles_50_single_load_single_wc(wc_data): load = 500. cycles = wc_data.woehler.basquin_cycles(load) @@ -138,7 +140,7 @@ def test_woehler_basquin_cycles_50_single_load_single_wc(): np.testing.assert_allclose(cycles, expected_cycles, rtol=1e-4) -def test_woehler_basquin_cycles_50_multiple_load_single_wc(): +def test_woehler_basquin_cycles_50_multiple_load_single_wc(wc_data): load = [200., 300., 400., 500.] cycles = wc_data.woehler.basquin_cycles(load) @@ -150,13 +152,13 @@ def test_woehler_basquin_cycles_50_multiple_load_single_wc(): def test_woehler_basquin_cycles_50_single_load_multiple_wc(): load = 400. - wc_data = pd.DataFrame({ + wc = pd.DataFrame({ 'k_1': [1., 2., 2.], 'SD': [300., 400., 500.], 'ND': [1e6, 1e6, 1e6] }) - cycles = wc_data.woehler.basquin_cycles(load) + cycles = wc.woehler.basquin_cycles(load) expected_cycles = [7.5e5, 1e6, np.inf] np.testing.assert_allclose(cycles, expected_cycles, rtol=1e-4) @@ -165,13 +167,13 @@ def test_woehler_basquin_cycles_50_single_load_multiple_wc(): def test_woehler_basquin_cycles_50_multiple_load_multiple_wc(): load = [3000., 400., 500.] - wc_data = pd.DataFrame({ + wc = pd.DataFrame({ 'k_1': [1., 2., 2.], 'SD': [300., 400., 500.], 'ND': [1e6, 1e6, 1e6] }) - cycles = wc_data.woehler.basquin_cycles(load) + cycles = wc.woehler.basquin_cycles(load) expected_cycles = [1e5, 1e6, 1e6] np.testing.assert_allclose(cycles, expected_cycles, rtol=1e-4) @@ -181,13 +183,13 @@ def test_woehler_basquin_cycles_50_multiple_load_multiple_wc_aligned_index(): index = pd.Index([1, 2, 3], name='element_id') load = pd.Series([3000., 400., 500.], index=index) - wc_data = pd.DataFrame({ + wc = pd.DataFrame({ 'k_1': [1., 2., 2.], 'SD': [300., 400., 500.], 'ND': [1e6, 1e6, 1e6] }, index=index) - cycles = wc_data.woehler.basquin_cycles(load) + cycles = wc.woehler.basquin_cycles(load) expected_cycles = pd.Series([1e5, 1e6, 1e6], index=index) pd.testing.assert_series_equal(cycles, expected_cycles, rtol=1e-4) @@ -196,13 +198,13 @@ def test_woehler_basquin_cycles_50_multiple_load_multiple_wc_aligned_index(): def test_woehler_basquin_cycles_50_multiple_load_multiple_wc_cross_index(): load = pd.Series([3000., 400., 500.], index=pd.Index([1, 2, 3], name='scenario')) - wc_data = pd.DataFrame({ + wc = pd.DataFrame({ 'k_1': [1., 2., 2.], 'SD': [300., 400., 500.], 'ND': [1e6, 1e6, 1e6] }, pd.Index([1, 2, 3], name='element_id')) - cycles = wc_data.woehler.basquin_cycles(load) + cycles = wc.woehler.basquin_cycles(load) expected_index = pd.MultiIndex.from_tuples([ (1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), @@ -212,7 +214,7 @@ def test_woehler_basquin_cycles_50_multiple_load_multiple_wc_cross_index(): pd.testing.assert_index_equal(cycles.index, expected_index) -def test_woehler_basquin_cycles_50_same_k(): +def test_woehler_basquin_cycles_50_same_k(wc_data): load = [200., 300., 400., 500.] wc = wc_data.copy() @@ -223,7 +225,7 @@ def test_woehler_basquin_cycles_50_same_k(): np.testing.assert_approx_equal(calculated_k, wc.k_1) -def test_woehler_basquin_cycles_10_90(): +def test_woehler_basquin_cycles_10_90(wc_data): load = [200., 300., 400., 500.] cycles_10 = wc_data.woehler.basquin_cycles(load, 0.1)[1:] @@ -233,7 +235,7 @@ def test_woehler_basquin_cycles_10_90(): np.testing.assert_allclose(cycles_90/cycles_10, expected) -def test_woehler_basquin_load_50_single_cycles_single_wc(): +def test_woehler_basquin_load_50_single_cycles_single_wc(wc_data): cycles = 27994 load = wc_data.woehler.basquin_load(cycles) @@ -242,7 +244,7 @@ def test_woehler_basquin_load_50_single_cycles_single_wc(): np.testing.assert_allclose(load, expected_load, rtol=1e-4) -def test_woehler_basquin_load_50_multiple_cycles_single_wc(): +def test_woehler_basquin_load_50_multiple_cycles_single_wc(wc_data): cycles = [np.inf, 1e6, 133484, 27994] load = wc_data.woehler.basquin_load(cycles) @@ -253,26 +255,26 @@ def test_woehler_basquin_load_50_multiple_cycles_single_wc(): def test_woehler_basquin_load_single_cycles_multiple_wc(): cycles = 1e6 - wc_data = pd.DataFrame({ + wc = pd.DataFrame({ 'k_1': [1., 2., 2.], 'SD': [300., 400., 500.], 'ND': [1e5, 1e6, 1e6] }) - load = wc_data.woehler.basquin_load(cycles) + load = wc.woehler.basquin_load(cycles) expected_load = [300., 400., 500.] np.testing.assert_allclose(load, expected_load) def test_woehler_basquin_load_multiple_cycles_multiple_wc(): cycles = [1e5, 1e6, 1e7] - wc_data = pd.DataFrame({ + wc = pd.DataFrame({ 'k_1': [1., 2., 2.], 'SD': [300., 400., 500.], 'ND': [1e6, 1e6, 1e6] }) - load = wc_data.woehler.basquin_load(cycles) + load = wc.woehler.basquin_load(cycles) expected_load = [3000., 400., 500.] np.testing.assert_allclose(load, expected_load) @@ -281,14 +283,14 @@ def test_woehler_basquin_load_multiple_cycles_multiple_wc_aligned_index(): index = pd.Index([1, 2, 3], name='element_id') cycles = pd.Series([1e5, 1e6, 1e7], index=index) - wc_data = pd.DataFrame({ + wc = pd.DataFrame({ 'k_1': [1., 2., 2.], 'SD': [300., 400., 500.], 'ND': [1e6, 1e6, 1e6], }, index=index) expected_load = pd.Series([3000., 400., 500.], index=cycles.index) - load = wc_data.woehler.basquin_load(cycles) + load = wc.woehler.basquin_load(cycles) pd.testing.assert_series_equal(load, expected_load) @@ -296,7 +298,7 @@ def test_woehler_basquin_load_multiple_cycles_multiple_wc_aligned_index(): def test_woehler_basquin_load_multiple_cycles_multiple_wc_cross_index(): cycles = pd.Series([1e5, 1e6, 1e7], index=pd.Index([1, 2, 3], name='scenario')) - wc_data = pd.DataFrame({ + wc = pd.DataFrame({ 'k_1': [1., 2., 2.], 'SD': [300., 400., 500.], 'ND': [1e6, 1e6, 1e6], @@ -308,12 +310,12 @@ def test_woehler_basquin_load_multiple_cycles_multiple_wc_cross_index(): (3, 1), (3, 2), (3, 3), ], names=['element_id', 'scenario']) - load = wc_data.woehler.basquin_load(cycles) + load = wc.woehler.basquin_load(cycles) pd.testing.assert_index_equal(load.index, expected_index) -def test_woehler_basquin_load_50_same_k(): +def test_woehler_basquin_load_50_same_k(wc_data): cycles = [1e7, 1e6, 1e5, 1e4] wc = wc_data.copy() @@ -324,7 +326,7 @@ def test_woehler_basquin_load_50_same_k(): np.testing.assert_approx_equal(calculated_k, wc.k_1) -def test_woehler_basquin_load_10_90(): +def test_woehler_basquin_load_10_90(wc_data): cycles = [1e2, 1e7] load_10 = wc_data.woehler.basquin_load(cycles, 0.1) @@ -335,45 +337,47 @@ def test_woehler_basquin_load_10_90(): np.testing.assert_allclose(load_90/load_10, expected, rtol=1e-4) -def test_woehler_basquin_load_integer_cycles(): +def test_woehler_basquin_load_integer_cycles(wc_data): wc_data.woehler.basquin_load(1000) -def test_woehler_basquin_cycles_integer_load(): +def test_woehler_basquin_cycles_integer_load(wc_data): wc_data.woehler.basquin_cycles(200) -wc_int_overflow = pd.Series({ - 'k_1': 4., - 'SD': 100., - 'ND': 1e6 -}) +@pytest.fixture +def wc_int_overflow(): + return pd.Series({ + 'k_1': 4., + 'SD': 100., + 'ND': 1e6 + }) -def test_woehler_integer_overflow_scalar(): +def test_woehler_integer_overflow_scalar(wc_int_overflow): assert wc_int_overflow.woehler.basquin_cycles(50) > 0.0 -def test_woehler_integer_overflow_list(): +def test_woehler_integer_overflow_list(wc_int_overflow): assert (wc_int_overflow.woehler.basquin_cycles([50, 50]) > 0.0).all() -def test_woehler_integer_overflow_series(): +def test_woehler_integer_overflow_series(wc_int_overflow): load = pd.Series([50, 50], index=pd.Index(['foo', 'bar'])) cycles = wc_int_overflow.woehler.basquin_cycles(load) assert (cycles > 0.0).all() pd.testing.assert_index_equal(cycles.index, load.index) -def test_woehler_ND(): +def test_woehler_ND(wc_data): assert wc_data.woehler.ND == 1e6 -def test_woehler_SD(): +def test_woehler_SD(wc_data): assert wc_data.woehler.SD == 300 -def test_woehler_k_1(): +def test_woehler_k_1(wc_data): assert wc_data.woehler.k_1 == 7. @@ -387,7 +391,7 @@ def test_woehler_TS_and_TN_guessed(): assert wc.woehler.TS == 1.0 -def test_woehler_TS_guessed(): +def test_woehler_TS_guessed(wc_data): wc = wc_data.copy() wc['k_1'] = 0.5 wc['TN'] = 1.5 @@ -406,43 +410,43 @@ def test_woehler_TN_guessed(): assert wc.woehler.TN == 1.5 -def test_woehler_TS_given(): +def test_woehler_TS_given(wc_data): wc_full = wc_data.copy() wc_full['TS'] = 1.25 assert wc_full.woehler.TS == 1.25 -def test_woehler_TN_given(): +def test_woehler_TN_given(wc_data): wc_full = wc_data.copy() wc_full['TN'] = 1.75 assert wc_full.woehler.TN == 1.75 -def test_woehler_pf_guessed(): +def test_woehler_pf_guessed(wc_data): assert wc_data.woehler.failure_probability == 0.5 -def test_woehler_pf_given(): +def test_woehler_pf_given(wc_data): wc = wc_data.copy() wc['failure_probability'] = 0.1 assert wc.woehler.failure_probability == 0.1 -def test_woehler_miner_original(): +def test_woehler_miner_original(wc_data): assert wc_data.woehler.k_2 == np.inf -def test_woehler_miner(): +def test_woehler_miner(wc_data): assert wc_data.woehler.miner_elementary().k_2 == wc_data.k_1 assert wc_data.woehler.miner_elementary().to_pandas().k_2 == wc_data.k_1 -def test_woehler_miner_haibach(): +def test_woehler_miner_haibach(wc_data): assert wc_data.woehler.miner_haibach().k_2 == 13.0 assert wc_data.woehler.miner_haibach().to_pandas().k_2 == 13.0 -def test_woehler_to_pandas(): +def test_woehler_to_pandas(wc_data): expected = pd.Series({ 'k_1': 0.5, 'k_2': np.inf, @@ -470,13 +474,19 @@ def test_woehler_to_pandas(): @pytest.mark.parametrize('pf', [0.1, 0.5, 0.9]) def test_woehler_miner_original_homogenious_load(pf): cycles = np.logspace(3., 7., 50) - load = wc_data.woehler.basquin_load(cycles, failure_probability=pf) - assert (np.diff(load) < 0.).all() + wc = pd.Series({ + 'k_1': 7., + 'TN': 1.75, + 'ND': 1e6, + 'SD': 300.0 + }) + load = wc.woehler.basquin_load(cycles, failure_probability=pf) + assert (np.diff(load) <= 0.).all() assert (np.diff(np.diff(load)) >= 0.).all() @pytest.mark.parametrize('pf', [0.1, 0.5, 0.9]) -def test_woehler_miner_original_homogenious_cycles(pf): +def test_woehler_miner_original_homogenious_cycles(pf, wc_data): load = np.logspace(3., 2., 50) cycles = wc_data.woehler.basquin_cycles(load, failure_probability=pf) assert (np.diff(cycles[np.isfinite(cycles)]) > 0.).all() From fe87bf6253b9bede49890da38fd26d6246256763 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 29 Sep 2023 15:25:45 +0200 Subject: [PATCH 2/4] Make miner modifiers of WoehlerCurve non destructive The methods .miner_elementary() and .miner_haibach() now return modified copies of the original WoehlerCurve object, rather than modifying the original. This avoids side effects but is is technically a breaking change Signed-off-by: Johannes Mueller --- CHANGELOG.md | 11 +++++++++++ src/pylife/materiallaws/woehlercurve.py | 14 ++++++++------ tests/materiallaws/test_woehlercurve.py | 12 +++++++++++- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 164a8a0e..3f226bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ In this file noteworthy changes of new releases of pyLife are documented since 2.0.0. +## since pylife-2.0.4 + +### Breaking changes + +* Non-destructive miner modifiers of `WoehlerCurve` + + The methods `WoehlerCurve.miner_elementary()` and + `WoehlerCurve.miner_haibach()` now return modified copies of the original + WoehlerCurve object, rather than modifying the original. + + ## pylife-2.0.4 Minor bugfix release diff --git a/src/pylife/materiallaws/woehlercurve.py b/src/pylife/materiallaws/woehlercurve.py index dee366b1..8451bb2d 100644 --- a/src/pylife/materiallaws/woehlercurve.py +++ b/src/pylife/materiallaws/woehlercurve.py @@ -134,20 +134,22 @@ def miner_elementary(self): Returns ------- - self + modified copy of self """ - self._obj['k_2'] = self._obj.k_1 - return self + new = self._obj.copy() + new['k_2'] = self._obj.k_1 + return self.__class__(new) def miner_haibach(self): """Set k_2 to value according Miner Haibach method (k_2 = 2 * k_1 - 1). Returns ------- - self + modified copy of self """ - self._obj['k_2'] = 2. * self._obj.k_1 - 1. - return self + new = self._obj.copy() + new['k_2'] = 2. * self._obj.k_1 - 1. + return self.__class__(new) def cycles(self, load, failure_probability=0.5): """Calculate the cycles numbers from loads. diff --git a/tests/materiallaws/test_woehlercurve.py b/tests/materiallaws/test_woehlercurve.py index 0022a3d9..ebeced8d 100644 --- a/tests/materiallaws/test_woehlercurve.py +++ b/tests/materiallaws/test_woehlercurve.py @@ -436,16 +436,26 @@ def test_woehler_miner_original(wc_data): assert wc_data.woehler.k_2 == np.inf -def test_woehler_miner(wc_data): +def test_woehler_miner_elementary(wc_data): assert wc_data.woehler.miner_elementary().k_2 == wc_data.k_1 assert wc_data.woehler.miner_elementary().to_pandas().k_2 == wc_data.k_1 +def test_woehler_miner_elementary_new_object(wc_data): + orig = wc_data.woehler + assert orig.miner_elementary() is not orig + + def test_woehler_miner_haibach(wc_data): assert wc_data.woehler.miner_haibach().k_2 == 13.0 assert wc_data.woehler.miner_haibach().to_pandas().k_2 == 13.0 +def test_woehler_miner_haibach_new_object(wc_data): + orig = wc_data.woehler + assert orig.miner_haibach() is not orig + + def test_woehler_to_pandas(wc_data): expected = pd.Series({ 'k_1': 0.5, From a7ef6fdafcb9e0962c821788bc4c4422adcb6dd1 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 29 Sep 2023 15:30:47 +0200 Subject: [PATCH 3/4] Introduce WoehlerCurve.miner_original() Signed-off-by: Johannes Mueller --- CHANGELOG.md | 4 ++++ src/pylife/materiallaws/woehlercurve.py | 11 +++++++++++ tests/materiallaws/test_woehlercurve.py | 12 +++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f226bc6..912215a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ In this file noteworthy changes of new releases of pyLife are documented since ## since pylife-2.0.4 +### New features + +* Introduce `WoehlerCurve.miner_original()` + ### Breaking changes * Non-destructive miner modifiers of `WoehlerCurve` diff --git a/src/pylife/materiallaws/woehlercurve.py b/src/pylife/materiallaws/woehlercurve.py index 8451bb2d..b849137b 100644 --- a/src/pylife/materiallaws/woehlercurve.py +++ b/src/pylife/materiallaws/woehlercurve.py @@ -129,6 +129,17 @@ def transform_to_failure_probability(self, failure_probability): return WoehlerCurve(transformed) + def miner_original(self): + """Set k_2 to inf according Miner Original method (k_2 = inf). + + Returns + ------- + modified copy of self + """ + new = self._obj.copy() + new['k_2'] = np.inf + return self.__class__(new) + def miner_elementary(self): """Set k_2 to k_1 according Miner Elementary method (k_2 = k_1). diff --git a/tests/materiallaws/test_woehlercurve.py b/tests/materiallaws/test_woehlercurve.py index ebeced8d..34916894 100644 --- a/tests/materiallaws/test_woehlercurve.py +++ b/tests/materiallaws/test_woehlercurve.py @@ -432,10 +432,20 @@ def test_woehler_pf_given(wc_data): assert wc.woehler.failure_probability == 0.1 -def test_woehler_miner_original(wc_data): +def test_woehler_miner_original_as_default(wc_data): assert wc_data.woehler.k_2 == np.inf +def test_woehler_miner_original_as_request(wc_data): + wc_data['k_2'] = wc_data.k_1 + assert wc_data.woehler.miner_original().k_2 == np.inf + + +def test_woehler_miner_original_new_object(wc_data): + orig = wc_data.woehler + assert orig.miner_original() is not orig + + def test_woehler_miner_elementary(wc_data): assert wc_data.woehler.miner_elementary().k_2 == wc_data.k_1 assert wc_data.woehler.miner_elementary().to_pandas().k_2 == wc_data.k_1 From 7498c32ee6f346b85776c28131fb0b60e167bda6 Mon Sep 17 00:00:00 2001 From: Johannes Mueller Date: Fri, 29 Sep 2023 15:55:09 +0200 Subject: [PATCH 4/4] Make some archaic looking code more readable Signed-off-by: Johannes Mueller --- src/pylife/materiallaws/woehlercurve.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/pylife/materiallaws/woehlercurve.py b/src/pylife/materiallaws/woehlercurve.py index b849137b..e734f1b4 100644 --- a/src/pylife/materiallaws/woehlercurve.py +++ b/src/pylife/materiallaws/woehlercurve.py @@ -227,16 +227,14 @@ def basquin_cycles(self, load, failure_probability=0.5): cycles : numpy.ndarray The cycle numbers at which the component fails for the given `load` values """ - transformed = self.transform_to_failure_probability(failure_probability) + def ensure_float_to_prevent_int_overflow(load): + if isinstance(load, pd.Series): + return pd.Series(load, dtype=np.float64) + return np.asarray(load, dtype=np.float64) - if hasattr(load, '__iter__'): - if hasattr(load, 'astype'): - load = load.astype('float64') - else: - load = np.array(load).astype('float64') - else: - load = float(load) + transformed = self.transform_to_failure_probability(failure_probability) + load = ensure_float_to_prevent_int_overflow(load) ld, wc = transformed.broadcast(load) cycles = np.full_like(ld, np.inf)