Coverage for aixcalibuha/sensitivity_analysis/plotting.py: 93%

235 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-01-27 10:48 +0000

1""" 

2Module containing functions for plotting sensitivity results 

3""" 

4import sys 

5import pandas as pd 

6import numpy as np 

7import matplotlib 

8import matplotlib.pyplot as plt 

9from SALib.plotting.bar import plot as barplot 

10from aixcalibuha.utils.visualizer import short_name 

11 

12 

13def plot_single(result: pd.DataFrame, 

14 cal_classes: [str] = None, 

15 goals: [str] = None, 

16 max_name_len: int = 15, 

17 **kwargs): 

18 """ 

19 Plot sensitivity results of first and total order analysis variables. 

20 For each calibration class one figure is created, which shows for each goal an axis 

21 with a barplot of the values of the analysis variables. 

22 

23 :param pd.DataFrame result: 

24 A result from run 

25 :param int max_name_len: 

26 Default is 15. Shortens the parameter names to max_name_len characters. 

27 :param [str] cal_classes: 

28 Default are all possible calibration classes. If a list of 

29 names of calibration classes is given only plots for these 

30 classes are created. 

31 :param [str] goals: 

32 Default are all possible goal names. If a list of specific 

33 goal names is given only these will be plotted. 

34 :keyword bool show_plot: 

35 Default is True. If False, all created plots are not shown. 

36 :keyword ([fig], [ax]) figs_axes: 

37 Default None. Set own figures of subfigures with corresponding axes for customization. 

38 :keyword bool use_suffix: 

39 Default is True: If True, the last part after the last point 

40 of Modelica variables is used for the x ticks. 

41 :return: 

42 Returns all created figures and axes in lists like [fig], [ax] 

43 with shapes (len(cal_classes)), (len(cal_classes), len(goals)) 

44 """ 

45 use_suffix = kwargs.pop('use_suffix', False) 

46 show_plot = kwargs.pop('show_plot', True) 

47 figs_axes = kwargs.pop('figs_axes', None) 

48 _func_name = kwargs.pop('_func_name', 'plot_single') 

49 

50 # get lists of the calibration classes and their goals in the result dataframe 

51 if cal_classes is None: 

52 cal_classes = _del_duplicates(list(result.index.get_level_values(0))) 

53 if goals is None: 

54 goals = _del_duplicates(list(result.index.get_level_values(1))) 

55 

56 result = _rename_tuner_names(result, use_suffix, max_name_len, _func_name) 

57 

58 # plotting with simple plot function of the SALib 

59 figs = [] 

60 axes = [] 

61 for col, cal_class in enumerate(cal_classes): 

62 fig, ax = _create_figs_axes(figs_axes, col, goals, f"Class: {cal_class}") 

63 figs.append(fig) 

64 axes.append(ax) 

65 for row, goal in enumerate(goals): 

66 result_df = result.loc[cal_class, goal] 

67 axes[col][row].grid(True, which='both', axis='y') 

68 barplot(result_df.T, ax=axes[col][row]) 

69 axes[col][row].set_title(f"Goal: {goal}") 

70 axes[col][row].legend() 

71 

72 if show_plot: 

73 plt.show() 

74 

75 return figs, axes 

76 

77 

78def plot_second_order(result: pd.DataFrame, 

79 cal_classes: [str] = None, 

80 goals: [str] = None, 

81 max_name_len: int = 15, 

82 **kwargs): 

83 """ 

84 Plot sensitivity results of second order analysis variables. 

85 For each calibration class and goal one figure of a 3d plot is created 

86 with the barplots of the interactions for each parameter. 

87 Only working for more than 2 parameter. 

88 

89 :param pd.DataFrame result: 

90 A result from run 

91 :param int max_name_len: 

92 Default is 15. Shortens the parameter names to max_name_len characters. 

93 :param [str] cal_classes: 

94 Default are all possible calibration classes. If a list of 

95 names of calibration classes is given only plots for these 

96 classes are created. 

97 :param [str] goals: 

98 Default are all possible goal names. If a list of specific 

99 goal names is given only these will be plotted. 

100 :keyword bool show_plot: 

101 Default is True. If False, all created plots are not shown. 

102 :keyword bool use_suffix: 

103 Default is True: If True, the last part after the last point 

104 of Modelica variables is used for the x ticks. 

105 :keyword [[fig]] figs: 

106 Default None. Set own figures of subfigures for customization. 

107 Shape (len(cal_classes), len(goals)) 

108 :return: 

109 Returns all created figures and axes in lists like [fig], [ax] 

110 """ 

