Coverage for src / sdynpy / doc / sdynpy_latex.py: 5%

560 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 a LaTeX report from SDynPy objects. 

4""" 

5""" 

6Copyright 2022 National Technology & Engineering Solutions of Sandia, 

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

8Government retains certain rights in this software. 

9 

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

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

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

13(at your option) any later version. 

14 

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

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

17MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18GNU General Public License for more details. 

19 

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

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

22""" 

23 

24import numpy as np 

25import matplotlib.pyplot as plt 

26import os 

27import pyqtgraph as pqtg 

28import PIL 

29from ..signal_processing.sdynpy_correlation import mac, matrix_plot 

30from ..core.sdynpy_geometry import Geometry,GeometryPlotter, ShapePlotter 

31from ..core.sdynpy_coordinate import CoordinateArray,coordinate_array as sd_coordinate_array 

32from ..core.sdynpy_shape import ShapeArray, mac as shape_mac,rigid_body_check 

33from ..fileio.sdynpy_pdf3D import create_animated_modeshape_content,get_view_parameters_from_plotter 

34from shutil import copy 

35from qtpy.QtWidgets import QApplication 

36import pandas as pd 

37from io import BytesIO 

38 

39try: 

40 from vtk import vtkU3DExporter 

41except ImportError: 

42 vtkU3DExporter = None 

43 

44def create_latex_summary(figure_basename, geometry, shapes, frfs, 

45 output_file=None, figure_basename_relative_to_latex=None, 

46 max_shapes=None, max_frequency=None, 

47 frequency_format='{:0.1f}', damping_format='{:0.2f}\\%', 

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

49 cmif_subplots_kwargs={}, 

50 mac_subplots_kwargs={}, mac_plot_kwargs={}, 

51 geometry_plot_kwargs={}, 

52 shape_plot_kwargs={}, 

53 save_animation_kwargs={'frames': 20}, 

54 latex_cmif_graphics_options=r'width=0.7\linewidth', 

55 latex_mac_graphics_options=r'width=0.5\linewidth', 

56 latex_shape_graphics_options=r'width=\linewidth,loop', 

57 latex_shape_subplot_options=r'[t]{0.45\linewidth}', 

58 latex_max_figures_per_page=6, 

59 latex_max_figures_first_page=None, 

60 latex_cmif_caption='Complex Mode Indicator Function showing experimental data compared to modal fitting.', 

61 latex_cmif_label='fig:cmif', 

62 latex_mac_caption='Auto Modal Assurance Criterion Plot showing independence of fit mode shapes.', 

63 latex_mac_label='fig:mac', 

64 latex_shape_subcaption='Shape {number:} at {frequency:} Hz, {damping:}\\ damping', 

65 latex_shape_sublabel='fig:shape{:}', 

66 latex_shape_caption='Mode shapes extracted from test data.', 

67 latex_shape_label='fig:modeshapes', 

68 latex_shape_table_columns='lllp{3.5in}', 

69 latex_shape_table_caption=( 

70 'List of modes extracted from the test data. Modal parameters are shown along with a brief description of the mode shape.'), 

71 latex_shape_table_label='tab:modelist'): 

72 

73 if figure_basename_relative_to_latex is None: 

74 figure_basename_relative_to_latex = figure_basename.replace('\\', '/') 

75 

76 if latex_max_figures_first_page is None: 

77 latex_max_figures_first_page = latex_max_figures_per_page 

78 

79 # Get the figure names 

80 figure_base_path, figure_base_filename = os.path.split(figure_basename) 

81 figure_base_filename, figure_base_ext = os.path.splitext(figure_base_filename) 

82 latex_figure_base_path, latex_figure_base_filename = os.path.split( 

83 figure_basename_relative_to_latex) 

84 latex_figure_base_filename, latex_figure_base_ext = os.path.splitext(latex_figure_base_filename) 

85 

86 cmif_file_name = os.path.join(figure_base_path, figure_base_filename + 

87 '_cmif_comparison' + figure_base_ext) 

88 mac_file_name = os.path.join(figure_base_path, figure_base_filename + '_mac' + figure_base_ext) 

89 shape_file_name = os.path.join( 

90 figure_base_path, figure_base_filename + '_shape_{:}' + figure_base_ext) 

91 

92 cmif_latex_file_name = (latex_figure_base_path + '/' + 

93 figure_base_filename + '_cmif_comparison').replace('\\', '/') 

94 mac_latex_file_name = (latex_figure_base_path + '/' + 

95 figure_base_filename + '_mac').replace('\\', '/') 

96 shape_latex_file_name = (latex_figure_base_path + '/' + figure_base_filename + '_shape_{:}-') 

97 

98 # Go through and save out all the files 

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

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

101 

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

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

104 analytic_cmif = analytic_frfs.compute_cmif(**cmif_kwargs) 

105 

106 # Compute CMIF 

107 

108 fig, ax = plt.subplots(num=figure_basename + ' CMIF', **cmif_subplots_kwargs) 

109 experimental_cmif[0].plot(ax, plot_kwargs={'color': 'b', 'linewidth': 1}) 

110 analytic_cmif[0].plot(ax, plot_kwargs={'color': 'r', 'linewidth': 1}) 

111 experimental_cmif[1:].plot(ax, plot_kwargs={'color': 'b', 'linewidth': 0.25}) 

112 analytic_cmif[1:].plot(ax, plot_kwargs={'color': 'r', 'linewidth': 0.25}) 

113 shapes.plot_frequency(experimental_cmif[0].abscissa, experimental_cmif[0].ordinate, ax) 

114 ax.legend(['Experiment', 'Fit']) 

115 ax.set_yscale('log') 

116 ax.set_ylim(experimental_cmif.min(abs) / 2, experimental_cmif.max(abs) * 2) 

117 ax.set_ylabel('CMIF (m/s^2/N)') 

118 ax.set_xlabel('Frequency (Hz)') 

119 fig.tight_layout() 

120 fig.savefig(cmif_file_name) 

121 

122 # Compute MAC 

123 mac_matrix = mac(shapes.flatten().shape_matrix.T) 

124 fig, ax = plt.subplots(num=figure_basename + ' MAC') 

125 matrix_plot(mac_matrix, ax, **mac_plot_kwargs) 

126 fig.tight_layout() 

127 fig.savefig(mac_file_name) 

128 

129 # Now go through and save the shapes 

130 plotter = geometry.plot_shape(shapes, plot_kwargs=geometry_plot_kwargs, **shape_plot_kwargs) 

131 plotter.save_animation_all_shapes( 

132 shape_file_name, individual_images=True, **save_animation_kwargs) 

133 

134 # Go through and create the latex document 

135 output_string = '' 

136 

137 # Add the CMIF plot 

138 output_string += r'''\begin{{figure}} 

139 \centering 

140 \includegraphics[{:}]{{{:}}} 

141 \caption{{{:}}} 

142 \label{{{:}}} 

143\end{{figure}}'''.format(latex_cmif_graphics_options, 

144 cmif_latex_file_name, 

145 latex_cmif_caption, 

146 latex_cmif_label) 

147 

148 output_string += r''' 

149 

150\begin{{figure}} 

151 \centering 

152 \includegraphics[{:}]{{{:}}} 

153 \caption{{{:}}} 

154 \label{{{:}}} 

155\end{{figure}}'''.format(latex_mac_graphics_options, 

156 mac_latex_file_name, 

157 latex_mac_caption, 

158 latex_mac_label) 

159 

160 # Create a table of natural frequencies, damping values, and comments 

161 output_string += r''' 

162 

163\begin{{table}} 

164 \centering 

165 \caption{{{:}}} 

166 \label{{{:}}} 

167 %\resizebox{{\linewidth}}{{!}}{{ 

168 \begin{{tabular}}{{{:}}} 

169 Mode & Freq (Hz) & Damping & Description \\ \hline'''.format( 

170 latex_shape_table_caption, latex_shape_table_label, latex_shape_table_columns) 

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

172 output_string += r''' 

173 {:} & {:} & {:} & {:} \\'''.format(i + 1, frequency_format.format(shape.frequency), 

174 damping_format.format(shape.damping * 100), shape.comment1) 

175 output_string += r''' 

176 \end{tabular} 

177 %} 

178\end{table}''' 

179 

180 # Now lets create the modeshape figure 

181 output_string += r''' 

182\begin{figure}[h] 

183 \centering''' 

184 for index, shape in enumerate(shapes.flatten()): 

185 if index == latex_max_figures_first_page or ((index - latex_max_figures_first_page) % latex_max_figures_per_page == 0 and index != 0): 

186 output_string += r''' 

187\end{figure} 

188\begin{figure}[h] 

189 \ContinuedFloat 

190 \centering''' 

191 output_string += r''' 

192 \begin{{subfigure}}{subfigure_options:} 

193 \centering 

194 \animategraphics[{graphics_options:}]{{{num_frames:}}}{{{base_name:}}}{{0}}{{{end_frame:}}} 

195 \caption{{{caption:}}} 

196 \label{{{label:}}} 

197 \end{{subfigure}}'''.format(graphics_options=latex_shape_graphics_options, num_frames=save_animation_kwargs['frames'], 

198 base_name=shape_latex_file_name.format(index + 1), end_frame=save_animation_kwargs['frames'] - 1, 

199 caption=latex_shape_subcaption.format( 

200 number=index + 1, 

201 frequency=frequency_format.format(shape.frequency), 

202 damping=damping_format.format(shape.damping * 100)), 

203 label=latex_shape_sublabel.format(index + 1), 

204 subfigure_options=latex_shape_subplot_options) 

205 output_string += r''' 

206 \caption{{{:}}} 

207 \label{{{:}}} 

208\end{{figure}} 

209'''.format(latex_shape_caption, latex_shape_label) 

210 if isinstance(output_file, str): 

211 close = True 

212 output_file = open(output_file, 'w') 

213 else: 

214 close = False 

215 try: 

216 output_file.write(output_string) 

217 except AttributeError: 

218 print('Error writing to output file {:}'.format(output_file)) 

219 if close: 

220 output_file.close() 

221 return output_string 

222 

223def create_geometry_overview(geometry, plot_kwargs = {}, 

224 coordinate_array = None, plot_coordinate_kwargs = {}, 

225 animation_style = '3d', 

226 animation_frames = 200, 

227 animation_frame_rate = 20, 

228 geometry_figure_label = 'fig:geometry', 

229 geometry_figure_caption = 'Geometry', 

230 geometry_graphics_options = r'width=0.7\linewidth', 

231 geometry_animate_graphics_options = r'width=0.7\linewidth,loop', 

232 geometry_figure_placement = '[h]', 

233 geometry_figure_save_name = None, 

234 coordinate_figure_label = 'fig:coordinate', 

235 coordinate_figure_caption = 'Local Coordinate Directions (Red: X+, Green: Y+, Blue: Z+)', 

236 coordinate_graphics_options = r'width=0.7\linewidth', 

237 coordinate_animate_graphics_options = r'width=0.7\linewidth,loop', 

238 coordinate_figure_placement = '[h]', 

239 coordinate_figure_save_name = None, 

240 latex_root = r'', 

241 figure_root = None, 

242 include_name = None, 

243 ): 

244 

245 if geometry_figure_save_name is None: 

246 if figure_root is None: 

247 geometry_figure_save_name = os.path.join(latex_root,'geometry') 

248 else: 

249 geometry_figure_save_name = os.path.join(figure_root,'geometry') 

250 

251 if coordinate_figure_save_name is None: 

252 if figure_root is None: 

253 coordinate_figure_save_name = os.path.join(latex_root,'coordinate') 

254 else: 

255 coordinate_figure_save_name = os.path.join(figure_root,'coordinate') 

256 

257 plot_local_coords = False 

258 if isinstance(geometry,Geometry): 

259 geom_plotter = geometry.plot(**plot_kwargs,plot_individual_items=True)[0] 

260 elif isinstance(geometry,GeometryPlotter): 

261 geom_plotter = geometry 

262 geometry = None 

263 else: 

264 raise ValueError('`geometry` should be a `Geometry` or `GeometryPlotter` object') 

265 if len(plot_coordinate_kwargs) == 0 and len(plot_kwargs) > 0: 

266 plot_coordinate_kwargs['plot_kwargs'] = plot_kwargs 

267 if isinstance(coordinate_array,CoordinateArray): 

268 if geometry is None: 

269 raise ValueError('If `coordinate_array` is a `CoordinateArray` object, then `geometry` must be a `Geometry` object.') 

270 coord_plotter = geometry.plot_coordinate(coordinate_array,**plot_coordinate_kwargs,plot_individual_items=True) 

271 elif isinstance(coordinate_array,GeometryPlotter): 

272 coord_plotter = coordinate_array 

273 coordinate_array = None 

274 elif coordinate_array == 'local': 

275 if geometry is None: 

276 raise ValueError('If `coordinate_array` is local, then `geometry` must be a `Geometry` object.') 

277 css_to_plot = geometry.coordinate_system.id[[not np.allclose(matrix,np.eye(3)) for matrix in geometry.coordinate_system.matrix[...,:3,:3]]] 

278 nodes_to_plot = geometry.node.id[np.isin(geometry.node.disp_cs, css_to_plot)] 

279 coordinate_array = sd_coordinate_array(nodes_to_plot,[1,2,3],force_broadcast=True) 

280 coord_plotter = geometry.plot_coordinate(coordinate_array,**plot_coordinate_kwargs,plot_individual_items=True) 

281 plot_local_coords = True 

282 elif coordinate_array is None: 

283 coord_plotter = None 

284 else: 

285 raise ValueError('`coordinate_array` should be a `CoordinateArray` or `GeometryPlotter` object or `None`') 

286 

287 latex_string = [ 

288"""To describe the data acquired in this activity, a geometry is constructed 

