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
« 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.
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.
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.
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"""
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
39try:
40 from vtk import vtkU3DExporter
41except ImportError:
42 vtkU3DExporter = None
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'):
73 if figure_basename_relative_to_latex is None:
74 figure_basename_relative_to_latex = figure_basename.replace('\\', '/')
76 if latex_max_figures_first_page is None:
77 latex_max_figures_first_page = latex_max_figures_per_page
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)
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)
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_{:}-')
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
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)
106 # Compute CMIF
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)
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)
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)
134 # Go through and create the latex document
135 output_string = ''
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)
148 output_string += r'''
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)
160 # Create a table of natural frequencies, damping values, and comments
161 output_string += r'''
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}'''
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
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 ):
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')
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')
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`')
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)]
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))
305 if coord_plotter is not None:
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 )
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))
325 if include_name is not None:
326 with open(include_name,'w') as f:
327 f.write('\n\n'.join(latex_string))
329 return latex_string
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 ):
384 latex_string = []
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. ')
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_{:}')
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))
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.')
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_{:}')
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))
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.')
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_{:}')
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))
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.')
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_{:}')
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))
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. ')
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_{:}')
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))
509 if include_name is not None:
510 with open(include_name,'w') as f:
511 f.write('\n\n'.join(latex_string))
513 return latex_string
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)
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.')
595 if fit_modes_table is None:
596 fit_modes_table = fit_modes.mode_table('pandas')
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]]
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))
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
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.')
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')
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))
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
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.')
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')
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))
737 if include_name is not None:
738 with open(include_name,'w') as f:
739 f.write('\n\n'.join(latex_string))
741 return latex_string
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)]
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)]
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_{:}')
775 if animation_style == 'one3d':
776 animation_style = '3d'
777 shapes = [shapes]
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))
787 if include_name is not None:
788 with open(include_name,'w') as f:
789 f.write('\n\n'.join(latex_string))
791 return latex_string
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 ):
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.']
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)]
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_{:}')
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))
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_{:}')
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')
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]
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.'))
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))
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))
909 if include_name is not None:
910 with open(include_name,'w') as f:
911 f.write('\n\n'.join(latex_string))
913 return latex_string
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.
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.
1008 Returns
1009 -------
1010 latex_string : str
1011 The latex source code to insert the figures into the document.
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.
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 """
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
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
1048 if max_subfigures_first_page is None:
1049 max_subfigures_first_page = max_subfigures_per_page
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))]
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()
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
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