1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | |
---|
4 | # Copyright (C) 2010 Modelon AB |
---|
5 | # |
---|
6 | # This program is free software: you can redistribute it and/or modify |
---|
7 | # it under the terms of the GNU General Public License as published by |
---|
8 | # the Free Software Foundation, version 3 of the License. |
---|
9 | # |
---|
10 | # This program is distributed in the hope that it will be useful, |
---|
11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
---|
12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
---|
13 | # GNU General Public License for more details. |
---|
14 | # |
---|
15 | # You should have received a copy of the GNU General Public License |
---|
16 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
---|
17 | |
---|
18 | """JModelica test package. |
---|
19 | |
---|
20 | This file holds base classes for simulation and optimization tests. |
---|
21 | """ |
---|
22 | |
---|
23 | import os |
---|
24 | |
---|
25 | from pymodelica.compiler import compile_fmu |
---|
26 | from pyfmi.fmi import load_fmu, FMUModelCS1, FMUModelME1, FMUModelCS2, FMUModelME2 |
---|
27 | from pyjmi.common.io import ResultDymolaTextual, ResultDymolaBinary |
---|
28 | from tests_jmodelica import get_files_path |
---|
29 | |
---|
30 | _model_name = '' |
---|
31 | |
---|
32 | class _BaseSimOptTest: |
---|
33 | """ |
---|
34 | Base class for simulation and optimization tests. |
---|
35 | Actual test classes should inherit SimulationTest or OptimizationTest. |
---|
36 | All assertion methods consider a value correct if it falls within either tolerance |
---|
37 | limit, absolute or relative. |
---|
38 | """ |
---|
39 | |
---|
40 | @classmethod |
---|
41 | def setup_class_base(cls, mo_file, class_name, options = {}, target="me", version=None): |
---|
42 | """ |
---|
43 | Set up a new test model. Compiles the model. |
---|
44 | Call this with proper args from setUpClass(). |
---|
45 | mo_file - the relative path from the files dir to the .mo file to compile |
---|
46 | class_name - the qualified name of the class to simulate |
---|
47 | options - a dict of options to set in the compiler, defaults to no options |
---|
48 | """ |
---|
49 | global _model_name |
---|
50 | cls.mo_path = os.path.join(get_files_path(), 'Modelica', mo_file) |
---|
51 | |
---|
52 | if version==None: |
---|
53 | _model_name = compile_fmu(class_name, cls.mo_path, compiler_options=options,target=target) |
---|
54 | else: |
---|
55 | _model_name = compile_fmu(class_name, cls.mo_path, compiler_options=options,target=target,version=version) |
---|
56 | |
---|
57 | |
---|
58 | def setup_base(self, rel_tol, abs_tol): |
---|
59 | """ |
---|
60 | Set up a new test case. Configures test and creates model. |
---|
61 | Call this with proper args from setUp(). |
---|
62 | rel_tol - the relative error tolerance when comparing values |
---|
63 | abs_tol - the absolute error tolerance when comparing values |
---|
64 | Any other named args are passed to the NLP constructor. |
---|
65 | """ |
---|
66 | global _model_name |
---|
67 | self.rel_tol = rel_tol |
---|
68 | self.abs_tol = abs_tol |
---|
69 | self.model_name = _model_name |
---|
70 | parts = _model_name.split('.') |
---|
71 | self.model = load_fmu(self.model_name) |
---|
72 | |
---|
73 | def run(self,cvode_options=None): |
---|
74 | """ |
---|
75 | Run simulation and load result. |
---|
76 | Call this from setUp() or within a test depending if all tests should run simulation. |
---|
77 | """ |
---|
78 | res = self._run_and_write_data(cvode_options) |
---|
79 | self.data = res.result_data#ResultDymolaTextual(self.model_name[:-len('.fmu')] + '_result.txt') |
---|
80 | |
---|
81 | |
---|
82 | def load_expected_data(self, name): |
---|
83 | """ |
---|
84 | Load the expected data to use for assert_all_paths() and assert_all_end_values(). |
---|
85 | name - the file name of the results file, relative to files dir |
---|
86 | """ |
---|
87 | path = os.path.join(get_files_path(), 'Results', name) |
---|
88 | if path.endswith("txt"): |
---|
89 | self.expected = ResultDymolaTextual(path) |
---|
90 | else: |
---|
91 | self.expected = ResultDymolaBinary(path) |
---|
92 | |
---|
93 | |
---|
94 | def assert_all_inital_values(self, variables, rel_tol = None, abs_tol = None): |
---|
95 | """ |
---|
96 | Assert that all given variables match expected intial values loaded by a call to |
---|
97 | load_expected_data(). |
---|
98 | variables - list of the names of the variables to test |
---|
99 | rel_tol - the relative error tolerance, defaults to the value set with setup_base() |
---|
100 | abs_tol - the absolute error tolerance, defaults to the value set with setup_base() |
---|
101 | """ |
---|
102 | self._assert_all_spec_values(variables, 0, rel_tol, abs_tol) |
---|
103 | |
---|
104 | |
---|
105 | def assert_all_end_values(self, variables, rel_tol = None, abs_tol = None): |
---|
106 | """ |
---|
107 | Assert that all given variables match expected end values loaded by a call to |
---|
108 | load_expected_data(). |
---|
109 | variables - list of the names of the variables to test |
---|
110 | rel_tol - the relative error tolerance, defaults to the value set with setup_base() |
---|
111 | abs_tol - the absolute error tolerance, defaults to the value set with setup_base() |
---|
112 | """ |
---|
113 | self._assert_all_spec_values(variables, -1, rel_tol, abs_tol) |
---|
114 | |
---|
115 | |
---|
116 | def assert_all_trajectories(self, variables, same_span = True, rel_tol = None, abs_tol = None): |
---|
117 | """ |
---|
118 | Assert that the trajectories of all given variables match expected trajectories |
---|
119 | loaded by a call to load_expected_data(). |
---|
120 | variables - list of the names of the variables to test |
---|
121 | same_span - if True, require that the paths span the same time interval |
---|
122 | if False, only compare overlapping part, default True |
---|
123 | rel_tol - the relative error tolerance, defaults to the value set with setup_base() |
---|
124 | abs_tol - the absolute error tolerance, defaults to the value set with setup_base() |
---|
125 | """ |
---|
126 | for var in variables: |
---|
127 | expected = self.expected.get_variable_data(var) |
---|
128 | expected_t = self.expected.get_variable_data('time') |
---|
129 | self.assert_trajectory(var, expected, expected_t, same_span, rel_tol, abs_tol) |
---|
130 | |
---|
131 | |
---|
132 | def assert_initial_value(self, variable, value, rel_tol = None, abs_tol = None): |
---|
133 | """ |
---|
134 | Assert that the inital value for a simulation variable matches expected value. |
---|
135 | variable - the name of the variable |
---|
136 | value - the expected value |
---|
137 | rel_tol - the relative error tolerance, defaults to the value set with setup_base() |
---|
138 | abs_tol - the absolute error tolerance, defaults to the value set with setup_base() |
---|
139 | """ |
---|
140 | self._assert_value(variable, value, 0, rel_tol, abs_tol) |
---|
141 | |
---|
142 | |
---|
143 | def assert_end_value(self, variable, value, rel_tol = None, abs_tol = None): |
---|
144 | """ |
---|
145 | Assert that the end result for a simulation variable matches expected value. |
---|
146 | variable - the name of the variable |
---|
147 | value - the expected value |
---|
148 | rel_tol - the relative error tolerance, defaults to the value set with setup_base() |
---|
149 | abs_tol - the absolute error tolerance, defaults to the value set with setup_base() |
---|
150 | """ |
---|
151 | self._assert_value(variable, value, -1, rel_tol, abs_tol) |
---|
152 | |
---|
153 | |
---|
154 | def assert_trajectory(self, variable, expected, expected_t, same_span = True, rel_tol = None, abs_tol = None): |
---|
155 | """ |
---|
156 | Assert that the trajectory of a simulation variable matches expected trajectory. |
---|
157 | variable - the name of the variable |
---|
158 | expected - the expected trajectory |
---|
159 | same_span - if True, require that the paths span the same time interval |
---|
160 | if False, only compare overlapping part, default True |
---|
161 | rel_tol - the relative error tolerance, defaults to the value set with setup_base() |
---|
162 | abs_tol - the absolute error tolerance, defaults to the value set with setup_base() |
---|
163 | """ |
---|
164 | if rel_tol is None: |
---|
165 | rel_tol = self.rel_tol |
---|
166 | if abs_tol is None: |
---|
167 | abs_tol = self.abs_tol |
---|
168 | ans = expected |
---|
169 | ans_t = expected_t |
---|
170 | res = self.data.get_variable_data(variable) |
---|
171 | res_t = self.data.get_variable_data('time') |
---|
172 | |
---|
173 | if same_span: |
---|
174 | msg = 'paths do not span the same time interval for ' + variable |
---|
175 | assert _check_error(ans.t[0], res.t[0], rel_tol, abs_tol), msg |
---|
176 | assert _check_error(ans.t[-1], res.t[-1], rel_tol, abs_tol), msg |
---|
177 | |
---|
178 | # Merge the time lists |
---|
179 | time = list(set(ans.t) | set(res.t)) |
---|
180 | |
---|
181 | # Get overlapping span |
---|
182 | (t1, t2) = (max(ans.t[0], res.t[0]), min(ans.t[-1], res.t[-1])) |
---|
183 | |
---|
184 | # Remove values outside overlap |
---|
185 | #time = filter((lambda t: t >= t1 and t <= t2), time) #This is not a good approach |
---|
186 | time = list(filter((lambda t: t >= t1 and t <= t2), res.t)) |
---|
187 | |
---|
188 | # Check error for each time point |
---|
189 | for i,t in enumerate(time): |
---|
190 | try: |
---|
191 | if time[i-1]==t or t==time[i+1]: #Necessary in case of jump discontinuities! For instance if there is a result at t_e^- and t_e^+ |
---|
192 | continue |
---|
193 | except IndexError: |
---|
194 | pass |
---|
195 | ans_x = _trajectory_eval(ans, ans_t, t) |
---|
196 | res_x = _trajectory_eval(res, res_t, t) |
---|
197 | (rel, abs) = _error(ans_x, res_x) |
---|
198 | msg = 'error of %s at time %f is too large (rel=%f, abs=%f)' % (variable, t, rel, abs) |
---|
199 | assert (rel <= 100*rel_tol or abs <= 100*abs_tol), msg |
---|
200 | |
---|
201 | |
---|
202 | def _assert_all_spec_values(self, variables, index, rel_tol = None, abs_tol = None): |
---|
203 | """ |
---|
204 | Assert that all given variables match expected values loaded by a call to |
---|
205 | load_expected_data(), for a given index in the value arrays. |
---|
206 | variables - list of the names of the variables to test |
---|
207 | index - the index in the array holding the values, 0 is initial, -1 is end |
---|
208 | rel_tol - the relative error tolerance, defaults to the value set with setup_base() |
---|
209 | abs_tol - the absolute error tolerance, defaults to the value set with setup_base() |
---|
210 | """ |
---|
211 | for var in variables: |
---|
212 | value = self.expected.get_variable_data(var)[index] |
---|
213 | self._assert_value(var, value, index, rel_tol, abs_tol) |
---|
214 | |
---|
215 | |
---|
216 | def _assert_value(self, variable, value, index, rel_tol = None, abs_tol = None): |
---|
217 | """ |
---|
218 | Assert that a specific value for a simulation variable matches expected value. |
---|
219 | variable - the name of the variable |
---|
220 | value - the expected value |
---|
221 | index - the index in the array holding the values, 0 is initial, -1 is end |
---|
222 | rel_tol - the relative error tolerance, defaults to the value set with setup_base() |
---|
223 | abs_tol - the absolute error tolerance, defaults to the value set with setup_base() |
---|
224 | """ |
---|
225 | res = self.data.get_variable_data(variable) |
---|
226 | msg = 'error of %s at index %i is too large' % (variable, index) |
---|
227 | self.assert_equals(msg, res.x[index], value, rel_tol = None, abs_tol = None) |
---|
228 | |
---|
229 | |
---|
230 | def assert_equals(self, message, actual, expected, rel_tol = None, abs_tol = None): |
---|
231 | """ |
---|
232 | Assert that a specific value matches expected value. |
---|
233 | actual - the expected value |
---|
234 | expected - the expected value |
---|
235 | message - the error message to use if values does not match |
---|
236 | rel_tol - the relative error tolerance, defaults to the value set with setup_base() |
---|
237 | abs_tol - the absolute error tolerance, defaults to the value set with setup_base() |
---|
238 | """ |
---|
239 | if rel_tol is None: |
---|
240 | rel_tol = self.rel_tol |
---|
241 | if abs_tol is None: |
---|
242 | abs_tol = self.abs_tol |
---|
243 | (rel, abs) = _error(actual, expected) |
---|
244 | assert (rel <= rel_tol or abs <= abs_tol), '%s (rel=%f, abs=%f)' % (message, rel, abs) |
---|
245 | |
---|
246 | |
---|
247 | class SimulationTest(_BaseSimOptTest): |
---|
248 | """ |
---|
249 | Base class for simulation tests. |
---|
250 | """ |
---|
251 | |
---|
252 | @classmethod |
---|
253 | def setup_class_base(cls, mo_file, class_name, options = {}, target="me", version=None): |
---|
254 | """ |
---|
255 | Set up a new test model. Compiles the model. |
---|
256 | Call this with proper args from setUpClass(). |
---|
257 | mo_file - the relative path from the files dir to the .mo file to compile |
---|
258 | class_name - the qualified name of the class to simulate |
---|
259 | options - a dict of options to set in the compiler, defaults to no options |
---|
260 | """ |
---|
261 | _BaseSimOptTest.setup_class_base(mo_file, class_name, options, target, version) |
---|
262 | |
---|
263 | def setup_base(self, rel_tol = 1.0e-4, abs_tol = 1.0e-6, |
---|
264 | start_time=0.0, final_time=10.0, time_step=0.01, input=(), |
---|
265 | write_scaled_result=False): |
---|
266 | """ |
---|
267 | Set up a new test case. Creates and configures the simulation. |
---|
268 | Call this with proper args from setUp(). |
---|
269 | rel_tol - the relative error tolerance when comparing values, default is 1.0e-4 |
---|
270 | abs_tol - the absolute error tolerance when comparing values, default is 1.0e-6 |
---|
271 | Any other named args are passed to sundials. |
---|
272 | """ |
---|
273 | _BaseSimOptTest.setup_base(self, rel_tol, abs_tol) |
---|
274 | |
---|
275 | self.start_time = start_time |
---|
276 | self.final_time = final_time |
---|
277 | self.time_step = time_step |
---|
278 | self.ncp = int((final_time-start_time)/time_step) |
---|
279 | self.input = input |
---|
280 | self.write_scaled_result = write_scaled_result |
---|
281 | |
---|
282 | def _run_and_write_data(self, cvode_options): |
---|
283 | """ |
---|
284 | Run optimization and write result to file. |
---|
285 | """ |
---|
286 | |
---|
287 | if not cvode_options: |
---|
288 | cvode_options = {'atol':self.abs_tol,'rtol':self.rel_tol, 'maxh':0.0} |
---|
289 | else: |
---|
290 | if not 'maxh' in cvode_options: |
---|
291 | cvode_options['maxh'] = 0.0 |
---|
292 | |
---|
293 | if isinstance(self.model, FMUModelME1) or isinstance(self.model, FMUModelME2): |
---|
294 | res = self.model.simulate(start_time=self.start_time, |
---|
295 | final_time=self.final_time, |
---|
296 | input=self.input, |
---|
297 | options={'ncp':self.ncp, |
---|
298 | 'CVode_options':cvode_options}) |
---|
299 | else: |
---|
300 | res = self.model.simulate(start_time=self.start_time, |
---|
301 | final_time=self.final_time, |
---|
302 | input=self.input, |
---|
303 | options={'ncp':self.ncp}) |
---|
304 | return res |
---|
305 | |
---|
306 | class OptimizationTest(_BaseSimOptTest): |
---|
307 | """ |
---|
308 | Base class for optimization tests. |
---|
309 | """ |
---|
310 | |
---|
311 | @classmethod |
---|
312 | def setup_class_base(cls, mo_file, class_name, options = {}): |
---|
313 | """ |
---|
314 | Set up a new test model. Compiles the model. |
---|
315 | Call this with proper args from setUpClass(). |
---|
316 | mo_file - the relative path from the files dir to the .mo file to compile |
---|
317 | class_name - the qualified name of the class to simulate |
---|
318 | options - a dict of options to set in the compiler, defaults to no options |
---|
319 | """ |
---|
320 | _BaseSimOptTest.setup_class_base(mo_file, class_name, options) |
---|
321 | |
---|
322 | def setup_base(self, rel_tol = 1.0e-4, abs_tol = 1.0e-6, opt_options = {}): |
---|
323 | """ |
---|
324 | Set up a new test case. Creates and configures the optimization. |
---|
325 | Call this with proper args from setUp(). |
---|
326 | rel_tol - the relative error tolerance when comparing values, default is 1.0e-4 |
---|
327 | abs_tol - the absolute error tolerance when comparing values, default is 1.0e-6 |
---|
328 | opt_options - a dict of options to set in the optimizer, defaults to no options (default options will be set) |
---|
329 | """ |
---|
330 | _BaseSimOptTest.setup_base(self, rel_tol, abs_tol) |
---|
331 | #self.nlp = ipopt.NLPCollocationLagrangePolynomials(self.model, *nlp_args) |
---|
332 | #self.ipopt = ipopt.CollocationOptimizer(self.nlp) |
---|
333 | self._opt_options = opt_options |
---|
334 | #_set_ipopt_options(self.ipopt, options) |
---|
335 | |
---|
336 | |
---|
337 | def _run_and_write_data(self, cvode_options=None): |
---|
338 | """ |
---|
339 | Run optimization and write result to file. |
---|
340 | """ |
---|
341 | res = self.model.optimize(options=self._opt_options) |
---|
342 | return res |
---|
343 | |
---|
344 | |
---|
345 | # =========== Helper functions ============= |
---|
346 | |
---|
347 | def _set_ipopt_options(nlp, opts): |
---|
348 | """ |
---|
349 | Set all options contained in dict opts in Ipopt NLP object nlp. |
---|
350 | Selects method to use from the type of each value. |
---|
351 | """ |
---|
352 | for k, v in opts.items(): |
---|
353 | if isinstance(v, int): |
---|
354 | nlp.opt_coll_ipopt_set_int_option(k, v) |
---|
355 | elif isinstance(v, float): |
---|
356 | nlp.opt_coll_ipopt_set_num_option(k, v) |
---|
357 | elif isinstance(v, str): |
---|
358 | nlp.opt_coll_ipopt_set_string_option(k, v) |
---|
359 | |
---|
360 | |
---|
361 | def _set_compiler_options(cmp, opts): |
---|
362 | """ |
---|
363 | Set all options contained in dict opts in compiler cmp. |
---|
364 | Selects method to use from the type of each value. |
---|
365 | """ |
---|
366 | for k, v in opts.items(): |
---|
367 | if isinstance(v, bool): |
---|
368 | cmp.set_boolean_option(k, v) |
---|
369 | elif isinstance(v, int): |
---|
370 | cmp.set_integer_option(k, v) |
---|
371 | elif isinstance(v, float): |
---|
372 | cmp.set_real_option(k, v) |
---|
373 | elif isinstance(v, str): |
---|
374 | cmp.set_string_option(k, v) |
---|
375 | |
---|
376 | |
---|
377 | def _error(v1, v2): |
---|
378 | """ |
---|
379 | Calculates the relative and absolute error between two values. |
---|
380 | """ |
---|
381 | if v1 == v2: |
---|
382 | return (0.0, 0.0) |
---|
383 | abs_err = abs(v1 - v2) |
---|
384 | return (abs_err / max(abs(v1), abs(v2)), abs_err) |
---|
385 | |
---|
386 | def _check_error(ans, res, rel_tol, abs_tol): |
---|
387 | """ |
---|
388 | Check that error is within tolerance. |
---|
389 | """ |
---|
390 | (rel, abs) = _error(ans, res) |
---|
391 | return rel <= rel_tol or abs <= abs_tol |
---|
392 | |
---|
393 | |
---|
394 | def _trajectory_eval(var, var_t, t): |
---|
395 | """ |
---|
396 | Given the variable var, evaluate the variable for the time t. |
---|
397 | Values in var.t must be in increasing order, and t must be within the span of var.t. |
---|
398 | """ |
---|
399 | (pt, px) = (var.t[0], var.x[0]) |
---|
400 | for (ct, cx) in zip(var.t, var.x): |
---|
401 | # Since the t values are copies of the ones in the trajectories, we can use equality |
---|
402 | if ct == t: |
---|
403 | return cx |
---|
404 | elif ct > t: |
---|
405 | # pt < t < ct - use linear interpolation |
---|
406 | return ((t - pt) * cx + (ct - t) * px) / (ct - pt) |
---|
407 | (pt, px) = (ct, cx) |
---|