• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

sandialabs / sdynpy / 13036987848

29 Jan 2025 05:23PM UTC coverage: 15.675%. Remained the same
13036987848

push

github

dprohe
Bugfix due to breaking change in qtpy/PyQt5

1 of 2 new or added lines in 2 files covered. (50.0%)

1 existing line in 1 file now uncovered.

2606 of 16625 relevant lines covered (15.68%)

0.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

6.63
/src/sdynpy/doc/sdynpy_latex.py
1
# -*- coding: utf-8 -*-
2
"""
3
Functions for creating a LaTeX report from SDynPy objects.
4

5
Copyright 2022 National Technology & Engineering Solutions of Sandia,
6
LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the U.S.
7
Government retains certain rights in this software.
8

9
This program is free software: you can redistribute it and/or modify
10
it under the terms of the GNU General Public License as published by
11
the Free Software Foundation, either version 3 of the License, or
12
(at your option) any later version.
13

14
This program is distributed in the hope that it will be useful,
15
but WITHOUT ANY WARRANTY; without even the implied warranty of
16
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
GNU General Public License for more details.
18

19
You should have received a copy of the GNU General Public License
20
along with this program.  If not, see <https://www.gnu.org/licenses/>.
21
"""
22

23
import numpy as np
3✔
24
import matplotlib.pyplot as plt
3✔
25
import os
3✔
26
import pyqtgraph as pqtg
3✔
27
import PIL
3✔
28
from ..signal_processing.sdynpy_correlation import mac, matrix_plot
3✔
29
from ..core.sdynpy_geometry import GeometryPlotter, ShapePlotter
3✔
30
from shutil import copy
3✔
31

32

33
def create_latex_summary(figure_basename, geometry, shapes, frfs,
3✔
34
                         output_file=None, figure_basename_relative_to_latex=None,
35
                         max_shapes=None, max_frequency=None,
36
                         frequency_format='{:0.1f}', damping_format='{:0.2f}\\%',
37
                         cmif_kwargs={'part': 'imag', 'tracking': None},
38
                         cmif_subplots_kwargs={},
39
                         mac_subplots_kwargs={}, mac_plot_kwargs={},
40
                         geometry_plot_kwargs={},
41
                         shape_plot_kwargs={},
42
                         save_animation_kwargs={'frames': 20},
43
                         latex_cmif_graphics_options=r'width=0.7\linewidth',
44
                         latex_mac_graphics_options=r'width=0.5\linewidth',
45
                         latex_shape_graphics_options=r'width=\linewidth,loop',
46
                         latex_shape_subplot_options=r'[t]{0.45\linewidth}',
47
                         latex_max_figures_per_page=6,
48
                         latex_max_figures_first_page=None,
49
                         latex_cmif_caption='Complex Mode Indicator Function showing experimental data compared to modal fitting.',
50
                         latex_cmif_label='fig:cmif',
51
                         latex_mac_caption='Auto Modal Assurance Criterion Plot showing independence of fit mode shapes.',
52
                         latex_mac_label='fig:mac',
53
                         latex_shape_subcaption='Shape {number:} at {frequency:} Hz, {damping:}\\ damping',
54
                         latex_shape_sublabel='fig:shape{:}',
55
                         latex_shape_caption='Mode shapes extracted from test data.',
56
                         latex_shape_label='fig:modeshapes',
57
                         latex_shape_table_columns='lllp{3.5in}',
58
                         latex_shape_table_caption=(
59
                             'List of modes extracted from the test data.  Modal parameters are shown along with a brief description of the mode shape.'),
60
                         latex_shape_table_label='tab:modelist'):
61

62
    if figure_basename_relative_to_latex is None:
×
63
        figure_basename_relative_to_latex = figure_basename.replace('\\', '/')
×
64

65
    if latex_max_figures_first_page is None:
×
66
        latex_max_figures_first_page = latex_max_figures_per_page
×
67

68
    # Get the figure names
69
    figure_base_path, figure_base_filename = os.path.split(figure_basename)
×
70
    figure_base_filename, figure_base_ext = os.path.splitext(figure_base_filename)
×
71
    latex_figure_base_path, latex_figure_base_filename = os.path.split(
×
72
        figure_basename_relative_to_latex)
73
    latex_figure_base_filename, latex_figure_base_ext = os.path.splitext(latex_figure_base_filename)
×
74

75
    cmif_file_name = os.path.join(figure_base_path, figure_base_filename +
×
76
                                  '_cmif_comparison' + figure_base_ext)