111 use_suffix = kwargs.pop('use_suffix', False) 

112 show_plot = kwargs.pop('show_plot', True) 

113 figs = kwargs.pop('figs', None) 

114 result = result.fillna(0) 

115 if cal_classes is None: 

116 cal_classes = _del_duplicates(list(result.index.get_level_values(0))) 

117 if goals is None: 

118 goals = _del_duplicates(list(result.index.get_level_values(1))) 

119 

120 result = _rename_tuner_names(result, use_suffix, max_name_len, "plot_second_order") 

121 

122 tuner_names = result.columns 

123 if len(tuner_names) < 2: 

124 return None 

125 xticks = np.arange(len(tuner_names)) 

126 

127 # when the index is not sorted pandas throws a performance warning 

128 result = result.sort_index() 

129 

130 # plot of S2 without S2_conf 

131 all_figs = [] 

132 all_axes = [] 

133 for class_idx, cal_class in enumerate(cal_classes): 

134 class_figs = [] 

135 class_axes = [] 

136 for goal_idx, goal in enumerate(goals): 

137 if figs is None: 

138 fig = plt.figure() 

139 else: 

140 fig = figs[class_idx][goal_idx] 

141 ax = fig.add_subplot(projection='3d') 

142 for idx, name in enumerate(tuner_names): 

143 ax.bar(tuner_names, 

144 result.loc[cal_class, goal, 'S2', name].to_numpy(), 

145 zs=idx, zdir='y', alpha=0.8) 

146 ax.set_title(f"Class: {cal_class} Goal: {goal}") 

147 ax.set_zlabel('S2 [-]') 

148 ax.set_yticks(xticks) 

149 ax.set_yticklabels(tuner_names) 

150 # rotate tick labels for better readability 

151 plt.setp(ax.get_xticklabels(), rotation=90, ha="right", rotation_mode="anchor") 

152 plt.setp(ax.get_yticklabels(), rotation=90, ha="right", rotation_mode="anchor") 

153 class_figs.append(fig) 

154 class_axes.append(ax) 

155 all_figs.append(class_figs) 

156 all_axes.append(class_axes) 

157 if show_plot: 

158 plt.show() 

159 return all_figs, all_axes 

160 

161 

162def plot_single_second_order(result: pd.DataFrame, 

163 para_name: str, 

164 cal_classes: [str] = None, 

165 goals: [str] = None, 

166 max_name_len: int = 15, 

167 **kwargs): 

168 """ 

169 Plot the value of S2 from one parameter with all other parameters. 

170 

171 :param pd.DataFrame result: 

172 Second order result from run. 

173 :param str para_name: 

174 Name of the parameter of which the results should be plotted. 

175 :param [str] cal_classes: 

176 Default are all possible calibration classes. If a list of 

177 names of calibration classes is given only plots for these 

178 classes are created. 

179 :param [str] goals: 

180 Default are all possible goal names. If a list of specific 

181 goal names is given only these will be plotted. 

182 :param int max_name_len: 

183 Default is 15. Shortens the parameter names to max_name_len characters. 

184 :keyword bool show_plot: 

185 Default is True. If False, all created plots are not shown. 

186 :keyword bool use_suffix: 

187 Default is True: If True, the last part after the last point 

188 of Modelica variables is used for the x ticks. 

189 :keyword ([fig], [ax]) figs_axes: 

190 Default None. Set own figures of subfigures with corresponding axes for customization. 

191 :return: 

192 Returns all created figures and axes in lists like [fig], [ax] 

193 with shapes (len(cal_classes)), (len(cal_classes), len(goals)) 

194 """ 

