Skip to content

Commit d437b5b

Browse files
Stop the model when time >= final_time
1 parent 5ae3e38 commit d437b5b

File tree

4 files changed

+52
-26
lines changed

4 files changed

+52
-26
lines changed

pysd/py_backend/functions.py

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,13 +1243,17 @@ def _build_euler_timeseries(self, return_timestamps=None, final_time=None):
12431243
except IndexError:
12441244
# return_timestamps is an empty list
12451245
# model default final time or passed argument value
1246-
t_f = self.final_time
1246+
t_f = self.components.final_time()
12471247

12481248
if final_time is not None:
12491249
t_f = max(final_time, t_f)
12501250

12511251
ts = np.arange(
1252-
t_0, t_f+self.time_step/2, self.time_step, dtype=np.float64)
1252+
t_0,
1253+
t_f+self.components.time_step()/2,
1254+
self.components.time_step(),
1255+
dtype=np.float64
1256+
)
12531257

12541258
# Add the returned time series into the integration array.
12551259
# Best we can do for now. This does change the integration ever
@@ -1278,11 +1282,10 @@ def _format_return_timestamps(self, return_timestamps=None):
12781282
# Vensim's standard is to expect that the data set includes
12791283
# the `final time`, so we have to add an extra period to
12801284
# make sure we get that value in what numpy's `arange` gives us.
1281-
12821285
return np.arange(
12831286
self.time(),
1284-
self.final_time + self.saveper/2,
1285-
self.saveper, dtype=float
1287+
self.components.final_time() + self.components.saveper()/2,
1288+
self.components.saveper(), dtype=float
12861289
)
12871290