77
    mac_file_name = os.path.join(figure_base_path, figure_base_filename + '_mac' + figure_base_ext)
×
78
    shape_file_name = os.path.join(
×
79
        figure_base_path, figure_base_filename + '_shape_{:}' + figure_base_ext)
80

81
    cmif_latex_file_name = (latex_figure_base_path + '/' +
×
82
                            figure_base_filename + '_cmif_comparison').replace('\\', '/')
83
    mac_latex_file_name = (latex_figure_base_path + '/' +
×
84
                           figure_base_filename + '_mac').replace('\\', '/')
85
    shape_latex_file_name = (latex_figure_base_path + '/' + figure_base_filename + '_shape_{:}-')
×
86

87
    # Go through and save out all the files
88
    experimental_cmif = None if frfs is None else frfs.compute_cmif(**cmif_kwargs)
×
89
    frequencies = None if experimental_cmif is None else experimental_cmif[0].abscissa
×
90

91
    analytic_frfs = None if (shapes is None or frfs is None) else shapes.compute_frf(frequencies, np.unique(frfs.coordinate[..., 0]),
×
92
                                                                                     np.unique(frfs.coordinate[..., 1]))
93
    analytic_cmif = analytic_frfs.compute_cmif(**cmif_kwargs)
×
94

95
    # Compute CMIF
96

97
    fig, ax = plt.subplots(num=figure_basename + ' CMIF', **cmif_subplots_kwargs)
×
98
    experimental_cmif[0].plot(ax, plot_kwargs={'color': 'b', 'linewidth': 1})
×
99
    analytic_cmif[0].plot(ax, plot_kwargs={'color': 'r', 'linewidth': 1})
×
100
    experimental_cmif[1:].plot(ax, plot_kwargs={'color': 'b', 'linewidth': 0.25})
×
101
    analytic_cmif[1:].plot(ax, plot_kwargs={'color': 'r', 'linewidth': 0.25})
×
102
    shapes.plot_frequency(experimental_cmif[0].abscissa, experimental_cmif[0].ordinate, ax)
×
103
    ax.legend(['Experiment', 'Fit'])
×
104
    ax.set_yscale('log')
×
105
    ax.set_ylim(experimental_cmif.min(abs) / 2, experimental_cmif.max(abs) * 2)
×
106
    ax.set_ylabel('CMIF (m/s^2/N)')
×
107
    ax.set_xlabel('Frequency (Hz)')
×
108
    fig.tight_layout()
×
109
    fig.savefig(cmif_file_name)
×
110

111
    # Compute MAC
112
    mac_matrix = mac(shapes.flatten().shape_matrix.T)
×
113
    fig, ax = plt.subplots(num=figure_basename + ' MAC')
×
114
    matrix_plot(mac_matrix, ax, **mac_plot_kwargs)
×
115
    fig.tight_layout()
×
116
    fig.savefig(mac_file_name)
×
117

118
    # Now go through and save the shapes
119
    plotter = geometry.plot_shape(shapes, plot_kwargs=geometry_plot_kwargs, **shape_plot_kwargs)
×
120
    plotter.save_animation_all_shapes(
×
121
        shape_file_name, individual_images=True, **save_animation_kwargs)
122

123
    # Go through and create the latex document
124
    output_string = ''
×
125

126
    # Add the CMIF plot
127
    output_string += r'''\begin{{figure}}
×
128
    \centering
129
    \includegraphics[{:}]{{{:}}}
130
    \caption{{{:}}}
131
    \label{{{:}}}
132
\end{{figure}}'''.format(latex_cmif_graphics_options,
133
                         cmif_latex_file_name,
134
                         latex_cmif_caption,
135
                         latex_cmif_label)
136

137
    output_string += r'''
×
138

139
\begin{{figure}}
140
    \centering
141
    \includegraphics[{:}]{{{:}}}
142
    \caption{{{:}}}
143
    \label{{{:}}}
144
\end{{figure}}'''.format(latex_mac_graphics_options,
145
                         mac_latex_file_name,
146
                         latex_mac_caption,
147
                         latex_mac_label)
148

149
    # Create a table of natural frequencies, damping values, and comments
150
    output_string += r'''
×
151

152
\begin{{table}}
153
    \centering
154
    \caption{{{:}}}
155
    \label{{{:}}}
156
    %\resizebox{{\linewidth}}{{!}}{{
157
    \begin{{tabular}}{{{:}}}
158
        Mode & Freq (Hz) & Damping & Description \\ \hline'''.format(
159
        latex_shape_table_caption, latex_shape_table_label, latex_shape_table_columns)
