Coverage for src / sdynpy / doc / sdynpy_ppt.py: 6%

360 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-11 16:22 +0000

1# -*- coding: utf-8 -*- 

2""" 

3Functions for creating PowerPoint presentations from SDynPy objects. 

4 

5This modules includes functions for assembling a PowerPoint presentation from 

6SDynPy objects, saving users the tediousness of putting a large number of images 

7and tables into the presentation. 

8""" 

9""" 

10Copyright 2022 National Technology & Engineering Solutions of Sandia, 

11LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S. 

12Government retains certain rights in this software. 

13 

14This program is free software: you can redistribute it and/or modify 

15it under the terms of the GNU General Public License as published by 

16the Free Software Foundation, either version 3 of the License, or 

17(at your option) any later version. 

18 

19This program is distributed in the hope that it will be useful, 

20but WITHOUT ANY WARRANTY; without even the implied warranty of 

21MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

22GNU General Public License for more details. 

23 

24You should have received a copy of the GNU General Public License 

25along with this program. If not, see <https://www.gnu.org/licenses/>. 

26""" 

27 

28import pptx 

29from pptx.util import Inches 

30from pptx.enum.text import MSO_AUTO_SIZE 

31import tempfile 

32import io 

33from PIL import Image 

34import matplotlib.pyplot as plt 

35import numpy as np 

36import os 

37import time 

38 

39from ..core.sdynpy_shape import mac, matrix_plot 

40 

41 

42def position_placeholder(presentation, placeholder, left=None, top=None, right=None, bottom=None): 

43 sw = presentation.slide_width 

44 sh = presentation.slide_height 

45 if left is None: 

46 left = placeholder.left 

47 if left < 0: 

48 left = sw - abs(left) 

49 if right is None: 

50 right = placeholder.left + placeholder.width 

51 if right < 0: 

52 right = sw - abs(right) 

53 if top is None: 

54 top = placeholder.top 

55 if top < 0: 

56 top = sh - abs(top) 

57 if bottom is None: 

58 bottom = placeholder.top + placeholder.height 

59 if bottom < 0: 

60 bottom = sh - abs(bottom) 

61 width = right - left 

62 height = bottom - top 

63 placeholder.left = left 

64 placeholder.top = top 

65 placeholder.width = width 

66 placeholder.height = height 

67 

68 

69def add_title_slide(presentation, title, subtitle='', title_slide_layout_index=0): 

70 title_slide_layout = presentation.slide_layouts[title_slide_layout_index] 

71 slide = presentation.slides.add_slide(title_slide_layout) 

72 slide.shapes.title.text = title 

73 position_placeholder(presentation, slide.shapes.title, right=-slide.shapes.title.left) 

74 slide.placeholders[1].text = subtitle 

75 

76 

77def add_section_header_slide(presentation, title, subtitle='', section_header_slide_layout_index=2): 

78 section_header_slide_layout = presentation.slide_layouts[section_header_slide_layout_index] 

79 slide = presentation.slides.add_slide(section_header_slide_layout) 

80 slide.shapes.title.text = title 

81 position_placeholder(presentation, slide.shapes.title, right=-slide.shapes.title.left) 

82 slide.placeholders[1].text = subtitle 

83 

84 

85def add_geometry_overview_slide(presentation, geometry, title='Geometry', 

86 geometry_plot_kwargs={}, 

87 animate_geometry=True, 

88 save_animation_kwargs={'frames': 200, 'frame_rate': 20}, 

89 content_slide_layout_index=1): 

90 bullet_slide = presentation.slide_layouts[content_slide_layout_index] 

91 geometry_plot = geometry.plot(**geometry_plot_kwargs)[0] 

92 slide = presentation.slides.add_slide(bullet_slide) 

93 text_placeholder = slide.placeholders[1] 

94 if animate_geometry: 

95 output_save_file = os.path.join(tempfile.gettempdir(), 

96 'Geometry.gif') 

97 geometry_plot.save_rotation_animation(output_save_file, **save_animation_kwargs) 

98 geometry_plot.close() 

99 pic = slide.shapes.add_picture(output_save_file, Inches(1), Inches(1)) 

100 else: 

101 with io.BytesIO() as output: 

102 time.sleep(0.5) 

103 img = Image.fromarray(geometry_plot.screenshot()) 

104 img.save(output, format='PNG') 

