Coverage for ebcpy/modelica/simres.py: 84%
110 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-19 12:21 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-09-19 12:21 +0000
1# Copyright (c) 2010-2014, Kevin Davies, Hawaii Natural Energy Institute (HNEI),
2# and Georgia Tech Research Corporation (GTRC).
3# All rights reserved.
4#
5# Redistribution and use in source and binary forms, with or without
6# modification, are permitted provided that the following conditions are met:
7#
8# * Redistributions of source code must retain the above copyright notice,
9# this list of conditions and the following disclaimer.
10# * Redistributions in binary form must reproduce the above copyright notice,
11# this list of conditions and the following disclaimer in the documentation
12# and/or other materials provided with the distribution.
13# * Neither the name of Georgia Tech Research Corporation nor the names of
14# its contributors may be used to endorse or promote products derived from
15# this software without specific prior written permission.
16# * This software is controlled under the jurisdiction of the United States
17# Department of Commerce and subject to Export Administration Regulations.
18# By downloading or using the Software, you are agreeing to comply with
19# U. S. export controls. Diversion contrary to law is prohibited. The
20# software cannot be exported or reexported to sanctioned countries that
21# are controlled for Anti-Terrorism (15 CFR Part 738 Supplement 1) or to
22# denied parties, http://beta-www.bis.doc.gov/index.php/policy-guidance/lists-of-parties-of-concern.
23# EAR99 items cannot be exported or reexported to Iraq for a military
24# purpose or to a military end-user (15 CFR Part 746.3). Export and
25# reexport include any release of technology to a foreign national within
26# the United States. Technology is released for export when it is
27# available to foreign nationals for visual inspection, when technology is
28# exchanged orally or when technology is made available by practice or
29# application under the guidance of persons with knowledge of the
30# technology.
31#
32# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
33# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
34# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
35# DISCLAIMED. IN NO EVENT SHALL GEORGIA TECH RESEARCH CORPORATION BE LIABLE FOR
36# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
37# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
39# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
40# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
41# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43"""
44Module based on the simres module of modelicares. As no new content is going to be
45merged upstream, this "fork" of the to_pandas() function is used.
47Update 18.01.2021:
48As modelicares is no longer compatible with matplotlib > 3.3.2, we integrated all
49necessary functions from modelicares to still be able and use loadsim functions.
51.. versionadded:: 0.1.7
52"""
53from itertools import count
54from collections import namedtuple
55from scipy.io import loadmat
56import pandas as pd
57import numpy as np
60# Namedtuple to store the time and value information of each variable
61Samples = namedtuple('Samples', ['times', 'values', 'negated'])
64def loadsim(fname, constants_only=False):
65 r"""Load Dymola\ :sup:`®` or OpenModelica simulation results.
67 **Arguments:**
69 - *fname*: Name of the results file, including the path
71 The file extension ('.mat') is optional.
73 - *constants_only*: *True* to load only the variables from the first data
74 matrix
76 The first data matrix usually contains all of the constants,
77 parameters, and variables that don't vary. If only that information is
78 needed, it may save resources to set *constants_only* to *True*.
80 **Returns:** An instance of dict
81 """
82 # This does the task of mfiles/traj/tload.m from the Dymola installation.
84 def parse(description):
85 """Parse the variable description string into description, unit, and
86 displayUnit.
87 """
88 description = description.rstrip(']')
89 display_unit = ''
90 try:
91 description, unit = description.rsplit('[', 1)
92 except ValueError:
93 unit = ''
94 else:
95 try:
96 unit, display_unit = unit.rsplit('|', 1)
97 except ValueError:
98 pass # (displayUnit = '')
99 description = description.rstrip()
101 return description, unit, display_unit
103 # Load the file.
104 mat, aclass = read(fname, constants_only)
106 # Check the type of results.
107 if aclass[0] == 'AlinearSystem':
108 raise AssertionError(fname + ' is a linearization result. Use LinRes '
109 'instead.')
110 if aclass[0] != 'Atrajectory':
111 raise AssertionError(fname + ' is not a simulation or '
112 'linearization result.')
114 # Determine if the data is transposed.
115 try:
116 transposed = aclass[3] == 'binTrans'
117 except IndexError:
118 transposed = False
119 else:
120 if not (transposed or aclass[3] == 'binNormal'):
121 raise AssertionError\
122 ('The orientation of the Dymola/OpenModelica results is not '
123 'recognized. The third line of the "Aclass" variable is "%s", but '
124 'it should be "binNormal" or "binTrans".' % aclass[3])
126 # Get the format version.
127 version = aclass[1]
129 # Process the name, description, parts of dataInfo, and data_i variables.
130 # This section has been optimized for speed. All time and value data
131 # remains linked to the memory location where it is loaded by scipy. The
132 # negated variable is carried through so that copies are not necessary. If
133 # changes are made to this code, be sure to compare the performance (e.g.,
134 # using %timeit in IPython).
135 if version == '1.0':
136 data = mat['data'].T if transposed else mat['data']
137 times = data[:, 0]
138 names = get_strings(mat['names'].T if transposed else mat['names'])
139 variables = {name: Variable(Samples(times, data[:, i], False),
140 '', '', '')
141 for i, name in enumerate(names)}
142 elif version != '1.1':
143 raise AssertionError('The version of the Dymola/OpenModelica '
144 f'result file ({version}) is not '
145 'supported.')
146 else:
147 names = get_strings(mat['name'].T if transposed else mat['name'])
148 descriptions = get_strings(mat['description'].T if transposed else
149 mat['description'])
150 data_info = mat['dataInfo'] if transposed else mat['dataInfo'].T
151 data_sets = data_info[0, :]
152 sign_cols = data_info[1, :]
153 variables = dict()
154 for i in count(1):
155 try:
156 data = (mat['data_%i' % i].T if transposed else
157 mat['data_%i' % i])
158 except KeyError:
159 break # There are no more "data_i" variables.
160 else:
161 if data.shape[1] > 1: # In case the data set is empty.
162 times = data[:, 0]
163 variables.update({name:
164 Variable(Samples(times,
165 data[:,
166 abs(sign_col) - 1],
167 sign_col < 0),
168 *parse(description))
169 for (name, description, data_set,
170 sign_col)
171 in zip(names, descriptions, data_sets,
172 sign_cols)
173 if data_set == i})
175 # Time is from the last data set.
176 variables['Time'] = Variable(Samples(times, times, False),
177 'Time', 's', '')
179 return variables
182def read(fname, constants_only=False):
183 r"""Read variables from a MATLAB\ :sup:`®` file with Dymola\ :sup:`®` or
184 OpenModelica results.
186 **Arguments:**
188 - *fname*: Name of the results file, including the path
190 This may be from a simulation or linearization.
192 - *constants_only*: *True* to load only the variables from the first data
193 matrix, if the result is from a simulation
195 **Returns:**
197 1. A dictionary of variables
199 2. A list of strings from the lines of the 'Aclass' matrix
200 """
202 # Load the file.
203 try:
204 if constants_only:
205 mat = loadmat(fname, chars_as_strings=False, appendmat=False,
206 variable_names=['Aclass', 'class', 'name', 'names',
207 'description', 'dataInfo', 'data',
208 'data_1', 'ABCD', 'nx', 'xuyName'])
209 else:
210 mat = loadmat(fname, chars_as_strings=False, appendmat=False)
211 except IOError as error:
212 raise IOError(f'"{fname}" could not be opened.'
213 ' Check that it exists.') from error
215 # Check if the file contains the Aclass variable.
216 try:
217 aclass = mat['Aclass']
218 except KeyError as error:
219 raise TypeError(f'"{fname}" does not appear to be a Dymola or OpenModelica '
220 'result file. The "Aclass" variable is '
221 'missing.') from error
223 return mat, get_strings(aclass)
226def get_strings(str_arr):
227 """Return a list of strings from a character array.
229 Strip the whitespace from the right and recode it as utf-8.
230 """
231 return ["".join(word_arr).replace(" ", "")
232 for word_arr in str_arr]
235class Variable(namedtuple('VariableNamedTuple', ['samples', 'description', 'unit', 'displayUnit'])):
236 """Special namedtuple to represent a variable in a simulation, with
237 methods to retrieve and perform calculations on its values
239 This class is usually not instantiated directly by the user, but instances
240 are returned when indexing a variable name from a simulation result
241 (:class:`SimRes` instance).
242 """
244 def times(self):
245 """Return sample times"""
246 return self.samples.times
248 def values(self):
249 """Return sample values"""
250 return -self.samples.values if self.samples.negated else self.samples.values
253def mat_to_pandas(fname='dsres.mat',
254 names=None,
255 aliases=None,
256 with_unit=True,
257 constants_only=False):
258 """
259 Return a `pandas.DataFrame` with values from selected variables
260 for the given .mat file.
262 The index is time. The column headings indicate the variable names and
263 units.
265 :param str fname:
266 The mat file to load.
267 :param list names:
268 If None (default), then all variables are included.
269 :param dict aliases:
270 Dictionary of aliases for the variable names
272 The keys are the "official" variable names from the Modelica model
273 and the values are the names as they should be included in the
274 column headings. Any variables not in this list will not be
275 aliased. Any unmatched aliases will not be used.
276 :param bool with_unit:
277 Boolean to determine format of keys. Default value is True.
279 If set to True, the unit will be added to the key. As not all modelica-
280 result files export the unit information, using with_unit=True can lead
281 to errors.
282 :param bool constants_only:
283 The first data matrix usually contains all of the constants,
284 parameters, and variables that don't vary. If only that information is
285 needed, it may save resources to set *constants_only* to *True*.
286 """
287 _variables = loadsim(fname, constants_only)
288 # Avoid mutable argument
289 if aliases is None:
290 aliases = {}
292 # Create the list of variable names.
293 if names:
294 if 'Time' not in names:
295 names = names.copy()
296 names.append('Time')
297 non_existing_variables = list(set(names).difference(_variables.keys()))
298 if non_existing_variables:
299 raise KeyError(f"The following variable names are not in the given .mat file: "
300 f"{', '.join(non_existing_variables)}")
301 else:
302 names = _variables.keys()
304 # Create a dictionary of names and values.
305 times = _variables['Time'].values()
306 data = {}
307 for name in names:
309 # Get the values
310 array_values = _variables[name].values()
311 if np.array_equal(_variables[name].times(), times):
312 values = array_values # Save computation.
313 elif np.isinf(array_values).all():
314 values = array_values[0] # Inf values can't be resampled
315 # Check if all values are constant to save resampling time
316 elif np.count_nonzero(array_values -
317 np.max(array_values)) == 0:
318 # Passing a scalar converts automatically to an array.
319 values = array_values[0]
320 else:
321 values = _variables[name].values(t=times) # Resample.
322 unit = _variables[name].unit
324 # Apply an alias if available.
325 try:
326 name = aliases[name]
327 except KeyError:
328 pass
330 if unit and with_unit:
331 data.update({name + ' / ' + unit: values})
332 else:
333 data.update({name: values})
335 # Create the pandas data frame.
336 if with_unit:
337 time_key = 'Time / s'
338 else:
339 time_key = 'Time'
340 return pd.DataFrame(data).set_index(time_key)