195 use_suffix = kwargs.pop('use_suffix', False) 

196 show_plot = kwargs.pop('show_plot', True) 

197 figs_axes = kwargs.pop('figs_axes', None) 

198 

199 result = result.loc[:, :, :, para_name][:].fillna(0) 

200 figs, axes = plot_single( 

201 result=result, 

202 show_plot=False, 

203 cal_classes=cal_classes, 

204 goals=goals, 

205 figs_axes=figs_axes, 

206 use_suffix=use_suffix, 

207 max_name_len=max_name_len, 

208 _func_name="plot_single_second_order" 

209 ) 

210 # set new title for the figures of each calibration class 

211 for fig in figs: 

212 fig.suptitle(f"Interaction of {para_name} in class {fig._suptitle.get_text()}") 

213 if show_plot: 

214 plt.show() 

215 return figs, axes 

216 

217 

218def heatmap(result: pd.DataFrame, 

219 cal_class: str, 

220 goal: str, 

221 ax: matplotlib.axes.Axes = None, 

222 max_name_len: int = 15, 

223 **kwargs): 

224 """ 

225 Plot S2 sensitivity results from one calibration class and goal as a heatmap. 

226 

227 :param pd.DataFrame result: 

228 A second order result from run 

229 :param str cal_class: 

230 Name of the class to plot S2 from. 

231 :param str goal: 

232 Name of the goal to plot S2 from. 

233 :param matplotlib.axes ax: 

234 Default is None. If an axes is given the heatmap will be plotted on it, else 

235 a new figure and axes is created. 

236 :param int max_name_len: 

237 Default is 15. Shortens the parameter names to max_name_len characters. 

238 :keyword bool show_plot: 

239 Default is True. If False, all created plots are not shown. 

240 :keyword bool use_suffix: 

241 Default is False. If True, only the last suffix of a Modelica variable is displayed. 

242 :return: 

243 Returns axes 

244 """ 

245 use_suffix = kwargs.pop('use_suffix', False) 

246 show_plot = kwargs.pop('show_plot', True) 

247 _func_name = kwargs.pop('_func_name', "heatmap") 

248 

249 result = _rename_tuner_names(result, use_suffix, max_name_len, _func_name) 

250 if ax is None: 

251 _, ax = plt.subplots(layout="constrained") 

252 data = result.sort_index().loc[cal_class, goal, 'S2'].fillna(0).reindex( 

253 index=result.columns) 

254 image = ax.imshow(data, cmap='Reds') 

255 ax.set_title(f'Class: {cal_class} Goal: {goal}') 

256 ax.set_xticks(np.arange(len(data.columns))) 

257 ax.set_yticks(np.arange(len(data.index))) 

258 ax.set_xticklabels(data.columns) 

259 ax.set_yticklabels(data.index) 

260 ax.spines[:].set_color('black') 

261 plt.setp(ax.get_xticklabels(), rotation=90, ha="right", rotation_mode="anchor") 

262 cbar = ax.figure.colorbar(image, ax=ax) 

263 cbar.ax.set_ylabel("S2", rotation=90) 

264 if show_plot: 

265 plt.show() 

266 return ax 

267 

268 

269def heatmaps(result: pd.DataFrame, 

270 cal_classes: [str] = None, 

271 goals: [str] = None, 

272 max_name_len: int = 15, 

273 **kwargs): 

274 """ 

275 Plot S2 sensitivity results as a heatmap for multiple 

276 calibration classes and goals in one figure. 

277 

278 :param pd.DataFrame result: 

279 A second order result from run 

280 :param [str] cal_classes: 

281 Default is a list of all calibration classes in the result. 

282 If a list of classes is given only these classes are plotted. 

283 :param [str] goals: 

284 Default is a list of all goals in the result. 

285 If a list of goals is given only these goals are plotted. 

286 :param int max_name_len: 

287 Default is 15. Shortens the parameter names to max_name_len characters. 

288 :keyword bool show_plot: 

289 Default is True. If False, all created plots are not shown. 

290 :keyword bool use_suffix: 

291 Default is False. If True, only the last suffix of a Modelica variable is displayed. 

292 """ 

