Skip to content

Commit 36efa50

Browse files
Merge pull request #282 from enekomartinmartinez/close_excels
Close files opened with openpyxl Correct bug when subseting ranges Correct bug of dynamic final time
2 parents b8a6585 + d437b5b commit 36efa50

10 files changed

+101
-33
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ install:
88
- pip install cython
99
- pip install --upgrade pip setuptools wheel
1010
- pip install -e .
11+
- pip install psutil
1112
- pip install pytest pytest-cov
1213
- pip install coveralls
1314
# command to run tests

pysd/_version.py

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

pysd/py_backend/external.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ def clean(cls):
5353
"""
5454
Clean the dictionary of read files
5555
"""
56+
for file in cls._Excels_opyxl.values():
57+
# close files open directly with openpyxls
58+
file.close()
59+
# files open with pandas are automatically closed
5660
cls._Excels, cls._Excels_opyxl = {}, {}
5761

5862

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):

pysd/py_backend/utils.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
import regex as re
1313
import progressbar
14+
import numpy as np
1415
import xarray as xr
1516

1617
# used to create python safe names
@@ -688,9 +689,7 @@ def rearrange(data, dims, coords):
688689
if data.shape == shape:
689690
# Allows switching dimensions names and transpositions
690691
return xr.DataArray(data=data.values, coords=coords, dims=dims)
691-
elif len(shape) == len(data.shape) and all(
692-
[shape[i] < data.shape[i] for i in range(len(shape))]
693-
):
692+
elif np.prod(shape) < np.prod(data.shape):
694693
# Allows subscripting a subrange
695694
return data.rename(
696695
{dim: new_dim for dim, new_dim in zip(data.dims, dims)}

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_external.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,33 @@ def test_read_clean_opyxl(self):
6666
self.assertEqual(list(pysd.external.Excels._Excels_opyxl),
6767
[])
6868

69+
def test_close_file(self):
70+
"""
71+
Test for checking if excel files were closed
72+
"""
73+
import pysd
74+
import psutil
75+
76+
p = psutil.Process()
77+
78+
# number of files already open
79+
n_files = len(p.open_files())
80+
81+
file_name = "data/input.xlsx"
82+
sheet_name = "Vertical"
83+
sheet_name2 = "Horizontal"
84+
85+
# reading files
86+
pysd.external.Excels.read(file_name, sheet_name)
87+
pysd.external.Excels.read(file_name, sheet_name2)
88+
pysd.external.Excels.read_opyxl(file_name)
89+
90+
self.assertGreater(len(p.open_files()), n_files)
91+
92+
# clean
93+
pysd.external.Excels.clean()
94+
self.assertEqual(len(p.open_files()), n_files)
95+
6996

7097
class TestExternalMethods(unittest.TestCase):
7198
"""
@@ -3083,7 +3110,6 @@ def test_data_interp_hnnm_keep(self):
30833110

30843111
self.assertTrue(data.data.equals(expected))
30853112

3086-
30873113
def test_lookup_data_attr(self):
30883114
"""
30893115
Test for keep in series when the series is not
@@ -3119,4 +3145,4 @@ def test_lookup_data_attr(self):
31193145
datL.initialize()
31203146

31213147
self.assertTrue(hasattr(datD, 'time_row_or_cols'))
3122-
self.assertTrue(hasattr(datL, 'x_row_or_cols'))
3148+
self.assertTrue(hasattr(datL, 'x_row_or_cols'))

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]))

tests/unit_test_utils.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,11 +438,16 @@ def test_rearrange(self):
438438
_subscript_dict = {
439439
'd1': ['a', 'b', 'c'],
440440
'd2': ['b', 'c'],
441-
'd3': ['b', 'c']
441+
'd3': ['b', 'c'],
442+
'd4': ['e', 'f']
442443
}
443444

444445
xr_input_subdim = xr.DataArray([1, 4, 2],
445446
{'d1': ['a', 'b', 'c']}, ['d1'])
447+
xr_input_subdim2 = xr.DataArray([[1, 4, 2], [3, 4, 5]],
448+
{'d1': ['a', 'b', 'c'],
449+
'd4': ['e', 'f']},
450+
['d4', 'd1'])
446451
xr_input_updim = xr.DataArray([1, 4],
447452
{'d2': ['b', 'c']}, ['d2'])
448453
xr_input_switch = xr.DataArray([[1, 4], [8, 5]],
@@ -452,6 +457,10 @@ def test_rearrange(self):
452457

453458
xr_out_subdim = xr.DataArray([4, 2],
454459
{'d2': ['b', 'c']}, ['d2'])
460+
xr_out_subdim2 = xr.DataArray([[4, 2], [4, 5]],
461+
{'d2': ['b', 'c'],
462+
'd4': ['e', 'f']},
463+
['d4', 'd2'])
455464
xr_out_updim = xr.DataArray([[1, 1], [4, 4]],
456465
{'d2': ['b', 'c'], 'd3': ['b', 'c']},
457466
['d2', 'd3'])
@@ -464,6 +473,9 @@ def test_rearrange(self):
464473
self.assertTrue(xr_out_subdim.equals(
465474
rearrange(xr_input_subdim, ['d2'], _subscript_dict)))
466475

476+
self.assertTrue(xr_out_subdim2.equals(
477+
rearrange(xr_input_subdim2, ['d4', 'd2'], _subscript_dict)))
478+
467479
self.assertTrue(xr_out_updim.equals(
468480
rearrange(xr_input_updim, ['d2', 'd3'], _subscript_dict)))
469481

0 commit comments

Comments
 (0)