289consisting of the measurement positions and orientations, as well as lines and 

290elements to aid in visualization of the geometry. The geometry for this 

291activity is shown in Figure \\ref{{{geometry_reference:}}}.""".format( 

292 geometry_reference = geometry_figure_label)] 

293 

294 latex_string.append(figure([geom_plotter],geometry_figure_label, 

295 geometry_figure_caption, 

296 geometry_graphics_options, 

297 geometry_animate_graphics_options, 

298 geometry_figure_placement, 

299 figure_save_names = [geometry_figure_save_name], 

300 latex_root = latex_root, 

301 animation_style = animation_style, 

302 animation_frames = animation_frames, 

303 animation_frame_rate = animation_frame_rate)) 

304 

305 if coord_plotter is not None: 

306 

307 if plot_local_coords: 

308 latex_string.append( 

309"""To describe orientations of measurements, coordinate systems are used to define 

310local directions. Figure \\ref{{{coordinate_reference:}}} shows the local 

311coordinate systems defined in the test.""".format(coordinate_reference = coordinate_figure_label) 

312 ) 

313 

314 latex_string.append(figure([coord_plotter],coordinate_figure_label, 

315 coordinate_figure_caption, 

316 coordinate_graphics_options, 

317 coordinate_animate_graphics_options, 

318 coordinate_figure_placement, 

319 figure_save_names = [coordinate_figure_save_name], 

320 latex_root = latex_root, 

321 animation_style = animation_style, 

322 animation_frames = animation_frames, 

323 animation_frame_rate = animation_frame_rate)) 

324 

325 if include_name is not None: 

326 with open(include_name,'w') as f: 

327 f.write('\n\n'.join(latex_string)) 

328 

329 return latex_string 

330 

331def create_data_quality_summary( 

332 reference_autospectra_figure = None, 

333 drive_point_frfs_figure = None, 

334 reciprocal_frfs_figure = None, 

335 frf_coherence_figure = None, 

336 coherence_figure = None, 

337 reference_autospectra_figure_label = 'fig:reference_autospectra', 

338 reference_autospectra_figure_caption = 'Autospectra of the reference channels', 

339 reference_autospectra_graphics_options = r'width=0.7\linewidth', 

340 reference_autospectra_figure_placement = '', 

341 reference_autospectra_subfigure_options = r'[t]{0.45\linewidth}', 

342 reference_autospectra_subfigure_labels = None, 

343 reference_autospectra_subfigure_captions = None, 

344 drive_point_frfs_figure_label = 'fig:drive_point_frf', 

345 drive_point_frfs_figure_caption = 'Drive point frequency response functions', 

346 drive_point_frfs_graphics_options = r'width=0.7\linewidth', 

347 drive_point_frfs_figure_placement = '', 

348 drive_point_frfs_subfigure_options = r'[t]{0.45\linewidth}', 

349 drive_point_frfs_subfigure_labels = None, 

350 drive_point_frfs_subfigure_captions = None, 

351 reciprocal_frfs_figure_label = 'fig:reciprocal_frfs', 

352 reciprocal_frfs_figure_caption = 'Reciprocal frequency response functions.', 

353 reciprocal_frfs_graphics_options = r'width=0.7\linewidth', 

354 reciprocal_frfs_figure_placement = '', 

355 reciprocal_frfs_subfigure_options = r'[t]{0.45\linewidth}', 

356 reciprocal_frfs_subfigure_labels = None, 

357 reciprocal_frfs_subfigure_captions = None, 

358 frf_coherence_figure_label = 'fig:frf_coherence', 

359 frf_coherence_figure_caption = 'Drive point frequency response functions with coherence overlaid', 

360 frf_coherence_graphics_options = r'width=0.7\linewidth', 

361 frf_coherence_figure_placement = '', 

362 frf_coherence_subfigure_options = r'[t]{0.45\linewidth}', 

363 frf_coherence_subfigure_labels = None, 

364 frf_coherence_subfigure_captions = None, 

365 coherence_figure_label = 'fig:coherence', 

366 coherence_figure_caption = 'Coherence of all channels in the test.', 

367 coherence_graphics_options = r'width=0.7\linewidth', 

368 coherence_figure_placement = '', 

369 coherence_subfigure_options = r'[t]{0.45\linewidth}', 

370 coherence_subfigure_labels = None, 

371 coherence_subfigure_captions = None, 

372 max_subfigures_per_page = None, 

373 max_subfigures_first_page = None, 

374 latex_root = r'', 

375 figure_root = None, 

376 include_name = None, 

377 reference_autospectra_figure_save_names = None, 

378 drive_point_frfs_figure_save_names = None, 

379 reciprocal_frfs_figure_save_names = None, 

380 frf_coherence_figure_save_names = None, 

381 coherence_figure_save_names = None, 

382 ): 

383 

384 latex_string = [] 

385 

386 if reference_autospectra_figure is not None: 

387 latex_string.append( 

388 f'Figure \\ref{{{reference_autospectra_figure_label:}}} shows the autospectra of the ' 

389 'reference channels in the test. ') 

390 

391 if reference_autospectra_figure_save_names is None: 

392 if figure_root is None: 

393 reference_autospectra_figure_save_names = os.path.join(latex_root,'reference_autospectra_{:}') 

394 else: 

395 reference_autospectra_figure_save_names = os.path.join(figure_root,'reference_autospectra_{:}') 

396 

397 latex_string.append(figure( 

398 figures = [reference_autospectra_figure], 

399 figure_label = reference_autospectra_figure_label, 

400 figure_caption = reference_autospectra_figure_caption, 

401 graphics_options = reference_autospectra_graphics_options, 

402 figure_placement = reference_autospectra_figure_placement, 

403 subfigure_options = reference_autospectra_subfigure_options, 

404 subfigure_labels = reference_autospectra_subfigure_labels, 

405 subfigure_captions = reference_autospectra_subfigure_captions, 

406 max_subfigures_per_page = max_subfigures_per_page, 

407 max_subfigures_first_page = max_subfigures_first_page, 

408 figure_save_names = reference_autospectra_figure_save_names, 

409 latex_root = latex_root)) 

410 

411 if drive_point_frfs_figure is not None: 

412 latex_string.append( 

413 f'Figure \\ref{{{drive_point_frfs_figure_label:}}} shows the imaginary part ' 

414 'of the drive point frequency response functions.') 

415 

416 if drive_point_frfs_figure_save_names is None: 

417 if figure_root is None: 

418 drive_point_frfs_figure_save_names = os.path.join(latex_root,'drive_point_frf_{:}') 

419 else: 

420 drive_point_frfs_figure_save_names = os.path.join(figure_root,'drive_point_frf_{:}') 

421 

422 latex_string.append(figure( 

423 figures = [drive_point_frfs_figure], 

424 figure_label = drive_point_frfs_figure_label, 

425 figure_caption = drive_point_frfs_figure_caption, 

426 graphics_options = drive_point_frfs_graphics_options, 

427 figure_placement = drive_point_frfs_figure_placement, 

428 subfigure_options = drive_point_frfs_subfigure_options, 

429 subfigure_labels = drive_point_frfs_subfigure_labels, 

430 subfigure_captions = drive_point_frfs_subfigure_captions, 

431 max_subfigures_per_page = max_subfigures_per_page, 

432 max_subfigures_first_page = max_subfigures_first_page, 

433 figure_save_names = drive_point_frfs_figure_save_names, 

434 latex_root = latex_root)) 

435 

436 if reciprocal_frfs_figure is not None: 

437 latex_string.append( 

438 f'Figure \\ref{{{reciprocal_frfs_figure_label:}}} shows the reciprocal frequency response functions in the test.') 

439 

440 if reciprocal_frfs_figure_save_names is None: 

441 if figure_root is None: 

442 reciprocal_frfs_figure_save_names = os.path.join(latex_root,'reciprocal_frfs_{:}') 

443 else: 

444 reciprocal_frfs_figure_save_names = os.path.join(figure_root,'reciprocal_frfs_{:}') 

445 

446 latex_string.append(figure( 

447 figures = [reciprocal_frfs_figure], 

448 figure_label = reciprocal_frfs_figure_label, 

449 figure_caption = reciprocal_frfs_figure_caption, 

450 graphics_options = reciprocal_frfs_graphics_options, 

451 figure_placement = reciprocal_frfs_figure_placement, 

452 subfigure_options = reciprocal_frfs_subfigure_options, 

453 subfigure_labels = reciprocal_frfs_subfigure_labels, 

454 subfigure_captions = reciprocal_frfs_subfigure_captions, 

455 max_subfigures_per_page = max_subfigures_per_page, 

456 max_subfigures_first_page = max_subfigures_first_page, 

457 figure_save_names = reciprocal_frfs_figure_save_names, 

458 latex_root = latex_root)) 

459 

460 if frf_coherence_figure is not None: 

461 latex_string.append( 

462 f'Figure \\ref{{{frf_coherence_figure_label:}}} shows the coherence overlaying the drive point frequency response functions.') 

463 

464 if frf_coherence_figure_save_names is None: 

465 if figure_root is None: 

466 frf_coherence_figure_save_names = os.path.join(latex_root,'frf_coherence_{:}') 

467 else: 

468 frf_coherence_figure_save_names = os.path.join(figure_root,'frf_coherence_{:}') 

469 

470 latex_string.append(figure( 

471 figures = [frf_coherence_figure], 

472 figure_label = frf_coherence_figure_label, 

473 figure_caption = frf_coherence_figure_caption, 

474 graphics_options = frf_coherence_graphics_options, 

475 figure_placement = frf_coherence_figure_placement, 

476 subfigure_options = frf_coherence_subfigure_options, 

477 subfigure_labels = frf_coherence_subfigure_labels, 

478 subfigure_captions = frf_coherence_subfigure_captions, 

479 max_subfigures_per_page = max_subfigures_per_page, 

480 max_subfigures_first_page = max_subfigures_first_page, 

481 figure_save_names = frf_coherence_figure_save_names, 

482 latex_root = latex_root)) 

483 

484 if coherence_figure is not None: 

485 latex_string.append( 

486 f'Figure \\ref{{{coherence_figure_label:}}} shows the coherence of the ' 

487 'response channels in the test. ') 

488 

489 if coherence_figure_save_names is None: 

490 if figure_root is None: 

491 coherence_figure_save_names = os.path.join(latex_root,'coherence_{:}') 

492 else: 

493 coherence_figure_save_names = os.path.join(figure_root,'coherence_{:}') 

494 

495 latex_string.append(figure( 

496 figures = [coherence_figure], 

497 figure_label = coherence_figure_label, 

498 figure_caption = coherence_figure_caption, 

499 graphics_options = coherence_graphics_options, 

500 figure_placement = coherence_figure_placement, 

501 subfigure_options = coherence_subfigure_options, 

502 subfigure_labels = coherence_subfigure_labels, 

503 subfigure_captions = coherence_subfigure_captions, 

504 max_subfigures_per_page = max_subfigures_per_page, 

505 max_subfigures_first_page = max_subfigures_first_page, 

506 figure_save_names = coherence_figure_save_names, 

507 latex_root = latex_root)) 

508 

509 if include_name is not None: 

510 with open(include_name,'w') as f: 

511 f.write('\n\n'.join(latex_string)) 

512 

513 return latex_string 

514 

515def create_mode_fitting_summary( 

516 # General information from the curve fitter 

517 fit_modes_information = None, 

518 # Information about the modes 

519 fit_modes = None, 

520 fit_modes_table = None, 

521 fit_mode_table_kwargs = {}, 

522 mac_figure = None, 

523 mac_plot_kwargs = None, 

524 # Information to create resynthesis plots 

525 experimental_frfs = None, 

526 resynthesized_frfs = None, 

527 resynthesis_comparison = 'cmif', 

528 resynthesis_figure = None, 

529 resynthesis_plot_kwargs = None, 

530 # Path options 

531 latex_root = r'', 

532 figure_root = None, 

533 fit_mode_information_save_names = None, 

534 mac_plot_save_name = None, 

535 resynthesis_plot_save_name = None, 

536 include_name = None, 

537 # Latex formatting information 

538 fit_modes_information_table_justification_string = None, 

539 fit_modes_information_table_longtable = True, 

540 fit_modes_information_table_header = True, 

541 fit_modes_information_table_horizontal_lines = False, 

542 fit_modes_information_table_placement = '', 

543 fit_modes_information_figure_graphics_options = r'width=0.7\linewidth', 

544 fit_modes_information_figure_placement = '', 

545 fit_modes_table_justification_string = None, 

546 fit_modes_table_label = 'tab:mode_fits', 

547 fit_modes_table_caption = 'Modal parameters fit to the test data.', 

548 fit_modes_table_longtable = True, 

549 fit_modes_table_header = True, 

550 fit_modes_table_horizontal_lines = False, 

551 fit_modes_table_placement = '', 

552 fit_modes_table_header_override = None, 

553 mac_plot_figure_label = 'fig:mac', 

554 mac_plot_figure_caption = 'Modal Assurance Criterion Matrix from Fit Modes', 

555 mac_plot_graphics_options = r'width=0.7\linewidth', 

556 mac_plot_figure_placement = '', 

557 resynthesis_plot_figure_label = 'fig:resynthesis', 

558 resynthesis_plot_figure_caption = 'Test data compared to equivalent data computed from modal fits.', 

559 resynthesis_plot_graphics_options = r'width=0.7\linewidth', 

560 resynthesis_plot_figure_placement = '', 

561 ): 

562 latex_string = [] 

563 if not fit_modes_information is None: 

564 figure_keys = [key for key in fit_modes_information.keys() if (key[:6].lower() == 'figure') and (key[-7:].lower() != 'caption')] 

565 table_keys = [key for key in fit_modes_information.keys() if key[:5].lower() == 'table' and key[-7:].lower() != 'caption'] 

566 fig_format_kwargs = {key+'ref':'\\ref{{fig:mode_fitting_{:}}}'.format(i) for i,key in enumerate(figure_keys)} 

567 table_format_kwargs = {key+'ref':'\\ref{{tab:mode_fitting_{:}}}'.format(i) for i,key in enumerate(table_keys)} 

568 text = '\n\n'.join([v for v in fit_modes_information['text']]).format(**fig_format_kwargs, **table_format_kwargs) 

569 latex_string.append(text) 

570 for i,key in enumerate(figure_keys): 

571 reference = 'fig:mode_fitting_{:}'.format(i) 

572 if isinstance(fit_modes_information_figure_graphics_options,dict): 

573 graphics_options = fit_modes_information_figure_graphics_options[key] 

574 else: 

575 graphics_options = fit_modes_information_figure_graphics_options 

576 if fit_mode_information_save_names is None: 

577 if figure_root is None: 

578 save_name = os.path.join(latex_root,'fit_mode_figure_{:}'.format(i)) 

579 else: 

580 save_name = os.path.join(figure_root,'fit_mode_figure_{:}'.format(i)) 

581 else: 

582 save_name = fit_mode_information_save_names.format(i) 

583 fig = figure([fit_modes_information[key]], 

584 reference, 

585 fit_modes_information[key+'caption'], 

586 graphics_options, 

587 figure_save_names=[save_name], 

588 latex_root = latex_root, 

589 ) 

590 latex_string.append(fig) 

591 

592 if fit_modes is not None: 

593 latex_string.append(f'Table \\ref{{{fit_modes_table_label:}}} shows the modal parameters fit to the test data.') 

594 

595 if fit_modes_table is None: 

596 fit_modes_table = fit_modes.mode_table('pandas') 

597 

598 if fit_modes_table_header_override is not None: 

599 if isinstance(fit_modes_table,pd.DataFrame): 

600 fit_modes_table = fit_modes_table.rename(columns=fit_modes_table_header_override) 

601 else: 

602 fit_modes_table[0] = [val if not val in fit_modes_table_header_override else 

603 fit_modes_table_header_override[val] for val in fit_modes_table[0]] 

604 

605 latex_string.append(table(fit_modes_table, 

606 fit_modes_table_justification_string, 

607 fit_modes_table_label, 

608 fit_modes_table_caption, 

609 fit_modes_table_longtable, 

610 fit_modes_table_header, 

611 fit_modes_table_horizontal_lines, 

612 fit_modes_table_placement)) 

613 

614 if (mac_figure is None) and (fit_modes is not None): 

615 # Create the MAC from fit modes 

616 mac = shape_mac(fit_modes) 

617 if mac_plot_kwargs is None: 

618 mac_plot_kwargs = {} 

619 ax = matrix_plot(mac,**mac_plot_kwargs) 

620 mac_figure = ax.figure 

621 

622 if mac_figure is not None: 

623 latex_string.append( 

624f'Figure \\ref{{{mac_plot_figure_label:}}} shows the Modal Assurance Criterion Matrix,' 

625'which is a measure of how similar each mode shape looks to all the other mode shapes.') 

626 

627 if mac_plot_save_name is None: 

628 if figure_root is None: 

629 mac_plot_save_name = os.path.join(latex_root,'mac') 

630 else: 

631 mac_plot_save_name = os.path.join(figure_root,'mac') 

632 

633 latex_string.append(figure([mac_figure], 

634 mac_plot_figure_label, 

635 'Modal Assurance Criterion Matrix of the fit mode shapes.', 

636 mac_plot_graphics_options, 

637 figure_placement = mac_plot_figure_placement, 

638 figure_save_names = [mac_plot_save_name], 

639 latex_root = latex_root)) 

640 

641 if (resynthesis_figure is None) and (experimental_frfs is not None) and (resynthesized_frfs is not None): 

642 if resynthesis_plot_kwargs is None: 

643 resynthesis_plot_kwargs = {} 

644 max_abscissa = np.min([np.max(experimental_frfs.abscissa), 

645 np.max(resynthesized_frfs.abscissa)]) 

646 min_abscissa = np.max([np.min(experimental_frfs.abscissa), 

647 np.min(resynthesized_frfs.abscissa)]) 

648 abscissa_range = max_abscissa - min_abscissa 

649 max_abscissa += abscissa_range/20 

650 min_abscissa -= abscissa_range/20 

651 if resynthesis_comparison == 'cmif': 

652 ds1 = experimental_frfs.compute_cmif() 

653 ds2 = resynthesized_frfs.compute_cmif() 

654 resynthesis_figure, ax = plt.subplots() 

655 kwargs1 = resynthesis_plot_kwargs.copy() 

656 kwargs2 = resynthesis_plot_kwargs.copy() 

657 kwargs1['color'] = 'b' 

658 kwargs2['color'] = 'r' 

659 ds1.plot(ax, plot_kwargs = kwargs1) 

660 ds2.plot(ax, plot_kwargs = kwargs2) 

661 ax.set_yscale('log') 

662 ax.set_xlabel('Frequency (Hz)') 

663 ax.set_ylabel('CMIF') 

664 ax.set_xlim([min_abscissa,max_abscissa]) 

665 elif resynthesis_comparison == 'qmif': 

666 ds1 = experimental_frfs.compute_cmif(part='imag') 

667 ds2 = resynthesized_frfs.compute_cmif(part='imag') 

668 resynthesis_figure, ax = plt.subplots() 

669 kwargs1 = resynthesis_plot_kwargs.copy() 

670 kwargs2 = resynthesis_plot_kwargs.copy() 

671 kwargs1['color'] = 'b' 

672 kwargs2['color'] = 'r' 

673 ds1.plot(ax, plot_kwargs = kwargs1) 

674 ds2.plot(ax, plot_kwargs = kwargs2) 

675 ax.set_yscale('log') 

676 ax.set_xlabel('Frequency (Hz)') 

677 ax.set_ylabel('QMIF') 

678 ax.set_xlim([min_abscissa,max_abscissa]) 

679 elif resynthesis_comparison == 'mmif': 

680 ds1 = experimental_frfs.compute_mmif() 

681 ds2 = resynthesized_frfs.compute_mmif() 

682 resynthesis_figure, ax = plt.subplots() 

683 kwargs1 = resynthesis_plot_kwargs.copy() 

684 kwargs2 = resynthesis_plot_kwargs.copy() 

685 kwargs1['color'] = 'b' 

686 kwargs2['color'] = 'r' 

687 ds1.plot(ax, plot_kwargs = kwargs1) 

688 ds2.plot(ax, plot_kwargs = kwargs2) 

689 ax.set_xlabel('Frequency (Hz)') 

690 ax.set_ylabel('MMIF') 

691 ax.set_xlim([min_abscissa,max_abscissa]) 

692 elif resynthesis_comparison == 'nmif': 

693 ds1 = experimental_frfs.compute_nmif() 

694 ds2 = resynthesized_frfs.compute_nmif() 

695 resynthesis_figure, ax = plt.subplots() 

696 kwargs1 = resynthesis_plot_kwargs.copy() 

697 kwargs2 = resynthesis_plot_kwargs.copy() 

698 kwargs1['color'] = 'b' 

699 kwargs2['color'] = 'r' 

700 ds1.plot(ax, plot_kwargs = kwargs1) 

701 ds2.plot(ax, plot_kwargs = kwargs2) 

702 ax.set_xlabel('Frequency (Hz)') 

703 ax.set_ylabel('MMIF') 

704 ax.set_xlim([min_abscissa,max_abscissa]) 

705 elif resynthesis_comparison == 'frf': 

706 ds1 = experimental_frfs 

707 ds2 = resynthesized_frfs 

708 kwargs1 = resynthesis_plot_kwargs.copy() 

709 kwargs2 = resynthesis_plot_kwargs.copy() 

710 kwargs1['color'] = 'b' 

711 kwargs2['color'] = 'r' 

712 ax = ds1.plot(False, plot_kwargs = kwargs1) 

713 for a,frf in zip(ax.flaten(),ds2[ds1.coordinate].flatten()): 

714 frf.plot(a, plot_kwargs = kwargs2) 

715 a.set_xlim([min_abscissa,max_abscissa]) 

716 resynthesis_figure = ax.flatten()[0].figure 

717 

718 if resynthesis_figure is not None: 

719 latex_string.append( 

720f'To judge the adequacy of the fit modes, Figure \\ref{{{resynthesis_plot_figure_label:}}} shows the data resynthesized from the ' 

721'fit modes compared to the equivalent experimental data.') 

722 

723 if resynthesis_plot_save_name is None: 

724 if figure_root is None: 

725 resynthesis_plot_save_name = os.path.join(latex_root,'resynthesis') 

726 else: 

727 resynthesis_plot_save_name = os.path.join(figure_root,'resynthesis') 

728 

729 latex_string.append(figure([resynthesis_figure], 

730 resynthesis_plot_figure_label, 

731 'Experimental data compared to that resynthesized from the fit modes.', 

732 resynthesis_plot_graphics_options, 

733 figure_placement = resynthesis_plot_figure_placement, 

734 figure_save_names = [resynthesis_plot_save_name], 

735 latex_root = latex_root)) 

736 

737 if include_name is not None: 

738 with open(include_name,'w') as f: 

739 f.write('\n\n'.join(latex_string)) 

740 

741 return latex_string 

742 

743def create_mode_shape_figures( 

744 geometry, shapes, figure_label = 'fig:modeshapes', 

745 figure_caption = 'Mode shapes extracted from test data.', 

746 graphics_options = r'width=0.7\linewidth', 

747 animate_graphics_options = r'width=0.7\linewidth,loop', 

748 figure_placement = '', 

749 subfigure_options = r'[t]{0.45\linewidth}', subfigure_labels = None, 

750 subfigure_captions = None, max_subfigures_per_page = None, 

751 max_subfigures_first_page = None, figure_save_names = None, 

752 latex_root = r'', 

753 figure_root = None, 

754 animation_style = None, 

755 animation_frames = 20, 

756 animation_frame_rate = 20, 

757 geometry_plot_shape_kwargs = {}, 

758 include_name = None): 

759 latex_string = ['Figure \\ref{{{:}}} shows the mode shapes extracted from the test.'.format(figure_label)] 

760 

761 if subfigure_captions is None: 

762 if isinstance(shapes,ShapePlotter): 

763 subfigure_captions = ['Mode {:} at {:0.2f} Hz with {:0.2f}\\% damping'.format( 

764 i+1,mode.frequency,mode.damping*100) for i,mode in enumerate(shapes.shapes)] 

765 else: 

766 subfigure_captions = ['Mode {:} at {:0.2f} Hz with {:0.2f}\\% damping'.format( 

767 i+1,mode.frequency,mode.damping*100) for i,mode in enumerate(shapes)] 

768 

769 if figure_save_names is None: 

770 if figure_root is None: 

771 figure_save_names = os.path.join(latex_root, 'modeshape_{:}') 

772 else: 

773 figure_save_names = os.path.join(figure_root, 'modeshape_{:}') 

774 

775 if animation_style == 'one3d': 

776 animation_style = '3d' 

777 shapes = [shapes] 

778 

779 latex_string.append( 

780 figure(shapes,figure_label,figure_caption,graphics_options, 

781 animate_graphics_options, figure_placement, subfigure_options, 

782 subfigure_labels, subfigure_captions, max_subfigures_per_page, 

783 max_subfigures_first_page, 

784 figure_save_names, latex_root, animation_style, animation_frames, 

785 animation_frame_rate, geometry, geometry_plot_shape_kwargs)) 

786 

787 if include_name is not None: 

788 with open(include_name,'w') as f: 

789 f.write('\n\n'.join(latex_string)) 

790 

791 return latex_string 

792 

793def create_rigid_body_analysis( 

794 geometry, rigid_shapes, 

795 complex_plane_figures = None, 

796 residual_figure = None, 

797 figure_label = 'fig:rigid_shapes', 

798 complex_plane_figure_label = 'fig:complex_plane', 

799 residual_figure_label = 'fig:rigid_shape_residual', 

800 figure_caption = 'Rigid body shapes extracted from test data.', 

801 complex_plane_caption = 'Complex Plane of the extracted shapes.', 

802 residual_caption = 'Rigid body residual showing non-rigid portions of the shapes.', 

803 graphics_options = r'width=0.7\linewidth', 

804 complex_plane_graphics_options = r'width=0.7\linewidth', 

805 residual_graphics_options = r'width=0.7\linewidth', 

806 animate_graphics_options = r'width=0.7\linewidth,loop', 

807 figure_placement = '', 

808 complex_plane_figure_placement = '', 

809 residual_figure_placement = '', 

810 subfigure_options = r'[t]{0.45\linewidth}', subfigure_labels = None, 

811 subfigure_captions = None, 

812 complex_plane_subfigure_options = r'[t]{0.45\linewidth}', 

813 complex_plane_subfigure_labels = None, 

814 max_subfigures_per_page = None, 

815 max_subfigures_first_page = None, 

816 figure_save_names = None, 

817 complex_plane_figure_save_names = None, 

818 residual_figure_save_names = None, 

819 latex_root = r'', 

820 figure_root = None, 

821 animation_style = None, 

822 animation_frames = 20, 

823 animation_frame_rate = 20, 

824 geometry_plot_shape_kwargs = {}, 

825 rigid_body_check_kwargs = {}, 

826 include_name = None 

827 ): 

828 

829 latex_string = [ 

830 f'Figure \\ref{{{figure_label:}}} shows the rigid shapes extracted from the ' 

831 'rigid body analysis. This analysis is performed to ensure all ' 

832 'sensors are installed and documented correctly in the channel table. ' 

833 'If a sensor had the wrong sensitivity, had its polarity flipped, or ' 

834 'if cables were plugged in incorrectly, that sensor would not be ' 

835 'moving rigidly with the rest of the test article.'] 

836 

837 if subfigure_captions is None: 

838 if isinstance(rigid_shapes,ShapePlotter): 

839 subfigure_captions = ['Rigid body shape {:}'.format( 

840 i+1) for i,mode in enumerate(rigid_shapes.shapes)] 

841 else: 

842 subfigure_captions = ['Rigid body shape {:}'.format( 

843 i+1) for i,mode in enumerate(rigid_shapes)] 

844 

845 if figure_save_names is None: 

846 if figure_root is None: 

847 figure_save_names = os.path.join(latex_root, 'rigid_shape_{:}') 

848 else: 

849 figure_save_names = os.path.join(figure_root, 'rigid_shape_{:}') 

850 

851 latex_string.append( 

852 figure(rigid_shapes,figure_label,figure_caption,graphics_options, 

853 animate_graphics_options, figure_placement, subfigure_options, 

854 subfigure_labels, subfigure_captions, max_subfigures_per_page, 

855 max_subfigures_first_page, 

856 figure_save_names, latex_root, animation_style, animation_frames, 

857 animation_frame_rate, geometry, geometry_plot_shape_kwargs)) 

858 

859 if complex_plane_figure_save_names is None: 

860 if figure_root is None: 

861 complex_plane_figure_save_names = os.path.join(latex_root, 'rigid_complex_plane_{:}') 

862 else: 

863 complex_plane_figure_save_names = os.path.join(figure_root, 'rigid_complex_plane_{:}') 

864 

865 if residual_figure_save_names is None: 

866 if figure_root is None: 

867 residual_figure_save_names = os.path.join(latex_root, 'rigid_residual') 

868 else: 

869 residual_figure_save_names = os.path.join(figure_root, 'rigid_residual') 

870 

871 if complex_plane_figures is None or residual_figure is None: 

872 rigid_body_kwargs = rigid_body_check_kwargs.copy() 

873 rigid_body_kwargs['return_figures'] = True 

874 out = rigid_body_check(geometry, rigid_shapes, **rigid_body_kwargs) 

875 if complex_plane_figures is None: 

876 complex_plane_figures = out[-(len(rigid_shapes)+1):-1] 

877 if residual_figure is None: 

878 residual_figure = out[-1] 

879 

880 latex_string.append(( 

881 'A more quantitiative analysis of the rigid body shapes is shown in ' 

882 f'Figure \\ref{{{complex_plane_figure_label:}}} and \\ref{{{residual_figure_label:}}}. ' 

883 f'Figure \\ref{{{complex_plane_figure_label:}}} shows the complex plane ' 

884 'of the shape, which should look like a line through the origin. ' 

885 f'Figure \\ref{{{residual_figure_label:}}} shows the shape residuals, ' 

886 'which are the remaining shape coefficient when the rigid portion of ' 

887 'the motion is subtracted away. The residual is then the remaining ' 

888 'non-rigid portion of the motion, so large residual suggest issues with ' 

889 'that channel.')) 

890 

891 latex_string.append(figure( 

892 list(complex_plane_figures),complex_plane_figure_label, 

893 complex_plane_caption, complex_plane_graphics_options, 

894 animate_graphics_options, complex_plane_figure_placement, 

895 complex_plane_subfigure_options, 

896 complex_plane_subfigure_labels, 

897 rigid_shapes.shapes.comment1 if isinstance(rigid_shapes,ShapePlotter) else rigid_shapes.comment1, 

898 max_subfigures_per_page, 

899 max_subfigures_first_page, 

900 complex_plane_figure_save_names, latex_root)) 

901 

902 latex_string.append(figure( 

903 [residual_figure],residual_figure_label, 

904 residual_caption, residual_graphics_options, 

905 animate_graphics_options, residual_figure_placement, 

906 figure_save_names = residual_figure_save_names, 

907 latex_root = latex_root)) 

908 

909 if include_name is not None: 

910 with open(include_name,'w') as f: 

911 f.write('\n\n'.join(latex_string)) 

912 

913 return latex_string 

914 

915def figure(figures, figure_label = None, figure_caption = None, 

916 graphics_options = r'width=0.7\linewidth', 

917 animate_graphics_options = r'width=0.7\linewidth,loop', 

918 figure_placement = '', 

919 subfigure_options = r'[t]{0.45\linewidth}', subfigure_labels = None, 

920 subfigure_captions = None, max_subfigures_per_page = None, 

921 max_subfigures_first_page = None, figure_save_names = None, 

922 latex_root = r'', 

923 animation_style = None, 

924 animation_frames = 20, 

925 animation_frame_rate = 20, 

926 geometry = None, 

927 geometry_plot_shape_kwargs = {} 

928 ): 

929 r""" 