293 use_suffix = kwargs.pop('use_suffix', False) 

294 show_plot = kwargs.pop('show_plot', True) 

295 

296 if cal_classes is None: 

297 cal_classes = result.index.get_level_values("Class").unique() 

298 if goals is None: 

299 goals = result.index.get_level_values("Goal").unique() 

300 

301 _, axes = plt.subplots(ncols=len(cal_classes), nrows=len(goals), sharex='all', sharey='all', 

302 layout="constrained") 

303 if len(goals) == 1: 

304 axes = [axes] 

305 if len(cal_classes) == 1: 

306 for idx, ax in enumerate(axes): 

307 axes[idx] = [ax] 

308 _func_name = "heatmaps" 

309 for col, class_name in enumerate(cal_classes): 

310 for row, goal_name in enumerate(goals): 

311 heatmap(result, 

312 class_name, 

313 goal_name, 

314 ax=axes[row][col], 

315 show_plot=False, 

316 use_suffix=use_suffix, 

317 max_name_len=max_name_len, 

318 _func_name=_func_name) 

319 _func_name = None 

320 if show_plot: 

321 plt.show() 

322 

323 

324def plot_time_dependent(result: pd.DataFrame, 

325 parameters: [str] = None, 

326 goals: [str] = None, 

327 analysis_variables: [str] = None, 

328 plot_conf: bool = True, 

329 **kwargs): 

330 """ 

331 Plot time dependent sensitivity results without interactions from run_time_dependent(). 

332 

333 For each goal one figure is created with one axes for each analysis variable. 

334 In these plots the time dependent sensitivity of the parameters is plotted. 

335 The confidence interval can also be plotted. 

336 

337 :param pd.DataFrame result: 

338 A result from run_time_dependent without second order results. 

339 :param [str] parameters: 

340 Default all parameters. List of parameters to plot the sensitivity. 

341 :param [str] analysis_variables: 

342 Default all analysis_variables. List of analysis variables to plot. 

343 :param bool plot_conf: 

344 Default True. If true, the confidence intervals for each parameter are plotted. 

345 :param [str] goals: 

346 Default are all possible goal names. If a list of specific 

347 goal names is given only these will be plotted. 

348 :keyword ([fig], [ax]) figs_axes: 

349 Default None. Optional custom figures and axes 

350 (see example for verbose sensitivity analysis). 

351 :return: 

352 Returns all created figures and axes in lists like [fig], [ax] 

353 with shapes (len(goals)), (len(goals), len(analysis_variables)) 

354 :keyword bool show_plot: 

355 Default is True. If False, all created plots are not shown. 

356 :keyword bool use_suffix: 

357 Default is True: If True, the last part after the last point 

358 of Modelica variables is used for the x ticks. 

359 :keyword int max_name_len: 

360 Default is 50. Shortens the parameter names to max_name_len characters. 

361 """ 

362 use_suffix = kwargs.pop('use_suffix', False) 

363 max_name_len = kwargs.pop('max_name_len', 50) 

364 show_plot = kwargs.pop('show_plot', True) 

365 figs_axes = kwargs.pop('figs_axes', None) 

366 

367 if goals is None: 

368 goals = _del_duplicates(list(result.index.get_level_values(0))) 

369 all_analysis_variables = _del_duplicates(list(result.index.get_level_values(1))) 

370 if analysis_variables is None: 

371 analysis_variables = [av for av in all_analysis_variables if '_conf' not in av] 

372 if parameters is None: 

373 parameters = result.columns.values 

374 

375 result = _rename_tuner_names(result, use_suffix, max_name_len, "plot_time_dependent") 

376 

377 renamed_parameters = [_format_name(para, use_suffix, max_name_len) for para in parameters] 

378 

379 figs = [] 

380 axes = [] 

381 for idx_goal, goal in enumerate(goals): 