160
    for i, shape in enumerate(shapes.flatten()):
×
161
        output_string += r'''
×
162
        {:} & {:} & {:} & {:} \\'''.format(i + 1, frequency_format.format(shape.frequency),
163
                                           damping_format.format(shape.damping * 100), shape.comment1)
164
    output_string += r'''
×
165
    \end{tabular}
166
    %}
167
\end{table}'''
168

169
    # Now lets create the modeshape figure
170
    output_string += r'''
×
171
\begin{figure}[h]
172
    \centering'''
173
    for index, shape in enumerate(shapes.flatten()):
×
174
        if index == latex_max_figures_first_page or ((index - latex_max_figures_first_page) % latex_max_figures_per_page == 0 and index != 0):
×
175
            output_string += r'''
×
176
\end{figure}
177
\begin{figure}[h]
178
    \ContinuedFloat
179
    \centering'''
180
        output_string += r'''
×
181
    \begin{{subfigure}}{subfigure_options:}
182
        \centering
183
        \animategraphics[{graphics_options:}]{{{num_frames:}}}{{{base_name:}}}{{0}}{{{end_frame:}}}
184
        \caption{{{caption:}}}
185
        \label{{{label:}}}
186
    \end{{subfigure}}'''.format(graphics_options=latex_shape_graphics_options, num_frames=save_animation_kwargs['frames'],
187
                                base_name=shape_latex_file_name.format(index + 1), end_frame=save_animation_kwargs['frames'] - 1,
188
                                caption=latex_shape_subcaption.format(
189
            number=index + 1,
190
            frequency=frequency_format.format(shape.frequency),
191
            damping=damping_format.format(shape.damping * 100)),
192
            label=latex_shape_sublabel.format(index + 1),
193
            subfigure_options=latex_shape_subplot_options)
194
    output_string += r'''
×
195
    \caption{{{:}}}
196
    \label{{{:}}}
197
\end{{figure}}
198
'''.format(latex_shape_caption, latex_shape_label)
199
    if isinstance(output_file, str):
×
200
        close = True
×
201
        output_file = open(output_file, 'w')
×
202
    else:
203
        close = False
×
204
    try:
×
205
        output_file.write(output_string)
×
206
    except AttributeError:
×
207
        print('Error writing to output file {:}'.format(output_file))
×
208
    if close:
×
209
        output_file.close()
×
210
    return output_string
×
211

212
def figure(figures, figure_label = None, figure_caption = None,
3✔
213
           graphics_options = r'width=0.7\linewidth', 
214
           animate_graphics_options = r'width=0.7\linewidth,loop',
215
           figure_placement = '',
216
           subfigure_options = r'[t]{0.45\linewidth}', subfigure_labels = None, 
217
           subfigure_captions = None, max_subfigures_per_page = None,
218
           max_subfigures_first_page = None, figure_save_names = None,
219
           latex_root = r''
220
           ):
