11from pathlib import Path
2- from warnings import simplefilter , catch_warnings
2+ import shutil
33
44import pytest
55import numpy as np
1414import pysd
1515
1616
17- _root = Path (__file__ ).parent .parent
18-
19- test_model_look = _root .joinpath (
17+ test_model_look = Path (
2018 "test-models/tests/get_lookups_subscripted_args/"
21- + "test_get_lookups_subscripted_args.mdl" )
22- test_model_constants = _root .joinpath (
19+ "test_get_lookups_subscripted_args.mdl"
20+ )
21+ test_model_constants = Path (
2322 "test-models/tests/get_constants_subranges/"
2423 "test_get_constants_subranges.mdl"
2524)
26- test_model_numeric_coords = _root . joinpath (
25+ test_model_numeric_coords = Path (
2726 "test-models/tests/subscript_1d_arrays/"
2827 "test_subscript_1d_arrays.mdl"
2928)
30- test_variable_step = _root . joinpath (
29+ test_variable_step = Path (
3130 "test-models/tests/control_vars/"
3231 "test_control_vars.mdl"
3332)
34- test_partial_definitions = _root . joinpath (
33+ test_partial_definitions = Path (
3534 "test-models/tests/partial_range_definitions/"
3635 "test_partial_range_definitions.mdl"
3736)
3837
3938
39+ @pytest .fixture
40+ @pytest .mark .filterwarnings ("ignore" )
41+ def model (_root , tmp_path , model_path ):
42+ """
43+ Copy model to the tmp_path and translate it
44+ """
45+
46+ target = tmp_path / model_path .parent
47+ shutil .copytree (_root / model_path .parent , target )
48+ return pysd .read_vensim (target / model_path .name )
49+
50+
4051class TestOutput ():
4152
4253 def test_output_handler_interface (self ):
@@ -124,151 +135,167 @@ class EmptyHandler(OutputHandlerInterface):
124135 EmptyHandler .add_run_elements (
125136 EmptyHandler , "model" , "capture" )
126137
127- def test_output_nc (self , shared_tmpdir ):
128- model = pysd .read_vensim (test_model_look )
129- model .progress = False
130-
131- out_file = shared_tmpdir .joinpath ("results.nc" )
132-
133- with catch_warnings (record = True ) as w :
134- simplefilter ("always" )
135- model .run (output_file = out_file )
138+ @pytest .mark .parametrize (
139+ "model_path,dims,values" ,
140+ [
141+ (
142+ test_model_look ,
143+ {
144+ "Rows" : 2 ,
145+ "Dim" : 2 ,
146+ "time" : 61
147+ },
148+ {
149+ "lookup_1d_time" : (("time" ,), None ),
150+ "d2d" : (("time" , "Rows" , "Dim" ), None ),
151+ "initial_time" : (tuple (), 0 ),
152+ "final_time" : (tuple (), 30 ),
153+ "saveper" : (tuple (), 0.5 ),
154+ "time_step" : (tuple (), 0.5 )
155+ }
156+
157+ ),
158+ (
159+ test_model_constants ,
160+ {
161+ "dim1" : 5 ,
162+ "dim1a" : 2 ,
163+ "dim1c" : 3 ,
164+ 'time' : 2
165+ },
166+ {
167+ "constant" : (
168+ ("dim1" ,),
169+ np .array ([0. , 0. , 1. , 15. , 50. ])
170+ )
171+ }
172+ ),
173+ (
174+ test_model_numeric_coords ,
175+ {
176+ "One Dimensional Subscript" : 3 ,
177+ 'time' : 101
178+ },
179+ {
180+ "rate_a" : (
181+ ("One Dimensional Subscript" ,),
182+ np .array ([0.01 , 0.02 , 0.03 ])),
183+ "stock_a" : (
184+ ("time" , "One Dimensional Subscript" ),
185+ np .array ([
186+ np .arange (0 , 1.0001 , 0.01 ),
187+ np .arange (0 , 2.0001 , 0.02 ),
188+ np .arange (0 , 3.0001 , 0.03 )],
189+ dtype = float ).transpose ()),
190+ "time" : (("time" ,), np .arange (0.0 , 101.0 , 1.0 ))
191+ }
192+ ),
193+ (
194+ test_variable_step ,
195+ {
196+ "time" : 25
197+ },
198+ {
199+ "final_time" : (
200+ ("time" ,),
201+ np .array ([
202+ 10. , 10. , 10. , 10. , 10. , 10. ,
203+ 50. , 50. , 50. , 50. , 50. , 50. ,
204+ 50. , 50. , 50. , 50. , 50. , 50. ,
205+ 50. , 50. , 50. , 50. , 50. , 50. , 50.
206+ ])),
207+ "initial_time" : (
208+ ("time" ,),
209+ np .array ([
210+ 0. , 0. , 0. , 0.2 , 0.2 , 0.2 ,
211+ 0.2 , 0.2 , 0.2 , 0.2 , 0.2 , 0.2 ,
212+ 0.2 , 0.2 , 0.2 , 0.2 , 0.2 , 0.2 ,
213+ 0.2 , 0.2 , 0.2 , 0.2 , 0.2 , 0.2 , 0.2
214+ ])),
215+ "time_step" : (
216+ ("time" ,),
217+ np .array ([
218+ 1. , 1. , 1. , 1. , 0.5 , 0.5 ,
219+ 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 ,
220+ 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 ,
221+ 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5
222+ ])),
223+ "saveper" : (
224+ ("time" ,),
225+ np .array ([
226+ 1. , 1. , 1. , 1. , 0.5 , 0.5 ,
227+ 0.5 , 0.5 , 0.5 , 0.5 , 0.5 , 0.5 ,
228+ 0.5 , 0.5 , 0.5 , 0.5 , 5. , 5. ,
229+ 5. , 5. , 5. , 5. , 5. , 5. , 5. ]))
230+ }
231+ ),
232+ (
233+ test_partial_definitions ,
234+ {
235+ "my range" : 5 ,
236+ "time" : 11
237+ },
238+ {
239+ "partial_data" : (("time" , "my range" ), None ),
240+ "partial_constants" : (("my range" ,), None )
241+ }
242+ )
243+ ],
244+ ids = ["lookups" , "constants" , "numeric_coords" , "variable_step" ,
245+ "partial_definitions" ]
246+ )
247+ @pytest .mark .filterwarnings ("ignore" )
248+ def test_output_nc (self , tmp_path , model , dims , values ):
249+
250+ out_file = tmp_path .joinpath ("results.nc" )
251+
252+ model .run (output_file = out_file )
136253
137254 with nc .Dataset (out_file , "r" ) as ds :
138255 assert ds .ncattrs () == [
139256 'description' , 'model_file' , 'timestep' , 'initial_time' ,
140257 'final_time' ]
141- assert list (ds .dimensions . keys ()) == [ "Rows" , "Dim" , "time" ]
258+ assert list (ds .dimensions ) == list ( dims )
142259 # dimensions are stored as variables
143- assert ds ["Rows" ].size == 2
144- assert "Rows" in ds .variables .keys ()
145- assert "time" in ds .variables .keys ()
146- # scalars do not have the time dimension
147- assert ds ["initial_time" ].size == 1
148- # cache step variables have the "time" dimension
149- assert ds ["lookup_1d_time" ].dimensions == ("time" ,)
150-
151- assert ds ["d2d" ].dimensions == ("time" , "Rows" , "Dim" )
152-
153- with catch_warnings (record = True ) as w :
154- simplefilter ("always" )
155- assert ds ["d2d" ].Comment == "Missing"
156- assert ds ["d2d" ].Units == "Missing"
157-
158- # test cache run variables with dimensions
159- model2 = pysd .read_vensim (test_model_constants )
160- model2 .progress = False
161-
162- out_file2 = shared_tmpdir .joinpath ("results2.nc" )
163-
164- with catch_warnings (record = True ) as w :
165- simplefilter ("always" )
166- model2 .run (output_file = out_file2 )
167-
168- with nc .Dataset (out_file2 , "r" ) as ds :
169- assert ds ["time_step" ].size == 1
170- assert "constant" in list (ds .variables .keys ())
171- assert ds ["constant" ].dimensions == ("dim1" ,)
172-
173- with catch_warnings (record = True ) as w :
174- simplefilter ("always" )
175- assert ds ["dim1" ][:].data .dtype == "S1"
176-
177- # dimension with numeric coords
178- model3 = pysd .read_vensim (test_model_numeric_coords )
179- model3 .progress = False
180-
181- out_file3 = shared_tmpdir .joinpath ("results3.nc" )
182-
183- with catch_warnings (record = True ) as w :
184- simplefilter ("always" )
185- model3 .run (output_file = out_file3 )
186-
187- # using xarray instead of netCDF4 to load the dataset
188-
189- with catch_warnings (record = True ) as w :
190- simplefilter ("always" )
191- ds = xr .open_dataset (out_file3 , engine = "netcdf4" )
192-
193- assert "time" in ds .dims
194- assert ds ["rate_a" ].dims == ("One Dimensional Subscript" ,)
195- assert ds ["stock_a" ].dims == ("time" , "One Dimensional Subscript" )
196-
197- # coordinates get dtype=object when their length is different
198- assert ds ["One Dimensional Subscript" ].data .dtype == "O"
199-
200- # check data
201- assert np .array_equal (
202- ds ["time" ].data , np .arange (0.0 , 101.0 , 1.0 ))
203- assert np .allclose (
204- ds ["stock_a" ][0 , :].data , np .array ([0.0 , 0.0 , 0.0 ]))
205- assert np .allclose (
206- ds ["stock_a" ][- 1 , :].data , np .array ([1.0 , 2.0 , 3.0 ]))
207- assert ds ["rate_a" ].shape == (3 ,)
208-
209- # variable attributes
210- assert list (model .doc .columns ) == list (ds ["stock_a" ].attrs .keys ())
211-
212- # global attributes
213- assert list (ds .attrs .keys ()) == [
214- 'description' , 'model_file' , 'timestep' , 'initial_time' ,
215- 'final_time' ]
216-
217- model4 = pysd .read_vensim (test_variable_step )
218- model4 .progress = False
219-
220- out_file4 = shared_tmpdir .joinpath ("results4.nc" )
221-
222- with catch_warnings (record = True ) as w :
223- simplefilter ("always" )
224- model4 .run (output_file = out_file4 )
225-
226- with catch_warnings (record = True ) as w :
227- simplefilter ("always" )
228- ds = xr .open_dataset (out_file4 , engine = "netcdf4" )
229-
230- # global attributes for variable timestep
231- assert ds .attrs ["timestep" ] == "Variable"
232- assert ds .attrs ["final_time" ] == "Variable"
233-
234- # saveper final_time and time_step have time dimension
235- assert ds ["saveper" ].dims == ("time" ,)
236- assert ds ["time_step" ].dims == ("time" ,)
237- assert ds ["final_time" ].dims == ("time" ,)
238-
239- assert np .unique (ds ["time_step" ]).size == 2
240-
241- def test_output_nc2 (self , shared_tmpdir ):
242- # dimension with numeric coords
243- with catch_warnings (record = True ) as w :
244- simplefilter ("always" )
245- model5 = pysd .read_vensim (test_partial_definitions )
246- model5 .progress = False
247-
248- out_file5 = shared_tmpdir .joinpath ("results5.nc" )
249-
250- with catch_warnings (record = True ) as w :
251- simplefilter ("always" )
252- model5 .run (output_file = out_file5 )
253-
254- # using xarray instead of netCDF4 to load the dataset
255-
256- with catch_warnings (record = True ) as w :
257- simplefilter ("always" )
258- ds = xr .open_dataset (out_file5 , engine = "netcdf4" )
259-
260- print (ds )
261-
262- @pytest .mark .parametrize ("fmt,sep" , [("csv" , "," ), ("tab" , "\t " )])
263- def test_output_csv (self , fmt , sep , capsys , shared_tmpdir ):
264- model = pysd .read_vensim (test_model_look )
265- model .progress = False
266-
267- out_file = shared_tmpdir .joinpath ("results." + fmt )
268-
269- with catch_warnings (record = True ) as w :
270- simplefilter ("always" )
271- model .run (output_file = out_file )
260+ for dim , n in dims .items ():
261+ # check dimension size
262+ assert ds [dim ].size == n
263+ assert dim in ds .variables .keys ()
264+ # check dimension type
265+ if dim != "time" :
266+ assert ds [dim ].dtype in ["S1" , str ]
267+ else :
268+ assert ds [dim ].dtype == float
269+
270+ for var , (dim , val ) in values .items ():
271+ # check variable dimensions
272+ assert ds [var ].dimensions == dim
273+ if val is not None :
274+ # check variable values if given
275+ assert np .all (np .isclose (ds [var ][:].data , val ))
276+
277+ # Check variable attributes
278+ doc = model .doc
279+ doc .set_index ("Py Name" , drop = False , inplace = True )
280+ doc .drop (columns = ["Subscripts" , "Limits" ], inplace = True )
281+
282+ for var in doc ["Py Name" ]:
283+ if doc .loc [var , "Type" ] == "Lookup" :
284+ continue
285+ for key in doc .columns :
286+ assert getattr (ds [var ], key ) == (doc .loc [var , key ]
287+ or "Missing" )
288+
289+ @pytest .mark .parametrize (
290+ "model_path,fmt,sep" ,
291+ [
292+ (test_model_look , "csv" , "," ),
293+ (test_model_look , "tab" , "\t " )])
294+ @pytest .mark .filterwarnings ("ignore" )
295+ def test_output_csv (self , fmt , sep , capsys , model , tmp_path ):
296+ out_file = tmp_path .joinpath ("results." + fmt )
297+
298+ model .run (output_file = out_file )
272299
273300 captured = capsys .readouterr () # capture stdout
274301 assert f"Data saved in '{ out_file } '" in captured .out
@@ -282,10 +309,10 @@ def test_output_csv(self, fmt, sep, capsys, shared_tmpdir):
282309 assert "lookup 3d time[B;Row1]" in df .columns or \
283310 "lookup 3d time[B,Row1]" in df .columns
284311
285- def test_dataset_handler_step_setter ( self , shared_tmpdir ):
286- model = pysd . read_vensim ( test_model_look )
312+ @ pytest . mark . parametrize ( "model_path" , [ test_model_look ])
313+ def test_dataset_handler_step_setter ( self , tmp_path , model ):
287314 capture_elements = set ()
288- results = shared_tmpdir .joinpath ("results.nc" )
315+ results = tmp_path .joinpath ("results.nc" )
289316 output = ModelOutput (model , capture_elements , results )
290317
291318 # Dataset handler step cannot be modified from the outside
0 commit comments