382 fig, ax = _create_figs_axes(figs_axes, idx_goal, analysis_variables, f"Goal: {goal}") 

383 figs.append(fig) 

384 axes.append(ax) 

385 for idx_av, analysis_var in enumerate(analysis_variables): 

386 axes[idx_goal][idx_av].plot(result.loc[goal, analysis_var][renamed_parameters]) 

387 axes[idx_goal][idx_av].set_ylabel(analysis_var) 

388 axes[idx_goal][idx_av].legend(renamed_parameters) 

389 if plot_conf and analysis_var + '_conf' in all_analysis_variables: 

390 for para in renamed_parameters: 

391 y = result.loc[goal, analysis_var][para] 

392 x = y.index.to_numpy() 

393 conv_int = result.loc[goal, analysis_var + '_conf'][para] 

394 large_values_indices = conv_int[conv_int > 1].index 

395 if list(large_values_indices): 

396 sys.stderr.write( 

397 f"plot_time_dependent INFO:" 

398 f"Confidence interval for {goal}, {analysis_var}, {para} was at the " 

399 f"following times {list(large_values_indices)} lager than 1 " 

400 f"and is smoothed out in the plot.\n") 

401 for idx in large_values_indices: 

402 prev_idx = conv_int.index.get_loc(idx) - 1 

403 if prev_idx >= 0: 

404 conv_int.iloc[conv_int.index.get_loc(idx)] = conv_int.iloc[prev_idx] 

405 else: 

406 conv_int.iloc[conv_int.index.get_loc(idx)] = 1 

407 axes[idx_goal][idx_av].fill_between(x, (y - conv_int), (y + conv_int), alpha=.1) 

408 axes[idx_goal][-1].set_xlabel('time') 

409 if show_plot: 

410 plt.show() 

411 return figs, axes 

412 

413 

414def plot_parameter_verbose(parameter: str, 

415 single_result: pd.DataFrame, 

416 second_order_result: pd.DataFrame = None, 

417 goals: [str] = None, 

418 **kwargs): 

419 """ 

420 Plot all time dependent sensitivity measure for one parameter. 

421 For each goal an axes is created within one figure. 

422 

423 If second_order_results form SobolAnalyzer.run_time_dependent are given 

424 the S2 results of the interaction with each other parameter are added on top 

425 of each other and the first order result. 

426 

427 :param str parameter: 

428 Parameter to plot all sensitivity results for. If use_suffix=True, then 

429 the name must also be only the suffix. 

430 :param pd.DataFrame single_result: 

431 First and total order result form run_time_dependent. 

432 :param pd.DataFrame second_order_result: 

433 Default None. Second order result of SobolAnalyzer.run_time_dependent. 

434 :param [str] goals: 

435 Default are all possible goal names. If a list of specific 

436 goal names is given only these will be plotted. 

437 :keyword (fig, [ax]) fig_axes: 

438 Default None. Optional custom figures and axes 

439 (see example for verbose sensitivity analysis). 

440 :return: 

441 Returns all created figures and axes in lists like fig, [ax] 

442 with shape (len(goals)) for the axes list 

443 :keyword bool show_plot: 

444 Default is True. If False, all created plots are not shown. 

445 :keyword bool use_suffix: 

446 Default is True: If True, the last part after the last point 

447 of Modelica variables is used for the x ticks. 

448 :keyword int max_name_len: 

449 Default is 15. Shortens the parameter names to max_name_len characters. 

450 """ 

451 use_suffix = kwargs.pop('use_suffix', False) 

452 max_name_len = kwargs.pop('max_name_len', 50) 

453 show_plot = kwargs.pop('show_plot', True) 

454 fig_axes = kwargs.pop('fig_axes', None) 

455 

456 if goals is None: 

457 goals = _del_duplicates(list(single_result.index.get_level_values(0))) 

458 all_analysis_variables = _del_duplicates(list(single_result.index.get_level_values(1))) 

459 analysis_variables = [av for av in all_analysis_variables if '_conf' not in av] 

460 

461 renamed_parameter = _format_name(parameter, use_suffix, max_name_len) 

