From 7db9c4f52dd62f03024cf02fa17c46a6f9394326 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Tue, 26 May 2026 11:09:50 +0100 Subject: [PATCH 1/8] Update tests --- tests/unit/test_optimisation.py | 10 +++++----- tests/unit/test_sampling.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_optimisation.py b/tests/unit/test_optimisation.py index 880283ea3..aad4252f1 100644 --- a/tests/unit/test_optimisation.py +++ b/tests/unit/test_optimisation.py @@ -754,7 +754,7 @@ def compare_result_data(self, result1, result2): np.testing.assert_array_equal(result1._time, result2._time) @pytest.mark.parametrize("to_format", ["json", "matlab", "pickle"]) - def test_save_result_data(self, result, problem, to_format, tmp_path): + def test_save_result_data(self, result, to_format, tmp_path): test_stub = tmp_path / "test" if to_format == "matlab": @@ -766,14 +766,14 @@ def test_save_result_data(self, result, problem, to_format, tmp_path): # Test save result result.save_data(filename, to_format=to_format) - result_load = OptimisationResult.load_data(filename, file_format=to_format) + result_load = pybop.Result.load_data(filename, file_format=to_format) self.compare_result_data(result, result_load) # Test save combined result - result_combined = OptimisationResult.combine([result, result]) + result_combined = pybop.Result.combine([result, result]) result_combined.save_data(filename, to_format=to_format) - result_load = OptimisationResult.load_data(filename, file_format=to_format) + result_load = pybop.Result.load_data(filename, file_format=to_format) self.compare_result_data(result_combined, result_load) def test_save_result(self, result, tmp_path): @@ -782,6 +782,6 @@ def test_save_result(self, result, tmp_path): # test save whole result filename = f"{test_stub}.pickle" result.save(filename) - result_load = OptimisationResult.load(filename) + result_load = pybop.Result.load(filename) self.compare_result_data(result, result_load) assert result.problem.parameters.names == result_load.problem.parameters.names diff --git a/tests/unit/test_sampling.py b/tests/unit/test_sampling.py index 1010d57ce..ce153fb0f 100644 --- a/tests/unit/test_sampling.py +++ b/tests/unit/test_sampling.py @@ -351,14 +351,14 @@ def test_save(self, log_pdf, n_chains, MCMC, tmp_path): result.save_data(filename, to_format=to_format) # load result - result_load = SamplingResult.load_data(filename, file_format=to_format) + result_load = pybop.Result.load_data(filename, file_format=to_format) self.compare_result_data(result, result_load) assert sampler2.logger is None # test save whole result filename = f"{test_stub}.pickle" result.save(filename) - result_load = SamplingResult.load(filename) + result_load = pybop.Result.load(filename) self.compare_result_data(result, result_load) assert result.problem.parameters.names == result_load.problem.parameters.names np.testing.assert_array_equal(result.chains, result_load.chains) From ac5fdc5febcb4babd88f95e3b82e9fb09e8cdaf8 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Tue, 26 May 2026 11:10:07 +0100 Subject: [PATCH 2/8] Save and load parameter names --- pybop/_result.py | 4 ++++ pybop/_utils.py | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pybop/_result.py b/pybop/_result.py index 5cbf48000..f0ecf3997 100644 --- a/pybop/_result.py +++ b/pybop/_result.py @@ -347,6 +347,7 @@ def data_dict(self) -> dict: return { "minimising": self._minimising, + "parameter_names": self.problem.parameters.names, "method_name": self.method_name, "n_runs": self.n_runs, "best_run": self._best_run, @@ -425,6 +426,7 @@ def load_data(filename: str, file_format: str = "pickle") -> dict: file_format=file_format, data_keys_0d=["_minimising", "n_runs", "best_run"], data_keys_1d=[ + "parameter_names", "method_name", "best_cost", "initial_cost", @@ -439,6 +441,8 @@ def load_data(filename: str, file_format: str = "pickle") -> dict: # Create a dummy problem problem = types.SimpleNamespace() problem.minimising = data["minimising"] + problem.parameters = types.SimpleNamespace() + problem.parameters.names = data["parameter_names"] # Create one logging result for each run n_runs = data["n_runs"] diff --git a/pybop/_utils.py b/pybop/_utils.py index 7fa6a0ebe..cf438ce42 100644 --- a/pybop/_utils.py +++ b/pybop/_utils.py @@ -117,11 +117,13 @@ def save_data_dict( df = pd.DataFrame(data_dict_copy) return df.to_csv(filename, index=False) elif to_format == "json": + # Format with identation + json_clean = json.dumps(data_dict, cls=NumpyEncoder, indent=4) + "\n" if filename is None: - return json.dumps(data_dict, cls=NumpyEncoder) + return json_clean else: with open(filename, "w") as outfile: - json.dump(data_dict, outfile, cls=NumpyEncoder) + outfile.write(json_clean + "\n") else: raise ValueError(f"format '{to_format}' is not supported") From 16953b62b71adfcca6634a22ce5a36e36a1c45a4 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Tue, 26 May 2026 11:10:11 +0100 Subject: [PATCH 3/8] Create saving_and_loading_results.py --- .../saving_and_loading_results.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 examples/scripts/getting_started/saving_and_loading_results.py diff --git a/examples/scripts/getting_started/saving_and_loading_results.py b/examples/scripts/getting_started/saving_and_loading_results.py new file mode 100644 index 000000000..e70c06171 --- /dev/null +++ b/examples/scripts/getting_started/saving_and_loading_results.py @@ -0,0 +1,71 @@ +import numpy as np +import pybamm + +import pybop + +""" +This example shows how to save and load an optimisation (or sampling) result. +First we run an example optimisation to generate a result. +""" + +# Define model and parameter values +model = pybamm.lithium_ion.SPM() +parameter_values = pybamm.ParameterValues("Chen2020") + +# Generate a synthetic dataset +sigma = 5e-3 +t_eval = np.linspace(0, 500, 240) +solution = pybamm.Simulation(model, parameter_values=parameter_values).solve( + t_eval=t_eval +) +dataset = pybop.Dataset( + { + "Time [s]": t_eval, + "Current [A]": solution["Current [A]"](t_eval), + "Voltage [V]": pybop.add_noise(solution["Voltage [V]"](t_eval), sigma), + } +) + +# Fitting parameters +parameter_values.update( + { + "Negative electrode active material volume fraction": pybop.Parameter( + distribution=pybop.Gaussian(0.68, 0.05, truncated_at=[0.4, 0.9]), + initial_value=0.45, + ), + "Positive electrode active material volume fraction": pybop.Parameter( + distribution=pybop.Gaussian(0.58, 0.05, truncated_at=[0.4, 0.9]), + initial_value=0.45, + ), + } +) + +# Build the problem +simulator = pybop.pybamm.Simulator( + model, parameter_values=parameter_values, protocol=dataset +) +cost = pybop.MeanAbsoluteError(dataset) +problem = pybop.Problem(simulator, cost) + +# Set up the optimiser +options = pybop.PintsOptions(max_iterations=150, verbose=True) +optim = pybop.PSO(problem, options=options) + +# Run the optimisation +result = optim.run() + +# Save the result: either pickle the whole result or save the data in +# one of these formats: "pickle", "json", "matlab" +result.save("examples/results/saved_result_object.pkl") +result.save_data("examples/results/saved_result_data.json", to_format="json") + +# Load the result +result = pybop.Result.load("examples/results/saved_result_object.pkl") +result = pybop.Result.load_data( + "examples/results/saved_result_data.json", file_format="json" +) + +# Plot the optimisation result +result.plot_convergence() +result.plot_parameters() +result.plot_surface(bounds=problem.parameters.get_bounds_array()) From ee0c0ee95a5219dffdd368c4411f89b77eb4016a Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Tue, 26 May 2026 11:10:29 +0100 Subject: [PATCH 4/8] Allow list or array --- pybop/plot/voronoi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybop/plot/voronoi.py b/pybop/plot/voronoi.py index 29be3c096..3a78f8c60 100644 --- a/pybop/plot/voronoi.py +++ b/pybop/plot/voronoi.py @@ -259,7 +259,7 @@ def surface( points = result.x_model parameters = result.problem.parameters - if points[0].shape[0] != 2: + if np.shape(points[0])[0] != 2: raise ValueError("This plot method requires two parameters.") x_optim, y_optim = map(list, zip(*points, strict=False)) From 652771e92f54309388814d52ee530066594acf48 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Tue, 26 May 2026 11:12:09 +0100 Subject: [PATCH 5/8] Format example scripts --- .../scripts/getting_started/linked_parameters.py | 12 ++---------- .../scripts/getting_started/maximum_likelihood.py | 6 +----- .../scripts/getting_started/optimising_with_adamw.py | 6 +----- .../optimising_with_scipy_minimize.py | 12 ++---------- .../optimising_with_simulated_annealing.py | 12 ++---------- 5 files changed, 8 insertions(+), 40 deletions(-) diff --git a/examples/scripts/getting_started/linked_parameters.py b/examples/scripts/getting_started/linked_parameters.py index 60e77c801..c6d48502e 100644 --- a/examples/scripts/getting_started/linked_parameters.py +++ b/examples/scripts/getting_started/linked_parameters.py @@ -62,18 +62,10 @@ parameter_values.update( { "Positive electrode thickness [m]": pybop.Parameter( - distribution=pybop.Gaussian( - 7.56e-05, - 0.1e-05, - truncated_at=[65e-06, 10e-05], - ), + distribution=pybop.Gaussian(75.6e-6, 1e-6, truncated_at=[65e-6, 100e-6]), ), "Positive electrode active material volume fraction": pybop.Parameter( - distribution=pybop.Gaussian( - 0.6, - 0.15, - truncated_at=[0.1, 0.9], - ), + distribution=pybop.Gaussian(0.6, 0.15, truncated_at=[0.1, 0.9]), ), } ) diff --git a/examples/scripts/getting_started/maximum_likelihood.py b/examples/scripts/getting_started/maximum_likelihood.py index d1700722d..59fba605e 100644 --- a/examples/scripts/getting_started/maximum_likelihood.py +++ b/examples/scripts/getting_started/maximum_likelihood.py @@ -35,11 +35,7 @@ parameter_values.update( { "Negative electrode active material volume fraction": pybop.Parameter( - distribution=pybop.Gaussian( - 0.6, - 0.05, - truncated_at=[0.5, 0.8], - ) + distribution=pybop.Gaussian(0.6, 0.05, truncated_at=[0.5, 0.8]) ), "Positive electrode active material volume fraction": pybop.Parameter( distribution=pybop.Gaussian(0.48, 0.05), diff --git a/examples/scripts/getting_started/optimising_with_adamw.py b/examples/scripts/getting_started/optimising_with_adamw.py index da26108a8..34fcace79 100644 --- a/examples/scripts/getting_started/optimising_with_adamw.py +++ b/examples/scripts/getting_started/optimising_with_adamw.py @@ -31,11 +31,7 @@ parameter_values.update( { "Negative electrode active material volume fraction": pybop.Parameter( - distribution=pybop.Gaussian( - 0.68, - 0.05, - truncated_at=[0.4, 0.9], - ), + distribution=pybop.Gaussian(0.68, 0.05, truncated_at=[0.4, 0.9]), initial_value=0.45, ), "Positive electrode active material volume fraction": pybop.Parameter( diff --git a/examples/scripts/getting_started/optimising_with_scipy_minimize.py b/examples/scripts/getting_started/optimising_with_scipy_minimize.py index 9799e1a73..97431456e 100644 --- a/examples/scripts/getting_started/optimising_with_scipy_minimize.py +++ b/examples/scripts/getting_started/optimising_with_scipy_minimize.py @@ -41,18 +41,10 @@ parameter_values.update( { "Negative electrode active material volume fraction": pybop.Parameter( - distribution=pybop.Gaussian( - 0.6, - 0.05, - truncated_at=[0.5, 0.8], - ), + distribution=pybop.Gaussian(0.6, 0.05, truncated_at=[0.5, 0.8]), ), "Positive electrode active material volume fraction": pybop.Parameter( - distribution=pybop.Gaussian( - 0.48, - 0.05, - truncated_at=[0.4, 0.7], - ), + distribution=pybop.Gaussian(0.48, 0.05, truncated_at=[0.4, 0.7]), ), } ) diff --git a/examples/scripts/getting_started/optimising_with_simulated_annealing.py b/examples/scripts/getting_started/optimising_with_simulated_annealing.py index 102fd0785..15c0696a0 100644 --- a/examples/scripts/getting_started/optimising_with_simulated_annealing.py +++ b/examples/scripts/getting_started/optimising_with_simulated_annealing.py @@ -33,18 +33,10 @@ parameter_values.update( { "Negative electrode active material volume fraction": pybop.Parameter( - distribution=pybop.Gaussian( - 0.6, - 0.1, - truncated_at=[0.4, 0.85], - ), + distribution=pybop.Gaussian(0.6, 0.1, truncated_at=[0.4, 0.85]), ), "Positive electrode active material volume fraction": pybop.Parameter( - distribution=pybop.Gaussian( - 0.6, - 0.1, - truncated_at=[0.4, 0.85], - ), + distribution=pybop.Gaussian(0.6, 0.1, truncated_at=[0.4, 0.85]), ), } ) From 1b70675a1cf66e77d0e743860aa634533a63a522 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Tue, 26 May 2026 11:42:24 +0100 Subject: [PATCH 6/8] Fix indentation --- pybop/_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pybop/_utils.py b/pybop/_utils.py index cf438ce42..900992a0f 100644 --- a/pybop/_utils.py +++ b/pybop/_utils.py @@ -117,13 +117,13 @@ def save_data_dict( df = pd.DataFrame(data_dict_copy) return df.to_csv(filename, index=False) elif to_format == "json": - # Format with identation + # Format with indentation json_clean = json.dumps(data_dict, cls=NumpyEncoder, indent=4) + "\n" if filename is None: return json_clean else: with open(filename, "w") as outfile: - outfile.write(json_clean + "\n") + outfile.write(json_clean) else: raise ValueError(f"format '{to_format}' is not supported") From 418612732d360809c16d9c19c5ffea662635e289 Mon Sep 17 00:00:00 2001 From: NicolaCourtier <45851982+NicolaCourtier@users.noreply.github.com> Date: Tue, 26 May 2026 11:50:24 +0100 Subject: [PATCH 7/8] Create save path --- .../getting_started/saving_and_loading_results.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/scripts/getting_started/saving_and_loading_results.py b/examples/scripts/getting_started/saving_and_loading_results.py index e70c06171..ed42313d5 100644 --- a/examples/scripts/getting_started/saving_and_loading_results.py +++ b/examples/scripts/getting_started/saving_and_loading_results.py @@ -1,3 +1,5 @@ +import os + import numpy as np import pybamm @@ -56,13 +58,15 @@ # Save the result: either pickle the whole result or save the data in # one of these formats: "pickle", "json", "matlab" -result.save("examples/results/saved_result_object.pkl") -result.save_data("examples/results/saved_result_data.json", to_format="json") +save_path = "examples/results/" +os.makedirs(os.path.dirname(save_path), exist_ok=True) +result.save(save_path + "saved_result_object.pkl") +result.save_data(save_path + "saved_result_data.json", to_format="json") # Load the result -result = pybop.Result.load("examples/results/saved_result_object.pkl") +result = pybop.Result.load(save_path + "saved_result_object.pkl") result = pybop.Result.load_data( - "examples/results/saved_result_data.json", file_format="json" + save_path + "saved_result_data.json", file_format="json" ) # Plot the optimisation result From 553b4cb24cf576c282da31cb3b73d6846790a0dd Mon Sep 17 00:00:00 2001 From: Ferran Brosa Planella Date: Thu, 4 Jun 2026 15:50:09 +0100 Subject: [PATCH 8/8] implement Sarah's suggestion --- .../saving_and_loading_results.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/examples/scripts/getting_started/saving_and_loading_results.py b/examples/scripts/getting_started/saving_and_loading_results.py index ed42313d5..57e240ae7 100644 --- a/examples/scripts/getting_started/saving_and_loading_results.py +++ b/examples/scripts/getting_started/saving_and_loading_results.py @@ -64,12 +64,17 @@ result.save_data(save_path + "saved_result_data.json", to_format="json") # Load the result -result = pybop.Result.load(save_path + "saved_result_object.pkl") -result = pybop.Result.load_data( +result_from_pkl = pybop.Result.load(save_path + "saved_result_object.pkl") +result_from_json = pybop.Result.load_data( save_path + "saved_result_data.json", file_format="json" ) -# Plot the optimisation result -result.plot_convergence() -result.plot_parameters() -result.plot_surface(bounds=problem.parameters.get_bounds_array()) +# Plot the optimisation result from .pkl +result_from_pkl.plot_convergence() +result_from_pkl.plot_parameters() +result_from_pkl.plot_surface(bounds=problem.parameters.get_bounds_array()) + +# Plot the optimisation result from .json (it is the same) +result_from_json.plot_convergence() +result_from_json.plot_parameters() +result_from_json.plot_surface(bounds=problem.parameters.get_bounds_array())