From c27c39a32d5e8918bfca4991dae47f1ef0ae6459 Mon Sep 17 00:00:00 2001 From: Daniel Weindl Date: Mon, 27 Oct 2025 13:58:07 +0100 Subject: [PATCH] Fix setting `Model.module` Ensure `Model.module` gets set during `Model.clone()`. So far, this was only set during `model_module.get_model`. Fixes unpickling of cloned Models. Also implement `ModelPtr.__reduce__` in addition to `Model.__reduce__` (#2985). --- CHANGELOG.md | 6 ++--- python/tests/test_swig_interface.py | 4 ++++ swig/model.i | 37 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91a7905c98..82181838f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,13 +61,13 @@ See also our [versioning policy](https://amici.readthedocs.io/en/latest/versioni This is a wrapper for both `amici.run_simulation` and `amici.run_simulations`, depending on the type of the `edata` argument. It also supports passing some `Solver` options as keyword arguments. -* Improved `pickle` support for `amici.{ModelPtr,Solver,ExpData`. +* Improved `pickle` support for `amici.{Model,ModelPtr,Solver,ExpData`. Note that AMICI's pickling support is only intended for short-term storage or inter-process communication. Reading pickled objects after updating AMICI or the model code will almost certainly fail. - * `amici.ModelPtr` now supports sufficient pickling for use in - multi-processing contexts. This works only if the amici-generated model + * `amici.Model`and `amici.ModelPtr` now support sufficient pickling for use + in multi-processing contexts. This works only if the amici-generated model package exists in the same file system location and does not change until unpickling. * `amici.Solver` is now picklable if amici was built with HDF5 support. diff --git a/python/tests/test_swig_interface.py b/python/tests/test_swig_interface.py index dedd6bf165..c2a3970d45 100644 --- a/python/tests/test_swig_interface.py +++ b/python/tests/test_swig_interface.py @@ -698,6 +698,10 @@ def test_pickle_model(sbml_example_presimulation_module): != model_pickled.get_steady_state_sensitivity_mode() ) + # ensure we can pickle after clone() + model_clone = model.clone() + pickle.loads(pickle.dumps(model_clone)) + def test_pickle_edata(): ny = 2 diff --git a/swig/model.i b/swig/model.i index 01ccab766c..174586e3c9 100644 --- a/swig/model.i +++ b/swig/model.i @@ -108,12 +108,38 @@ using namespace amici; %newobject amici::Model::clone; %rename(create_solver) amici::Model::get_solver; +%rename(_cpp_model_clone) amici::Model::clone; %extend amici::Model { %pythoncode %{ +def clone(self): + """Clone the model instance.""" + clone = self._cpp_model_clone() + try: + # copy module reference if present + clone.module = self.module + except Exception: + pass + + return clone + def __deepcopy__(self, memo): return self.clone() +def __reduce__(self): + from amici.swig_wrappers import restore_model, get_model_settings, file_checksum + + return ( + restore_model, + ( + self.get_name(), + Path(self.module.__spec__.origin).parent, + get_model_settings(self), + file_checksum(self.module.extension_path), + ), + {} + ) + @overload def simulate( self: AmiciModel, @@ -192,6 +218,17 @@ def simulate( %extend std::unique_ptr { %pythoncode %{ +def clone(self): + """Clone the model instance.""" + clone = self._cpp_model_clone() + try: + # copy module reference if present + clone.module = self.module + except Exception: + pass + + return clone + def __deepcopy__(self, memo): return self.clone()