Skip to content

Commit

Permalink
Merge pull request #282 from enekomartinmartinez/close_excels
Browse files Browse the repository at this point in the history
Close files opened with openpyxl
Correct bug when subseting ranges
Correct bug of dynamic final time
  • Loading branch information
enekomartinmartinez authored Jul 13, 2021
2 parents b8a6585 + d437b5b commit 36efa50
Show file tree
Hide file tree
Showing 10 changed files with 101 additions and 33 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ install:
- pip install cython
- pip install --upgrade pip setuptools wheel
- pip install -e .
- pip install psutil
- pip install pytest pytest-cov
- pip install coveralls
# command to run tests
Expand Down
2 changes: 1 addition & 1 deletion pysd/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.8.0"
__version__ = "1.8.1"
4 changes: 4 additions & 0 deletions pysd/py_backend/external.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ def clean(cls):
"""
Clean the dictionary of read files
"""
for file in cls._Excels_opyxl.values():
# close files open directly with openpyxls
file.close()
# files open with pandas are automatically closed
cls._Excels, cls._Excels_opyxl = {}, {}


Expand Down
66 changes: 44 additions & 22 deletions pysd/py_backend/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,13 +1243,17 @@ def _build_euler_timeseries(self, return_timestamps=None, final_time=None):
except IndexError:
# return_timestamps is an empty list
# model default final time or passed argument value
t_f = self.final_time
t_f = self.components.final_time()

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

ts = np.arange(
t_0, t_f+self.time_step/2, self.time_step, dtype=np.float64)
t_0,
t_f+self.components.time_step()/2,
self.components.time_step(),
dtype=np.float64
)

# Add the returned time series into the integration array.
# Best we can do for now. This does change the integration ever
Expand Down Expand Up @@ -1278,11 +1282,10 @@ def _format_return_timestamps(self, return_timestamps=None):
# Vensim's standard is to expect that the data set includes
# the `final time`, so we have to add an extra period to
# make sure we get that value in what numpy's `arange` gives us.

return np.arange(
self.time(),
self.final_time + self.saveper/2,
self.saveper, dtype=float
self.components.final_time() + self.components.saveper()/2,
self.components.saveper(), dtype=float
)