930 Adds figures, subfigures, and animations to a running latex document. 

931 

932 Parameters 

933 ---------- 

934 figures : list 

935 Figure or figures that can be inserted into a latex document. See  

936 note for various figure types and configurations that can be used. 

937 figure_label : str, optional 

938 The label that will be used for the figure in the latex document. 

939 If not specified, the figure will not be labeled. 

940 figure_caption : str, optional 

941 The caption that will be used for the figure in the latex document. 

942 If not specified, the figure will only be captioned with the figure 

943 number. 

944 graphics_options : str, optional 

945 Graphics options that will be used for the figure. If not specified 

946 this will be r'width=0.7\linewidth'. 

947 animate_graphics_options : str, optional 

948 Graphics options that will be used for an animation. If not specified 

949 this will be r'width=0.7\linewidth,loop'. 

950 figure_placement : str, optional 

951 Specify the placement of the figure with strings such as '[t]', '[b]', 

952 or '[h]'. If not specified, the figure will have no placement 

953 specified. 

954 subfigure_options : str, optional 

955 The options that will be applied to each subfigure in the figure, if 

956 subfigures are specified. By default, this will be r'[t]{0.45\linewidth}' 

957 subfigure_labels : str, optional 

958 Labels to apply to the subfigure. This can either be a list of strings 