221
    r"""
222
    Adds figures, subfigures, and animations to a running latex document.
223

224
    Parameters
225
    ----------
226
    figures : list
227
        Figure or figures that can be inserted into a latex document.  See 
228
        note for various figure types and configurations that can be used.
229
    figure_label : str, optional
230
        The label that will be used for the figure in the latex document.
231
        If not specified, the figure will not be labeled.
232
    figure_caption : str, optional
233
        The caption that will be used for the figure in the latex document.
234
        If not specified, the figure will only be captioned with the figure
235
        number.
236
    graphics_options : str, optional
237
        Graphics options that will be used for the figure.  If not specified
238
        this will be r'width=0.7\linewidth'.
239
    animate_graphics_options : str, optional
240
        Graphics options that will be used for an animation.  If not specified
241
        this will be r'width=0.7\linewidth,loop'.
242
    figure_placement : str, optional
243
        Specify the placement of the figure with strings such as '[t]', '[b]',
244
        or '[h]'.  If not specified, the figure will have no placement
245
        specified.
246
    subfigure_options : str, optional
247
        The options that will be applied to each subfigure in the figure, if
248
        subfigures are specified.  By default, this will be r'[t]{0.45\linewidth}'
249
    subfigure_labels : str, optional
250
        Labels to apply to the subfigure.  This can either be a list of strings
251
        the same size as the list of figures, or a string with a format specifier
252
        accepting the subfigure index.  If not specified, the subfigures will
253
        not be labeled.
254
    subfigure_captions : list, optional
255
        A list of strings the same length as the list of figures to use as
256
        captions for the subfigures.  If not specified, the subfigures will
257
        only be captioned with the subfigure number.
258
    max_subfigures_per_page : int, optional
259
        The maximum number of subfigures on a page.  Longer figures will be
260
        broken up into multiple figures using \ContinuedFloat.  If not specified,
261
        a single figure environment will be generated.
262
    max_subfigures_first_page : int, optional
263
        The maximum number of subfigures on the first page.  Longer figures will be
264
        broken up into multiple figures using \ContinuedFloat.  If not specified,
265
        the max_subfigures_per_page value will be used if specified, otherwise
266
        a single figure environment will be generated.
267
    figure_save_names : str or list of str, optional
268
        File names to save the figures as.  This can be specified as a string
269
        with a format specifier in it that will accept the figure index, or
270
        a list of strings the same length as the list of figures.
271
        If not specified, files will be specified as 'figure_0',
272
        'figure_1', etc.  If file names are not present, then the file name
273
        will be automatically selected for the type of figure given.
274
    latex_root : str, optional
275
        Directory in which the latex .tex file will be constructed.  This is
276
        used to create relative paths to the save_figure_names within the latex
277
        document.  If not specified, then the current directory will be assumed.
278
     
279
    Returns
280
    -------
281
    latex_string : str
282
        The latex source code to insert the figures into the document.
283

284
    Notes
285
    -----
286
    The `figures` argument must be a list of figures.  If only one entry
287
    is present in the list, a figure will be made in the latex document.  If
288
    multiple entries are present, a figure will be made and subfigures will be
289
    made for each entry in the list.  If an entry in the list is also a list,
290
    then that figure or subfigure will be made into an animation.
291
    
292
    The list of figures can contain many types of objects that a figure will be
293
    made from, including:
294
        - A 2D numpy array
295
        - A Matplotlib figure
296
        - A pyqtgraph plotitem
297
        - A bytes object that represents an image
298
        - A string to a file name
299
    """
300
    if len(figures) == 1:
×
301
        subfigures = False
×
302
    else:
303
        subfigures = True
×
304
        
305
    if max_subfigures_first_page is None:
×
306
        max_subfigures_first_page = max_subfigures_per_page
×
307
        
308
    if figure_save_names is None:
×
309
        figure_save_names = ['figure_{:}'.format(i) for i in range(len(figures))]
×
310
    elif isinstance(figure_save_names,str):
×
311
        figure_save_names = [figure_save_names.format(i) for i in range(len(figures))]
×
312
        
313
    latex_string = r'\begin{figure}'+figure_placement+'\n    \\centering'
×
314
    # Go through and save all of the files out to disk
315
    for i,figure in enumerate(figures):
×
316
        # Check the type of figure.  If it's a list of figures, then it's an
317
        # animation.
318
        if isinstance(figure, list):
×
319
            animate = True
×
320
            num_frames = len(list)
×
321
        # Otherwise it's just a figure, but we turn it into a list anyways to
322
        # make it so we only have to program this once.
323
        else:
324
            animate = False
×
325
            num_frames = 1
×
326
            figure = [figure]
×
327
        # We need to get the extension of the file name to figure out what
328
        # type of file to save the image to.
329
        base,ext = os.path.splitext(figure_save_names[i])
×
330
        # We also want to get the directory so we can get the relative path to
331
        # the file
332
        relpath = os.path.relpath(base,latex_root).replace('\\','/')
×
333
        for j,this_figure in enumerate(figure):
×
334
            if animate:
×
335
                this_filename = base+'_{:}'.format(j)+ext
×
336
                relpath += '_'
×
337
            else:
338
                this_filename = figure_save_names[i]
×
339
            # Matplotlib Figure
340
            if isinstance(this_figure,plt.Figure):
×
341
                if ext == '':
×
342
                    this_filename += '.pdf'
×
343
                this_figure.savefig(this_filename)
×
344
            # Pyqtgraph PlotItem
345
            elif isinstance(this_figure,pqtg.PlotItem):
×
346
                if ext == '':
×
347
                    this_filename += '.png'
×
348
                this_figure.writeImage(this_filename)
×
349
            # 2D NumpyArray
350
            elif isinstance(this_figure, np.ndarray):
×
351
                if ext == '':
×
352
                    this_filename += '.png'
×
353
                PIL.Image.fromarray(this_figure).save(this_filename)
×
354
            # ShapePlotter
355
            elif isinstance(this_figure, ShapePlotter):
×
356
                raise NotImplementedError('ShapePlotter is not implemented yet')
×
357
            # GeometryPlotter
