Skip to content

Commit

Permalink
Add copy method
Browse files Browse the repository at this point in the history
  • Loading branch information
enekomartinmartinez committed Dec 24, 2023
1 parent 1cd0cba commit 30bda49
Show file tree
Hide file tree
Showing 9 changed files with 432 additions and 92 deletions.
2 changes: 2 additions & 0 deletions docs/advanced_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ A submodel of a translated model can be run as a standalone model. This can be d
.. automethod:: pysd.py_backend.model.Model.select_submodel
:noindex:

.. note::
This method will mutate the original model. If you want to have a copy of the model with the selected variables/modules you can use :py:data:`inplace=False` argument.

In order to preview the needed exogenous variables, the :py:meth:`.get_dependencies` method can be used:

Expand Down
8 changes: 8 additions & 0 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,11 @@ We can easily access the current value of a model component using square bracket
If you try to get the current values of a lookup variable, the previous method will fail, as lookup variables take arguments. However, it is possible to get the full series of a lookup or data object with :py:meth:`.get_series_data` method::

>>> model.get_series_data('Growth lookup')


Copying a model
---------------
Sometimes, you may want to run several versions of a model. For this purpose, copying an already-loaded model to make changes while keeping an untouched one is useful. The :py:meth:`.copy` method will help do that; it will load a new model from the translated file and apply to it the same changes that have been applied to the original model (modifying components, selecting submodels, etc.). You can also load a copy of the source model (without applying) any change setting the argument :py:data:`reload=True`.

.. warning::
The copy function will load a new model from the file and apply the same changes to it. If any of these changes have replaced a variable with a function that references other variables in the model, the copy will not work properly since the function will still reference the variables in the original model, in which case the function should be redefined.
1 change: 1 addition & 0 deletions docs/python_api/model_class.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Macro class
-----------
.. autoclass:: pysd.py_backend.model.Macro
:members:
:exclude-members: export
7 changes: 6 additions & 1 deletion docs/whats_new.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
What's New
==========
v3.12.1 (2023/12/XX)
v3.13.0 (2023/12/25)
--------------------
New Features
~~~~~~~~~~~~
- Include new method :py:meth:`pysd.py_backend.model.Model.copy` which allows copying a model (:issue:`131`). (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)
- :py:meth:`pysd.py_backend.model.Model.select_submodel` now takes an optional argument `inplace` when set to :py:data:`False` it will return a modified copy of the model instead of modifying the original model (:issue:`131`). (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)
- :py:meth:`pysd.py_backend.model.Model.export` will now save also time component information if changed (e.g. final time, time step...). (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)

Breaking changes
~~~~~~~~~~~~~~~~
Expand All @@ -14,10 +17,12 @@ Deprecations
Bug fixes
~~~~~~~~~
- Set the pointer of :py:class:`pysd.py_backend.statefuls.DelayFixed` to 0 during initialization (:issue:`427`). (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)
- :py:meth:`pysd.py_backend.model.Model.export` now works with Macros. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)

Documentation
~~~~~~~~~~~~~
- Improve documentation of methods in :py:class:`pysd.py_backend.model.Model` and :py:class:`pysd.py_backend.model.Macro` includying cross-references and rewrite the one from :py:meth:`pysd.py_backend.model.Macro.set_components`. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)
- Include documentation about the new method :py:meth:`pysd.py_backend.model.Model.copy` and update documentation from :py:meth:`pysd.py_backend.model.Model.select_submodel`. (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)

Performance
~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion pysd/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "3.12.1"
__version__ = "3.13.0"
72 changes: 59 additions & 13 deletions pysd/py_backend/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import random
import inspect
import importlib.util
from copy import deepcopy

import numpy as np

Expand Down Expand Up @@ -129,13 +130,45 @@ def __init__(self):
self._time = None
self.stage = None
self.return_timestamps = None
self._next_return = None
self._control_vars_tracker = {}

def __call__(self):
return self._time

def export(self):
"""Exports time values to a dictionary."""
return {
"control_vars": self._get_control_vars(),
"stage": self.stage,
"_time": self._time,
"return_timestamps": self.return_timestamps,
"_next_return": self._next_return
}

def _get_control_vars(self):
"""
Make control vars changes exportable.
"""
out = {}
for cvar, value in self._control_vars_tracker.items():
if callable(value):
out[cvar] = value()
else:
out[cvar] = value
return out

def _set_time(self, time_dict):
"""Copy values from other Time object, used by Model.copy"""
self.set_control_vars(**time_dict['control_vars'])
for key, value in time_dict.items():
if key == 'control_vars':
continue
setattr(self, key, value)

def set_control_vars(self, **kwargs):
"""
Set the control variables valies
Set the control variables values
Parameters
----------
Expand All @@ -149,6 +182,20 @@ def set_control_vars(self, **kwargs):
saveper: float, callable or None
Saveper.
"""
# filter None values
kwargs = {
key: value for key, value in kwargs.items()
if value is not None
}
# track changes
self._control_vars_tracker.update(kwargs)
self._set_control_vars(**kwargs)

def _set_control_vars(self, **kwargs):
"""
Set the control variables values. Private version to be used
to avoid tracking changes.
"""
def _convert_value(value):
# this function is necessary to avoid copying the pointer in the
Expand All @@ -159,8 +206,7 @@ def _convert_value(value):
return lambda: value

for key, value in kwargs.items():
if value is not None:
setattr(self, key, _convert_value(value))
setattr(self, key, _convert_value(value))

if "initial_time" in kwargs:
self._initial_time = self.initial_time()
Expand All @@ -184,16 +230,16 @@ def in_return(self):

if self.return_timestamps is not None:
# this allows managing float precision error
if self.next_return is None:
if self._next_return is None:
return False
if np.isclose(self._time, self.next_return, prec):
if np.isclose(self._time, self._next_return, prec):
self._update_next_return()
return True
else:
while self.next_return is not None\
and self._time > self.next_return:
while self._next_return is not None\
and self._time > self._next_return:
warn(
f"The returning time stamp '{self.next_return}' "
f"The returning time stamp '{self._next_return}' "
"seems to not be a multiple of the time step. "
"This value will not be saved in the output. "
"Please, modify the returning timestamps or the "
Expand All @@ -218,12 +264,12 @@ def add_return_timestamps(self, return_timestamps):
and len(return_timestamps) > 0:
self.return_timestamps = list(return_timestamps)
self.return_timestamps.sort(reverse=True)
self.next_return = self.return_timestamps.pop()
self._next_return = self.return_timestamps.pop()
elif isinstance(return_timestamps, (float, int)):
self.next_return = return_timestamps
self._next_return = return_timestamps
self.return_timestamps = []
else:
self.next_return = None
self._next_return = None
self.return_timestamps = None

def update(self, value):
Expand All @@ -233,9 +279,9 @@ def update(self, value):
def _update_next_return(self):
""" Update the next_return value """
if self.return_timestamps:
self.next_return = self.return_timestamps.pop()
self._next_return = self.return_timestamps.pop()
else:
self.next_return = None
self._next_return = None

def reset(self):
""" Reset time value to the initial """
Expand Down
Loading

0 comments on commit 30bda49

Please sign in to comment.