959 the same size as the list of figures, or a string with a format specifier 

960 accepting the subfigure index. If not specified, the subfigures will 

961 not be labeled. 

962 subfigure_captions : list, optional 

963 A list of strings the same length as the list of figures to use as 

964 captions for the subfigures. If not specified, the subfigures will 

965 only be captioned with the subfigure number. 

966 max_subfigures_per_page : int, optional 

967 The maximum number of subfigures on a page. Longer figures will be 

968 broken up into multiple figures using \ContinuedFloat. If not specified, 

969 a single figure environment will be generated. 

970 max_subfigures_first_page : int, optional 

971 The maximum number of subfigures on the first page. Longer figures will be 

972 broken up into multiple figures using \ContinuedFloat. If not specified, 

973 the max_subfigures_per_page value will be used if specified, otherwise 

974 a single figure environment will be generated. 

975 figure_save_names : str or list of str, optional 

976 File names to save the figures as. This can be specified as a string 

977 with a format specifier in it that will accept the figure index, or 

978 a list of strings the same length as the list of figures. 

979 If not specified, files will be specified as 'figure_0', 

980 'figure_1', etc. If file names are not present, then the file name 

981 will be automatically selected for the type of figure given. 

982 latex_root : str, optional 

983 Directory in which the latex .tex file will be constructed. This is 

