Skip to content

Commit d2eeb44

Browse files
Add copy method
1 parent db2932f commit d2eeb44

File tree

9 files changed

+432
-92
lines changed

9 files changed

+432
-92
lines changed

docs/advanced_usage.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ A submodel of a translated model can be run as a standalone model. This can be d
145145
.. automethod:: pysd.py_backend.model.Model.select_submodel
146146
:noindex:
147147

148+
.. note::
149+
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.
148150

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

docs/getting_started.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,3 +272,11 @@ We can easily access the current value of a model component using square bracket
272272
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::
273273

274274
>>> model.get_series_data('Growth lookup')
275+
276+
277+
Copying a model
278+
---------------
279+
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`.
280+
281+
.. warning::
282+
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.

docs/python_api/model_class.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Macro class
1111
-----------
1212
.. autoclass:: pysd.py_backend.model.Macro
1313
:members:
14+
:exclude-members: export

docs/whats_new.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
What's New
22
==========
3-
v3.12.1 (2023/12/XX)
3+
v3.13.0 (2023/12/25)
44
--------------------
55
New Features
66
~~~~~~~~~~~~
7+
- Include new method :py:meth:`pysd.py_backend.model.Model.copy` which allows copying a model (:issue:`131`). (`@enekomartinmartinez <https://github.com/enekomartinmartinez>`_)
8+
- :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>`_)
9+
- :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>`_)
710

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

1822
Documentation
1923
~~~~~~~~~~~~~
2024
- 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>`_)
25+
- 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>`_)
2126

2227
Performance
2328
~~~~~~~~~~~

pysd/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "3.12.1"
1+
__version__ = "3.13.0"

pysd/py_backend/components.py

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import random
88
import inspect
99
import importlib.util
10+
from copy import deepcopy
1011

1112
import numpy as np
1213

@@ -129,13 +130,45 @@ def __init__(self):
129130
self._time = None
130131
self.stage = None
131132
self.return_timestamps = None
133+
self._next_return = None
134+
self._control_vars_tracker = {}
132135

133136
def __call__(self):
134137
return self._time
135138

139+
def export(self):
140+
"""Exports time values to a dictionary."""
141+
return {
142+
"control_vars": self._get_control_vars(),
143+
"stage": self.stage,
144+
"_time": self._time,
145+
"return_timestamps": self.return_timestamps,
146+
"_next_return": self._next_return
147+
}
148+
149+
def _get_control_vars(self):
150+
"""
151+
Make control vars changes exportable.
152+
"""
153+
out = {}
154+
for cvar, value in self._control_vars_tracker.items():
155+
if callable(value):
156+
out[cvar] = value()
157+
else:
158+
out[cvar] = value
159+
return out
160+
161+
def _set_time(self, time_dict):
162+
"""Copy values from other Time object, used by Model.copy"""
163+
self.set_control_vars(**time_dict['control_vars'])
164+
for key, value in time_dict.items():
165+
if key == 'control_vars':
166+
continue
167+
setattr(self, key, value)
168+
136169
def set_control_vars(self, **kwargs):
137170
"""
138-
Set the control variables valies
171+
Set the control variables values
139172
140173
Parameters
141174
----------
@@ -149,6 +182,20 @@ def set_control_vars(self, **kwargs):
149182
saveper: float, callable or None
150183
Saveper.
151184
185+
"""
186+
# filter None values
187+
kwargs = {
188+
key: value for key, value in kwargs.items()
189+
if value is not None
190+
}
191+
# track changes
192+
self._control_vars_tracker.update(kwargs)
193+
self._set_control_vars(**kwargs)
194+
195+
def _set_control_vars(self, **kwargs):
196+
"""
197+
Set the control variables values. Private version to be used
198+
to avoid tracking changes.
152199
"""
153200
def _convert_value(value):
154201
# this function is necessary to avoid copying the pointer in the
@@ -159,8 +206,7 @@ def _convert_value(value):
159206
return lambda: value
160207

161208
for key, value in kwargs.items():
162-
if value is not None:
163-
setattr(self, key, _convert_value(value))
209+
setattr(self, key, _convert_value(value))
164210

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

185231
if self.return_timestamps is not None:
186232
# this allows managing float precision error
187-
if self.next_return is None:
233+
if self._next_return is None:
188234
return False
189-
if np.isclose(self._time, self.next_return, prec):
235+
if np.isclose(self._time, self._next_return, prec):
190236
self._update_next_return()
191237
return True
192238
else:
193-
while self.next_return is not None\
194-
and self._time > self.next_return:
239+
while self._next_return is not None\
240+
and self._time > self._next_return:
195241
warn(
196-
f"The returning time stamp '{self.next_return}' "
242+
f"The returning time stamp '{self._next_return}' "
197243
"seems to not be a multiple of the time step. "
198244
"This value will not be saved in the output. "
199245
"Please, modify the returning timestamps or the "
@@ -218,12 +264,12 @@ def add_return_timestamps(self, return_timestamps):
218264
and len(return_timestamps) > 0:
219265
self.return_timestamps = list(return_timestamps)
220266
self.return_timestamps.sort(reverse=True)
221-
self.next_return = self.return_timestamps.pop()
267+
self._next_return = self.return_timestamps.pop()
222268
elif isinstance(return_timestamps, (float, int)):
223-
self.next_return = return_timestamps
269+
self._next_return = return_timestamps
224270
self.return_timestamps = []
225271
else:
226-
self.next_return = None
272+
self._next_return = None
227273
self.return_timestamps = None
228274

229275
def update(self, value):
@@ -233,9 +279,9 @@ def update(self, value):
233279
def _update_next_return(self):
234280
""" Update the next_return value """
235281
if self.return_timestamps:
236-
self.next_return = self.return_timestamps.pop()
282+
self._next_return = self.return_timestamps.pop()
237283
else:
238-
self.next_return = None
284+
self._next_return = None
239285

240286
def reset(self):
241287
""" Reset time value to the initial """

0 commit comments

Comments
 (0)