source: branches/dev-5819/Python/src/tests_jmodelica/general/base_simul.py @ 13461

Last change on this file since 13461 was 13461, checked in by randersson, 3 months ago

#5819 Performed 2to3 on the entire src/Python directory in order to make the code compatible. I've not reviewed all changes while doing this commit but will do while starting to run the tests. I'm not sure everything is converted correctly, for example the change in test_casadi_collocation row 1610.

File size: 17.4 KB
Line 
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
20This file holds base classes for simulation and optimization tests.
21"""
22
23import os
24
25from pymodelica.compiler import compile_fmu
26from pyfmi.fmi import load_fmu, FMUModelCS1, FMUModelME1, FMUModelCS2, FMUModelME2
27from pyjmi.common.io import ResultDymolaTextual, ResultDymolaBinary
28from tests_jmodelica import get_files_path
29
30_model_name = ''
31
32class _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
247class 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}
289       
290        if isinstance(self.model, FMUModelME1) or isinstance(self.model, FMUModelME2):
291            res = self.model.simulate(start_time=self.start_time,
292                                final_time=self.final_time,
293                                input=self.input,
294                                options={'ncp':self.ncp,
295                                         'CVode_options':cvode_options})
296        else:
297            res = self.model.simulate(start_time=self.start_time,
298                                final_time=self.final_time,
299                                input=self.input,
300                                options={'ncp':self.ncp})
301        return res
302       
303class OptimizationTest(_BaseSimOptTest):
304    """
305    Base class for optimization tests.
306    """
307
308    @classmethod
309    def setup_class_base(cls, mo_file, class_name, options = {}):
310        """
311        Set up a new test model. Compiles the model.
312        Call this with proper args from setUpClass().
313          mo_file     - the relative path from the files dir to the .mo file to compile
314          class_name  - the qualified name of the class to simulate
315          options     - a dict of options to set in the compiler, defaults to no options
316        """
317        _BaseSimOptTest.setup_class_base(mo_file, class_name, options)
318
319    def setup_base(self, rel_tol = 1.0e-4, abs_tol = 1.0e-6, opt_options = {}):
320        """
321        Set up a new test case. Creates and configures the optimization.
322        Call this with proper args from setUp().
323          rel_tol -  the relative error tolerance when comparing values, default is 1.0e-4
324          abs_tol -  the absolute error tolerance when comparing values, default is 1.0e-6
325          opt_options   -  a dict of options to set in the optimizer, defaults to no options (default options will be set)
326        """
327        _BaseSimOptTest.setup_base(self, rel_tol, abs_tol)
328        #self.nlp = ipopt.NLPCollocationLagrangePolynomials(self.model, *nlp_args)
329        #self.ipopt = ipopt.CollocationOptimizer(self.nlp)
330        self._opt_options = opt_options
331        #_set_ipopt_options(self.ipopt, options)
332
333
334    def _run_and_write_data(self, cvode_options=None):
335        """
336        Run optimization and write result to file.
337        """
338        res = self.model.optimize(options=self._opt_options)
339        return res
340
341
342# =========== Helper functions =============
343
344def _set_ipopt_options(nlp, opts):
345    """
346    Set all options contained in dict opts in Ipopt NLP object nlp.
347    Selects method to use from the type of each value.
348    """
349    for k, v in opts.items():
350        if isinstance(v, int):
351            nlp.opt_coll_ipopt_set_int_option(k, v)
352        elif isinstance(v, float):
353            nlp.opt_coll_ipopt_set_num_option(k, v)
354        elif isinstance(v, str):
355            nlp.opt_coll_ipopt_set_string_option(k, v)
356
357
358def _set_compiler_options(cmp, opts):
359    """
360    Set all options contained in dict opts in compiler cmp.
361    Selects method to use from the type of each value.
362    """
363    for k, v in opts.items():
364        if isinstance(v, bool):
365            cmp.set_boolean_option(k, v)
366        elif isinstance(v, int):
367            cmp.set_integer_option(k, v)
368        elif isinstance(v, float):
369            cmp.set_real_option(k, v)
370        elif isinstance(v, str):
371            cmp.set_string_option(k, v)
372
373
374def _error(v1, v2):
375    """
376    Calculates the relative and absolute error between two values.
377    """
378    if v1 == v2:
379        return (0.0, 0.0)
380    abs_err = abs(v1 - v2)
381    return (abs_err / max(abs(v1), abs(v2)), abs_err)
382
383def _check_error(ans, res, rel_tol, abs_tol):
384    """
385    Check that error is within tolerance.
386    """
387    (rel, abs) = _error(ans, res)
388    return rel <= rel_tol or abs <= abs_tol
389
390
391def _trajectory_eval(var, var_t, t):
392    """
393    Given the variable var, evaluate the variable for the time t.
394    Values in var.t must be in increasing order, and t must be within the span of var.t.
395    """
396    (pt, px) = (var.t[0], var.x[0])
397    for (ct, cx) in zip(var.t, var.x):
398        # Since the t values are copies of the ones in the trajectories, we can use equality
399        if ct == t:
400            return cx
401        elif ct > t:
402            # pt < t < ct - use linear interpolation
403            return ((t - pt) * cx + (ct - t) * px) / (ct - pt)
404        (pt, px) = (ct, cx)
Note: See TracBrowser for help on using the repository browser.