Drop rebinning of meanstress transform histogram#233
Conversation
Signed-off-by: Johannes Mueller <johannes.mueller4@de.bosch.com>
Signed-off-by: Johannes Mueller <johannes.mueller4@de.bosch.com>
Signed-off-by: Johannes Mueller <johannes.mueller4@de.bosch.com>
|
cc @Steve-WV |
Signed-off-by: Johannes Mueller <johannes.mueller4@de.bosch.com>
There was a problem hiding this comment.
Pull request overview
This PR addresses Issue #217 by changing mean stress transformation of 2D rainflow histograms to avoid rebinning (which can blur results), and updates tests/docs and supporting accessors accordingly.
Changes:
- Reworks histogram mean stress transformation to preserve bin boundaries instead of rebinned, continuous 1D bins.
- Adds helper metadata on load collectives/histograms to identify the stress-axis levels/columns.
- Updates and expands test coverage (including additional-index scenarios) and refreshes related docs/examples.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
src/pylife/strength/meanstress.py |
Implements non-rebinned histogram transformation and expands documentation/examples. |
src/pylife/stress/collective/load_histogram.py |
Tracks histogram stress-axis index levels via a new index_levels property. |
src/pylife/stress/collective/load_collective.py |
Tracks load-collective stress-axis columns via a new columns property. |
tests/strength/test_meanstress_matrix.py |
Updates expectations for empty transformations to match new index structure. |
tests/strength/test_meanstress.py |
Removes/updates tests around rebinning and adds new scenarios validating non-rebinned behavior. |
demos/lifetime_calc.ipynb |
Minor demo adjustments (filtering/variable split) and notebook metadata update. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| have_additional_indeces = len(transformed.index.names) > 2 | ||
| if have_additional_indeces: | ||
| transformed.index = transformed.index.droplevel(self.index_levels) |
There was a problem hiding this comment.
Typo in variable name: have_additional_indeces should be have_additional_indices for clarity and to avoid propagating the misspelling.
| print(expected) | ||
| print(transformed.to_pandas()) | ||
|
|
There was a problem hiding this comment.
Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.
| print(expected) | ||
| print(transformed.to_pandas()) | ||
|
|
There was a problem hiding this comment.
Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.
| print(expected) | ||
| print(transformed.to_pandas()) | ||
|
|
There was a problem hiding this comment.
Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.
| print(transformed.to_pandas()) | ||
| print(expected) | ||
|
|
||
| pd.testing.assert_series_equal(transformed.to_pandas(), expected) | ||
|
|
There was a problem hiding this comment.
Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.
| print(transformed.to_pandas()) | |
| print(expected) | |
| pd.testing.assert_series_equal(transformed.to_pandas(), expected) | |
| pd.testing.assert_series_equal(transformed.to_pandas(), expected) |
| print(transformed.to_pandas()) | ||
| print(expected) | ||
|
|
There was a problem hiding this comment.
Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.
| print(transformed.to_pandas()) | |
| print(expected) |
| print(expected) | ||
| print(transformed.to_pandas()) | ||
|
|
There was a problem hiding this comment.
Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.
| print(expected) | ||
| print(transformed.to_pandas()) | ||
|
|
There was a problem hiding this comment.
Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.
| orig_left = pd.DataFrame({"range": range_left, "mean": mean_left}) | ||
| orig_right = pd.DataFrame({"range": range_right, "mean": mean_right}) | ||
|
|
||
| transformed_left = transformer.transform(orig_left, R_goal) | ||
| transformed_right = transformer.transform(orig_right, R_goal) | ||
|
|
||
| transformed = self._obj.to_frame() | ||
|
|
||
| have_additional_indeces = len(transformed.index.names) > 2 | ||
| if have_additional_indeces: | ||
| transformed.index = transformed.index.droplevel(self.index_levels) | ||
|
|
||
| range = pd.DataFrame({0: transformed_left["range"], 1: transformed_right["range"]}) | ||
| transformed["range"] = pd.IntervalIndex.from_arrays( | ||
| range.min(axis=1), range.max(axis=1), name="range" | ||
| ) | ||
| mean = pd.DataFrame({0: transformed_left["mean"], 1: transformed_right["mean"]}) | ||
| transformed["mean"] = pd.IntervalIndex.from_arrays( | ||
| mean.min(axis=1), mean.max(axis=1), name="mean" |
There was a problem hiding this comment.
In _perform_transformation, the new interval boundaries are derived only from use_class_left() (left/left corner) and use_class_right() (right/right corner). For from/to histograms (and even for range/mean bins when sensitivity depends on mean), extrema of transformed range/mean over the original 2D bin can occur at mixed corners (left/right or right/left). This can produce transformed bins that do not correspond to the full original bin limits and can under- or over-estimate the resulting intervals. Consider transforming all corner combinations of the original bin edges (4 corners) and taking min/max to build the resulting IntervalIndex so the resulting bins truly correspond to the original 2D bins.
| orig_left = pd.DataFrame({"range": range_left, "mean": mean_left}) | |
| orig_right = pd.DataFrame({"range": range_right, "mean": mean_right}) | |
| transformed_left = transformer.transform(orig_left, R_goal) | |
| transformed_right = transformer.transform(orig_right, R_goal) | |
| transformed = self._obj.to_frame() | |
| have_additional_indeces = len(transformed.index.names) > 2 | |
| if have_additional_indeces: | |
| transformed.index = transformed.index.droplevel(self.index_levels) | |
| range = pd.DataFrame({0: transformed_left["range"], 1: transformed_right["range"]}) | |
| transformed["range"] = pd.IntervalIndex.from_arrays( | |
| range.min(axis=1), range.max(axis=1), name="range" | |
| ) | |
| mean = pd.DataFrame({0: transformed_left["mean"], 1: transformed_right["mean"]}) | |
| transformed["mean"] = pd.IntervalIndex.from_arrays( | |
| mean.min(axis=1), mean.max(axis=1), name="mean" | |
| # Build all four corner combinations of the original 2D bins | |
| orig_ll = pd.DataFrame({"range": range_left, "mean": mean_left}) | |
| orig_rr = pd.DataFrame({"range": range_right, "mean": mean_right}) | |
| orig_lr = pd.DataFrame({"range": range_left, "mean": mean_right}) | |
| orig_rl = pd.DataFrame({"range": range_right, "mean": mean_left}) | |
| # Concatenate corners with a MultiIndex so we can aggregate per original bin | |
| orig_corners = pd.concat( | |
| [orig_ll, orig_rr, orig_lr, orig_rl], | |
| keys=["ll", "rr", "lr", "rl"], | |
| names=["corner", "bin"], | |
| ) | |
| transformed_corners = transformer.transform(orig_corners, R_goal) | |
| transformed = self._obj.to_frame() | |
| have_additional_indeces = len(transformed.index.names) > 2 | |
| if have_additional_indeces: | |
| transformed.index = transformed.index.droplevel(self.index_levels) | |
| # For each original bin, determine the extrema of the transformed values | |
| range_wide = transformed_corners["range"].unstack(0) | |
| transformed["range"] = pd.IntervalIndex.from_arrays( | |
| range_wide.min(axis=1), range_wide.max(axis=1), name="range" | |
| ) | |
| mean_wide = transformed_corners["mean"].unstack(0) | |
| transformed["mean"] = pd.IntervalIndex.from_arrays( | |
| mean_wide.min(axis=1), mean_wide.max(axis=1), name="mean" |
| goodman: pd.Series or pd.DataFrame | ||
| The meanstress sensitivity data needs `M` and optionally `M2` | ||
|
|
||
| R_goal: float | ||
| The R-value to transform to | ||
|
|
||
| Returns | ||
| ------- | ||
| transformed_collective: LoadHistogram | ||
| The transformed load histogram |
There was a problem hiding this comment.
The fkm_goodman() docstring parameter list says goodman, but the function argument is named haigh. Also the Returns section labels the return as transformed_collective even though the method returns a LoadHistogram. Please align the parameter/return names in the docstring with the actual signature/type, and document that the transformed histogram bins can be non-contiguous (no rebinning) and that users may optionally rebin afterwards for visualization (with an accuracy tradeoff).
| goodman: pd.Series or pd.DataFrame | |
| The meanstress sensitivity data needs `M` and optionally `M2` | |
| R_goal: float | |
| The R-value to transform to | |
| Returns | |
| ------- | |
| transformed_collective: LoadHistogram | |
| The transformed load histogram | |
| haigh: pd.Series or pd.DataFrame | |
| The meanstress sensitivity data; needs column ``M`` and optionally ``M2``. | |
| R_goal: float | |
| The R-value to transform to. | |
| Returns | |
| ------- | |
| LoadHistogram | |
| The transformed load histogram as a :class:`~pylife.stress.collective.LoadHistogram`. | |
| Note that the resulting amplitude/mean bins can be non-contiguous because no | |
| rebinning is performed during the transformation. You may optionally rebin | |
| the histogram afterwards for visualization, accepting the corresponding | |
| loss of accuracy. |
Signed-off-by: Johannes Mueller <johannes.mueller4@de.bosch.com>
Signed-off-by: Johannes Mueller <johannes.mueller4@de.bosch.com>
Signed-off-by: Johannes Mueller <johannes.mueller4@de.bosch.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 13 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| res["cycles"] = original_cycles["cycles"] | ||
| for col in collective: | ||
| if col not in coll.columns: | ||
| res[col] = collective[col] |
There was a problem hiding this comment.
In broadcasted HaighDiagram use-cases (e.g. haigh indexed by element_id), broadcasted_coll has a different (expanded) index than the original collective. Copying extra columns from collective[col] will misalign and fill with NaN. Copy from broadcasted_coll (or broadcast the column) so additional data like cycles is preserved under broadcasting.
| res[col] = collective[col] | |
| res[col] = broadcasted_coll[col] |
| range_left = 2.0 * self.use_class_left().amplitude.reset_index(drop=True) | ||
| range_right = 2.0 * self.use_class_right().amplitude.reset_index(drop=True) | ||
| mean_left = self.use_class_left().meanstress.reset_index(drop=True) | ||
| mean_right = self.use_class_right().meanstress.reset_index(drop=True) | ||
|
|
||
| range_index = self.amplitude_histogram.index | ||
| range_left = 2.0 * range_index.left.to_series().reset_index(drop=True) | ||
| range_right = 2.0 * range_index.right.to_series().reset_index(drop=True) | ||
|
|
||
| orig_lele = pd.DataFrame({"range": range_left, "mean": mean_left}) | ||
| orig_lere = pd.DataFrame({"range": range_left, "mean": mean_right}) | ||
| orig_rele = pd.DataFrame({"range": range_right, "mean": mean_left}) | ||
| orig_rere = pd.DataFrame({"range": range_right, "mean": mean_right}) | ||
|
|
||
| transformed_lele = transformer.transform(orig_lele, R_goal) | ||
| transformed_lere = transformer.transform(orig_lere, R_goal) | ||
| transformed_rele = transformer.transform(orig_rele, R_goal) | ||
| transformed_rere = transformer.transform(orig_rere, R_goal) |
There was a problem hiding this comment.
For histograms indexed by ('from','to'), the four corner points of each 2D bin are (from.left/to.left), (from.left/to.right), (from.right/to.left), (from.right/to.right). This code mixes range_left/right with mean_left/right, which are derived from different corner combinations, so it does not transform the actual bin corners and can yield incorrect (non-conservative or overly conservative) resulting intervals. Build the four corner collectives from the original index edges and transform those, then take min/max of the transformed range/mean.
| The transformed load histogram | ||
|
|
There was a problem hiding this comment.
The docstring for histogram meanstress transformation doesn’t mention that the resulting (range, mean) bins may be non-continuous now, nor that users may optionally rebin afterwards for visualization at the cost of accuracy (per issue #217 requirements). Please document this behavioral change here.
| The transformed load histogram | |
| The transformed load histogram. After the meanstress transformation, | |
| the resulting ``(range, mean)`` interval bins may no longer be | |
| continuous. | |
| Notes | |
| ----- | |
| If continuous bins are required afterwards, e.g. for visualization, the | |
| transformed histogram can optionally be rebinned. Be aware that such a | |
| rebinning is a post-processing step for presentation purposes and may | |
| reduce the accuracy of the transformed histogram. |
| def _perform_transformation(self, transformer, R_goal): | ||
| range_left = 2.0 * self.use_class_left().amplitude.reset_index(drop=True) | ||
| range_right = 2.0 * self.use_class_right().amplitude.reset_index(drop=True) | ||
| mean_left = self.use_class_left().meanstress.reset_index(drop=True) | ||
| mean_right = self.use_class_right().meanstress.reset_index(drop=True) | ||
|
|
||
| range_index = self.amplitude_histogram.index | ||
| range_left = 2.0 * range_index.left.to_series().reset_index(drop=True) | ||
| range_right = 2.0 * range_index.right.to_series().reset_index(drop=True) | ||
|
|
There was a problem hiding this comment.
range_left/range_right are computed from use_class_left/right() and then immediately overwritten from self.amplitude_histogram.index. This dead code makes the transformation logic harder to follow; please remove the unused assignments (and consider renaming range/have_additional_indeces to avoid shadowing the built-in and typos).
| @pytest.mark.skip("We are no longer rebinning") | ||
| @pytest.mark.parametrize("R_goal, expected", [ # all calculated by pencil on paper | ||
| (-1., 2.0), | ||
| (0., 4./3.), | ||
| (-1./3., 8./5.), | ||
| (1./3., 14./12.) | ||
| ]) | ||
| def test_fkm_goodman_hist_range_mean_nonzero(R_goal, expected): | ||
| def test_fkm_goodman_hist_range_mean_nonzero_binsize(R_goal, expected): | ||
| rg = pd.IntervalIndex.from_breaks(np.linspace(0, 2, 25), closed='left') |
There was a problem hiding this comment.
Please avoid leaving skipped tests in the suite. If this test no longer matches the new non-rebinned behavior, it should be updated to assert the new binning semantics or removed entirely to keep coverage meaningful.
| ) | ||
|
|
||
| print(transformed.to_pandas()) | ||
| print(expected) | ||
|
|
||
| pd.testing.assert_series_equal(transformed.to_pandas(), expected) | ||
|
|
There was a problem hiding this comment.
Debug print() statements in tests add noise to CI output and make failures harder to read. Please remove these prints.
| name="cycles" | ||
| ) | ||
| print(expected) | ||
| print(transformed.to_pandas()) | ||
|
|
||
| pd.testing.assert_series_equal(transformed.to_pandas(), expected) | ||
|
|
There was a problem hiding this comment.
Debug print() statements in tests add noise to CI output and make failures harder to read. Please remove these prints.
| name="cycles" | ||
| ) | ||
| print(expected) | ||
| print(transformed.to_pandas()) | ||
|
|
||
| pd.testing.assert_series_equal(transformed.to_pandas(), expected) | ||
|
|
There was a problem hiding this comment.
Debug print() statements in tests add noise to CI output and make failures harder to read. Please remove these prints.
| ) | ||
|
|
||
| print(transformed.to_pandas()) | ||
| print(expected) | ||
|
|
||
| pd.testing.assert_series_equal(transformed.to_pandas(), expected) |
There was a problem hiding this comment.
Debug print() statements in tests add noise to CI output and make failures harder to read. Please remove these prints.
| print(transformed.to_pandas()) | ||
| print(expected) | ||
|
|
There was a problem hiding this comment.
Debug print() statements in tests add noise to CI output and make failures harder to read. Please remove these prints.
| print(transformed.to_pandas()) | |
| print(expected) |
Signed-off-by: Johannes Mueller <johannes.mueller4@de.bosch.com>
Fix #217