984 used to create relative paths to the save_figure_names within the latex 

985 document. If not specified, then the current directory will be assumed. 

986 animation_style : str, optional 

987 If a GeometryPlotter or ShapePlotter object is passed, this argument 

988 will determine what is saved from it. To save just a screen shot of 

989 the plotter, use `animation_style = None` or `animation_style = 'none'. 

990 To save an animated 2D figure, use `animation_style= '2d'`. To save 

991 an animated 3D figure, use `animation_style = '3d'`. If not specified, 

992 a screenshot will be saved. 

993 animation_frames : int 

994 If a GeometryPlotter or ShapePlotter object is passed with a 2D 

995 `animation_style`, this argument will determine how many frames are 

996 rendered. 

997 animation_frame_rate : int 

998 This is the frame rate used in the animation. 

999 geometry : Geometry, optional 

1000 If a ShapeArray is passed as a figure type, then a geometry must also 

1001 be specified to define how the shape should be plotted. 

1002 geometry_plot_shape_kwargs : dict, optional 

1003 If a ShapeArray and Geometry are passed, then this is a dictionary of 

1004 keyword arguments into the Geometry.plot_shape function in 

1005 sdynpy.pdf3D. 

1006  

1007  

1008 Returns 

1009 ------- 

1010 latex_string : str 

1011 The latex source code to insert the figures into the document. 

1012 

1013 Notes 

1014 ----- 

1015 The `figures` argument must be a list of figures. If only one entry 

1016 is present in the list, a figure will be made in the latex document. If 

1017 multiple entries are present, a figure will be made and subfigures will be 

1018 made for each entry in the list. If an entry in the list is also a list, 

1019 then that figure or subfigure will be made into an animation. 

1020  

1021 The list of figures can contain many types of objects that a figure will be 

1022 made from, including: 

1023 - A 2D numpy array 

1024 - A Matplotlib figure 

1025 - A pyqtgraph plotitem 

1026 - A bytes object that represents an image 

1027 - A string to a file name 

1028 - A GeometryPlotter containing a geometry 

1029 - A ShapePlotter containing a mode shape 

1030 - A ShapeArray object containing a mode 

1031 """ 

1032 

1033 

1034 if ((isinstance(figures,ShapePlotter) and len(figures.shapes) == 1) or 

1035 (not isinstance(figures,ShapePlotter) and len(figures) == 1)): 

1036 subfigures = False 

1037 else: 

1038 subfigures = True 

1039 

1040 # If it's a shapeplotter, we need to break it out into the multiple shapes 

1041 # but keep track of the iterable 

1042 if isinstance(figures,ShapePlotter): 

1043 shapeplotter = figures 

1044 figures = [i for i in shapeplotter.shapes] 

1045 else: 

1046 shapeplotter = None 

1047 

1048 if max_subfigures_first_page is None: 

1049 max_subfigures_first_page = max_subfigures_per_page 

1050 

1051 if figure_save_names is None: 

1052 figure_save_names = ['figure_{:}'.format(i) for i in range(len(figures))] 

1053 elif isinstance(figure_save_names,str): 

1054 figure_save_names = [figure_save_names.format(i) for i in range(len(figures))] 

1055 

1056 latex_string = r'\begin{figure}'+figure_placement+'\n \\centering' 

1057 # Go through and save all of the files out to disk 

1058 for i,figure in enumerate(figures): 

1059 # Check the type of figure. If it's a list of figures, then it's an 

1060 # animation. 

1061 if isinstance(figure, list): 

1062 animate = True 

1063 num_frames = len(list) 

1064 # Otherwise it's just a figure, but we turn it into a list anyways to 

1065 # make it so we only have to program this once. 

1066 else: 

1067 animate = False 

1068 num_frames = 1 

1069 figure = [figure] 

1070 # We need to get the extension of the file name to figure out what 

1071 # type of file to save the image to. 

1072 base,ext = os.path.splitext(figure_save_names[i]) 

1073 # We also want to get the directory so we can get the relative path to 

1074 # the file 

1075 relpath = os.path.relpath(base,latex_root).replace('\\','/') 

1076 for j,this_figure in enumerate(figure): 

1077 pdf3d = False 

1078 if animate: 

1079 this_filename = base+'_{:}'.format(j)+ext 

1080 relpath += '_' 

1081 else: 

1082 this_filename = figure_save_names[i] 

1083 # Matplotlib Figure 

1084 if isinstance(this_figure,plt.Figure): 

1085 if ext == '': 

1086 this_filename += '.pdf' 

1087 this_figure.savefig(this_filename) 

1088 # Pyqtgraph PlotItem 

1089 elif isinstance(this_figure,pqtg.PlotItem): 

1090 if ext == '': 

1091 this_filename += '.png' 

1092 this_figure.writeImage(this_filename) 

1093 # ShapePlotter 

1094 elif isinstance(this_figure, ShapePlotter): 

1095 if ext == '': 

1096 ext = '.png' 

1097 this_filename += ext 

1098 if animation_style is None or animation_style.lower() == 'none': 

1099 PIL.Image.fromarray(this_figure.screenshot()).save(this_filename) 

1100 elif animation_style.lower() == '2d': 

1101 this_figure.save_animation(this_filename,frames=animation_frames, 

1102 frame_rate = animation_frame_rate, 

1103 individual_images = True) 

1104 animate = True 

1105 relpath += '-' 

1106 num_frames = animation_frames 

1107 elif animation_style.lower() == '3d': 

1108 raise ValueError('3D Animation is not supported for ShapePlotter. Pass ShapeArray for interactive mode shape plots.') 

1109 else: 

1110 raise ValueError('Invalid animation_style. Must be one of None, "2d", or "3d".') 

1111 # ShapeArray 

1112 elif isinstance(this_figure,ShapeArray): 

1113 if geometry is None: 

1114 raise ValueError('If a ShapeArray is passed as a figure, a Geometry must also be passed to the geometry argument.') 

1115 if ext == '': 

1116 ext = '.png' 

1117 this_filename += ext 

1118 plotter = geometry.plot_shape(this_figure,**geometry_plot_shape_kwargs) 

1119 if animation_style is None or animation_style.lower() == 'none': 

1120 PIL.Image.fromarray(plotter.screenshot()).save(this_filename) 

1121 elif animation_style.lower() == '2d': 

1122 plotter.save_animation(this_filename,frames=animation_frames, 

1123 frame_rate = animation_frame_rate, 

1124 individual_images = True) 

1125 animate = True 

1126 relpath += '-' 

1127 num_frames = animation_frames 

1128 elif animation_style.lower() == '3d': 

1129 if vtkU3DExporter is None: 

1130 raise ValueError('Cannot Import vtkU3DExporter. It must first be installed with `pip install vtk-u3dexporter`') 

1131 PIL.Image.fromarray(plotter.screenshot()).save(this_filename) 

1132 u3d_filename = this_filename.replace(ext,'.u3d') 

1133 js_filename = this_filename.replace(ext,'.js') 

1134 rel_path_u3d = os.path.relpath(u3d_filename,latex_root).replace('\\','/') 

1135 rel_path_js = os.path.relpath(js_filename,latex_root).replace('\\','/') 

1136 # Pick out appropriate arguments 

1137 kwargs = {} 

1138 try: 

1139 kwargs['node_size'] = geometry_plot_shape_kwargs['plot_kwargs']['node_size'] 

1140 except KeyError: 

1141 pass 

1142 try: 

1143 kwargs['line_width'] = geometry_plot_shape_kwargs['plot_kwargs']['line_width'] 

1144 except KeyError: 

1145 pass 

1146 try: 

1147 kwargs['opacity'] = geometry_plot_shape_kwargs['deformed_opacity'] 

1148 except KeyError: 

1149 pass 

1150 try: 

1151 kwargs['show_edges'] = geometry_plot_shape_kwargs['plot_kwargs']['show_edges'] 

1152 except KeyError: 

1153 pass 

1154 try: 

1155 kwargs['displacement_scale'] = geometry_plot_shape_kwargs['starting_scale'] 

1156 except KeyError: 

1157 pass 

1158 create_animated_modeshape_content(geometry,this_figure, 

1159 u3d_name=u3d_filename.replace('.u3d',''), 

1160 js_name = js_filename, one_js = True, 

1161 **kwargs) 

1162 pdf3d = True 

1163 pdf3d_parameters = ', '.join([key+'='+val for key,val in get_view_parameters_from_plotter(plotter).items()]) 

1164 pdf3d_parameters += ', '+graphics_options+', add3Djscript='+rel_path_js 

1165 else: 

1166 plotter.close() 

1167 raise ValueError('Invalid animation_style. Must be one of None, "2d", or "3d".') 

1168 plotter.close() 

1169 # GeometryPlotter 

1170 elif isinstance(this_figure, GeometryPlotter): 

1171 if ext == '': 

1172 ext = '.png' 

1173 this_filename += ext 

1174 if animation_style is None or animation_style.lower() == 'none': 

1175 PIL.Image.fromarray(this_figure.screenshot()).save(this_filename) 

1176 elif animation_style.lower() == '2d': 

1177 this_figure.save_rotation_animation(this_filename,frames=animation_frames, 

1178 frame_rate = animation_frame_rate, 

1179 individual_images = True) 

1180 animate = True 

1181 relpath += '-' 

1182 num_frames = animation_frames 

1183 elif animation_style.lower() == '3d': 

1184 if vtkU3DExporter is None: 

1185 raise ValueError('Cannot Import vtkU3DExporter. It must first be installed with `pip install vtk-u3dexporter`') 

1186 PIL.Image.fromarray(this_figure.screenshot()).save(this_filename) 

1187 u3d_filename = this_filename.replace(ext,'.u3d') 

1188 rel_path_u3d = os.path.relpath(u3d_filename,latex_root).replace('\\','/') 

1189 exporter = vtkU3DExporter.vtkU3DExporter() 

1190 exporter.SetFileName(u3d_filename.replace('.u3d','')) 

1191 exporter.SetInput(this_figure.render_window) 

1192 exporter.Write() 

1193 

1194 pdf3d = True 

1195 pdf3d_parameters = ', '.join([key+'='+val for key,val in get_view_parameters_from_plotter(this_figure).items()]) 

1196 pdf3d_parameters += ', '+graphics_options 

1197 else: 

1198 raise ValueError('Invalid animation_style. Must be one of None, "2d", or "3d".') 

1199 elif isinstance(this_figure, int): 

1200 if shapeplotter is not None: 

1201 shapeplotter.current_shape = j 

1202 shapeplotter.compute_displacements() 

1203 shapeplotter.update_shape_mode(0) 

1204 shapeplotter.show_comment() 

1205 QApplication.processEvents() 

1206 if animation_style is None or animation_style.lower() == 'none': 

1207 PIL.Image.fromarray(shapeplotter.screenshot()).save(this_filename) 

1208 elif animation_style.lower() == '2d': 

1209 shapeplotter.save_animation(this_filename,frames=animation_frames, 

1210 frame_rate = animation_frame_rate, 

1211 individual_images = True) 

1212 animate = True 

1213 relpath += '-' 

1214 num_frames = animation_frames 

1215 else: 

1216 raise ValueError('Bad type with integer figure.') 

1217 # Bytes object 

1218 elif isinstance(this_figure, bytes): 

1219 if ext == '': 

1220 this_filename += '.png' 

1221 PIL.Image.open(BytesIO(this_figure)).save(this_filename) 

1222 # String to file name 

1223 elif isinstance(this_figure, str): 

1224 if ext == '': 

1225 this_filename += os.path.splitext(this_figure)[-1] 

1226 copy(this_figure,this_filename) 

1227 # 2D NumpyArray 

1228 elif isinstance(this_figure, np.ndarray): 

1229 if ext == '': 

1230 this_filename += '.png' 

1231 PIL.Image.fromarray(this_figure).save(this_filename) 

1232 # Otherwise 

1233 else: 

1234 raise ValueError('Unknown Figure Type: {:}'.format(type(this_figure))) 

1235 # Now we end the figure and create a new one if we are at the right 

1236 # subfigure number 

1237 if (subfigures and max_subfigures_per_page is not None and 

1238 ((i-max_subfigures_first_page)%max_subfigures_per_page == 0 

1239 and i > 0)): 

1240 latex_string += r""" 

1241\end{figure} 

1242\begin{figure}[h] 

1243 \ContinuedFloat 

1244 \centering""" 

1245 # If we have subfigures we need to stick in the subfigure environment 

1246 if subfigures: 

1247 latex_string += r""" 

1248 \begin{subfigure}"""+subfigure_options+r""" 

1249 \centering""" 

1250 # Now we have to insert the includegraphics or animategraphics command 

1251 if animate: 

1252 latex_string += r""" 

1253 \animategraphics[{graphics_options:}]{{{num_frames:}}}{{{base_name:}}}{{0}}{{{end_frame:}}}""".format( 

1254 graphics_options=animate_graphics_options, 

1255 num_frames=num_frames, 

1256 base_name=relpath, end_frame=num_frames - 1) 

1257 elif pdf3d: 

1258 latex_string += r""" 

1259 \includemedia[{graphics_options_3D:}]{{\includegraphics[{graphics_options:}]{{{base_name:}}}}}{{{base_name_u3d:}}}""".format( 

1260 graphics_options_3D = pdf3d_parameters, 

1261 graphics_options = graphics_options, 

1262 base_name = relpath, 

1263 base_name_u3d = rel_path_u3d) 

1264 else: 

1265 latex_string += r""" 

1266 \includegraphics[{:}]{{{:}}}""".format( 

1267 graphics_options,relpath) 

1268 # Now add captions and labels if they exist 

1269 if subfigures: 

1270 latex_string += r""" 

1271 \caption{{{:}}}""".format('' if subfigure_captions is None else subfigure_captions[i]) 

1272 if subfigure_labels is not None: 

1273 if isinstance(subfigure_labels,str): 

1274 label = subfigure_labels.format(i) 

1275 else: 

1276 label = subfigure_labels[i] 

1277 latex_string += r""" 

1278 \label{{{:}}}""".format(label) 

1279 latex_string += r""" 

1280 \end{subfigure}""" 

1281 # Add the figure caption and label 

1282 latex_string += r""" 

1283 \caption{{{:}}}""".format('' if figure_caption is None else figure_caption) 

1284 if figure_label is not None: 

1285 latex_string += r""" 

1286 \label{{{:}}}""".format(figure_label) 

1287 latex_string += r""" 

1288\end{figure} 

1289 """ 

1290 return latex_string 

1291 

1292def table(table, justification_string = None, 

1293 table_label = None, table_caption = None, longtable = False, 

1294 header = True, horizontal_lines = False, table_placement = ''): 

1295 if isinstance(table,pd.DataFrame): 

1296 table_as_list = table.to_numpy().tolist() 

1297 if header: 

1298 table_as_list.insert(0,table.columns.tolist()) 

1299 table = table_as_list 

1300 nrows = len(table) 

1301 ncols = len(table[0]) 

1302 if justification_string is None: 

1303 justification_string = 'c'*ncols 

1304 if longtable: 

1305 latex_string = r'\begin{{longtable}}{{{:}}}'.format(justification_string)+r''' 

1306 \caption{{{:}}}'''.format('' if table_caption is None else table_caption) 

1307 if table_label is not None: 

1308 latex_string += r''' 

1309 \label{{{:}}}'''.format(table_label) 

1310 latex_string += '\\\\' 

1311 else: 

1312 latex_string = r'''\begin{{table}}{:} 

1313 \centering 

1314 \caption{{{:}}}'''.format(table_placement,'' if table_caption is None else table_caption) 

1315 if table_label is not None: 

1316 latex_string += r''' 

1317 \label{{{:}}}'''.format(table_label) 

1318 latex_string += r''' 

1319 \begin{{tabular}}{{{:}}}'''.format(justification_string) 

1320 # Now create the meat of the table 

1321 if horizontal_lines: 

1322 latex_string += r''' 

1323 \hline''' 

1324 for i in range(nrows): 

1325 row = ' '+' & '.join([str(table[i][j]).replace('%','\\%') for j in range(ncols)]) + '\\\\' 

1326 if header and i == 0: 

1327 row += r'\hline' 

1328 if longtable: 

1329 row += '\n \\endhead' 

1330 latex_string += '\n'+row 

1331 if horizontal_lines: 

1332 latex_string += r''' 

1333 \hline''' 

1334 if longtable: 

1335 latex_string += r''' 

1336\end{longtable}''' 

1337 else: 

1338 latex_string += r''' 

1339 \end{tabular} 

1340\end{table}''' 

1341 return latex_string