105 pic = slide.shapes.add_picture(output, Inches(1), Inches(1)) 

106 ar = pic.width / pic.height 

107 slide.shapes.title.text = title 

108 # Move the bullet points to the left 

109 position_placeholder(presentation, slide.placeholders[1], right=( 

110 presentation.slide_width - slide.placeholders[1].left) // 2) 

111 # Pic to the right 

112 position_placeholder(presentation, pic, left=(presentation.slide_width + slide.placeholders[1].left) // 2, 

113 right=-slide.placeholders[1].left, 

114 top=slide.placeholders[1].top) 

115 pic.height = int(pic.width / ar) 

116 geometry_plot.close() 

117 

118 text_placeholder.text_frame.paragraphs[0].text = 'Geometry Information:' 

119 for label, array in zip(['Nodes', 'Coordinate Systems', 'Tracelines', 'Elements'], 

120 [geometry.node, geometry.coordinate_system, geometry.traceline, geometry.element]): 

121 p = text_placeholder.text_frame.add_paragraph() 

122 p.text = '{:}: {:}'.format(label, array.size) 

123 p.level = 1 

124 

125 

126def add_shape_overview_slide(presentation, shapes, title='Modal Parameters', 

127 exp_data=None, fit_data=None, 

128 subplots_kwargs={}, plot_kwargs={}, axes_modifiers={}, 

129 mac_subplots_kwargs={}, matrix_plot_kwargs={}, 

130 frequency_format='{:0.2f}', damping_format='{:.03f}', 

131 empty_slide_layout_index=5): 

132 empty_slide = presentation.slide_layouts[empty_slide_layout_index] 

133 slide = presentation.slides.add_slide(empty_slide) 

134 slide.shapes.title.text = title 

135 # Add a table for the mode shapes 

136 table_shape = slide.shapes.add_table(shapes.size + 1, 4, slide.shapes.title.left, 

137 slide.shapes.title.top + 

138 slide.shapes.title.height + Inches(0.5), 

139 presentation.slide_width // 2 - int(1.5 * slide.shapes.title.left), Inches(1)) 

140 table = table_shape.table 

141 table.cell(0, 0).text = 'Mode' 

142 table.cell(0, 1).text = 'Freq (Hz)' 

143 table.cell(0, 2).text = 'Damp (%)' 

144 table.cell(0, 3).text = 'Description' 

145 

146 table.columns[0].width = Inches(1) 

147 table.columns[1].width = Inches(1) 

148 table.columns[2].width = Inches(1) 

149 table.columns[3].width = presentation.slide_width // 2 - \ 

150 int(1.5 * slide.shapes.title.left) - sum([table.columns[i].width for i in range(3)]) 

151 

152 for i, shape in enumerate(shapes.flatten()): 

153 table.cell(i + 1, 0).text = '{:}'.format(i + 1) 

154 table.cell(i + 1, 1).text = frequency_format.format(shape.frequency) 

155 table.cell(i + 1, 2).text = damping_format.format(shape.damping * 100) 

156 table.cell(i + 1, 3).text = shape.comment1 

157 

158 mac_matrix = mac(shapes) 

159 fig, ax = plt.subplots(1, 1, **mac_subplots_kwargs) 

160 matrix_plot(mac_matrix, ax, **matrix_plot_kwargs) 

161 

162 with io.BytesIO() as output: 

163 fig.savefig(output) 

164 pic = slide.shapes.add_picture(output, Inches(1), Inches(1)) 

165 ar = pic.width / pic.height 

166 position_placeholder(presentation, pic, left=(presentation.slide_width + table_shape.left) // 2, 

167 right=-table_shape.left, 

168 top=table_shape.top, 

169 bottom=table_shape.top + (presentation.slide_height - table_shape.top) // 2) 

170 pic.width = int(pic.height * ar) 

171 plt.close(fig) 

172 

173 if exp_data is not None: 

174 fig, ax = plt.subplots(1, 1, **subplots_kwargs) 

175 h1 = ax.plot(exp_data.abscissa.T, exp_data.ordinate.T, 'r', **plot_kwargs) 

176 if fit_data is not None: 

177 h2 = ax.plot(fit_data.abscissa.T, fit_data.ordinate.T, 'b', **plot_kwargs) 

178 ax.legend([np.atleast_1d(h1)[0], np.atleast_1d(h2)[0]], 

179 ['Experiment', 'Analytic Fit']) 

180 for key, val in axes_modifiers.items(): 

181 getattr(ax, key)(val) 

182 if ('set_yscale', 'log'.lower()) in [(key, val) for key, val in axes_modifiers.items()]: 

183 ax.set_ylim(exp_data.ordinate.min() / 1.5, exp_data.ordinate.max() * 1.5) 

184 else: 

185 rng = exp_data.ordinate.max() - exp_data.ordinate.min() 

186 ax.set_ylim(exp_data.ordinate.min() - rng / 20, exp_data.ordinate.max() + rng / 20) 

187 with io.BytesIO() as output: 

188 fig.savefig(output) 

189 pic = slide.shapes.add_picture(output, Inches(1), Inches(1)) 

190 ar = pic.width / pic.height 

191 plt.close(fig) 

192 position_placeholder(presentation, pic, left=(presentation.slide_width + table_shape.left) // 2, 

193 right=-table_shape.left, 

194 top=table_shape.top + 

195 (presentation.slide_height - table_shape.top) // 2, 

196 bottom=-slide.shapes.title.top) 

197 pic.width = int(pic.height * ar) 

198 

199 

200def add_shape_animation_slides(presentation, geometry, shapes, title_format='Mode {number:}', 

201 text_format='Mode {number:}\nFrequency: {frequency:0.2f}\nDamping: {damping:0.3f}%', 

202 save_animation_kwargs={'frames': 20, 'frame_rate': 20}, 

203 geometry_plot_kwargs={}, 

204 plot_shape_kwargs={}, 

205 content_slide_layout_index=1, 

206 left_right=True): 

207 bullet_slide = presentation.slide_layouts[content_slide_layout_index] 

208 output_save_file = os.path.join(tempfile.gettempdir(), 

209 'Shape_{:}.gif') 

210 shapes_plot = geometry.plot_shape( 

211 shapes.flatten(), plot_kwargs=geometry_plot_kwargs, **plot_shape_kwargs) 

212 shapes_plot.save_animation_all_shapes(output_save_file, **save_animation_kwargs) 

213 shapes_plot.close() 

214 for i, shape in enumerate(shapes.flatten()): 

215 slide = presentation.slides.add_slide(bullet_slide) 

216 text_placeholder = slide.placeholders[1] 

217 pic = slide.shapes.add_picture(output_save_file.format(i + 1), Inches(1), Inches(1)) 

218 ar = pic.width / pic.height 

219 slide.shapes.title.text = title_format.format(index=i, number=i + 1, frequency=shape.frequency, 

220 damping=shape.damping * 100, modal_mass=shape.modal_mass, 

221 comment1=shape.comment1, comment2=shape.comment2, 

222 comment3=shape.comment3, comment4=shape.comment4, 

223 comment5=shape.comment5) 

224 if left_right: 

225 # Move the bullet points to the left 

226 position_placeholder(presentation, text_placeholder, 

227 right=presentation.slide_width // 2 - Inches(0.25)) 

228 # Pic to the right 

229 position_placeholder(presentation, pic, left=presentation.slide_width // 2 + Inches(0.25), 

230 right=-text_placeholder.left, 

231 top=text_placeholder.top) 

232 else: 

233 # Move the bullet points to the left 

234 position_placeholder(presentation, text_placeholder, right=-text_placeholder.left, 

235 bottom=text_placeholder.top + Inches(1)) 

236 # Pic to the right 

237 position_placeholder(presentation, pic, left=text_placeholder.left, 

238 right=-text_placeholder.left, 

239 top=text_placeholder.top + text_placeholder.height + Inches(0.25)) 

240 pic.height = int(pic.width / ar) 

241 # Check to see if it is off the screen 

242 distance_over = (pic.height + pic.top - presentation.slide_height + 

243 Inches(0.25)) / pic.height 

244 if distance_over > 0: 

245 pic.height = int(pic.height * (1 - distance_over)) 

246 pic.width = int(pic.width * (1 - distance_over)) 

247 

248 os.remove(output_save_file.format(i + 1)) 

249 # Now add text 

250 for j, text_line in enumerate(text_format.split('\n')): 

251 text = text_line.replace('\t', '').format(index=i, number=i + 1, frequency=shape.frequency, 

252 damping=shape.damping * 100, modal_mass=shape.modal_mass, 

253 comment1=shape.comment1, comment2=shape.comment2, 

254 comment3=shape.comment3, comment4=shape.comment4, 

255 comment5=shape.comment5) 

256 if j == 0: 

257 paragraph = text_placeholder.text_frame.paragraphs[0] 

258 else: 

259 paragraph = text_placeholder.text_frame.add_paragraph() 

260 paragraph.text = text 

261 paragraph.level = text_line.count('\t') 

262 text_placeholder.text_frame.fit_text() 

263 text_placeholder.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE 

264 

265 

266def add_shape_comparison_overview_slide(presentation, shapes_1, shapes_2, 

267 title='Modal Parameter Comparison', 

268 shapes_1_label=None, shapes_2_label=None, 

269 subplots_kwargs={}, plot_kwargs={}, axes_modifiers={}, 

270 frequency_format='{:0.2f}', damping_format='{:.03f}', 

271 mac_format='{:0.2f}', mac_subplots_kwargs={}, 

272 matrix_plot_kwargs={}, 

273 table_threshold=0.75, 

274 empty_slide_layout_index=5): 

275 empty_slide = presentation.slide_layouts[empty_slide_layout_index] 

276 slide = presentation.slides.add_slide(empty_slide) 

277 slide.shapes.title.text = title 

278 mac_matrix = mac(shapes_1, shapes_2) 

279 shape_indices = np.where(mac_matrix > table_threshold) 

280 # Plot the MAC matrix 

281 fig, ax = plt.subplots(1, 1, **mac_subplots_kwargs) 

282 matrix_plot(mac_matrix, ax, **matrix_plot_kwargs) 

283 if shapes_1_label is not None: 

284 ax.set_ylabel(shapes_1_label) 

285 if shapes_2_label is not None: 

286 ax.set_xlabel(shapes_2_label) 

287 fig.tight_layout() 

288 # Add a table for the mode shapes 

289 table_shape = slide.shapes.add_table(shape_indices[0].size + 1, 7, slide.shapes.title.left, 

290 slide.shapes.title.top + 

291 slide.shapes.title.height + Inches(0.5), 

292 presentation.slide_width // 2 - int(1.5 * slide.shapes.title.left), Inches(1)) 

293 table = table_shape.table 

294 table.cell(0, 0).text = '{:} Mode'.format( 

295 'Shape 1' if shapes_1_label is None else shapes_1_label) 

296 table.cell(0, 1).text = 'Freq (Hz)' 

297 table.cell(0, 2).text = 'Damp (%)' 

298 table.cell(0, 3).text = '{:} Mode'.format( 

299 'Shape 2' if shapes_2_label is None else shapes_2_label) 

300 table.cell(0, 4).text = 'Freq (Hz)' 

301 table.cell(0, 5).text = 'Damp (%)' 

302 table.cell(0, 6).text = 'MAC' 

303 

304 for i, indices in enumerate(zip(*shape_indices)): 

305 index_1, index_2 = indices 

306 shape_1 = shapes_1[index_1] 

307 shape_2 = shapes_2[index_2] 

308 table.cell(i + 1, 0).text = '{:}'.format(index_1 + 1) 

309 table.cell(i + 1, 3).text = '{:}'.format(index_2 + 1) 

310 table.cell(i + 1, 1).text = frequency_format.format(shape_1.frequency) 

311 table.cell(i + 1, 4).text = frequency_format.format(shape_2.frequency) 

312 table.cell(i + 1, 2).text = damping_format.format(shape_1.damping * 100) 

313 table.cell(i + 1, 5).text = damping_format.format(shape_2.damping * 100) 

314 table.cell(i + 1, 6).text = mac_format.format(mac_matrix[index_1, index_2]) 

315 

316 with io.BytesIO() as output: 

317 fig.savefig(output) 

318 pic = slide.shapes.add_picture(output, Inches(1), Inches(1)) 

319 ar = pic.width / pic.height 

320 position_placeholder(presentation, pic, left=(presentation.slide_width + table_shape.left) // 2, 

321 right=-table_shape.left, 

322 top=table_shape.top, 

323 bottom=-slide.shapes.title.top) 

324 pic.height = int(pic.width / ar) 

325 plt.close(fig) 

326 

327 

328def add_shape_comparison_animation_slides(presentation, geometry_1, shapes_1, 

329 geometry_2, shapes_2, 

330 title_format='Comparison for Mode {number:}', 

331 text_format='Mode {number:}\nFrequency: {frequency:0.2f}\nDamping: {damping:0.3f}%', 

332 save_animation_kwargs={'frames': 20, 'frame_rate': 20}, 

333 geometry_plot_kwargs_1={}, 

334 plot_shape_kwargs_1={}, 

335 geometry_plot_kwargs_2={}, 

336 plot_shape_kwargs_2={}, 

337 two_content_slide_layout_index=3): 

338 two_content_slide = presentation.slide_layouts[two_content_slide_layout_index] 

339 output_save_file = os.path.join(tempfile.gettempdir(), 

340 'Shape_{:}.gif') 

341 slides = [] 

342 for j, (shapes, geometry, geometry_plot_kwargs, plot_shape_kwargs) in enumerate( 

343 [(shapes_1, geometry_1, geometry_plot_kwargs_1, plot_shape_kwargs_1), 

344 (shapes_2, geometry_2, geometry_plot_kwargs_2, plot_shape_kwargs_2)]): 

345 shapes_plot = geometry.plot_shape( 

346 shapes.flatten(), plot_kwargs=geometry_plot_kwargs, **plot_shape_kwargs) 

347 shapes_plot.save_animation_all_shapes(output_save_file, **save_animation_kwargs) 

348 shapes_plot.close() 

349 for i, shape in enumerate(shapes.flatten()): 

350 if j == 0: 

351 slide = presentation.slides.add_slide(two_content_slide) 

352 slides.append(slide) 

353 else: 

354 slide = slides[i] 

355 text_placeholder = slide.shapes[1 + j] 

356 pic = slide.shapes.add_picture(output_save_file.format(i + 1), Inches(1), Inches(1)) 

357 ar = pic.width / pic.height 

358 if j == 0: 

359 slide.shapes.title.text = title_format.format(index=i, number=i + 1, frequency=shape.frequency, 

360 damping=shape.damping * 100, modal_mass=shape.modal_mass, 

361 comment1=shape.comment1, comment2=shape.comment2, 

362 comment3=shape.comment3, comment4=shape.comment4, 

363 comment5=shape.comment5) 

364 position_placeholder(presentation, pic, left=text_placeholder.left, 

365 right=text_placeholder.left + text_placeholder.width, 

366 top=text_placeholder.top + Inches(1.5)) 

367 pic.height = int(pic.width / ar) 

368 # Check to see if it is off the screen 

369 distance_over = (pic.height + pic.top - presentation.slide_height + 

370 Inches(0.25)) / pic.height 

371 if distance_over > 0: 

372 pic.height = int(pic.height * (1 - distance_over)) 

373 pic.width = int(pic.width * (1 - distance_over)) 

374 

375 os.remove(output_save_file.format(i + 1)) 

376 # Now add text 

377 for k, text_line in enumerate(text_format.split('\n')): 

378 text = text_line.replace('\t', '').format(index=i, number=i + 1, frequency=shape.frequency, 

379 damping=shape.damping * 100, modal_mass=shape.modal_mass, 

380 comment1=shape.comment1, comment2=shape.comment2, 

381 comment3=shape.comment3, comment4=shape.comment4, 

382 comment5=shape.comment5) 

383 if k == 0: 

384 paragraph = text_placeholder.text_frame.paragraphs[0] 

385 else: 

386 paragraph = text_placeholder.text_frame.add_paragraph() 

387 paragraph.text = text 

388 paragraph.level = text_line.count('\t') 

389 text_placeholder.text_frame.fit_text() 

390 text_placeholder.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE 

391 

392 

393def create_summary_pptx(presentation, title=None, subtitle='', 

394 geometry=None, shapes=None, frfs=None, 

395 slide_width=None, slide_height=None, 

396 max_shapes=None, max_frequency=None, 

397 frequency_format='{:0.1f}', damping_format='{:.02f}', 

398 cmif_kwargs={'part': 'imag', 'tracking': None}, 

399 subplots_kwargs={}, plot_kwargs={}, 

400 save_animation_kwargs={'frames': 20, 'frame_rate': 20}, 

401 geometry_plot_kwargs={}, 

402 title_slide_layout_index=0, 

403 content_slide_layout_index=1, 

404 empty_slide_layout_index=5 

405 ): 

406 experimental_cmif = None if frfs is None else frfs.compute_cmif(**cmif_kwargs) 

407 frequencies = None if experimental_cmif is None else experimental_cmif[0].abscissa 

408 

409 analytic_frfs = None if (shapes is None or frfs is None) else shapes.compute_frf(frequencies, np.unique(frfs.coordinate[..., 0]), 

410 np.unique(frfs.coordinate[..., 1])) 

411 analytic_cmif = analytic_frfs.compute_cmif(**cmif_kwargs) 

412 

413 if isinstance(presentation, pptx.presentation.Presentation): 

414 prs = presentation 

415 else: 

416 prs = pptx.Presentation() 

417 if slide_width is not None: 

418 prs.slide_width = Inches(slide_width) 

419 if slide_height is not None: 

420 prs.slide_height = Inches(slide_height) 

421 

422 def position_placeholder(placeholder, left=None, top=None, right=None, bottom=None): 

423 sw = prs.slide_width 

424 sh = prs.slide_height 

425 if left is None: 

426 left = placeholder.left 

427 if left < 0: 

428 left = sw - abs(left) 

429 if right is None: 

430 right = placeholder.left + placeholder.width 

431 if right < 0: 

432 right = sw - abs(right) 

433 if top is None: 

434 top = placeholder.top 

435 if top < 0: 

436 top = sh - abs(top) 

437 if bottom is None: 

438 bottom = placeholder.top + placeholder.height 

439 if bottom < 0: 

440 bottom = sh - abs(bottom) 

441 width = right - left 

442 height = bottom - top 

443 placeholder.left = left 

444 placeholder.top = top 

445 placeholder.width = width 

446 placeholder.height = height 

447 

448 # Add Title Slide 

449 if title is not None: 

450 title_slide_layout = prs.slide_layouts[title_slide_layout_index] 

451 slide = prs.slides.add_slide(title_slide_layout) 

452 slide.shapes.title.text = title 

453 position_placeholder(slide.shapes.title, right=-slide.shapes.title.left) 

454 slide.placeholders[1].text = subtitle 

455 position_placeholder(slide.placeholders[1], right=-slide.placeholders[1].left) 

456 

457 bullet_slide = prs.slide_layouts[content_slide_layout_index] 

458 empty_slide = prs.slide_layouts[empty_slide_layout_index] 

459 

460 # Plot the test geometry 

461 if geometry is not None: 

462 geometry_plot = geometry.plot(**geometry_plot_kwargs)[0] 

463 slide = prs.slides.add_slide(bullet_slide) 

464 text_placeholder = slide.placeholders[1] 

465 with io.BytesIO() as output: 

466 time.sleep(0.5) 

467 img = Image.fromarray(geometry_plot.screenshot()) 

468 img.save(output, format='PNG') 

469 pic = slide.shapes.add_picture(output, Inches(1), Inches(1)) 

470 slide.shapes.title.text = 'Test Geometry' 

471 # Move the bullet points to the left 

472 position_placeholder(slide.placeholders[1], right=( 

473 prs.slide_width - slide.placeholders[1].left) // 2) 

474 # Pic to the right 

475 position_placeholder(pic, left=(prs.slide_width + slide.placeholders[1].left) // 2, 

476 right=-slide.placeholders[1].left, 

477 top=slide.placeholders[1].top) 

478 pic.height = int(pic.width * img.size[1] / img.size[0]) 

479 geometry_plot.close() 

480 

481 text_placeholder.text_frame.paragraphs[0].text = 'Geometry Information:' 

482 for label, array in zip(['Nodes', 'Coordinate Systems', 'Tracelines', 'Elements'], 

483 [geometry.node, geometry.coordinate_system, geometry.traceline, geometry.element]): 

484 p = text_placeholder.text_frame.add_paragraph() 

485 p.text = '{:}: {:}'.format(label, array.size) 

486 p.level = 1 

487 

488 # Plot the list of shapes 

489 if shapes is not None: 

490 if max_shapes is not None: 

491 shapes = shapes.flatten()[:max_shapes] 

492 if max_frequency is not None: 

493 shapes = shapes.flatten()[shapes.flatten().frequency < max_frequency] 

494 slide = prs.slides.add_slide(empty_slide) 

495 

496 slide.shapes.title.text = 'Modal Parameters' 

497 # Add a table for the mode shapes 

498 table_shape = slide.shapes.add_table(shapes.size + 1, 4, slide.shapes.title.left, 

499 slide.shapes.title.top + 

500 slide.shapes.title.height + Inches(0.5), 

501 prs.slide_width // 2 - int(1.5 * slide.shapes.title.left), Inches(1)) 

502 table = table_shape.table 

503 table.cell(0, 0).text = 'Mode' 

504 table.cell(0, 1).text = 'Freq (Hz)' 

505 table.cell(0, 2).text = 'Damp (%)' 

506 table.cell(0, 3).text = 'Description' 

507 

508 table.columns[0].width = Inches(1) 

509 table.columns[1].width = Inches(1) 

510 table.columns[2].width = Inches(1) 

511 table.columns[3].width = prs.slide_width // 2 - \ 

512 int(1.5 * slide.shapes.title.left) - sum([table.columns[i].width for i in range(3)]) 

513 

514 for i, shape in enumerate(shapes.flatten()): 

515 table.cell(i + 1, 0).text = '{:}'.format(i + 1) 

516 table.cell(i + 1, 1).text = frequency_format.format(shape.frequency) 

517 table.cell(i + 1, 2).text = damping_format.format(shape.damping * 100) 

518 table.cell(i + 1, 3).text = shape.comment1 

519 

520 if analytic_cmif is not None: 

521 fig, ax = plt.subplots(1, 1, **subplots_kwargs) 

522 experimental_cmif.plot(ax, **plot_kwargs) 

523 analytic_cmif.plot(ax, **plot_kwargs) 

524 ax.legend(['Experiment', 'Analytic Fit']) 

525 ax.set_yscale('log') 

526 ax.set_ylim(experimental_cmif.ordinate.min() / 1.5, 

527 experimental_cmif.ordinate.max() * 1.5) 

528 with io.BytesIO() as output: 

529 fig.savefig(output) 

530 pic = slide.shapes.add_picture(output, Inches(1), Inches(1)) 

531 ar = pic.width / pic.height 

532 plt.close(fig) 

533 position_placeholder(pic, left=(prs.slide_width + table_shape.left) // 2, 

534 right=-table_shape.left, 

535 top=table_shape.top) 

536 pic.height = int(pic.width / ar) 

537 

538 # Now we need to plot each shape if possible 

539 if geometry is not None: 

540 output_save_file = os.path.join(tempfile.gettempdir(), 

541 'Shape_{:}.gif') 

542 shapes_plot = geometry.plot_shape(shapes.flatten(), plot_kwargs=geometry_plot_kwargs) 

543 shapes_plot.save_animation_all_shapes(output_save_file, **save_animation_kwargs) 

544 shapes_plot.close() 

545 for i, shape in enumerate(shapes.flatten()): 

546 slide = prs.slides.add_slide(bullet_slide) 

547 text_placeholder = slide.placeholders[1] 

548 pic = slide.shapes.add_picture(output_save_file.format(i + 1), Inches(1), Inches(1)) 

549 ar = pic.width / pic.height 

550 slide.shapes.title.text = 'Mode {:}'.format(i + 1) 

551 # Move the bullet points to the left 

552 position_placeholder(text_placeholder, right=-text_placeholder.left, 

553 bottom=text_placeholder.top + Inches(1)) 

554 # Pic to the right 

555 position_placeholder(pic, left=text_placeholder.left, 

556 right=-text_placeholder.left, 

557 top=text_placeholder.top + text_placeholder.height + Inches(0.25)) 

558 pic.height = int(pic.width / ar) 

559 # Check to see if it is off the screen 

560 distance_over = (pic.height + pic.top - prs.slide_height + 

561 Inches(0.25)) / pic.height 

562 if distance_over > 0: 

563 pic.height = int(pic.height * (1 - distance_over)) 

564 pic.width = int(pic.width * (1 - distance_over)) 

565 

566 os.remove(output_save_file.format(i + 1)) 

567 text_placeholder.text_frame.paragraphs[0].text = 'Mode {:}'.format(i + 1) 

568 for label, value in zip(['Frequency: ' + frequency_format + ' Hz', 'Damping: ' + damping_format + ' %'], 

569 [shape.frequency, shape.damping * 100]): 

570 p = text_placeholder.text_frame.add_paragraph() 

571 p.text = label.format(value) 

572 p.level = 1 

573 text_placeholder.text_frame.fit_text() 

574 text_placeholder.text_frame.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE 

575 

576 if isinstance(presentation, pptx.presentation.Presentation): 

577 return prs 

578 else: 

579 prs.save(presentation) 

580 return prs