├── examples ├── results │ └── .gitignore ├── alum1mm.xlsx ├── validation.py └── example_code.py ├── lambwaves ├── __init__.py ├── plot_utils.py ├── utils.py └── lambwaves.py ├── setup.py ├── LICENSE ├── .gitignore └── README.md /examples/results/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /lambwaves/__init__.py: -------------------------------------------------------------------------------- 1 | from .lambwaves import Lamb 2 | -------------------------------------------------------------------------------- /examples/alum1mm.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/franciscorotea/Lamb-Wave-Dispersion/HEAD/examples/alum1mm.xlsx -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | with open('README.md') as f: 7 | readme = f.read() 8 | 9 | with open('LICENSE') as f: 10 | license = f.read() 11 | 12 | setup( 13 | name='lambwaves', 14 | version='0.0.0', 15 | description='"A module with tools to calculate and plot Lamb wave dispersion curves', 16 | long_description=readme, 17 | author='Francisco Rotea', 18 | author_email='francisco.rotea@gmail.com', 19 | url='github.com/franciscorotea/Lamb-Wave-Dispersion/', 20 | license=license, 21 | packages=find_packages(), 22 | install_requires=['numpy', 'scipy', 'matplotlib'] 23 | ) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Francisco Rotea 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /examples/validation.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import pandas as pd 3 | from lambwaves import Lamb 4 | 5 | # Load phase and group velocity data from an Excel file exported from 6 | # Dispersion software (1 mm aluminum plate) 7 | 8 | df_vp = pd.read_excel('alum1mm.xlsx', skiprows = 9, usecols = 'U:AN', 9 | header=None) 10 | df_vg = pd.read_excel('alum1mm.xlsx', skiprows = 9, usecols = 'A:T', 11 | header=None) 12 | 13 | # Create an instance of the same material using the Lamb class. 14 | 15 | alum = Lamb(thickness=1, 16 | nmodes_sym=5, 17 | nmodes_antisym=5, 18 | fd_max=10000, 19 | vp_max=15000, 20 | c_L=6420, 21 | c_S=3040) 22 | 23 | # Plot phase velocity using the Lamb class. 24 | 25 | fig1, ax1 = alum.plot_phase_velocity(material_velocities=False, 26 | cutoff_frequencies=False, 27 | sym_style={'color' : 'black'}, 28 | antisym_style={'color' : 'black'}) 29 | 30 | # Remove the legend that labels Symmetric and Antisymmetric modes 31 | # (we are interested in labeling only Lamb module and Dispersion). 32 | 33 | ax1.get_legend().remove() 34 | 35 | # Plot phase velocity obtained by Dispersion. 36 | 37 | line1 = ax1.lines[0] 38 | 39 | for mode in df_vp.columns[::2]: 40 | ax1.plot(df_vp[mode]*1e3, df_vp[mode+1]*1e3, 41 | color = 'orange', 42 | linestyle='--') 43 | 44 | line2 = ax1.lines[-1] 45 | 46 | ax1.legend((line1, line2), ('Lamb module', 'Dispersion')) 47 | 48 | # Plot group velocity using the Lamb class. 49 | 50 | fig2, ax2 = alum.plot_group_velocity(cutoff_frequencies=False, 51 | sym_style={'color' : 'black'}, 52 | antisym_style={'color' : 'black'}) 53 | 54 | # Remove the legend that labels Symmetric and Antisymmetric modes 55 | # (we are interested in labeling only Lamb module and Dispersion). 56 | 57 | ax2.get_legend().remove() 58 | 59 | # Plot group velocity obtained by Dispersion. 60 | 61 | line1 = ax2.lines[0] 62 | 63 | for mode in df_vg.columns[::2]: 64 | ax2.plot(df_vg[mode]*1e3, df_vg[mode+1]*1e3, 65 | color = 'orange', 66 | linestyle='--') 67 | 68 | line2 = ax2.lines[-1] 69 | 70 | ax2.legend((line1, line2), ('Lamb module', 'Dispersion')) 71 | 72 | plt.show() 73 | -------------------------------------------------------------------------------- /examples/example_code.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from lambwaves import Lamb 4 | 5 | """ 6 | In order to set up the Lamb class, you will need the following 7 | parameters: 8 | 9 | thickness : Thickness of the plate, in mm. 10 | nmodes_sym : Number of symmetric modes to calculate. 11 | nmodes_antisym : Number of antisymmetric modes to calculate. 12 | fd_max : Maximum value of frequency × thickness to calculate, 13 | in kHz × mm. 14 | vp_max : Maximum value of phase velocity to calculate, in m/s. 15 | c_L : Longitudinal wave velocity of the material, in m/s. 16 | c_S: Shear wave velocity of the material, in m/s. 17 | 18 | The following parameters are optional: 19 | 20 | c_R : Rayleigh wave velocity of the material, in m/s. 21 | fd_points : Number of frequency × thickness points. 22 | vp_step : Increment between phase velocity intervals. 23 | material : Name of the material being analyzed. 24 | 25 | """ 26 | 27 | # You can obtain the values of c_L and c_S and an approximate value for 28 | # c_R (if v > 0.3) from the material's mechanical properties by using 29 | # the following equations: 30 | 31 | E = 68.9e9 # E = Young's modulus, in Pa. 32 | p = 2700 # p = Density (rho), in kg/m3. 33 | v = 0.33 # v = Poisson's ratio (nu). 34 | 35 | c_L = np.sqrt(E*(1-v) / (p*(1+v)*(1-2*v))) 36 | c_S = np.sqrt(E / (2*p*(1+v))) 37 | c_R = c_S * ((0.862+1.14*v) / (1+v)) 38 | 39 | # Example: A 10 mm aluminum plate. 40 | 41 | alum = Lamb(thickness=10, 42 | nmodes_sym=5, 43 | nmodes_antisym=5, 44 | fd_max=10000, 45 | vp_max=15000, 46 | c_L=c_L, 47 | c_S=c_S, 48 | c_R=c_R, 49 | material='Aluminum') 50 | 51 | # Plot phase velocity, group velocity and wavenumber. 52 | 53 | alum.plot_phase_velocity() 54 | alum.plot_group_velocity() 55 | alum.plot_wave_number() 56 | 57 | # Plot wave structure (displacement profiles across thickness) for A0 58 | # and S0 modes at different fd values. 59 | 60 | alum.plot_wave_structure(mode='A0', nrows=3, ncols=2, 61 | fd=[500,1000,1500,2000,2500,3000]) 62 | 63 | alum.plot_wave_structure(mode='S0', nrows=4, ncols=2, 64 | fd=[500,1000,1500,2000,2500,3000,3500,4000]) 65 | 66 | # Generate animations for A0 and S0 modes at 1000 kHz mm. 67 | 68 | alum.animate_displacement(mode='S0', fd=1000) 69 | alum.animate_displacement(mode='A0', fd=1000) 70 | 71 | # Save all results to a txt file. 72 | 73 | alum.save_results() 74 | 75 | plt.show() 76 | -------------------------------------------------------------------------------- /lambwaves/plot_utils.py: -------------------------------------------------------------------------------- 1 | """Functions used to create dispersion plots. 2 | 3 | Author: Francisco Rotea 4 | (Buenos Aires, Argentina) 5 | Repository: https://github.com/franciscorotea 6 | Email: francisco.rotea@gmail.com 7 | 8 | """ 9 | 10 | def add_plot(ax, result, mode, fd, **plt_kwargs): 11 | """Add a dispersion plot for a specific mode to a matplotlib axes 12 | object. 13 | 14 | Parameters 15 | ---------- 16 | ax : axes 17 | Matplotlib axes in which the plot will be added. 18 | result : dict 19 | A dictionary with a result (vp, vg or k) interpolator at each 20 | mode. 21 | mode : str 22 | Mode to plot. Can be "A0", "A1", "A2", ..., "An" or "S0", "S1", 23 | "S2", ..., "Sn", with 'n' being the order of the corresponding 24 | mode. 25 | fd : array 26 | An array of frequency thickness values to plot. 27 | cutoff_freq : bool 28 | Set to True to show cutoff frequencies in the plot. 29 | plt_kwargs : dict, optional 30 | Matplotlib kwargs (to change color, linewidth, linestyle, etc.). 31 | 32 | """ 33 | 34 | var = result[mode](fd) 35 | n = int(mode[1:]) + 1 36 | 37 | # Mode 'A0' and 'S0' are different from the rest (they have labels 38 | # for legend and different text positioning when indicating each 39 | # mode). 40 | 41 | if n == 1: 42 | legend = 'Symmetric' if mode[0] == 'S' else 'Antisymmetric' 43 | mode_plot = ax.plot(fd, var, label=legend, **plt_kwargs) 44 | 45 | # Get the plot color so that the text indicating the mode is the 46 | # same color as the curve. 47 | 48 | plot_color = mode_plot[0].get_color() 49 | ax.text(x=fd[0], y=var[0], color=plot_color, 50 | s='$\mathregular{' + mode[0] + '_' + mode[1:] + '}$', 51 | va='bottom' if mode[0] == 'A' else 'bottom') 52 | else: 53 | mode_plot = ax.plot(fd, var, **plt_kwargs) 54 | 55 | # Get the plot color so that the text indicating the mode is the 56 | # same color as the curve. 57 | 58 | plot_color = mode_plot[0].get_color() 59 | ax.text(x=fd[0], y=var[0], color=plot_color, 60 | s='$\mathregular{' + mode[0] + '_' + mode[1:] + 61 | '}$', 62 | va='top', ha='right') 63 | 64 | def add_cutoff_freqs(ax, mode, arrow_dir, y_max, c_L, c_S, 65 | plt_kwargs={'color': 'k', 'ls': '--', 'lw': 0.5}): 66 | """Add vertical lines indicating cutoff frequencies to a matplotlib 67 | axes object. 68 | 69 | Parameters 70 | ---------- 71 | ax : axes 72 | Matplotlib axes in which the plot will be added. 73 | mode : str 74 | Mode to plot. Can be "A0", "A1", "A2", ..., "An" or "S0", "S1", 75 | "S2", ..., "Sn", with 'n' being the order of the corresponding 76 | mode. 77 | arrow_dir : str 78 | Set arrows' direction in the plot. Can be 'up' (for group 79 | velocity plots) or 'down' (for phase velocity plots). 80 | y_max : float 81 | Maximum y value in the plot. Used to position arrows in phase 82 | velocity plots. 83 | c_L : float 84 | Longitudinal wave velocity of the material, in m/s. 85 | c_S: float 86 | Shear wave velocity of the material, in m/s. 87 | plt_kwargs : dict, optional 88 | Matplotlib kwargs (to change color, linewidth, linestyle, etc.). 89 | 90 | """ 91 | 92 | if arrow_dir == 'down': 93 | arrow_y_pos = y_max 94 | arrow_str = r'$\downarrow$' 95 | arrow_va = 'top' 96 | elif arrow_dir == 'up': 97 | arrow_y_pos = 0 98 | arrow_str = r'$\uparrow$' 99 | arrow_va = 'bottom' 100 | 101 | n = int(mode[1:]) + 1 102 | 103 | ax.axvline(x = n*c_S if mode[0] == 'S' else n*c_L, 104 | **plt_kwargs) 105 | 106 | ax.text(x = n*c_S if mode[0] == 'S' else n*c_L, 107 | y=arrow_y_pos, s=arrow_str, va=arrow_va, ha='center', 108 | clip_on=True) 109 | 110 | if n % 2 != 0: 111 | ax.axvline(x = n*c_L/2 if mode[0] == 'S' else n*c_S/2, 112 | **plt_kwargs) 113 | 114 | ax.text(x = n*c_L/2 if mode[0] == 'S' else n*c_S/2, 115 | y=arrow_y_pos, s=arrow_str, va=arrow_va, ha='center', 116 | clip_on=True) 117 | 118 | def add_velocities(ax, c_L, c_S, c_R, x_max, 119 | plt_kwargs={'color': 'k', 'ls': ':', 'lw': 0.5}): 120 | """Add horizontal lines indicating material velocities to a 121 | matplotlib axes object. 122 | 123 | Parameters 124 | ---------- 125 | ax : axes 126 | Matplotlib axes in which the plot will be added. 127 | x_max : float or int 128 | Maximum x value in the plot. Used to position the velocity 129 | labels. 130 | c_L : float or int 131 | Longitudinal wave velocity of the material, in m/s. 132 | c_S: float or int 133 | Shear wave velocity of the material, in m/s. 134 | c_R: float or int, optional 135 | Rayleigh wave velocity of the material, in m/s. 136 | plt_kwargs : dict, optional 137 | Matplotlib kwargs (to change color, linewidth, linestyle, etc.). 138 | 139 | """ 140 | 141 | ax.axhline(y=c_L, **plt_kwargs) 142 | ax.text(x=x_max, y=c_L, s=r'$\mathregular{c_L}$', va='center', ha='left') 143 | 144 | ax.axhline(y=c_S, **plt_kwargs) 145 | ax.text(x=x_max, y=c_S, s=r'$\mathregular{c_S}$', va='center', ha='left') 146 | 147 | if c_R: 148 | ax.axhline(y=c_R, **plt_kwargs) 149 | ax.text(x=0, y=c_R, s=r'$\mathregular{c_R}$', va='center', 150 | ha='right') 151 | -------------------------------------------------------------------------------- /lambwaves/utils.py: -------------------------------------------------------------------------------- 1 | """Some helper functions used to calculate dispersion curves. 2 | 3 | Author: Francisco Rotea 4 | (Buenos Aires, Argentina) 5 | Repository: https://github.com/franciscorotea 6 | Email: francisco.rotea@gmail.com 7 | 8 | """ 9 | 10 | import itertools 11 | 12 | import numpy as np 13 | import scipy.interpolate 14 | 15 | def interpolate(result, d, kind='cubic'): 16 | """Interpolate the results for phase velocity, group velocity and 17 | wave number. 18 | 19 | Parameters 20 | ---------- 21 | result : dict 22 | Dictionary with the phase velocity values obtained by solving 23 | the dispersion equations. 24 | kind : str, optional 25 | Specifies the kind of interpolation as a string. Can be 26 | ‘linear’, ‘nearest’, ‘zero’, ‘slinear’, ‘quadratic’, ‘cubic’, 27 | ‘previous’, ‘next’. Defaults to 'cubic'. 28 | 29 | Returns 30 | ------- 31 | interp_vp : dict 32 | Dictionary with phase velocity interpolator at each mode. 33 | interp_vg : dict 34 | Dictionary with group velocity interpolator at each mode. 35 | interp_k : dict 36 | Dictionary with wave number interpolator at each mode. 37 | 38 | """ 39 | 40 | interp_vp = {} 41 | interp_vg = {} 42 | interp_k = {} 43 | 44 | for mode, arr in result.items(): 45 | 46 | if arr[1].size > 3: 47 | 48 | fd = arr[0] 49 | vp = arr[1] 50 | 51 | interp_vp[mode] = scipy.interpolate.interp1d(fd, vp, kind=kind) 52 | 53 | k = (fd*2*np.pi/d)/vp 54 | 55 | interp_k[mode] = scipy.interpolate.interp1d(fd, k, kind=kind) 56 | 57 | # Find the derivative of phase velocity using a 58 | # interpolating spline. 59 | 60 | univ_s = scipy.interpolate.InterpolatedUnivariateSpline(fd, vp) 61 | vp_prime = univ_s.derivative() 62 | 63 | vg = np.square(vp) * (1/(vp - vp_prime(fd)*fd)) 64 | 65 | interp_vg[mode] = scipy.interpolate.interp1d(fd, vg, kind=kind) 66 | 67 | return interp_vp, interp_vg, interp_k 68 | 69 | def correct_instability(result, function): 70 | """A function to correct the instability produced when two roots are 71 | in close proximity, making the function change sign twice or more in 72 | the phase velocity interval under analysis. Since these values of 73 | phase velocity are not computed, it ultimately produces a wrong mode 74 | assignation, e.g., a phase velocity value corresponding to the S1 75 | mode is wrongly assigned to S0. 76 | 77 | Since phase velocity is strictly decreasing for each mode order 78 | (except for A0), the function works by looping through each mode, 79 | detecting if a phase velocity value is not decreasing. If this is 80 | the case, the value is appended to the next mode, and replaced by 0. 81 | 82 | Parameters 83 | ---------- 84 | result : array 85 | An array of shape (fd_points, nmodes+1), where the first column 86 | contains the fd values and the following columns are the phase 87 | velocity values for the requested modes (S0, S1, S2, etc., or 88 | A0, A1, A2, etc.) 89 | function : object 90 | Family of modes to solve (symmetric or antisymmetric). 91 | 92 | Returns 93 | ------- 94 | corr : array 95 | The corrected result array. 96 | 97 | """ 98 | 99 | # Compensate for antisymmetric mode (A0 is strictly increasing). 100 | 101 | n = 1 if function.__name__ == '_symmetric' else 2 102 | nmodes = result.shape[1] - 1 103 | 104 | corr = np.copy(result) 105 | 106 | for idx, col in enumerate(corr.T[n:,:]): 107 | if np.any(col): 108 | i = 0 109 | while col[i] == 0 and i < len(col)-1: 110 | i += 1 111 | if idx < nmodes-n: 112 | corr[i][idx+n+1] = 0 113 | 114 | for idx, col in enumerate(corr.T[n:,:]): 115 | for i in range(len(col)-1): 116 | if i == len(col)-2: 117 | corr[i+1][idx+n] = 0 118 | if col[i] != 0: 119 | j = i + 1 120 | if col[j] == 0: 121 | while col[j] == 0 and j < len(col)-1: 122 | j += 1 123 | if j < len(col)-1: 124 | if col[i] < col[j] or col[j] == 0: 125 | while (col[i] < col[j] or col[j] == 0) and j < len(col)-1: 126 | if col[j] == 0: 127 | j += 1 128 | else: 129 | for idx2 in range(nmodes): 130 | if idx == idx2: 131 | corr[j][idx+n] = 0 132 | p = n + 1 133 | while p <= nmodes - idx2: 134 | corr[j][idx+p] = result[j][idx+p-1] 135 | p += 1 136 | j += 1 137 | 138 | return corr 139 | 140 | def write_txt(data_sym, data_antisym, kind, filename, header): 141 | """Function to write the results to a txt file. 142 | 143 | Parameters 144 | ---------- 145 | data_sym : dict 146 | A dictionary consisting of interpolators for the specified 147 | symmetric modes. 148 | data_antisym : dict 149 | A dictionary consisting of interpolators for the specified 150 | antisymmetric modes. 151 | kind : {'Phase Velocity', 'Group Velocity', 'Wavenumber'} 152 | The type of results to write. Can be 'Phase Velocity', 'Group 153 | Velocity' or 'Wavenumber'. 154 | filename : str 155 | The filename of the txt file. 156 | header : str 157 | The header of the txt file (to include material information for 158 | example) 159 | 160 | """ 161 | 162 | if kind == 'Phase Velocity': 163 | label = 'vp [m/s]' 164 | elif kind == 'Group Velocity': 165 | label = 'vg [m/s]' 166 | else: 167 | label = 'k [1/m]' 168 | 169 | # Get the calculated (non-interpolated) data. 170 | 171 | results = [] 172 | 173 | for n in range(len(data_sym)): 174 | x_vals = data_sym[f'S{n}'].x 175 | y_vals = data_sym[f'S{n}'](x_vals) 176 | results.append(np.around(x_vals, 1)) 177 | results.append(np.around(y_vals, 1)) 178 | 179 | for n in range(len(data_antisym)): 180 | x_vals = data_antisym[f'A{n}'].x 181 | y_vals = data_antisym[f'A{n}'](x_vals) 182 | results.append(np.around(x_vals, 1)) 183 | results.append(np.around(y_vals, 1)) 184 | 185 | # Write the results in a txt file. 186 | 187 | with open('results/' + kind + ' - ' + filename, 'w') as f: 188 | f.write(header) 189 | 190 | f.write('\t\t\t\t'.join(data_sym.keys()) + '\t\t\t\t') 191 | f.write('\t\t\t\t'.join(data_antisym.keys()) + '\n') 192 | 193 | for _ in range(len(data_sym) + len(data_antisym)): 194 | f.write('fd [kHz mm]\t' + label + '\t') 195 | 196 | f.write('\n') 197 | 198 | for k in itertools.zip_longest(*results, fillvalue=''): 199 | f.write('\t\t'.join(map(str, k)) + '\n') 200 | 201 | def find_max(result): 202 | """Find the maximum value in all modes analyzed. Used to limit the 203 | scale of the dispersion plots. 204 | 205 | Parameters 206 | ---------- 207 | result : dict 208 | A dictionary with a result (vp, vg or k) interpolator at each 209 | mode. 210 | 211 | """ 212 | 213 | max_val_arr = [] 214 | 215 | for mode, arr in result.items(): 216 | fd = np.arange(np.amin(arr.x), np.amax(arr.x), 0.1) 217 | max_val_arr.append(np.amax(result[mode](fd))) 218 | 219 | return max(max_val_arr) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lamb Wave Dispersion 2 | 3 | Lamb waves are a type of ultrasonic elastic waves that propagate guided between the two parallel surfaces of solid plates. Lamb waves have recently gained popularity from researchers and engineers in the non-destructive evaluation community for damage identification, due to their relatively low attenuation ratio, strong penetration capability, convenience of generation and collection, and high sensitivity to structural damage even of small size. 4 | 5 | Lamb waves propagate as two infinite sets of modes: **symmetric modes**, in which the displacement is symmetrical about the midplane of the plate, and **anti-symmetric modes**, with displacements anti-symmetric about the midplane. Another important characteristic of Lamb waves is their dispersive behavior: Lamb wave velocity depends on both the excitation frequency (f) and the thickness (d) of the plate combined in a frequency–thickness (f·d) product. 6 | 7 | This Python package presents tools to calculate and plot Lamb wave dispersion curves and particle displacement in traction-free, homogeneous and isotropic plates. 8 | 9 | ## Getting Started 10 | 11 | The code is tested with Python 3.7. Next section provides the prerequisites to run the program. 12 | 13 | ### Prerequisites 14 | 15 | The code is dependant on the following external libraries: Numpy, Scipy, Matplotlib. These can be installed with Python's inbuilt package management system, [pip](https://pip.pypa.io/en/stable/). See Python's tutorial on [installing packages](https://packaging.python.org/tutorials/installing-packages/#id17) for information about this issue. 16 | 17 | ### Install using pip 18 | 19 | You can install this package with pip by doing without worrying about downloading the prerequisites by doing: 20 | 21 | ``` 22 | pip install git+https://github.com/franciscorotea/Lamb-Wave-Dispersion 23 | ``` 24 | 25 | ### Manually installation 26 | 27 | The prerequisites can be installed by doing: 28 | 29 | ``` 30 | pip install numpy 31 | pip install scipy 32 | pip install matplotlib 33 | ``` 34 | 35 | In order to run `validation.py`, you will also need Pandas, which can be installed as: 36 | 37 | ``` 38 | pip install pandas 39 | ``` 40 | 41 | Then, you just need to download or clone this repository into an appropriate location. 42 | 43 | ## Usage 44 | 45 | First, you need to import the `Lamb` class from the `lambwaves` module, and instanciate it. For this example, we are going to use a 10 mm Aluminum plate. 46 | 47 | ```python 48 | from lambwaves import Lamb 49 | 50 | alum = Lamb(thickness=10, 51 | nmodes_sym=5, 52 | nmodes_antisym=5, 53 | fd_max=10000, 54 | vp_max=15000, 55 | c_L=6420, 56 | c_S=3040) 57 | ``` 58 | 59 | #### Parameters: 60 | 61 | `thickness`: Thickness of the plate, in mm. 62 | `nmodes_sym`: Number of symmetric modes to calculate. 63 | `nmodes_antisym`: Number of antisymmetric modes to calculate. 64 | `fd_max`: Maximum value of frequency × thickness to calculate, in kHz × mm. 65 | `vp_max`: Maximum value of phase velocity to calculate, in m/s. 66 | `c_L`: Longitudinal wave velocity of the material, in m/s. 67 | `c_S`: Shear wave velocity of the material, in m/s. 68 | 69 | The following parameters are optional: 70 | 71 | `c_R`: Rayleigh wave velocity of the material, in m/s. Defaults to None. 72 | `fd_points`: Number of frequency × thickness points. Defaults to 100. 73 | `vp_step`: Increment between phase velocity intervals. Defaults to 100. 74 | `material`: Name of the material being analyzed. Defaults to "". 75 | 76 | ### Methods 77 | 78 | * **Phase Velocity** 79 | 80 | To generate a plot of phase velocity as a function of frequency × thickness, you can use: 81 | 82 | ```python 83 | alum.plot_phase_velocity() 84 | ``` 85 | 86 | This method produces the following plot: 87 | 88 | ![alt text](https://i.imgur.com/yrHQj9L.png) 89 | 90 | #### Parameters: 91 | 92 | You can use the following optional parameters with this method: 93 | 94 | `modes`: Which family of modes to plot. Can be 'symmetric', 'antisymmetric' or 'both'. Defaults to 'both'. 95 | `cutoff_frequencies`: Add cutoff frequencies to the plot. Defaults to True. 96 | `material_velocities`: Add material velocities (longitudinal, shear and Rayleigh) to the plot. Defaults to True. 97 | `save_img`: Save the result image as png in the `results` folder. Defaults to False. 98 | `sym_style`: A dictionary with [matplotlib kwargs](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html) to modify the symmetric curves (to change color, linewidth, linestyle, etc.). 99 | `antisym_style`: A dictionary with [matplotlib kwargs](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html) to modify the antisymmetric curves (to change color, linewidth, linestyle, etc.). 100 | 101 | * **Group Velocity** 102 | 103 | To generate a plot of group velocity as a function of frequency × thickness, you can use: 104 | 105 | ```python 106 | alum.plot_group_velocity() 107 | ``` 108 | 109 | This method produces the following plot: 110 | 111 | ![alt text](https://i.imgur.com/HfcJfJI.png) 112 | 113 | #### Parameters: 114 | 115 | You can use the following optional parameters with this method: 116 | 117 | `modes`: Which family of modes to plot. Can be 'symmetric', 'antisymmetric' or 'both'. Defaults to 'both'. 118 | `cutoff_frequencies`: Add cutoff frequencies to the plot. Defaults to True. 119 | `save_img`: Save the result image as png in the `results` folder. Defaults to False. 120 | `sym_style`: A dictionary with [matplotlib kwargs](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html) to modify the symmetric curves (to change color, linewidth, linestyle, etc.). 121 | `antisym_style`: A dictionary with [matplotlib kwargs](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html) to modify the antisymmetric curves (to change color, linewidth, linestyle, etc.). 122 | 123 | * **Wave Number** 124 | 125 | To generate a plot of wave number as a function of frequency × thickness, you can use: 126 | 127 | ```python 128 | alum.plot_wave_number() 129 | ``` 130 | 131 | This method produces the following plot: 132 | 133 | ![alt text](https://i.imgur.com/uLitFVR.png) 134 | 135 | #### Parameters: 136 | 137 | You can use the following optional parameters with this method: 138 | 139 | `modes`: Which family of modes to plot. Can be 'symmetric', 'antisymmetric' or 'both'. Defaults to 'both'. 140 | `save_img`: Save the result image as png in the `results` folder. Defaults to False. 141 | `sym_style`: A dictionary with [matplotlib kwargs](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html) to modify the symmetric curves (to change color, linewidth, linestyle, etc.). 142 | `antisym_style`: A dictionary with [matplotlib kwargs](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html) to modify the antisymmetric curves (to change color, linewidth, linestyle, etc.). 143 | 144 | * **Wave Structure** 145 | 146 | To generate a plot of the wave structure (i.e., the displacement profile across the thickness of the plate), you can use: 147 | 148 | ```python 149 | alum.plot_wave_structure(mode='A0', nrows=3, ncols=2, fd=[500, 1000, 1500, 2000, 2500, 3000]) 150 | ``` 151 | 152 | This method produces the following plot: 153 | 154 | ![alt text](https://i.imgur.com/F3fNEvL.png) 155 | 156 | #### Parameters: 157 | 158 | This method has to be used with the following parameters: 159 | 160 | `mode`: Mode to be analyzed. Can be "A0", "A1", "A2", ..., "An" or "S0", "S1", "S2", ..., "Sn", with 'n' being the order of the corresponding mode. 161 | `nrows`: Number of rows in the subplot. 162 | `ncols`: Number of columns in the subplot. 163 | `fd`: Array with the frequency × thickness values to analyze. The length of the array must be equal to `nrows` x `ncols`. 164 | 165 | The following parameters are optional: 166 | 167 | `save_img`: Save the result image as png in the `results` folder. Defaults to False. 168 | `inplane_style`: A dictionary with [matplotlib kwargs](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html) to modify the in-plane curves (to change color, linewidth, linestyle, etc.). 169 | `outofplane_style`: A dictionary with [matplotlib kwargs](https://matplotlib.org/api/_as_gen/matplotlib.pyplot.plot.html) to modify the out-of-plane curves (to change color, linewidth, linestyle, etc.). 170 | 171 | * **Particle Displacement Field** 172 | 173 | To generate an animation of the particle displacement field, you can use: 174 | 175 | ```python 176 | alum.animate_displacement(mode='A0', fd=1000) 177 | ``` 178 | 179 | This method produces the following animation: 180 | 181 | ![alt text](https://thumbs.gfycat.com/OrderlyEnviousBoilweevil-size_restricted.gif) 182 | 183 | #### Parameters: 184 | 185 | This method has to be used with the following parameters: 186 | 187 | `mode`: Mode to be animated. Can be "A0", "A1", "A2", ..., "An" or "S0", "S1", "S2", ..., "Sn", with 'n' being the order of the corresponding mode. 188 | `fd`: Frequency × thickness value to animate. 189 | 190 | The following parameters are optional: 191 | 192 | `speed`: Delay between frames in milliseconds. It can be used to control the speed of the rotating vectors in the animation (a smaller value produces a faster animation). Defaults to 30. 193 | `save_gif`: Set to True if you want to save the result animation as a gif in the `results` folder. Defaults to False. 194 | `save_video`: Choose a video format if you want to save the result animation as a video in the `results` folder. Can be 'mp4', 'mov' or 'avi'. Defaults to False. 195 | 196 | ***Note***: If you want to save the animation as a gif, you should install [ImageMagick](http://www.imagemagick.org/script/download.php) and specify the full path to magick.exe like this before using the `animate_displacement` method: 197 | 198 | ```python 199 | Lamb.magick_path = 'C:/Program Files/ImageMagick-7.0.10-Q16/magick.exe' 200 | ``` 201 | 202 | If you want to save the animation as .mp4, .avi or .mov, you should specify the full path to the ffmpeg executable in ImageMagick installation folder: 203 | 204 | ```python 205 | Lamb.ffmpeg_path = 'C:/Program Files/ImageMagick-7.0.10-Q16/ffmpeg.exe' 206 | ``` 207 | 208 | If you are using some flavor of Unix, chances are ImageMagick is already installed on your computer. 209 | 210 | * **Save Results** 211 | 212 | To save all results in a txt file in the `results` folder, you can use: 213 | 214 | ```python 215 | alum.save_results() 216 | ``` 217 | 218 | ### Attributes 219 | 220 | * **Phase Velocity** 221 | 222 | You can use the attributes `vp_sym` and `vp_antisym` to find the phase velocity at a particular `fd` value or an array of `fd` values. They are dictionaries with interpolators at each mode, where the keys are "A0", "A1", "A2", ..., "An" (for `vp_antisym`) and "S0", "S1", "S2", ..., "Sn" (for `vp_sym`), with 'n' being the order of the corresponding mode. 223 | 224 | For example, if you need the phase velocity for the S0 mode at 1000 kHz × mm, you can do: 225 | 226 | ```python 227 | alum.vp_sym['S0'](1000) 228 | ``` 229 | 230 | And this should return 5265.14 m/s. Always make sure that the fd values are within the valid range for the corresponding mode (i. e., above the cutoff frequency and below the `fd_max` you chose). Also, make sure the mode selected is within the selected `nmodes`. For example, if you chose `nmodes_sym = 5`, you can use 'S0', 'S1', 'S2', 'S3' or 'S4'. 231 | 232 | * **Group Velocity** 233 | 234 | You can use the attributes `vg_sym` and `vg_antisym` to find the group velocity at a particular `fd` value or an array of `fd` values. They are dictionaries with interpolators at each mode, where the keys are "A0", "A1", "A2", ..., "An" (for `vg_antisym`) and "S0", "S1", "S2", ..., "Sn" (for `vg_sym`), with 'n' being the order of the corresponding mode. 235 | 236 | For example, if you need the group velocity for the A1 mode at 2000, 3000, and 4000 kHz × mm, you can do: 237 | 238 | ```python 239 | alum.vg_antisym['A1']([2000,3000,4000]) 240 | ``` 241 | 242 | And this should return 3241.72, 3577.26, and 2486.33 m/s. Always make sure that the fd values are within the valid range for the corresponding mode (i. e., above the cutoff frequency and below the `fd_max` you chose). Also, make sure the mode selected is within the selected `nmodes`. For example, if you chose `nmodes_antisym = 5`, you can use 'A0', 'A1', 'A2', 'A3' or 'A4'. 243 | 244 | * **Wave Number** 245 | 246 | You can use the attributes `k_sym` and `k_antisym` to find the wave number at a particular `fd` value or an array of `fd` values. They are dictionaries with interpolators at each mode, where the keys are "A0", "A1", "A2", ..., "An" (for `k_antisym`) and "S0", "S1", "S2", ..., "Sn" (for `k_sym`), with 'n' being the order of the corresponding mode. 247 | 248 | For example, if you need the wave number for the S3 mode at 8000 kHz × mm, you can do: 249 | 250 | ```python 251 | alum.k_sym['S0'](8000) 252 | ``` 253 | 254 | And this should return 726.38 m-1. Always make sure that the fd values are within the valid range for the corresponding mode (i. e., below the `fd_max` you chose). Also, make sure the mode selected is within the selected `nmodes`. For example, if you chose `nmodes_sym = 5`, you can use 'S0', 'S1', 'S2', 'S3' or 'S4'. 255 | 256 | ## Validation 257 | 258 | In order to verify that the results obtained with this code are correct, a simple validation is performed using the Dispersion software included in the [AGU-Vallen Wavelet](https://www.vallen.de/products/software/) tool, developed by in collaboration between Vallen Systeme GmbH, Aoyama Gakuin University (AGU), and University of Denver. 259 | 260 | Run `validation.py` to perform the validation. The result should look like this: 261 | 262 | * **Phase velocity validation:** 263 | 264 | ![alt text](https://i.imgur.com/2GmzPTM.png) 265 | 266 | * **Group velocity validation:** 267 | 268 | ![alt text](https://i.imgur.com/NwpiOUB.png) 269 | 270 | As can be seen, the results are almost identical. The only difference is that Dispersion has an extended velocity range. 271 | 272 | ## References 273 | 274 | For information about the equations implemented, please refer to: 275 | 276 | [1] Rose, J. L., Ultrasonic Guided Waves in Solid Media, Chapter 6: Waves in Plates, Cambridge University Press, 2014. 277 | [2] Graff, K. F., Wave Motion in Elastic Solids, Chapter 8: Wave Propagation in Plates and Rods, Dover Publications, 1975. 278 | 279 | ## License 280 | 281 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 282 | -------------------------------------------------------------------------------- /lambwaves/lambwaves.py: -------------------------------------------------------------------------------- 1 | """A module with tools to calculate and plot Lamb wave dispersion 2 | curves. 3 | 4 | Usage: 5 | 6 | First, you need to create an instance of the Lamb class: 7 | 8 | mat = Lamb(thickness, nmodes_sym, nmodes_antisym, fd_max, vp_max, 9 | c_L, c_S [, c_R=None][, fd_points=100][, vp_step=100] 10 | [, material='']) 11 | 12 | Then, you can use this instance with the following methods: 13 | 14 | plot_phase_velocity(modes, cutoff_frequencies, material_velocities, 15 | save_img, sym_style, antisym_style): 16 | Plot phase velocity as a function of frequency × thickness. 17 | plot_group_velocity(modes, cutoff_frequencies, save_img, sym_style, 18 | antisym_style): 19 | Plot group velocity as a function of frequency × thickness. 20 | plot_wave_number(modes, save_img, sym_style, antisym_style): 21 | Plot wavenumber as a function of frequency × thickness. 22 | plot_wave_structure(mode, nrows, ncols, fd, save_img, inplane_style, 23 | outofplane_style): 24 | Plot particle displacement across the thickness of the plate. 25 | animate_displacement(mode, fd, speed, save_gif, save_video): 26 | Generate an animation of the displacement vector field. 27 | save_results() 28 | Save all results to a txt file. 29 | 30 | You can also use the following attributes: 31 | 32 | vp_sym: 33 | Phase velocity interpolators for symmetric modes. 34 | vg_sym: 35 | Group velocity interpolators for symmetric modes. 36 | k_sym: 37 | Wavenumber interpolators for symmetric modes. 38 | vp_antisym: 39 | Phase velocity interpolators for antisymmetric modes. 40 | vg_antisym: 41 | Group velocity interpolators for antisymmetric modes. 42 | k_antisym: 43 | Wavenumber interpolators for antisymmetric modes. 44 | 45 | For example, if you need the phase velocity for the S0 mode at 1000 46 | kHz × mm, you can do: 47 | 48 | mat.vp_sym['S0'](1000) 49 | 50 | You can also use a `np.array` instead of a single fd value. Always make 51 | sure that the fd values are within the valid range for the corresponding 52 | mode (i. e., above the cutoff frequency and below the fd_max you chose). 53 | Also, make sure the mode selected is within the selected `nmodes`. For 54 | example, if you chose `nmodes_sym = 4`, you can use 'S0', 'S1', 'S2' or 55 | 'S3'. 56 | 57 | For information about the equations implemented, please refer to: 58 | 59 | Rose, J. L., Ultrasonic Guided Waves in Solid Media, Chapter 6: Waves in 60 | Plates, Cambridge University Press, 2014. 61 | 62 | Graff, K. F., Wave Motion in Elastic Solids, Chapter 8: Wave Propagation 63 | in Plates and Rods, Dover Publications, 1975. 64 | 65 | Author: Francisco Rotea 66 | (Buenos Aires, Argentina) 67 | Repository: https://github.com/franciscorotea 68 | Email: francisco.rotea@gmail.com 69 | 70 | """ 71 | 72 | import numpy as np 73 | 74 | import matplotlib.pyplot as plt 75 | import matplotlib.animation 76 | 77 | import scipy.optimize 78 | 79 | from .plot_utils import add_plot, add_cutoff_freqs, add_velocities 80 | from .utils import interpolate, correct_instability, write_txt, find_max 81 | 82 | class Lamb: 83 | """A class used to calculate and plot Lamb wave dispersion curves 84 | for traction-free, homogeneous and isotropic plates. It also allows 85 | to generate an animation of the displacement vector field. 86 | 87 | Methods 88 | ------- 89 | plot_phase_velocity(modes, cutoff_frequencies, material_velocities, 90 | save_img, sym_style, antisym_style): 91 | Plot phase velocity as a function of frequency × thickness. 92 | plot_group_velocity(modes, cutoff_frequencies, save_img, sym_style, 93 | antisym_style): 94 | Plot group velocity as a function of frequency × thickness. 95 | plot_wave_number(modes, save_img, sym_style, antisym_style): 96 | Plot wavenumber as a function of frequency × thickness. 97 | plot_wave_structure(mode, nrows, ncols, fd, save_img, inplane_style, 98 | outofplane_style): 99 | Plot particle displacement across the thickness of the plate. 100 | animate_displacement(mode, fd, speed, save_gif, save_video): 101 | Generate an animation of the displacement vector field. 102 | save_results() 103 | Save all results to a txt file. 104 | 105 | Attributes 106 | ---------- 107 | vp_sym: 108 | Dictionary with phase velocity interpolators for symmetric 109 | modes. 110 | vg_sym: 111 | Dictionary with group velocity interpolators for symmetric 112 | modes. 113 | k_sym: 114 | Dictionary with wavenumber interpolators for symmetric 115 | modes. 116 | vp_antisym: 117 | Dictionary with phase velocity interpolators for antisymmetric 118 | modes. 119 | vg_antisym: 120 | Dictionary with group velocity interpolators for antisymmetric 121 | modes. 122 | k_antisym: 123 | Dictionary with wavenumber interpolators for antisymmetric 124 | modes. 125 | 126 | """ 127 | 128 | # If you want to save the animation as a gif, you should install 129 | # ImageMagick from http://www.imagemagick.org/script/download.php 130 | # and specify the full path to magick.exe: 131 | 132 | magick_path = 'C:/Program Files/ImageMagick-7.0.10-Q16/magick.exe' 133 | 134 | # If you want to save the animation as .mp4, .avi or .mov, you 135 | # should specify the full path to the ffmpeg executable in 136 | # ImageMagick installation folder: 137 | 138 | ffmpeg_path = 'C:/Program Files/ImageMagick-7.0.10-Q16/ffmpeg.exe' 139 | 140 | def __init__(self, thickness, nmodes_sym, nmodes_antisym, fd_max, vp_max, 141 | c_L, c_S, c_R = None, fd_points=100, vp_step=100, 142 | material=''): 143 | """" 144 | Parameters 145 | ---------- 146 | thickness : float or int 147 | Thickness of the plate, in mm. 148 | nmodes_sym : int 149 | Number of symmetric modes to calculate. 150 | nmodes_antisym : int 151 | Number of antisymmetric modes to calculate. 152 | fd_max : float or int 153 | Maximum value of frequency × thickness to calculate. 154 | vp_max : float or int 155 | Maximum value of phase velocity to calculate, in m/s. 156 | c_L : float or int 157 | Longitudinal wave velocity of the material, in m/s. 158 | c_S: float or int 159 | Shear wave velocity of the material, in m/s. 160 | c_R: float or int, optional 161 | Rayleigh wave velocity of the material, in m/s. 162 | fd_points : int, optional 163 | Number of frequency × thickness points. 164 | vp_step : int, optional 165 | Increment between phase velocity intervals. 166 | material : str, optional 167 | Name of the material being analyzed. 168 | 169 | """ 170 | 171 | self.d = thickness/1e3 172 | self.h = (thickness/2)/1e3 173 | self.nmodes_sym = nmodes_sym 174 | self.nmodes_antisym = nmodes_antisym 175 | self.fd_max = fd_max 176 | self.vp_max = vp_max 177 | self.c_L = c_L 178 | self.c_S = c_S 179 | self.c_R = c_R 180 | self.fd_points = fd_points 181 | self.vp_step = vp_step 182 | self.material = material 183 | 184 | # Solve the dispersion equations. 185 | 186 | sym = self._solve_disp_eqn(function=self._symmetric, 187 | nmodes=nmodes_sym, 188 | c=c_S, 189 | label='S') 190 | 191 | antisym = self._solve_disp_eqn(function=self._antisymmetric, 192 | nmodes=nmodes_antisym, 193 | c=c_L, 194 | label='A') 195 | 196 | # Calculate group velocity (vg) and wavenumber (k) from phase 197 | # velocity (vp) and interpolate all results. 198 | 199 | self.vp_sym, self.vg_sym, self.k_sym = interpolate(sym, self.d) 200 | self.vp_antisym, self.vg_antisym, self.k_antisym = interpolate(antisym, 201 | self.d) 202 | 203 | def _calc_constants(self, vp, fd): 204 | """Calculate the constants p and q (defined to simplify the 205 | dispersion equations) and wavenumber from a pair of phase 206 | velocity and frequency × thickness product. 207 | 208 | Parameters 209 | ---------- 210 | vp : float or int 211 | Phase velocity. 212 | fd : float or int 213 | Frequency × thickness product. 214 | 215 | Returns 216 | ------- 217 | k : float 218 | Wavenumber. 219 | p, q : float 220 | A pair of constants introduced to simplify the dispersion 221 | relations. 222 | 223 | """ 224 | 225 | omega = 2*np.pi*(fd/self.d) 226 | 227 | k = omega/vp 228 | 229 | p = np.sqrt((omega/self.c_L)**2 - k**2, dtype=np.complex128) 230 | q = np.sqrt((omega/self.c_S)**2 - k**2, dtype=np.complex128) 231 | 232 | return k, p, q 233 | 234 | def _symmetric(self, vp, fd): 235 | """Rayleigh-Lamb frequency relation for symmetric modes, used to 236 | determine the velocity at which a wave of a particular frequency 237 | will propagate within the plate. The roots of this equation are 238 | used to generate the dispersion curves. 239 | 240 | Parameters 241 | ---------- 242 | vp : float or int 243 | Phase velocity. 244 | fd : float or int 245 | Frequency × thickness product. 246 | 247 | Returns 248 | ------- 249 | symmetric : float 250 | Dispersion relation for symmetric modes. 251 | 252 | """ 253 | 254 | k, p, q = self._calc_constants(vp, fd) 255 | 256 | symmetric = (np.tan(q*self.h)/q 257 | + (4*(k**2)*p*np.tan(p*self.h))/(q**2 - k**2)**2) 258 | 259 | return np.real(symmetric) 260 | 261 | def _antisymmetric(self, vp, fd): 262 | """Rayleigh-Lamb frequency relation for antisymmetric modes, 263 | used to determine the velocity at which a wave of a particular 264 | frequency will propagate within the plate. The roots of this 265 | equation are used to generate the dispersion curves. 266 | 267 | Parameters 268 | ---------- 269 | vp : float or int 270 | Phase velocity. 271 | fd : float or int 272 | Frequency × thickness product. 273 | 274 | Returns 275 | ------- 276 | antisymmetric : float 277 | Dispersion relation for antisymmetric modes. 278 | 279 | """ 280 | 281 | k, p, q = self._calc_constants(vp, fd) 282 | 283 | antisymmetric = (q * np.tan(q*self.h) 284 | + (((q**2 - k**2)**2)*np.tan(p*self.h))/(4*(k**2)*p)) 285 | 286 | return np.real(antisymmetric) 287 | 288 | def _calc_wave_structure(self, modes, vp, fd, y): 289 | """Calculate the wave structure across the thickness of the 290 | plate. 291 | 292 | Parameters 293 | ---------- 294 | modes : {'A', 'S'} 295 | Family of modes to analyze. Can be 'A' (antisymmetric modes) 296 | or 'S' (symmetric modes). 297 | vp : float or int 298 | Phase velocity. 299 | fd : float or int 300 | Frequency × thickness product. 301 | y : array 302 | Array representing thickness values to calculate wave 303 | structure, from -d/2 to d/2. 304 | 305 | Returns 306 | ------- 307 | u : array 308 | In plane displacement profile. 309 | w : array 310 | Out of plane plane displacement profile. 311 | 312 | """ 313 | 314 | k, p, q = self._calc_constants(vp, fd) 315 | 316 | if modes == 'S': 317 | C = 1 318 | B = -2*k*q*np.cos(q*self.h) / ((k**2 - q**2) * np.cos(p*self.h)) 319 | u = 1j*(k*B*np.cos(p*y) + q*C*np.cos(q*y)) 320 | w = -p*B*np.sin(p*y) + k*C*np.sin(q*y) 321 | elif modes == 'A': 322 | D = 1 323 | A = 2*k*q*np.sin(q*self.h) / ((k**2 - q**2) * np.sin(p*self.h)) 324 | u = 1j*(k*A*np.sin(p*y) - q*D*np.sin(q*y)) 325 | w = p*A*np.cos(p*y) + k*D*np.cos(q*y) 326 | 327 | return u, w 328 | 329 | def _solve_disp_eqn(self, function, nmodes, c, label): 330 | """Function to calculate the numerical solution to the 331 | dispersion equations. 332 | 333 | The algorithm works as follows: 334 | 335 | 1) Fix a value of frequency × thickness product. 336 | 2) Evaluate the function at two values of phase velocity 337 | (vp and vp+step) and check their signs. 338 | 3) Since the function is continuous, if the sign changes 339 | in the interval under analysis, a root exists in this 340 | interval. Use the bisection method to locate it 341 | precisely. 342 | 4) Continue searching for other roots at this value of 343 | frequency × thickness. 344 | 5) Change the value of frequency × thickness and repeat 345 | steps 2 to 4. 346 | 347 | Parameters 348 | ---------- 349 | function : {self._symmetric, self._antisymmetric} 350 | Family of modes to solve. Can be `self._symmetric` (to solve 351 | symmetric modes) or `self._antisymmetric` (to solve 352 | antisymmetric modes). 353 | 354 | Returns 355 | ------- 356 | result_dict : dict 357 | A dictionary, where the keys are the corresponding mode 358 | (e.g., 'A0', 'A1', 'A2', ..., 'An' for antisymmetric modes 359 | or 'S0', 'S1', 'S2', ..., 'Sn' for symmetric modes) and the 360 | values are numpy arrays of dimensions (2, fd_points), where 361 | the first row has the fd values and the second row has the 362 | phase velocity values calculated. 363 | 364 | """ 365 | 366 | fd_arr = np.linspace(0, self.fd_max, self.fd_points) 367 | result = np.zeros((len(fd_arr), nmodes + 1)) 368 | 369 | print(f'\nCalculating {function.__name__[1:]} modes..\n') 370 | 371 | for i, fd in enumerate(fd_arr): 372 | 373 | print(f'{i}/{self.fd_points} - {np.around(fd, 1)} kHz × mm') 374 | 375 | result[i][0] = fd 376 | 377 | j = 1 378 | 379 | vp_1 = 0 380 | vp_2 = self.vp_step 381 | 382 | while vp_2 < self.vp_max: 383 | x_1 = function(vp_1, fd) 384 | x_2 = function(vp_2, fd) 385 | 386 | if j < nmodes + 1: 387 | if not np.isnan(x_1) and not np.isnan(x_2): 388 | if np.sign(x_1) != np.sign(x_2): 389 | bisection = scipy.optimize.bisect(f=function, 390 | a=vp_1, 391 | b=vp_2, 392 | args=(fd,)) 393 | 394 | # TO FIX: I don't know why at some points 395 | # the function changes sign, but the roots 396 | # found by the bisect method don't evaluate 397 | # to zero. 398 | 399 | # For now, these values are ignored (only 400 | # take into account those values that 401 | # evaluate to 0.01 or less). 402 | 403 | if (np.abs(function(bisection, fd)) < 1e-2 and not 404 | np.isclose(bisection, c)): 405 | 406 | result[i][j] = bisection 407 | j += 1 408 | 409 | vp_1 = vp_2 410 | vp_2 = vp_2 + self.vp_step 411 | 412 | # Correct some instabilities and replace zeros with NaN, so it 413 | # is easier to filter. 414 | 415 | result = correct_instability(result, function) 416 | result[result == 0] = np.nan 417 | 418 | result_dict = {} 419 | 420 | for nmode in range(nmodes): 421 | 422 | # Filter all NaN values. 423 | 424 | mode_result = np.vstack((result[:, 0], result[:, nmode + 1])) 425 | mode_result = mode_result[:, ~np.isnan(mode_result).any(axis=0)] 426 | 427 | # Append to a dictionary with keys 'An' or 'Sn'. 428 | 429 | result_dict[label + str(nmode)] = mode_result 430 | 431 | return result_dict 432 | 433 | def animate_displacement(self, mode, fd, speed=30, 434 | save_gif=False, save_video=False): 435 | """Generate an animation of the displacement vector field across 436 | the plate. The mesh grid created cover a full wavelength of the 437 | current selected wave mode and fd value. 438 | 439 | Parameters 440 | ---------- 441 | mode : str 442 | Mode to be animated. Can be "A0", "A1", "A2", ..., "An" or 443 | "S0", "S1", "S2", ..., "Sn", with 'n' being the order of the 444 | corresponding mode. 445 | fd : float or int 446 | Frequency × thickness product. 447 | speed : int 448 | Delay between frames in milliseconds. It can be used to 449 | control the speed of the rotating vectors in the animation 450 | (a smaller value produces a faster animation). Default to 30. 451 | save_gif : bool 452 | Set to True if you want to save the result animation as a 453 | gif. Defaults to False. 454 | save_video : {'mp4', 'mov', 'avi'} 455 | Choose a video format if you want to save the result 456 | animation as a video. Can be 'mp4', 'mov' or 'avi'. 457 | Defaults to False. 458 | 459 | Returns 460 | ------- 461 | fig, ax : matplotlib objects 462 | The figure and the axes of the generated plot. 463 | 464 | """ 465 | 466 | if mode[0] == 'S' and int(mode[1:]) < self.nmodes_sym: 467 | vp = self.vp_sym[mode](fd) 468 | elif mode[0] == 'A' and int(mode[1:]) < self.nmodes_antisym: 469 | vp = self.vp_antisym[mode](fd) 470 | else: 471 | raise Exception('mode not recognized. Mode must be "Sn" or ' 472 | '"An", where n is an integer greater or equal ' 473 | 'than 0. For example: "S0", "S1", "A0", "A1", ' 474 | 'etc. Make sure the mode order selected is within ' 475 | 'the number of modes requested when setting up the' 476 | ' Lamb class.') 477 | 478 | # Generate the mesh grid, with the x-values covering a full 479 | # wavelength and the y-values covering the thickness of the 480 | # plate (from -thickness/2 to +thickness/2). 481 | 482 | wavelength = vp/(fd/self.d) 483 | 484 | xx = np.linspace(0, wavelength, 40) 485 | yy = np.linspace(-self.h, self.h, 40) 486 | 487 | x, y = np.meshgrid(xx, yy) 488 | u, w = np.zeros_like(x), np.zeros_like(y) 489 | 490 | # Generate the time vector necessary to complete one cycle 491 | # (i.e., wave period). 492 | 493 | time = np.linspace(0, 1/(fd/self.d), 30) 494 | 495 | # Calculate angular frequency and wavenumber. 496 | 497 | omega = 2*np.pi*(fd/self.d) 498 | k = omega/vp 499 | 500 | def compute_displacement(t): 501 | """Calculate particle displacement as a function of time.""" 502 | 503 | u, w = self._calc_wave_structure(mode[0], vp, fd, y) 504 | 505 | u = u * np.exp(1j*(k*x-omega*t)) 506 | w = w * np.exp(1j*(k*x-omega*t)) 507 | 508 | return np.real(u), np.real(w) 509 | 510 | # Find the largest displacement vector to use for normalization. 511 | 512 | max_disp_arr = [] 513 | 514 | for t in time: 515 | u, w = compute_displacement(t) 516 | max_disp_arr.append(np.amax(np.sqrt(u**2 + w**2))) 517 | 518 | max_disp = max(max_disp_arr) 519 | 520 | # Generate the quiver plot animation. 521 | 522 | fig, ax = plt.subplots(figsize=(8, 5)) 523 | fig.canvas.manager.set_window_title(f'Displacement Field (mode {mode})') 524 | 525 | quiver = ax.quiver(x, y, u, w, scale=5*max_disp, scale_units='inches') 526 | 527 | ax.set_title('Mode $\mathregular{' + mode[0] + '_' + mode[1:] + '}$') 528 | ax.text(0.5, 0.05, f'fd = {np.around(fd, 1)} kHz × mm', ha='center', 529 | va='center', transform = ax.transAxes) 530 | 531 | ax.tick_params(axis='x', which='both', bottom=False, labelbottom=False) 532 | 533 | ax.set_yticks([-self.h, 0, self.h]) 534 | ax.set_yticklabels(['-d/2', '0', 'd/2']) 535 | 536 | ax.set_ylabel('Thickness') 537 | 538 | ax.set_xlim([0 - wavelength/4, wavelength + wavelength/4]) 539 | ax.set_ylim([-self.d, self.d]) 540 | 541 | def init(): 542 | return quiver, 543 | 544 | def animate(t): 545 | u, w = compute_displacement(t) 546 | quiver.set_UVC(u, w) 547 | 548 | return quiver, 549 | 550 | anim = matplotlib.animation.FuncAnimation(fig, animate, init_func=init, 551 | frames=time, interval=speed, 552 | blit=True) 553 | 554 | if save_gif: 555 | plt.rcParams['animation.convert_path'] = Lamb.magick_path 556 | anim.save(f'results/Mode_{mode}_fd_{int(fd)}_animation.gif', 557 | writer='imagemagick', extra_args='convert') 558 | 559 | if save_video: 560 | plt.rcParams['animation.ffmpeg_path'] = Lamb.ffmpeg_path 561 | anim.save(f'results/Mode_{mode}_fd_{int(fd)}_animation.' 562 | f'{save_video}', writer='imagemagick') 563 | 564 | return fig, ax 565 | 566 | def plot(self, ax, result, y_max, cutoff_frequencies=False, 567 | arrow_dir=None, material_velocities=False, plt_kwargs={}): 568 | """Generate a dispersion plot for a family of modes (symmetric 569 | or antisymmetric). 570 | 571 | Parameters 572 | ---------- 573 | ax : axes 574 | Matplotlib axes in which the plot will be added. 575 | result : dict 576 | A dictionary with a result (vp, vg or k) interpolator at 577 | each mode. 578 | y_max : float or int 579 | Maximum y value in the plot. 580 | cutoff_frequencies : bool, optional 581 | Set to True to add cutoff frequencies to the plot. 582 | arrow_dir : {'up', 'down'}, optional 583 | Set arrows direction of cutoff frequencies. Can be 'up' (for 584 | group velocity plots) or 'down' (for phase velocity plots). 585 | material_velocities : bool, optional 586 | Add material velocities (longitudinal, shear and Rayleigh) 587 | to the plot. Defaults to True. 588 | plt_kwargs : dict, optional 589 | Matplotlib kwargs (to change color, linewidth, linestyle, 590 | etc.). 591 | 592 | """ 593 | 594 | for mode, arr in result.items(): 595 | 596 | # Generate an fd array for each mode and add the 597 | # corresponding mode plot. 598 | 599 | fd = np.arange(np.amin(arr.x), np.amax(arr.x), 0.1) 600 | add_plot(ax, result, mode, fd, **plt_kwargs) 601 | 602 | if cutoff_frequencies: 603 | add_cutoff_freqs(ax, mode, arrow_dir, y_max, 604 | self.c_L, self.c_S) 605 | 606 | if material_velocities: 607 | add_velocities(ax, self.c_L, self.c_S, self.c_R, self.fd_max) 608 | 609 | ax.set_xlim([0, self.fd_max]) 610 | ax.set_ylim([0, y_max]) 611 | 612 | ax.set_xlabel('Frequency × thickness [KHz × mm]') 613 | 614 | def plot_phase_velocity(self, modes='both', cutoff_frequencies=True, 615 | material_velocities=True, save_img=False, 616 | sym_style={'color': 'blue'}, 617 | antisym_style={'color': 'red'}): 618 | """Generate a plot of phase velocity as a function of frequency 619 | × thickness. 620 | 621 | Parameters 622 | ---------- 623 | modes : {'both', 'symmetric', 'antisymmetric'}, optional 624 | Which family of modes to plot. Can be 'symmetric', 625 | 'antisymmetric' or 'both'. Defaults to 'both'. 626 | cutoff_frequencies : bool, optional 627 | Add cutoff frequencies to the plot. Defaults to True. 628 | material_velocities : bool, optional 629 | Add material velocities (longitudinal, shear and Rayleigh) 630 | to the plot. Defaults to True. 631 | save_img : bool, optional 632 | Save the result image as png. Defaults to False. 633 | sym_style : dict, optional 634 | A dictionary with matplotlib kwargs to modify the symmetric 635 | curves (to change color, linewidth, linestyle, etc.). 636 | antisym_style : dict, optional 637 | A dictionary with matplotlib kwargs to modify the 638 | antisymmetric curves (to change color, linewidth, linestyle, 639 | etc.). 640 | 641 | Returns 642 | ------- 643 | fig, ax : matplotlib objects 644 | The figure and the axes of the generated plot. 645 | 646 | """ 647 | 648 | fig, ax = plt.subplots(figsize=(7, 4)) 649 | fig.canvas.manager.set_window_title('Phase Velocity') 650 | 651 | # Calculate the maximum value to scale the ylim of the axes. 652 | 653 | max_sym, max_antisym = find_max(self.vp_sym), find_max(self.vp_antisym) 654 | 655 | if modes == 'symmetric': 656 | self.plot(ax, self.vp_sym, max_sym, cutoff_frequencies, 'down', 657 | material_velocities, plt_kwargs=sym_style) 658 | elif modes == 'antisymmetric': 659 | self.plot(ax, self.vp_antisym, max_antisym, cutoff_frequencies, 660 | 'down', material_velocities, plt_kwargs=antisym_style) 661 | elif modes == 'both': 662 | max_ = max(max_sym, max_antisym) 663 | self.plot(ax, self.vp_sym, max_, cutoff_frequencies, 664 | 'down', material_velocities, plt_kwargs=sym_style) 665 | self.plot(ax, self.vp_antisym, max_, cutoff_frequencies, 666 | 'down', material_velocities, plt_kwargs=antisym_style) 667 | else: 668 | raise Exception('modes must be "symmetric", "antisymmetric"' 669 | 'or "both".') 670 | 671 | ax.legend(loc='lower right') 672 | ax.set_ylabel('Phase Velocity [m/s]') 673 | 674 | if save_img: 675 | fig.savefig(f'results/Phase Velocity - {self.d*1e3} mm ' 676 | f'{self.material} plate.png', 677 | bbox_inches='tight') 678 | 679 | return fig, ax 680 | 681 | def plot_group_velocity(self, modes='both', cutoff_frequencies=True, 682 | save_img=False, sym_style={'color': 'blue'}, 683 | antisym_style={'color': 'red'}): 684 | """Generate a plot of group velocity as a function of frequency 685 | × thickness. 686 | 687 | Parameters 688 | ---------- 689 | modes : {'both', 'symmetric', 'antisymmetric'}, optional 690 | Which family of modes to plot. Can be 'symmetric', 691 | 'antisymmetric' or 'both'. Defaults to 'both'. 692 | cutoff_frequencies : bool, optional 693 | Add cutoff frequencies to the plot. Defaults to True. 694 | save_img : bool, optional 695 | Save the result image as png. Defaults to False. 696 | sym_style : dict, optional 697 | A dictionary with matplotlib kwargs to modify the symmetric 698 | curves (to change color, linewidth, linestyle, etc.). 699 | antisym_style : dict, optional 700 | A dictionary with matplotlib kwargs to modify the 701 | antisymmetric curves (to change color, linewidth, linestyle, 702 | etc.). 703 | 704 | Returns 705 | ------- 706 | fig, ax : matplotlib objects 707 | The figure and the axes of the generated plot. 708 | 709 | """ 710 | 711 | fig, ax = plt.subplots(figsize=(7, 4)) 712 | fig.canvas.manager.set_window_title('Group Velocity') 713 | 714 | # Calculate the maximum value to scale the ylim of the axes. 715 | 716 | max_sym, max_antisym = find_max(self.vg_sym), find_max(self.vg_antisym) 717 | 718 | if modes == 'symmetric': 719 | self.plot(ax, self.vg_sym, max_sym, cutoff_frequencies, 720 | 'up', plt_kwargs=sym_style) 721 | elif modes == 'antisymmetric': 722 | self.plot(ax, self.vg_antisym, max_antisym, cutoff_frequencies, 723 | 'up', plt_kwargs=antisym_style) 724 | elif modes == 'both': 725 | max_ = max(max_sym, max_antisym) 726 | self.plot(ax, self.vg_sym, max_, cutoff_frequencies, 727 | 'up', plt_kwargs=sym_style) 728 | self.plot(ax, self.vg_antisym, max_, cutoff_frequencies, 729 | 'up', plt_kwargs=antisym_style) 730 | else: 731 | raise Exception('modes must be "symmetric", "antisymmetric"' 732 | 'or "both".') 733 | 734 | ax.legend(loc='lower right') 735 | ax.set_ylabel('Group Velocity [m/s]') 736 | 737 | if save_img: 738 | fig.savefig(f'results/Group Velocity - {self.d*1e3} mm ' 739 | f'{self.material} plate.png', 740 | bbox_inches='tight') 741 | 742 | return fig, ax 743 | 744 | def plot_wave_number(self, modes='both', save_img=False, 745 | sym_style={'color': 'blue'}, 746 | antisym_style={'color': 'red'}): 747 | """Generate a plot of wavenumber as a function of frequency × 748 | thickness. 749 | 750 | Parameters 751 | ---------- 752 | modes : {'both', 'symmetric', 'antisymmetric'}, optional 753 | Which family of modes to plot. Can be 'symmetric', 754 | 'antisymmetric' or 'both'. Defaults to 'both'. 755 | save_img : bool, optional 756 | Save the result image as png. Defaults to False. 757 | sym_style : dict, optional 758 | A dictionary with matplotlib kwargs to modify the symmetric 759 | curves (to change color, linewidth, linestyle, etc.). 760 | antisym_style : dict, optional 761 | A dictionary with matplotlib kwargs to modify the 762 | antisymmetric curves (to change color, linewidth, linestyle, 763 | etc.). 764 | 765 | Returns 766 | ------- 767 | fig, ax : matplotlib objects 768 | The figure and the axes of the generated plot. 769 | 770 | """ 771 | 772 | fig, ax = plt.subplots(figsize=(7, 4)) 773 | fig.canvas.manager.set_window_title('Wave Number') 774 | 775 | # Calculate the maximum value to scale the ylim of the axes. 776 | 777 | max_sym, max_antisym = find_max(self.k_sym), find_max(self.k_antisym) 778 | 779 | if modes == 'symmetric': 780 | self.plot(ax, self.k_sym, max_sym, plt_kwargs=sym_style) 781 | elif modes == 'antisymmetric': 782 | self.plot(ax, self.k_antisym, max_antisym, plt_kwargs=antisym_style) 783 | elif modes == 'both': 784 | max_ = max(max_sym, max_antisym) 785 | self.plot(ax, self.k_sym, max_, plt_kwargs=sym_style) 786 | self.plot(ax, self.k_antisym, max_, plt_kwargs=antisym_style) 787 | else: 788 | raise Exception('modes must be "symmetric", "antisymmetric"' 789 | 'or "both".') 790 | 791 | ax.legend(loc='upper left') 792 | ax.set_ylabel('Wave Number [1/m]') 793 | 794 | if save_img: 795 | fig.savefig(f'results/Wave Number - {self.d*1e3} mm ' 796 | f'{self.material} plate.png', 797 | bbox_inches='tight') 798 | 799 | return fig, ax 800 | 801 | def plot_wave_structure(self, mode, nrows, ncols, fd, save_img=False, 802 | inplane_style={'color': 'blue'}, 803 | outofplane_style={'color': 'red'}): 804 | """Generate a plot of the wave structure, i.e., the in-plane and 805 | out-of-plane displacement profiles across the thickness of the 806 | plate. 807 | 808 | Parameters 809 | ---------- 810 | mode : str 811 | Mode to be analyzed. Can be "A0", "A1", "A2", ..., "An" or 812 | "S0", "S1", "S2", ..., "Sn", with 'n' being the order of the 813 | corresponding mode. 814 | nrows : int 815 | Number of rows in the subplot. 816 | ncols : int 817 | Number of columns in the subplot. 818 | fd : array 819 | Array with the frequency × thickness points to analyze. The 820 | length of the array must be equal to nrows x ncols. 821 | save_img : bool, optional 822 | Save the result image as png. Defaults to False. 823 | inplane_style : dict, optional 824 | A dictionary with matplotlib kwargs to modify the in-plane 825 | curves (to change color, linewidth, linestyle, etc.). 826 | outofplane_style : dict, optional 827 | A dictionary with matplotlib kwargs to modify the 828 | out-of-plane curves (to change color, linewidth, linestyle, 829 | etc.). 830 | 831 | Returns 832 | ------- 833 | fig, axs : matplotlib objects 834 | The figure and the axes of the generated plot. 835 | 836 | """ 837 | 838 | y = np.linspace(-self.h, self.h, 100) 839 | 840 | fig, axs = plt.subplots(nrows=nrows, ncols=ncols) 841 | fig.canvas.manager.set_window_title(f'Wave Structure (mode {mode})') 842 | 843 | fig.suptitle('Mode $\mathregular{' + mode[0] + '_' + mode[1:] + '}$') 844 | 845 | for ax, freq in zip(axs.flatten(), fd): 846 | if mode[0] == 'S' and int(mode[1:]) < self.nmodes_sym: 847 | vp = self.vp_sym[mode](freq) 848 | elif mode[0] == 'A' and int(mode[1:]) < self.nmodes_antisym: 849 | vp = self.vp_antisym[mode](freq) 850 | else: 851 | raise Exception('mode not recognized. Mode must be "Sn" or ' 852 | '"An", where n is an integer greater or equal ' 853 | 'than 0. For example: "S0", "S1", "A0", "A1", ' 854 | 'etc. Make sure the mode order selected is ' 855 | 'within the number of modes requested when ' 856 | 'setting up the Lamb class.') 857 | 858 | u, w = self._calc_wave_structure(mode[0], vp, freq, y) 859 | 860 | # All values of u, w are purely real or purely imaginary. 861 | 862 | if np.all(np.iscomplex(u)): 863 | ax.plot(np.imag(u), y, label='In plane', **inplane_style) 864 | else: 865 | ax.plot(np.real(u), y, label='In plane', **inplane_style) 866 | 867 | if np.all(np.isreal(w)): 868 | ax.plot(np.real(w), y, label='Out of plane', **outofplane_style) 869 | else: 870 | ax.plot(np.imag(w), y, label='Out of plane', **outofplane_style) 871 | 872 | ax.set_title(f'fd: {np.around(freq, 1)} KHz × mm') 873 | 874 | ax.set_ylim([-self.h, self.h]) 875 | 876 | ax.set_yticks([-self.h, 0, self.h]) 877 | ax.set_yticklabels(['-d/2', '0', 'd/2']) 878 | 879 | ax.spines['left'].set_position('zero') 880 | ax.spines['right'].set_color('none') 881 | ax.spines['bottom'].set_position('zero') 882 | ax.spines['top'].set_color('none') 883 | 884 | ax.xaxis.set_ticks_position('bottom') 885 | ax.yaxis.set_ticks_position('left') 886 | 887 | # TO FIX: For some reason tight_layout() isn't working with some 888 | # subplot configurations, producing overlaping plots (e.g. 889 | # nrows=2 and ncols=4). This happens even if I remove the 890 | # fig.suptitle() and fig.legend() (not considered by 891 | # tight_layout()) 892 | 893 | fig.tight_layout() 894 | 895 | handles, labels = ax.get_legend_handles_labels() 896 | fig.legend(handles, labels, loc='lower center', ncol=2) 897 | 898 | if save_img: 899 | fig.savefig(f'results/Wave Structure - {self.d*1e3} mm ' 900 | f'{self.material} plate - Mode {mode}.png', 901 | bbox_inches='tight') 902 | 903 | return fig, axs 904 | 905 | def save_results(self): 906 | """Save all results to a txt file.""" 907 | 908 | if self.material: 909 | filename = f'{self.material} plate - {self.d*1e3} mm.txt' 910 | else: 911 | filename = f'{self.d*1e3} mm plate.txt' 912 | 913 | header = (f'Material: {self.material}\n' 914 | f'Thickness: {str(self.d*1e3)} mm\n' 915 | f'Longitudinal wave velocity: {str(self.c_L)} m/s\n' 916 | f'Shear wave velocity: {str(self.c_S)} m/s\n\n') 917 | 918 | write_txt(self.vp_sym, self.vp_antisym, 'Phase Velocity', 919 | filename, header) 920 | write_txt(self.vg_sym, self.vg_antisym, 'Group Velocity', 921 | filename, header) 922 | write_txt(self.k_sym, self.k_antisym, 'Wavenumber', 923 | filename, header) --------------------------------------------------------------------------------