Skip to content

Drop rebinning of meanstress transform histogram#233

Merged
johannes-mueller merged 9 commits into
developfrom
217-meanstress-transform-histogram
Apr 9, 2026
Merged

Drop rebinning of meanstress transform histogram#233
johannes-mueller merged 9 commits into
developfrom
217-meanstress-transform-histogram

Conversation

@johannes-mueller
Copy link
Copy Markdown
Member

Fix #217

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>
@johannes-mueller
Copy link
Copy Markdown
Member Author

cc @Steve-WV

Signed-off-by: Johannes Mueller <johannes.mueller4@de.bosch.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +710 to +712
have_additional_indeces = len(transformed.index.names) > 2
if have_additional_indeces:
transformed.index = transformed.index.droplevel(self.index_levels)
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in variable name: have_additional_indeces should be have_additional_indices for clarity and to avoid propagating the misspelling.

Copilot uses AI. Check for mistakes.
Comment on lines +450 to +452
print(expected)
print(transformed.to_pandas())

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.

Copilot uses AI. Check for mistakes.
Comment on lines +478 to +480
print(expected)
print(transformed.to_pandas())

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.

Copilot uses AI. Check for mistakes.
Comment on lines +519 to +521
print(expected)
print(transformed.to_pandas())

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.

Copilot uses AI. Check for mistakes.
Comment on lines +685 to +689
print(transformed.to_pandas())
print(expected)

pd.testing.assert_series_equal(transformed.to_pandas(), expected)

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.

Suggested change
print(transformed.to_pandas())
print(expected)
pd.testing.assert_series_equal(transformed.to_pandas(), expected)
pd.testing.assert_series_equal(transformed.to_pandas(), expected)

Copilot uses AI. Check for mistakes.
Comment on lines +561 to +563
print(transformed.to_pandas())
print(expected)

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.

Suggested change
print(transformed.to_pandas())
print(expected)

Copilot uses AI. Check for mistakes.
Comment on lines +602 to +604
print(expected)
print(transformed.to_pandas())

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.

Copilot uses AI. Check for mistakes.
Comment on lines +643 to +645
print(expected)
print(transformed.to_pandas())

Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove debug print() calls from tests; they add noise to test output and can slow down CI runs.

Copilot uses AI. Check for mistakes.
Comment thread src/pylife/strength/meanstress.py Outdated
Comment on lines +702 to +720
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"
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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"

Copilot uses AI. Check for mistakes.
Comment thread src/pylife/strength/meanstress.py Outdated
Comment on lines +656 to +665
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
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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.

Copilot uses AI. Check for mistakes.
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>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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]
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
res[col] = collective[col]
res[col] = broadcasted_coll[col]

Copilot uses AI. Check for mistakes.
Comment thread src/pylife/strength/meanstress.py Outdated
Comment on lines +697 to +714
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)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread src/pylife/strength/meanstress.py Outdated
Comment on lines +665 to +666
The transformed load histogram

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Copilot uses AI. Check for mistakes.
Comment on lines +696 to +705
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)

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment thread tests/strength/test_meanstress.py Outdated
Comment on lines 277 to 285
@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')
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +559 to +565
)

print(transformed.to_pandas())
print(expected)

pd.testing.assert_series_equal(transformed.to_pandas(), expected)

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug print() statements in tests add noise to CI output and make failures harder to read. Please remove these prints.

Copilot uses AI. Check for mistakes.
Comment on lines +600 to +606
name="cycles"
)
print(expected)
print(transformed.to_pandas())

pd.testing.assert_series_equal(transformed.to_pandas(), expected)

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug print() statements in tests add noise to CI output and make failures harder to read. Please remove these prints.

Copilot uses AI. Check for mistakes.
Comment on lines +641 to +647
name="cycles"
)
print(expected)
print(transformed.to_pandas())

pd.testing.assert_series_equal(transformed.to_pandas(), expected)

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug print() statements in tests add noise to CI output and make failures harder to read. Please remove these prints.

Copilot uses AI. Check for mistakes.
Comment on lines +683 to +688
)

print(transformed.to_pandas())
print(expected)

pd.testing.assert_series_equal(transformed.to_pandas(), expected)
Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug print() statements in tests add noise to CI output and make failures harder to read. Please remove these prints.

Copilot uses AI. Check for mistakes.
Comment on lines +755 to +757
print(transformed.to_pandas())
print(expected)

Copy link

Copilot AI Apr 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug print() statements in tests add noise to CI output and make failures harder to read. Please remove these prints.

Suggested change
print(transformed.to_pandas())
print(expected)

Copilot uses AI. Check for mistakes.
Signed-off-by: Johannes Mueller <johannes.mueller4@de.bosch.com>
@johannes-mueller johannes-mueller merged commit a5b5b2a into develop Apr 9, 2026
24 checks passed
@johannes-mueller johannes-mueller deleted the 217-meanstress-transform-histogram branch April 9, 2026 07:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Meanstress transformation of histograms must not rebin result

2 participants