358
            elif isinstance(this_figure, GeometryPlotter):
×
359
                raise NotImplementedError('GeometryPlotter is not implemented yet')
×
360
            # Bytes object
361
            elif isinstance(this_figure, bytes):
×
362
                if ext == '':
×
363
                    this_filename += '.png'
×
364
                PIL.Image.frombytes(this_figure).save(this_filename)
×
365
            # String to file name
366
            elif isinstance(this_figure, str):
×
367
                if ext == '':
×
368
                    this_filename += os.path.splitext(this_figure)[-1]
×
369
                copy(this_figure,this_filename)
×
370
        # Now we end the figure and create a new one if we are at the right
371
        # subfigure number
372
        if (subfigures and max_subfigures_per_page is not None and 
×
373
            ((i-max_subfigures_first_page)%max_subfigures_per_page == 0
374
              and i > 0)):
375
            latex_string += r"""
×
376
\end{figure}
377
\begin{figure}[h]
378
    \ContinuedFloat
379
    \centering"""
380
        # If we have subfigures we need to stick in the subfigure environment
381
        if subfigures:
×
382
            latex_string += r"""
×
383
    \begin{subfigure}"""+subfigure_options+r"""
384
        \centering"""
385
        # Now we have to insert the includegraphics or animategraphics command
386
        if animate:
×
387
            latex_string += r"""
×
388
        \animategraphics[{graphics_options:}]{{{num_frames:}}}{{{base_name:}}}{{0}}{{{end_frame:}}}""".format(
389
            graphics_options=animate_graphics_options,
390
            num_frames=num_frames,
391
            base_name=relpath, end_frame=num_frames - 1)
392
        else:
393
            latex_string += r"""
×
394
        \includegraphics[{:}]{{{:}}}""".format(
395
        graphics_options,relpath)
396
        # Now add captions and labels if they exist
397
        if subfigures:
×
398
            latex_string += r"""
×
399
        \caption{{{:}}}""".format('' if subfigure_captions is None else subfigure_captions[i])
400
            if subfigure_labels is not None:
×
401
                if isinstance(subfigure_labels,str):
×
402
                    label = subfigure_labels.format(i)
×
403
                else:
404
                    label = subfigure_labels[i]
×
405
                latex_string += r"""
×
406
        \label{{{:}}}""".format(label)
407
            latex_string += r"""
×
408
    \end{subfigure}"""
409
    # Add the figure caption and label
410
    latex_string += r"""
×
411
    \caption{{{:}}}""".format('' if figure_caption is None else figure_caption)
412
    if figure_label is not None:
×
413
        latex_string += r"""
×
414
    \label{{{:}}}""".format(figure_label)
415
    latex_string += r"""
×
416
\end{figure}
417
    """
418
    return latex_string
×
419

420
def table(table, justification_string = None, 
3✔
421
          table_label = None, table_caption = None, longtable = False,
422
          header = True, horizontal_lines = False):
423
    nrows = len(table)
×
424
    ncols = len(table[0])
×
425
    if justification_string is None:
×
426
        justification_string = 'c'*ncols
×
427
    if longtable:
×
428
        latex_string = r'\begin{{longtable}}{{{:}}}'.format(justification_string)+r'''
×
429
    \caption{{{:}}}'''.format('' if table_caption is None else table_caption)
430
        if table_label is not None:
×
431
            latex_string += r'''
×
432
    \label{{{:}}}'''.format(table_label)
433
    else:
434
        latex_string = r'''\begin{{table}}
×
435
    \centering
436
    \caption{{{:}}}'''.format('' if table_caption is None else table_caption)
437
        if table_label is not None:
×
438
            latex_string += r'''
×
439
    \label{{{:}}}'''.format(table_label)
440
        latex_string += r'''
×
441
    \begin{{tabular}}{{{:}}}'''.format(justification_string)
442
    # Now create the meat of the table
443
    if horizontal_lines:
×
444
        latex_string += r'''
×
445
        \hline'''
446
    for i in range(nrows):
×
447
        row = '        '+' & '.join([str(table[i][j]) for j in range(ncols)]) + '\\\\'
×
448
        if header and i == 0:
×
449
            row += r'\hline'
×
450
        latex_string += '\n'+row
×
451
        if horizontal_lines:
×
452
            latex_string += r'''
×
453
        \hline'''
454
    if longtable:
×
455
        latex_string += r'''
×
456
\end{longtable}'''
457
    else:
458
        latex_string += r'''
×
459
    \end{tabular}
460
\end{table}'''
461
    return latex_string
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc