Coverage for ebcpy/modelica/simres.py: 83%
115 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-05-06 09:34 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2025-05-06 09:34 +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
58from ebcpy.utils import get_names
61# Namedtuple to store the time and value information of each variable
62Samples = namedtuple('Samples', ['times', 'values', 'negated'])
65def loadsim(fname, constants_only=False):
66 r"""Load Dymola\ :sup:`®` or OpenModelica simulation results.
68 **Arguments:**
70 - *fname*: Name of the results file, including the path
72 The file extension ('.mat') is optional.
74 - *constants_only*: *True* to load only the variables from the first data
75 matrix
77 The first data matrix usually contains all of the constants,
78 parameters, and variables that don't vary. If only that information is
79 needed, it may save resources to set *constants_only* to *True*.
81 **Returns:** An instance of dict
82 """
83 # This does the task of mfiles/traj/tload.m from the Dymola installation.
85 def parse(description):
86 """Parse the variable description string into description, unit, and
87 displayUnit.
88 """
89 description = description.rstrip(']')
90 display_unit = ''
91 try:
92 description, unit = description.rsplit('[', 1)
93 except ValueError:
94 unit = ''
95 else:
96 try:
97 unit, display_unit = unit.rsplit('|', 1)
98 except ValueError:
99 pass # (displayUnit = '')
100 description = description.rstrip()
102 return description, unit, display_unit
104 # Load the file.
105 mat, aclass = read(fname, constants_only)
107 # Check the type of results.
108 if aclass[0] == 'AlinearSystem':
109 raise AssertionError(fname + ' is a linearization result. Use LinRes '
110 'instead.')
111 if aclass[0] != 'Atrajectory':
112 raise AssertionError(fname + ' is not a simulation or '
113 'linearization result.')
115 # Determine if the data is transposed.
116 try:
117 transposed = aclass[3] == 'binTrans'
118 except IndexError:
119 transposed = False
120 else:
121 if not (transposed or aclass[3] == 'binNormal'):
122 raise AssertionError\
123 ('The orientation of the Dymola/OpenModelica results is not '
124 'recognized. The third line of the "Aclass" variable is "%s", but '
125 'it should be "binNormal" or "binTrans".' % aclass[3])
127 # Get the format version.
128 version = aclass[1]
130 # Process the name, description, parts of dataInfo, and data_i variables.
131 # This section has been optimized for speed. All time and value data
132 # remains linked to the memory location where it is loaded by scipy. The
133 # negated variable is carried through so that copies are not necessary. If
134 # changes are made to this code, be sure to compare the performance (e.g.,
135 # using %timeit in IPython).
136 if version == '1.0':
137 data = mat['data'].T if transposed else mat['data']
138 times = data[:, 0]
139 names = get_strings(mat['names'].T if transposed else mat['names'])
140 variables = {name: Variable(Samples(times, data[:, i], False),
141 '', '', '')
142 for i, name in enumerate(names)}
143 elif version != '1.1':
144 raise AssertionError('The version of the Dymola/OpenModelica '
145 f'result file ({version}) is not '
146 'supported.')
147 else:
148 names = get_strings(mat['name'].T if transposed else mat['name'])
149 descriptions = get_strings(mat['description'].T if transposed else
150 mat['description'])
151 data_info = mat['dataInfo'] if transposed else mat['dataInfo'].T
152 data_sets = data_info[0, :]
153 sign_cols = data_info[1, :]
154 variables = dict()
155 for i in count(1):
156 try:
157 data = (mat['data_%i' % i].T if transposed else
158 mat['data_%i' % i])
159 except KeyError:
160 break # There are no more "data_i" variables.
161 else:
162 if data.shape[1] > 1: # In case the data set is empty.
163 times = data[:, 0]
164 variables.update({name:
165 Variable(Samples(times,
166 data[:,
167 abs(sign_col) - 1],
168 sign_col < 0),
169 *parse(description))
170 for (name, description, data_set,
171 sign_col)
172 in zip(names, descriptions, data_sets,
173 sign_cols)
174 if data_set == i})
176 # Time is from the last data set.
177 variables['Time'] = Variable(Samples(times, times, False),
178 'Time', 's', '')
180 return variables
183def read(fname, constants_only=False):
184 r"""Read variables from a MATLAB\ :sup:`®` file with Dymola\ :sup:`®` or
185 OpenModelica results.
187 **Arguments:**
189 - *fname*: Name of the results file, including the path
191 This may be from a simulation or linearization.
193 - *constants_only*: *True* to load only the variables from the first data
194 matrix, if the result is from a simulation
196 **Returns:**
198 1. A dictionary of variables
200 2. A list of strings from the lines of the 'Aclass' matrix
201 """
203 # Load the file.
204 try:
205 if constants_only:
206 mat = loadmat(fname, chars_as_strings=False, appendmat=False,
207 variable_names=['Aclass', 'class', 'name', 'names',
208 'description', 'dataInfo', 'data',
209 'data_1', 'ABCD', 'nx', 'xuyName'])
210 else:
211 mat = loadmat(fname, chars_as_strings=False, appendmat=False)
212 except IOError as error:
213 raise IOError(f'"{fname}" could not be opened.'
214 ' Check that it exists.') from error
216 # Check if the file contains the Aclass variable.
217 try:
218 aclass = mat['Aclass']
219 except KeyError as error:
220 raise TypeError(f'"{fname}" does not appear to be a Dymola or OpenModelica '
221 'result file. The "Aclass" variable is '
222 'missing.') from error
224 return mat, get_strings(aclass)
227def get_strings(str_arr):
228 """Return a list of strings from a character array.
230 Strip the whitespace from the right and recode it as utf-8.
231 """
232 return ["".join(word_arr).replace(" ", "")
233 for word_arr in str_arr]
236class Variable(namedtuple('VariableNamedTuple', ['samples', 'description', 'unit', 'displayUnit'])):
237 """Special namedtuple to represent a variable in a simulation, with
238 methods to retrieve and perform calculations on its values
240 This class is usually not instantiated directly by the user, but instances
241 are returned when indexing a variable name from a simulation result
242 (:class:`SimRes` instance).
243 """
245 def times(self):
246 """Return sample times"""
247 return self.samples.times
249 def values(self):
250 """Return sample values"""
251 return -self.samples.values if self.samples.negated else self.samples.values
254def mat_to_pandas(fname='dsres.mat',
255 names=None,
256 aliases=None,
257 with_unit=True,
258 constants_only=False):
259 """
260 Return a `pandas.DataFrame` with values from selected variables
261 for the given .mat file.
263 The index is time. The column headings indicate the variable names and
264 units.
266 :param str fname:
267 The mat file to load.
268 :param list names:
269 If None (default), then all variables are included. You can also
270 supply wildcard patterns (e.g. "*wall.layer[*].T", etc.) to match
271 multiple variables at once.
272 :param dict aliases:
273 Dictionary of aliases for the variable names
275 The keys are the "official" variable names from the Modelica model
276 and the values are the names as they should be included in the
277 column headings. Any variables not in this list will not be
278 aliased. Any unmatched aliases will not be used.
279 :param bool with_unit:
280 Boolean to determine format of keys. Default value is True.
282 If set to True, the unit will be added to the key. As not all modelica-
283 result files export the unit information, using with_unit=True can lead
284 to errors.
285 :param bool constants_only:
286 The first data matrix usually contains all of the constants,
287 parameters, and variables that don't vary. If only that information is
288 needed, it may save resources to set *constants_only* to *True*.
289 """
290 _variables = loadsim(fname, constants_only)
291 # Avoid mutable argument
292 if aliases is None:
293 aliases = {}
295 # Create the list of variable names.
296 if names:
297 # ensure Time is always included
298 patterns = list(names)
299 if 'Time' not in patterns:
300 patterns.append('Time')
302 names = get_names(list(_variables.keys()), patterns)
303 else:
304 names = list(_variables.keys())
306 # Create a dictionary of names and values.
307 times = _variables['Time'].values()
308 data = {}
309 for name in names:
311 # Get the values
312 array_values = _variables[name].values()
313 if np.array_equal(_variables[name].times(), times):
314 values = array_values # Save computation.
315 elif np.isinf(array_values).all():
316 values = array_values[0] # Inf values can't be resampled
317 # Check if all values are constant to save resampling time
318 elif np.count_nonzero(array_values -
319 np.max(array_values)) == 0:
320 # Passing a scalar converts automatically to an array.
321 values = array_values[0]
322 else:
323 values = _variables[name].values(t=times) # Resample.
324 unit = _variables[name].unit
326 # Apply an alias if available.
327 try:
328 name = aliases[name]
329 except KeyError:
330 pass
332 if unit and with_unit:
333 data.update({name + ' / ' + unit: values})
334 else:
335 data.update({name: values})
337 # Create the pandas data frame.
338 if with_unit:
339 time_key = 'Time / s'
340 else:
341 time_key = 'Time'
342 try:
343 time_values = data.pop(time_key)
344 except KeyError:
345 raise KeyError(f"Time column {time_key} not found in the data dictionary.")
346 df = pd.DataFrame(data, index=time_values)
347 df.index.name = time_key
348 return df