12881291
try:
@@ -1377,23 +1380,35 @@ def run(self, params=None, return_columns=None, return_timestamps=None,
13771380

13781381
self.progress = progress
13791382

1383+
# TODO move control variables to a class
1384+
if params is None:
1385+
params = {}
1386+
if final_time:
1387+
params['final_time'] = final_time
1388+
elif return_timestamps is not None:
1389+
params['final_time'] =\
1390+
self._format_return_timestamps(return_timestamps)[-1]
1391+
if time_step:
1392+
params['time_step'] = time_step
1393+
if saveper:
1394+
params['saveper'] = saveper
1395+
# END TODO
1396+
13801397
if params:
13811398
self.set_components(params)
13821399

13831400
self.set_initial_condition(initial_condition)
13841401

1402+
# TODO move control variables to a class
13851403
# save control variables
1386-
self.initial_time = self.time()
1387-
self.final_time = final_time or self.components.final_time()
1388-
self.time_step = time_step or self.components.time_step()
1389-
self.saveper = saveper or max(self.time_step,
1390-
self.components.saveper())
1391-
# need to take bigger saveper if time_step is > saveper
1404+
replace = {
1405+
'initial_time': self.time()
1406+
}
1407+
# END TODO
13921408

13931409
return_timestamps = self._format_return_timestamps(return_timestamps)
13941410

13951411
t_series = self._build_euler_timeseries(return_timestamps, final_time)
1396-
self.final_time = t_series[-1]
13971412

13981413
if return_columns is None or isinstance(return_columns, str):
13991414
return_columns = self._default_return_columns(return_columns)
@@ -1410,10 +1425,9 @@ def run(self, params=None, return_columns=None, return_timestamps=None,
14101425
res = self._integrate(t_series, capture_elements['step'],
14111426
return_timestamps)
14121427

1413-
self._add_run_elements(res, capture_elements['run'])
1428+
self._add_run_elements(res, capture_elements['run'], replace=replace)
14141429

14151430
return_df = utils.make_flat_df(res, return_addresses, flatten_output)
1416-
return_df.index = return_timestamps
14171431

14181432
return return_df
14191433

@@ -1562,8 +1576,7 @@ def _integrate(self, time_steps, capture_elements, return_timestamps):
15621576
outputs: list of dictionaries
15631577
15641578
"""
1565-
outputs = pd.DataFrame(index=return_timestamps,
1566-
columns=capture_elements)
1579+
outputs = pd.DataFrame(columns=capture_elements)
15671580

15681581
if self.progress:
15691582
# initialize progress bar
@@ -1580,6 +1593,10 @@ def _integrate(self, time_steps, capture_elements, return_timestamps):
15801593
self.time.update(t2) # this will clear the stepwise caches
15811594
self.components.cache.reset(t2)
15821595
progressbar.update()
1596+
# TODO move control variables to a class and automatically stop
1597+
# when updating time
1598+
if self.time() >= self.components.final_time():
1599+
break
15831600

15841601
# need to add one more time step, because we run only the state
15851602
# updates in the previous loop and thus may be one short.
@@ -1591,7 +1608,7 @@ def _integrate(self, time_steps, capture_elements, return_timestamps):
15911608

15921609
return outputs
15931610

1594-
def _add_run_elements(self, df, capture_elements):
1611+
def _add_run_elements(self, df, capture_elements, replace={}):
15951612
"""
15961613
Adds constant elements to a dataframe.
15971614
@@ -1603,6 +1620,10 @@ def _add_run_elements(self, df, capture_elements):
16031620
capture_elements: list
16041621
List of constant elements
16051622
1623+
replace: dict
1624+
Ouputs values to replace.
1625+
TODO: move control variables to a class and avoid this.
1626+
16061627
Returns
16071628
-------
16081629
None
@@ -1612,16 +1633,17 @@ def _add_run_elements(self, df, capture_elements):
16121633
for element in capture_elements:
16131634
df[element] = [getattr(self.components, element)()] * nt
16141635

1636+
# TODO: move control variables to a class and avoid this.
16151637
# update initial time values in df (necessary if initial_conditions)
1616-
for it in ['initial_time', 'final_time', 'saveper', 'time_step']:
1638+
for it, value in replace.items():
16171639
if it in df:
1618-
df[it] = getattr(self, it)
1640+
df[it] = value
16191641
elif it.upper() in df:
1620-
df[it.upper()] = getattr(self, it)
1642+
df[it.upper()] = value
16211643
elif it.replace('_', ' ') in df:
1622-
df[it.replace('_', ' ')] = getattr(self, it)
1644+
df[it.replace('_', ' ')] = value
16231645
elif it.replace('_', ' ').upper() in df:
1624-
df[it.replace('_', ' ').upper()] = getattr(self, it)
1646+
df[it.replace('_', ' ').upper()] = value
16251647

16261648

16271649
def ramp(time, slope, start, finish=0):

tests/integration_test_vensim_pathway.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ def test_delay_fixed(self):
5050
output, canon = runner('test-models/tests/delay_fixed/test_delay_fixed.mdl')
5151
assert_frames_close(output, canon, rtol=rtol)
5252

53-
@unittest.skip('to be fixed #225')
5453
def test_delay_numeric_error(self):
5554
# issue https://github.com/JamesPHoughton/pysd/issues/225
5655
output, canon = runner('test-models/tests/delay_numeric_error/test_delay_numeric_error.mdl')
@@ -72,6 +71,11 @@ def test_delays(self):
7271
output, canon = runner('test-models/tests/delays/test_delays.mdl')
7372
assert_frames_close(output, canon, rtol=rtol)
7473

74+
def test_dynamic_final_time(self):
75+
# issue https://github.com/JamesPHoughton/pysd/issues/278
76+
output, canon = runner('test-models/tests/dynamic_final_time/test_dynamic_final_time.mdl')
77+
assert_frames_close(output, canon, rtol=rtol)
78+
7579
def test_euler_step_vs_saveper(self):
7680
output, canon = runner('test-models/tests/euler_step_vs_saveper/test_euler_step_vs_saveper.mdl')
7781
assert_frames_close(output, canon, rtol=rtol)

tests/unit_test_pysd.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1590,8 +1590,8 @@ def test__build_euler_timeseries(self):
15901590

15911591
model = pysd.read_vensim(test_model)
15921592
model.components.initial_time = lambda: 3
1593-
model.final_time = 50
1594-
model.time_step = 1
1593+
model.components.final_time = lambda: 50
1594+
model.components.time_step = lambda: 1
15951595
model.initialize()
15961596

15971597
actual = list(model._build_euler_timeseries(return_timestamps=[10]))

0 commit comments

Comments
 (0)