From 23bff0abc22ed89f5f78d70272331ad41e2f73ea Mon Sep 17 00:00:00 2001 From: Cedric Verstege Date: Mon, 27 Jul 2026 12:05:45 +0200 Subject: [PATCH 1/2] Fix Plot crashing when given a MultiFit inside a list (#246) Plot.__init__ only recognized a MultiFit if it was passed directly; a MultiFit wrapped in a sequence (e.g. Plot([multi_fit])) fell through to the regular fit-object path, where MultiFit.PLOT_ADAPTER_TYPE is None, causing a confusing "'NoneType' object is not callable" crash. This is exactly what the plot() wrapper function does internally, so k2.plot(multi_fit) was broken even though Plot(multi_fit) worked. Plot now unwraps a lone MultiFit found inside a sequence the same way it unwraps one passed directly, and raises a clear NotImplementedError if a MultiFit is mixed with other fits in the same sequence (not yet supported). Co-Authored-By: Claude Sonnet 5 --- kafe2/fit/_base/plot.py | 19 +++++++++++++++++-- kafe2/test/fit/test_plot.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/kafe2/fit/_base/plot.py b/kafe2/fit/_base/plot.py index f809e165..72a910dc 100644 --- a/kafe2/fit/_base/plot.py +++ b/kafe2/fit/_base/plot.py @@ -725,9 +725,24 @@ def __init__(self, fit_objects, separate_figures=False): else: fit_objects = [_f["fit"] for _f in _fit_history[fit_objects:]] try: - iter(fit_objects) + fit_objects = list(fit_objects) except TypeError: - fit_objects = (fit_objects,) + fit_objects = [fit_objects] + + if self._multifit is None: + # A MultiFit is not iterable, so passing one via e.g. a list (as the plot() wrapper + # function does) ends up here rather than being caught by the isinstance check above. + _multifits_in_seq = [_fo for _fo in fit_objects if isinstance(_fo, MultiFit)] + if _multifits_in_seq: + if len(fit_objects) != 1: + raise NotImplementedError( + "Plotting a MultiFit together with other fits in the same Plot is not supported. " + "Pass the MultiFit object on its own instead, e.g. Plot(multi_fit) instead of " + "Plot([multi_fit, other_fit])." + ) + self._multifit = _multifits_in_seq[0] + fit_objects = self._multifit.fits + self._from_container = tuple(isinstance(_fo, DataContainerBase) for _fo in fit_objects) self._fits = tuple(Fit(_fo) if _fc else _fo for _fc, _fo in zip(self._from_container, fit_objects)) diff --git a/kafe2/test/fit/test_plot.py b/kafe2/test/fit/test_plot.py index 6ea7fe82..2891ec27 100644 --- a/kafe2/test/fit/test_plot.py +++ b/kafe2/test/fit/test_plot.py @@ -5,7 +5,8 @@ import matplotlib.pyplot as plt import numpy as np -from kafe2 import ContoursProfiler, Plot, XYFit +from kafe2 import ContoursProfiler, MultiFit, Plot, XYFit +from kafe2.fit.util.wrapper import plot as wrapper_plot class TestXYPlot(unittest.TestCase): @@ -280,6 +281,36 @@ def test_save_raise(self): self.plot_sep.save(fname=["fit_0.png", "fit_1.png", "fit_2.png"]) +class TestMultiFitPlot(unittest.TestCase): + # Regression tests for https://github.com/PhiLFitters/kafe2/issues/246 + def setUp(self): + self.fit1 = XYFit(xy_data=[[0, 1, 2], [0.2, 1.1, 1.2]]) + self.fit1.add_error("y", 0.1) + self.fit2 = XYFit(xy_data=[[0, 1, 2], [0.3, 0.9, 1.1]]) + self.fit2.add_error("y", 0.1) + self.multi_fit = MultiFit([self.fit1, self.fit2]) + self.multi_fit.do_fit() + + def tearDown(self): + plt.close("all") + + def test_plot_multifit_directly(self): + Plot(self.multi_fit).plot() + + def test_plot_multifit_in_list(self): + Plot([self.multi_fit]).plot() + + def test_plot_wrapper_with_multifit(self): + wrapper_plot(self.multi_fit, show=False, save=False) + + def test_plot_multifit_mixed_with_other_fit_raises(self): + _fit3 = XYFit(xy_data=[[0, 1, 2], [0.3, 0.9, 1.1]]) + _fit3.add_error("y", 0.1) + _fit3.do_fit() + with self.assertRaises(NotImplementedError): + Plot([self.multi_fit, _fit3]) + + class TestContoursProfiler(unittest.TestCase): def setUp(self): self._ref_data = [[1, 2, 3], [0.9, 2.1, 3.0]] From d843ad3eb67a7625e70f4feab03b9f37afcc52c1 Mon Sep 17 00:00:00 2001 From: Cedric Verstege Date: Mon, 27 Jul 2026 13:03:34 +0200 Subject: [PATCH 2/2] Reformat with current black to fix CI linting Multi-line textwrap.dedent(...) calls and string concatenations that used to need wrapping now fit on one line under the black version pip currently installs, so make lint (and CI's Linting job) flagged them as needing reformatting. No functional changes. Co-Authored-By: Claude Sonnet 5 --- kafe2/fit/_base/plot.py | 18 +++----- .../fit/test_representers_constraint_yaml.py | 10 +---- .../fit/test_representers_container_yaml.py | 15 ++----- kafe2/test/fit/test_representers_fit_yaml.py | 25 +++-------- .../test/fit/test_representers_format_yaml.py | 5 +-- .../test_representers_model_function_yaml.py | 30 +++---------- ...test_representers_parametric_model_yaml.py | 45 ++++--------------- 7 files changed, 32 insertions(+), 116 deletions(-) diff --git a/kafe2/fit/_base/plot.py b/kafe2/fit/_base/plot.py index 72a910dc..9cae9751 100644 --- a/kafe2/fit/_base/plot.py +++ b/kafe2/fit/_base/plot.py @@ -675,29 +675,23 @@ class Plot: subplot and legend management. """ - FIT_INFO_STRING_FORMAT_CHI2 = textwrap.dedent( - """\ + FIT_INFO_STRING_FORMAT_CHI2 = textwrap.dedent("""\ {model_function} {parameters} $\\hookrightarrow${fit_quality} $\\hookrightarrow \\chi^2 \\, \\mathrm{{probability =}}${chi2_probability} - """ - ) - FIT_INFO_STRING_FORMAT_SATURATED = textwrap.dedent( - """\ + """) + FIT_INFO_STRING_FORMAT_SATURATED = textwrap.dedent("""\ {model_function} {parameters} $\\hookrightarrow${fit_quality} - """ - ) - FIT_INFO_STRING_FORMAT_NOT_SATURATED = textwrap.dedent( - """\ + """) + FIT_INFO_STRING_FORMAT_NOT_SATURATED = textwrap.dedent("""\ {model_function} {parameters} $\\hookrightarrow${cost} $\\hookrightarrow${fit_quality} - """ - ) + """) def __init__(self, fit_objects, separate_figures=False): """ diff --git a/kafe2/test/fit/test_representers_constraint_yaml.py b/kafe2/test/fit/test_representers_constraint_yaml.py index d4f3547f..e923c20c 100644 --- a/kafe2/test/fit/test_representers_constraint_yaml.py +++ b/kafe2/test/fit/test_representers_constraint_yaml.py @@ -31,12 +31,9 @@ uncertainty: 1.2 """ -TEST_SIMPLE_GAUSSIAN_CONSTRAINT_EXTRA_KEYWORD = ( - TEST_SIMPLE_GAUSSIAN_CONSTRAINT_ABS - + """ +TEST_SIMPLE_GAUSSIAN_CONSTRAINT_EXTRA_KEYWORD = TEST_SIMPLE_GAUSSIAN_CONSTRAINT_ABS + """ extra_keyword: 3.14 """ -) class TestSimpleGaussianConstraintRepresenter(unittest.TestCase): @@ -133,12 +130,9 @@ def test_round_trip_with_stringstream(self): matrix: [[0.1, 0.1, 2.0], [0.1, 10.0, 30.0], [2.0, 30.0, 1000.0]] """ -TEST_MATRIX_GAUSSIAN_CONSTRAINT_EXTRA_KEYWORD = ( - TEST_MATRIX_GAUSSIAN_CONSTRAINT_COV_ABS - + """ +TEST_MATRIX_GAUSSIAN_CONSTRAINT_EXTRA_KEYWORD = TEST_MATRIX_GAUSSIAN_CONSTRAINT_COV_ABS + """ extra_keyword: 3.14 """ -) class TestMatrixGaussianConstraintRepresenter(unittest.TestCase): diff --git a/kafe2/test/fit/test_representers_container_yaml.py b/kafe2/test/fit/test_representers_container_yaml.py index a3d17b33..1e7e08e9 100644 --- a/kafe2/test/fit/test_representers_container_yaml.py +++ b/kafe2/test/fit/test_representers_container_yaml.py @@ -81,12 +81,9 @@ type: matrix """ -TEST_DATASET_INDEXED_EXTRA_KEYWORD = ( - TEST_DATASET_INDEXED - + """ +TEST_DATASET_INDEXED_EXTRA_KEYWORD = TEST_DATASET_INDEXED + """ extra_keyword: 3.14 """ -) class TestIndexedContainerYamlRepresentation(unittest.TestCase): @@ -252,12 +249,9 @@ def test_round_trip_with_stringstream(self): type: matrix """ -TEST_DATASET_XY_EXTRA_KEYWORD = ( - TEST_DATASET_XY - + """ +TEST_DATASET_XY_EXTRA_KEYWORD = TEST_DATASET_XY + """ extra_keyword: 3.14 """ -) class TestXYContainerYamlRepresentation(unittest.TestCase): @@ -421,12 +415,9 @@ def test_round_trip_with_stringstream(self): type: histogram """ -TEST_DATASET_HIST_EXTRA_KEYWORD = ( - TEST_DATASET_HIST - + """ +TEST_DATASET_HIST_EXTRA_KEYWORD = TEST_DATASET_HIST + """ extra_keyword: 3.14 """ -) TEST_DATASET_HIST_MANUAL_HEIGHTS = """ bin_edges: diff --git a/kafe2/test/fit/test_representers_fit_yaml.py b/kafe2/test/fit/test_representers_fit_yaml.py index 493724cd..055f3b27 100644 --- a/kafe2/test/fit/test_representers_fit_yaml.py +++ b/kafe2/test/fit/test_representers_fit_yaml.py @@ -126,12 +126,9 @@ def chi2(a=1.5, b=-0.5): type: custom """ -TEST_FIT_CUSTOM_EXTRA_KEYWORD = ( - TEST_FIT_CUSTOM - + """ +TEST_FIT_CUSTOM_EXTRA_KEYWORD = TEST_FIT_CUSTOM + """ extra_keyword: 3.14 """ -) class TestCustomFitYamlRepresenter(unittest.TestCase, AbstractTestFitRepresenter): @@ -257,12 +254,9 @@ def hist_model_density(x, mu, sigma): model_parameters: [0.1, 1.0] """ -TEST_FIT_HIST_EXTRA_KEYWORD = ( - TEST_FIT_HIST - + """ +TEST_FIT_HIST_EXTRA_KEYWORD = TEST_FIT_HIST + """ extra_keyword: 3.14 """ -) TEST_FIT_HIST_SIMPLE = """ type: histogram @@ -440,12 +434,9 @@ def linear_model(a, b): model_parameters: [1.5, -0.5] """ -TEST_FIT_INDEXED_EXTRA_KEYWORD = ( - TEST_FIT_INDEXED - + """ +TEST_FIT_INDEXED_EXTRA_KEYWORD = TEST_FIT_INDEXED + """ extra_keyword: 3.14 """ -) TEST_FIT_INDEXED_SIMPLE = """ type: indexed @@ -608,12 +599,9 @@ def linear_model(x, a, b): model_parameters: [1.5, -0.5] """ -TEST_FIT_XY_EXTRA_KEYWORD = ( - TEST_FIT_XY - + """ +TEST_FIT_XY_EXTRA_KEYWORD = TEST_FIT_XY + """ extra_keyword: 3.14 """ -) TEST_FIT_XY_SIMPLE = """ x_data: [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] @@ -832,12 +820,9 @@ def normal_distribution_pdf(x, mu, sigma): type: unbinned """ -TEST_FIT_UNBINNED_EXTRA_KEYWORD = ( - TEST_FIT_UNBINNED - + """ +TEST_FIT_UNBINNED_EXTRA_KEYWORD = TEST_FIT_UNBINNED + """ extra_keyword: 3.14 """ -) TEST_FIT_UNBINNED_SIMPLE = """ type: unbinned diff --git a/kafe2/test/fit/test_representers_format_yaml.py b/kafe2/test/fit/test_representers_format_yaml.py index 8acd3a5d..1f2fe749 100644 --- a/kafe2/test/fit/test_representers_format_yaml.py +++ b/kafe2/test/fit/test_representers_format_yaml.py @@ -129,12 +129,9 @@ def test_round_trip_with_stringstream(self): latex_expression_string: '{0}{x}^2 + {1}{x} + {2}' """ -TEST_MODEL_FUNCTION_FORMATTER_INDEXED_EXTRA_KEYWORD = ( - TEST_MODEL_FUNCTION_FORMATTER_INDEXED - + """ +TEST_MODEL_FUNCTION_FORMATTER_INDEXED_EXTRA_KEYWORD = TEST_MODEL_FUNCTION_FORMATTER_INDEXED + """ extra_keyword: 3.14 """ -) class TestIndexedModelFunctionFormatterYamlRepresenter(unittest.TestCase): diff --git a/kafe2/test/fit/test_representers_model_function_yaml.py b/kafe2/test/fit/test_representers_model_function_yaml.py index 30d5230a..d1c32405 100644 --- a/kafe2/test/fit/test_representers_model_function_yaml.py +++ b/kafe2/test/fit/test_representers_model_function_yaml.py @@ -18,16 +18,11 @@ type: histogram """ -TEST_MODEL_FUNCTION_HIST_EXTRA_KEYWORD = ( - TEST_MODEL_FUNCTION_HIST - + """ +TEST_MODEL_FUNCTION_HIST_EXTRA_KEYWORD = TEST_MODEL_FUNCTION_HIST + """ extra_keyword: 3.14 """ -) -TEST_MODEL_FUNCTION_HIST_WITH_FORMATTER = ( - TEST_MODEL_FUNCTION_HIST - + r""" +TEST_MODEL_FUNCTION_HIST_WITH_FORMATTER = TEST_MODEL_FUNCTION_HIST + r""" model_function_formatter: name: linear_model latex_name: linear model @@ -42,7 +37,6 @@ expression_string: '{0} * {x} + {1}' latex_expression_string: '{0}{x} + {1}' """ -) class TestHistModelFunctionYamlRepresenter(unittest.TestCase): @@ -145,16 +139,11 @@ def linear_model(a, b): type: indexed """ -TEST_MODEL_FUNCTION_INDEXED_EXTRA_KEYWORD = ( - TEST_MODEL_FUNCTION_INDEXED - + """ +TEST_MODEL_FUNCTION_INDEXED_EXTRA_KEYWORD = TEST_MODEL_FUNCTION_INDEXED + """ extra_keyword: 3.14 """ -) -TEST_MODEL_FUNCTION_INDEXED_WITH_FORMATTER = ( - TEST_MODEL_FUNCTION_INDEXED - + r""" +TEST_MODEL_FUNCTION_INDEXED_WITH_FORMATTER = TEST_MODEL_FUNCTION_INDEXED + r""" model_function_formatter: name: linear_model latex_name: linear model @@ -170,7 +159,6 @@ def linear_model(a, b): expression_string: '{0} * {r} + {1}' latex_expression_string: '{0}{r} + {1}' """ -) class TestIndexedModelFunctionYamlRepresenter(unittest.TestCase): @@ -273,16 +261,11 @@ def test_round_trip_with_stringstream(self): type: base """ -TEST_MODEL_FUNCTION_XY_EXTRA_KEYWORD = ( - TEST_MODEL_FUNCTION_BASE - + """ +TEST_MODEL_FUNCTION_XY_EXTRA_KEYWORD = TEST_MODEL_FUNCTION_BASE + """ extra_keyword: 3.14 """ -) -TEST_MODEL_FUNCTION_XY_WITH_FORMATTER = ( - TEST_MODEL_FUNCTION_BASE - + r""" +TEST_MODEL_FUNCTION_XY_WITH_FORMATTER = TEST_MODEL_FUNCTION_BASE + r""" model_function_formatter: name: linear_model latex_name: linear model @@ -297,7 +280,6 @@ def test_round_trip_with_stringstream(self): expression_string: '{0} * {x} + {1}' latex_expression_string: '{0}{x} + {1}' """ -) class TestModelFunctionBaseYamlRepresenter(unittest.TestCase): diff --git a/kafe2/test/fit/test_representers_parametric_model_yaml.py b/kafe2/test/fit/test_representers_parametric_model_yaml.py index 54b2f727..532843be 100644 --- a/kafe2/test/fit/test_representers_parametric_model_yaml.py +++ b/kafe2/test/fit/test_representers_parametric_model_yaml.py @@ -34,16 +34,11 @@ def linear_model(x, a, b): model_parameters: [0.0, 0.08] """ -TEST_PARAMETRIC_MODEL_HIST_EXTRA_KEYWORD = ( - TEST_PARAMETRIC_MODEL_HIST - + """ +TEST_PARAMETRIC_MODEL_HIST_EXTRA_KEYWORD = TEST_PARAMETRIC_MODEL_HIST + """ extra_keyword: 3.14 """ -) -TEST_PARAMETRIC_MODEL_HIST_WITH_ERRORS = ( - TEST_PARAMETRIC_MODEL_HIST - + """ +TEST_PARAMETRIC_MODEL_HIST_WITH_ERRORS = TEST_PARAMETRIC_MODEL_HIST + """ errors: - correlation_coefficient: 0.0 error_value: 0.1 @@ -51,33 +46,23 @@ def linear_model(x, a, b): relative: false type: simple """ -) -TEST_PARAMETRIC_MODEL_HIST_LEGACY = ( - TEST_PARAMETRIC_MODEL_HIST - + """ +TEST_PARAMETRIC_MODEL_HIST_LEGACY = TEST_PARAMETRIC_MODEL_HIST + """ model_density_func_antiderivative: null """ -) -TEST_PARAMETRIC_MODEL_HIST_ANTIDERIVATIVE = ( - TEST_PARAMETRIC_MODEL_HIST - + """ +TEST_PARAMETRIC_MODEL_HIST_ANTIDERIVATIVE = TEST_PARAMETRIC_MODEL_HIST + """ bin_evaluation: | def linear_model_antiderivative(x, a, b): return 0.5 * a * x ** 2 + b * x """ -) -TEST_PARAMETRIC_MODEL_HIST_NUMPY_VECTORIZE = ( - TEST_PARAMETRIC_MODEL_HIST - + """ +TEST_PARAMETRIC_MODEL_HIST_NUMPY_VECTORIZE = TEST_PARAMETRIC_MODEL_HIST + """ bin_evaluation: | @np.vectorize def linear_model_antiderivative(x, a, b): return 0.5 * a * x ** 2 + b * x """ -) class TestHistParametricModelYamlRepresenter(unittest.TestCase): @@ -230,16 +215,11 @@ def linear_model(a, b): model_parameters: [1.1, -1.5] """ -TEST_PARAMETRIC_MODEL_INDEXED_EXTRA_KEYWORD = ( - TEST_PARAMETRIC_MODEL_INDEXED - + """ +TEST_PARAMETRIC_MODEL_INDEXED_EXTRA_KEYWORD = TEST_PARAMETRIC_MODEL_INDEXED + """ extra_keyword: 3.14 """ -) -TEST_PARAMETRIC_MODEL_INDEXED_WITH_ERRORS = ( - TEST_PARAMETRIC_MODEL_INDEXED - + """ +TEST_PARAMETRIC_MODEL_INDEXED_WITH_ERRORS = TEST_PARAMETRIC_MODEL_INDEXED + """ errors: - correlation_coefficient: 0.0 error_value: 0.1 @@ -247,7 +227,6 @@ def linear_model(a, b): relative: false type: simple """ -) class TestIndexedParametricModelYamlRepresenter(unittest.TestCase): @@ -389,16 +368,11 @@ def linear_model(x, a, b): model_parameters: [1.1, -1.5] """ -TEST_PARAMETRIC_MODEL_XY_EXTRA_KEYWORD = ( - TEST_PARAMETRIC_MODEL_XY - + """ +TEST_PARAMETRIC_MODEL_XY_EXTRA_KEYWORD = TEST_PARAMETRIC_MODEL_XY + """ extra_keyword: 3.14 """ -) -TEST_PARAMETRIC_MODEL_XY_WITH_ERRORS = ( - TEST_PARAMETRIC_MODEL_XY - + """ +TEST_PARAMETRIC_MODEL_XY_WITH_ERRORS = TEST_PARAMETRIC_MODEL_XY + """ x_errors: - correlation_coefficient: 0.0 error_value: 0.1 @@ -406,7 +380,6 @@ def linear_model(x, a, b): relative: false type: simple """ -) class TestXYParametricModelYamlRepresenter(unittest.TestCase):