try:
Expand Down Expand Up @@ -1377,23 +1380,35 @@ def run(self, params=None, return_columns=None, return_timestamps=None,

self.progress = progress

# TODO move control variables to a class
if params is None:
params = {}
if final_time:
params['final_time'] = final_time
elif return_timestamps is not None:
params['final_time'] =\
self._format_return_timestamps(return_timestamps)[-1]
if time_step:
params['time_step'] = time_step
if saveper:
params['saveper'] = saveper
# END TODO

if params:
self.set_components(params)

self.set_initial_condition(initial_condition)

# TODO move control variables to a class
# save control variables
self.initial_time = self.time()
self.final_time = final_time or self.components.final_time()
self.time_step = time_step or self.components.time_step()
self.saveper = saveper or max(self.time_step,
self.components.saveper())
# need to take bigger saveper if time_step is > saveper
replace = {
'initial_time': self.time()
}
# END TODO

return_timestamps = self._format_return_timestamps(return_timestamps)

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

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

self._add_run_elements(res, capture_elements['run'])
self._add_run_elements(res, capture_elements['run'], replace=replace)

return_df = utils.make_flat_df(res, return_addresses, flatten_output)
return_df.index = return_timestamps

return return_df

Expand Down Expand Up @@ -1562,8 +1576,7 @@ def _integrate(self, time_steps, capture_elements, return_timestamps):
outputs: list of dictionaries
"""
outputs = pd.DataFrame(index=return_timestamps,
columns=capture_elements)
outputs = pd.DataFrame(columns=capture_elements)

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

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

return outputs

def _add_run_elements(self, df, capture_elements):
def _add_run_elements(self, df, capture_elements, replace={}):
"""
Adds constant elements to a dataframe.
Expand All @@ -1603,6 +1620,10 @@ def _add_run_elements(self, df, capture_elements):
capture_elements: list
List of constant elements
replace: dict
Ouputs values to replace.
TODO: move control variables to a class and avoid this.
Returns
-------
None
Expand All @@ -1612,16 +1633,17 @@ def _add_run_elements(self, df, capture_elements):
for element in capture_elements:
df[element] = [getattr(self.components, element)()] * nt

# TODO: move control variables to a class and avoid this.
# update initial time values in df (necessary if initial_conditions)
for it in ['initial_time', 'final_time', 'saveper', 'time_step']:
for it, value in replace.items():
if it in df:
df[it] = getattr(self, it)
df[it] = value
elif it.upper() in df:
df[it.upper()] = getattr(self, it)
df[it.upper()] = value
elif it.replace('_', ' ') in df:
df[it.replace('_', ' ')] = getattr(self, it)
df[it.replace('_', ' ')] = value
elif it.replace('_', ' ').upper() in df:
df[it.replace('_', ' ').upper()] = getattr(self, it)
df[it.replace('_', ' ').upper()] = value


def ramp(time, slope, start, finish=0):
Expand Down
5 changes: 2 additions & 3 deletions pysd/py_backend/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import regex as re
import progressbar
import numpy as np
import xarray as xr

# used to create python safe names
Expand Down Expand Up @@ -688,9 +689,7 @@ def rearrange(data, dims, coords):
if data.shape == shape:
# Allows switching dimensions names and transpositions
return xr.DataArray(data=data.values, coords=coords, dims=dims)
elif len(shape) == len(data.shape) and all(
[shape[i] < data.shape[i] for i in range(len(shape))]
):
elif np.prod(shape) < np.prod(data.shape):
# Allows subscripting a subrange
return data.rename(
{dim: new_dim for dim, new_dim in zip(data.dims, dims)}
Expand Down
6 changes: 5 additions & 1 deletion tests/integration_test_vensim_pathway.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ def test_delay_fixed(self):
output, canon = runner('test-models/tests/delay_fixed/test_delay_fixed.mdl')
assert_frames_close(output, canon, rtol=rtol)

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

def test_dynamic_final_time(self):
# issue https://github.com/JamesPHoughton/pysd/issues/278
output, canon = runner('test-models/tests/dynamic_final_time/test_dynamic_final_time.mdl')
assert_frames_close(output, canon, rtol=rtol)

def test_euler_step_vs_saveper(self):
output, canon = runner('test-models/tests/euler_step_vs_saveper/test_euler_step_vs_saveper.mdl')
assert_frames_close(output, canon, rtol=rtol)
Expand Down
30 changes: 28 additions & 2 deletions tests/unit_test_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,33 @@ def test_read_clean_opyxl(self):
self.assertEqual(list(pysd.external.Excels._Excels_opyxl),
[])

def test_close_file(self):
"""
Test for checking if excel files were closed
"""
import pysd
import psutil

p = psutil.Process()

# number of files already open
n_files = len(p.open_files())

file_name = "data/input.xlsx"
sheet_name = "Vertical"
sheet_name2 = "Horizontal"

# reading files
pysd.external.Excels.read(file_name, sheet_name)
pysd.external.Excels.read(file_name, sheet_name2)
pysd.external.Excels.read_opyxl(file_name)

self.assertGreater(len(p.open_files()), n_files)

# clean
pysd.external.Excels.clean()
self.assertEqual(len(p.open_files()), n_files)


class TestExternalMethods(unittest.TestCase):
"""
Expand Down Expand Up @@ -3083,7 +3110,6 @@ def test_data_interp_hnnm_keep(self):

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


def test_lookup_data_attr(self):
"""
Test for keep in series when the series is not
Expand Down Expand Up @@ -3119,4 +3145,4 @@ def test_lookup_data_attr(self):
datL.initialize()

self.assertTrue(hasattr(datD, 'time_row_or_cols'))
self.assertTrue(hasattr(datL, 'x_row_or_cols'))
self.assertTrue(hasattr(datL, 'x_row_or_cols'))
4 changes: 2 additions & 2 deletions tests/unit_test_pysd.py
Original file line number Diff line number Diff line change
Expand Up @@ -1590,8 +1590,8 @@ def test__build_euler_timeseries(self):

model = pysd.read_vensim(test_model)
model.components.initial_time = lambda: 3
model.final_time = 50
model.time_step = 1
model.components.final_time = lambda: 50
model.components.time_step = lambda: 1
model.initialize()

actual = list(model._build_euler_timeseries(return_timestamps=[10]))
Expand Down
14 changes: 13 additions & 1 deletion tests/unit_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -438,11 +438,16 @@ def test_rearrange(self):
_subscript_dict = {
'd1': ['a', 'b', 'c'],
'd2': ['b', 'c'],
'd3': ['b', 'c']
'd3': ['b', 'c'],
'd4': ['e', 'f']
}

xr_input_subdim = xr.DataArray([1, 4, 2],
{'d1': ['a', 'b', 'c']}, ['d1'])
xr_input_subdim2 = xr.DataArray([[1, 4, 2], [3, 4, 5]],
{'d1': ['a', 'b', 'c'],
'd4': ['e', 'f']},
['d4', 'd1'])
xr_input_updim = xr.DataArray([1, 4],
{'d2': ['b', 'c']}, ['d2'])
xr_input_switch = xr.DataArray([[1, 4], [8, 5]],
Expand All @@ -452,6 +457,10 @@ def test_rearrange(self):

xr_out_subdim = xr.DataArray([4, 2],
{'d2': ['b', 'c']}, ['d2'])
xr_out_subdim2 = xr.DataArray([[4, 2], [4, 5]],
{'d2': ['b', 'c'],
'd4': ['e', 'f']},
['d4', 'd2'])
xr_out_updim = xr.DataArray([[1, 1], [4, 4]],
{'d2': ['b', 'c'], 'd3': ['b', 'c']},
['d2', 'd3'])
Expand All @@ -464,6 +473,9 @@ def test_rearrange(self):
self.assertTrue(xr_out_subdim.equals(
rearrange(xr_input_subdim, ['d2'], _subscript_dict)))

self.assertTrue(xr_out_subdim2.equals(
rearrange(xr_input_subdim2, ['d4', 'd2'], _subscript_dict)))

self.assertTrue(xr_out_updim.equals(
rearrange(xr_input_updim, ['d2', 'd3'], _subscript_dict)))

Expand Down

0 comments on commit 36efa50

Please sign in to comment.