462 

463 single_result = _rename_tuner_names(single_result, use_suffix, max_name_len, 

464 "plot_parameter_verbose") 

465 if second_order_result is not None: 

466 second_order_result = _rename_tuner_names(second_order_result, use_suffix, max_name_len) 

467 

468 if fig_axes is None: 

469 fig, ax = plt.subplots(len(goals), sharex='all', layout="constrained") 

470 else: 

471 fig = fig_axes[0] 

472 ax = fig_axes[1] 

473 fig.suptitle(f"Parameter: {parameter}") 

474 if not isinstance(ax, np.ndarray): 

475 ax = [ax] 

476 for g_i, goal in enumerate(goals): 

477 if second_order_result is not None: 

478 result_2_goal = second_order_result.loc[goal, 'S2', renamed_parameter] 

479 mean = result_2_goal.mean().drop([renamed_parameter]) 

480 mean.sort_values(ascending=False, inplace=True) 

481 sorted_interactions = list(mean.index) 

482 time_ar = _del_duplicates(list(result_2_goal.index.get_level_values(0))) 

483 value = single_result.loc[goal, 'S1'][renamed_parameter].to_numpy() 

484 ax[g_i].plot(single_result.loc[goal, 'S1'][renamed_parameter], label='S1') 

485 ax[g_i].fill_between(time_ar, np.zeros_like(value), value, alpha=0.1) 

486 for para in sorted_interactions: 

487 value_2 = value + result_2_goal[para].to_numpy() 

488 ax[g_i].plot(time_ar, value_2, label='S2 ' + para) 

489 ax[g_i].fill_between(time_ar, value, value_2, alpha=0.1) 

490 value = value_2 

491 ax[g_i].plot(single_result.loc[goal, 'ST'][renamed_parameter], label='ST') 

492 legend = ['S1'] 

493 legend.extend(analysis_variables) 

494 legend.append('ST') 

495 ax[g_i].set_title(f"Goal: {goal}") 

496 ax[g_i].legend() 

497 else: 

498 for analysis_var in analysis_variables: 

499 ax[g_i].plot(single_result.loc[goal, analysis_var][renamed_parameter]) 

500 ax[g_i].legend(analysis_variables) 

501 if show_plot: 

502 plt.show() 

503 return fig, ax 

504 

505 

506def _del_duplicates(x): 

507 """Helper function""" 

508 return list(dict.fromkeys(x)) 

509 

510 

511def _rename_tuner_names(result, use_suffix, max_len, func_name=None): 

512 """Helper function""" 

513 tuner_names = list(result.columns) 

514 renamed_names = {name: _format_name(name, use_suffix, max_len) for name in tuner_names} 

515 result = result.rename(columns=renamed_names, index=renamed_names) 

516 result = result.sort_index() 

517 for old, new in renamed_names.items(): 

518 if old != new and func_name is not None: 

519 sys.stderr.write(f"{func_name} INFO: parameter name {old} changed to {new}\n") 

520 return result 

521 

522 

523def _format_name(name, use_suffix, max_len): 

524 """ 

525 Format tuner names. 

526 """ 

527 if use_suffix: 

528 name = _get_suffix(name) 

529 name = short_name(name, max_len) 

530 return name 

531 

532 

533def _get_suffix(modelica_var_name): 

534 """Helper function""" 

535 index_last_dot = modelica_var_name.rfind('.') 

536 suffix = modelica_var_name[index_last_dot + 1:] 

537 return suffix 

538 

539 

540def _create_figs_axes(figs_axes, fig_index, ax_len_list, fig_title): 

541 """ 

542 Check if figs and axes are already given, if not create them. 

543 """ 

544 if figs_axes is None: 

545 fig, ax = plt.subplots(len(ax_len_list), sharex='all', layout="constrained") 

546 else: 

547 fig = figs_axes[0][fig_index] 

548 ax = figs_axes[1][fig_index] 

549 fig.suptitle(fig_title) 

550 if not isinstance(ax, np.ndarray): 

551 ax = [ax] 

552 return fig, ax