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

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 

58from ebcpy.utils import get_names 

59 

60 

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

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

63 

64 

65def loadsim(fname, constants_only=False): 

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

67 

68 **Arguments:** 

69 

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

71 

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

73 

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

75 matrix 

76 

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

80 

81 **Returns:** An instance of dict 

82 """ 

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

84 

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

101 

102 return description, unit, display_unit 

103 

104 # Load the file. 

105 mat, aclass = read(fname, constants_only) 

106 

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

114 

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

126 

127 # Get the format version. 

128 version = aclass[1] 

129 

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

175 

176 # Time is from the last data set. 

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

178 'Time', 's', '') 

179 

180 return variables 

181 

182 

183def read(fname, constants_only=False): 

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

185 OpenModelica results. 

186 

187 **Arguments:** 

188 

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

190 

191 This may be from a simulation or linearization. 

192 

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

194 matrix, if the result is from a simulation 

195 

196 **Returns:** 

197 

198 1. A dictionary of variables 

199 

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

201 """ 

202 

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 

215 

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 

223 

224 return mat, get_strings(aclass) 

225 

226 

227def get_strings(str_arr): 

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

229 

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] 

234 

235 

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 

239 

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

244 

245 def times(self): 

246 """Return sample times""" 

247 return self.samples.times 

248 

249 def values(self): 

250 """Return sample values""" 

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

252 

253 

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. 

262 

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

264 units. 

265 

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 

274 

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. 

281 

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

294 

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

301 

302 names = get_names(list(_variables.keys()), patterns) 

303 else: 

304 names = list(_variables.keys()) 

305 

306 # Create a dictionary of names and values. 

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

308 data = {} 

309 for name in names: 

310 

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 

325 

326 # Apply an alias if available. 

327 try: 

328 name = aliases[name] 

329 except KeyError: 

330 pass 

331 

332 if unit and with_unit: 

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

334 else: 

335 data.update({name: values}) 

336 

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