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

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. 

42 

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. 

46 

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. 

50 

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 

58 

59 

60# Namedtuple to store the time and value information of each variable 

61Samples = namedtuple('Samples', ['times', 'values', 'negated']) 

62 

63 

64def loadsim(fname, constants_only=False): 

65 r"""Load Dymola\ :sup:`®` or OpenModelica simulation results. 

66 

67 **Arguments:** 

68 

69 - *fname*: Name of the results file, including the path 

70 

71 The file extension ('.mat') is optional. 

72 

73 - *constants_only*: *True* to load only the variables from the first data 

74 matrix 

75 

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*. 

79 

80 **Returns:** An instance of dict 

81 """ 

82 # This does the task of mfiles/traj/tload.m from the Dymola installation. 

83 

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() 

100 

101 return description, unit, display_unit 

102 

103 # Load the file. 

104 mat, aclass = read(fname, constants_only) 

105 

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.') 

113 

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]) 

125 

126 # Get the format version. 

127 version = aclass[1] 

128 

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}) 

174 

175 # Time is from the last data set. 

176 variables['Time'] = Variable(Samples(times, times, False), 

177 'Time', 's', '') 

178 

179 return variables 

180 

181 

182def read(fname, constants_only=False): 

183 r"""Read variables from a MATLAB\ :sup:`®` file with Dymola\ :sup:`®` or 

184 OpenModelica results. 

185 

186 **Arguments:** 

187 

188 - *fname*: Name of the results file, including the path 

189 

190 This may be from a simulation or linearization. 

191 

192 - *constants_only*: *True* to load only the variables from the first data 

193 matrix, if the result is from a simulation 

194 

195 **Returns:** 

196 

197 1. A dictionary of variables 

198 

199 2. A list of strings from the lines of the 'Aclass' matrix 

200 """ 

201 

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 

214 

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 

222 

223 return mat, get_strings(aclass) 

224 

225 

226def get_strings(str_arr): 

227 """Return a list of strings from a character array. 

228 

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] 

233 

234 

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 

238 

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 """ 

243 

244 def times(self): 

245 """Return sample times""" 

246 return self.samples.times 

247 

248 def values(self): 

249 """Return sample values""" 

250 return -self.samples.values if self.samples.negated else self.samples.values 

251 

252 

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. 

261 

262 The index is time. The column headings indicate the variable names and 

263 units. 

264 

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 

271 

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. 

278 

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 = {} 

291 

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() 

303 

304 # Create a dictionary of names and values. 

305 times = _variables['Time'].values() 

306 data = {} 

307 for name in names: 

308 

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 

323 

324 # Apply an alias if available. 

325 try: 

326 name = aliases[name] 

327 except KeyError: 

328 pass 

329 

330 if unit and with_unit: 

331 data.update({name + ' / ' + unit: values}) 

332 else: 

333 data.update({name: values}) 

334 

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)