├── .buildinfo ├── .gitattributes ├── .gitignore ├── .nojekyll ├── .travis.yml ├── CONTRIBUTING.rst ├── LICENSE.txt ├── Makefile ├── build └── lib │ ├── readme.rst │ └── vibration_toolbox │ ├── __init__.py │ ├── conftest.py │ ├── continuous_systems.py │ ├── data │ ├── case1.mat │ ├── case2.mat │ └── frf_data1.mat │ ├── ema.py │ ├── mdof.py │ ├── readme.rst │ ├── sdof.py │ └── vibesystem.py ├── create_distro.rst ├── dist └── vibration_toolbox-0.6.10-py35-none-any.whl ├── docs ├── Installing_Python.rst ├── Makefile ├── conf.py ├── contributors.rst ├── downloads.rst ├── examples │ ├── Beats.ipynb │ ├── Power spectrum density of data.ipynb │ ├── index.rst │ ├── requirements.txt │ ├── system_damp.png │ ├── vibesystem_notebook-damper.ipynb │ ├── vtb1_1.ipynb │ ├── vtoolbox Demos.ipynb │ └── working_notebook.ipynb ├── index.rst ├── installation.rst ├── readme.rst ├── reference │ ├── continuous.rst │ ├── ema.rst │ ├── index.rst │ ├── mdof.rst │ ├── sdof.rst │ └── vibe_system.rst └── tutorial │ ├── Playground.ipynb │ ├── continuous_systems_notebook.ipynb │ ├── index.rst │ ├── mdof_notebook.ipynb │ ├── requirements.txt │ ├── sdof_characteristic_responses.ipynb │ ├── sdof_notebook.ipynb │ ├── system.png │ └── vibesystem_notebook.ipynb ├── pytest.ini ├── readme.rst ├── requirements.txt ├── setup.cfg ├── setup.py └── vibration_toolbox ├── __init__.py ├── conftest.py ├── continuous_systems.py ├── data ├── case1.mat ├── case2.mat └── frf_data1.mat ├── ema.py ├── mdof.py ├── readme.rst ├── sdof.py ├── tests ├── test_continuous.py ├── test_ema.py ├── test_mdof.py ├── test_sdof.py └── test_vibesystem.py └── vibesystem.py /.buildinfo: -------------------------------------------------------------------------------- 1 | # Sphinx build info version 1 2 | # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. 3 | config: 1bb4368e641abbfb002901bf1c849d8e 4 | tags: 645f666f9bcd5a90fca523b33c5a78b7 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/.gitattributes -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.*~ 2 | 3 | ======= 4 | 5 | Vibration_Toolbox.egg-info 6 | Vibration_Toolbox.egg-info/* 7 | VibrationToolbox.egg-info 8 | 9 | # OS generated files # 10 | .DS_Store 11 | .DS_Store? 12 | ._* 13 | .Spotlight-V100 14 | .Trashes 15 | ehthumbs.db 16 | Thumbs.db 17 | 18 | .cache/ 19 | docs/_build 20 | .ipynb_check* 21 | .ipynb*check*/* 22 | .#.* 23 | \.ipynb_checkpoints/** 24 | .ipynb_checkpoints 25 | .ipynb* 26 | .#vtoolbox.py 27 | *.pdf 28 | .eggs 29 | 30 | Untitled*.ipynb 31 | 32 | 33 | *.*~ 34 | __pycache__ 35 | .idea/ 36 | /vibration_toolbox/.cache/ 37 | /.project 38 | .ropeproject/config.py 39 | .pytest_cache/v/cache/nodeids 40 | .pytest_cache/v/cache/lastfailed 41 | .vscode/settings.json 42 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/.nojekyll -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | # - "3.5" 5 | - "3.6" 6 | 7 | before_install: 8 | - mkdir -p $HOME/.config/matplotlib 9 | 10 | install: 11 | - pip install -r requirements.txt 12 | - pip install . 13 | 14 | cache: 15 | directories: 16 | - $HOME/.cache/matplotlib 17 | 18 | script: 19 | - pytest 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Reporting bugs 2 | -------------- 3 | If you find a bug, please open an issue on the `Github issues tracker `_. 4 | Please provide some code that reproduces the error and versions of the packages installed. 5 | 6 | Contributing code 7 | ----------------- 8 | To contribute code we recommend you follow these steps: 9 | 10 | 1. Fork the repository on github 11 | 12 | 2. Set up travis-ci for your fork. This is actually pretty quick and easy: 13 | 14 | #. Go to travis-ci.org and Sign in with GitHub. 15 | 16 | #. Account page will show all the repositories attached to GitHub. 17 | 18 | #. Find the right repository and enable Travis CI. 19 | 20 | #. Once this is done, Travis CI will be turned-on in GitHub fork. 21 | 22 | #. Go back to the fork on GitHub, click 23 | 24 | ``Settings`` -> ``Webhooks`` -> ``updated travis-ci.org link``. 25 | 26 | #. Default or customize the options based on needs and click 27 | 28 | ``Update webhook``. 29 | 30 | 3. Clone the repository to your favorite location on your drive where you want to work on it. 31 | 32 | 4. Before installing in developer mode, please be sure to ``pip uninstall vibration_toolbox`` if it is already installed. 33 | 34 | 5. To work in `developer mode `_, at the top level directory inside the ``vibration toolbox`` type:: 35 | 36 | $ pip install -e . 37 | 38 | This will allow you to edit the code while having it pretend to be installed. Keep in mind, if you have actually installed the ``vibration toolbox`` you may have a conflict. You must uninstall it and install your development version with the command above.[*]_ 39 | 40 | 6. Add your code/make your modifications, committing to your branch. 41 | 42 | #. On some applications you need to stage then commit. 43 | 44 | #. After committing, some applications will require you to do a push-pull 45 | 46 | #. The pull makes sure that what's on your computer is up to date with what's in your fork on GitHub. 47 | 48 | #. The push moves your changes to the GitHub repository. 49 | 50 | 7. If a new function is added 51 | please provide docstrings following the `Numpy standards for docstrings `_. 52 | The docstrings should contain examples to be tested. 53 | 54 | Specifically note: 55 | 56 | 1. Parameters should be listed similarly to: 57 | 58 | | filename : str 59 | | copy : bool 60 | | dtype : data-type 61 | | iterable : iterable object 62 | | shape : int or tuple of int 63 | | files : list of str 64 | | time : array_like 65 | 66 | 2. First line should be inline with the ``"""`` and brief enough to fit on one line. 67 | 68 | 3. There must be a blank line after the first line. 69 | 70 | This is not exhaustive. It just highlights some consistent errors made. 71 | 72 | 8. Run the doctests regularly when you make edits. 73 | 74 | To run the doctests, `pytest `_ is needed and can be installed with ``pip install -U pytest``. 75 | 76 | To run the tests from the shell you can `cd` to the project's root directory and type:: 77 | 78 | $ pytest 79 | 80 | 1. To run the tests from ``pycharm`` you can do: Run -> Edit Configurations -> Add -> python tests -> pytest Then just set the path to the project directory. 81 | 82 | 2. To run the tests from ``spyder`` see `spyder-unittest `_ tests regularly. Having a great number of changes before a commit can make tracing errors very hard. Make sure you are looking at your branch when assessing whether it's working. 85 | 86 | 10. `Update from the main repository `_ regularly, and certainly before submitting a pull request. This allows you to see the complete results before we look at them. If it doesn't work, the pull will (should) be denied. 87 | 88 | Alternatively, from the command line at the top directory within the repository: 89 | 90 | .. code-block:: bash 91 | 92 | git pull origin master 93 | 94 | `Reconcile any merge conflicts`_. Basically, you will edit the files that it complains about to choose which edits win and which edits lose. 95 | 96 | After this push commits to your repository (most likely using GitHub Desktop) and **then** submit a pull request if it passes the TravisCI tests. 97 | 98 | 11. If the tests are passing, make a git pull (in your GitHub app) to assure that your code is up to date with your master branch and that your code has no conflicts with the current base. Doing this regularly ensures that your accumulated edits won't be massively in conflict with the existing code base. After that, push your branch to GitHub and then open a pull request on GitHub. 99 | 100 | Instructions bellow are directed to main developers 101 | =================================================== 102 | 103 | To make distribution and release 104 | -------------------------------- 105 | 106 | 1) Edit the version number in ``vibration_toolbox/__init__.py`` 107 | 2) Use the Makefile, ``make release`` 108 | 109 | The ``conf.py`` file for the documentation pulls the version from ``__init__.py`` 110 | 111 | To make a distribition (for testing or posting to github) 112 | ----------------------------------------------------------- 113 | 114 | .. code-block:: bash 115 | 116 | >> make wheel 117 | 118 | To test before release 119 | ---------------------- 120 | 121 | Check the Travis CI logs. They are more comprehensive. 122 | 123 | To test distribution installabilty 124 | ----------------------------------- 125 | Note: these are out of date and saved only for historical reasons. 126 | 127 | python setup.py register -r pypitest 128 | python setup.py sdist upload -r pypitest 129 | 130 | look at https://testpypi.python.org/pypi 131 | 132 | Other information sites 133 | ------------------------ 134 | 135 | `twine notes `_ 136 | 137 | https://pypi.python.org/pypi/wheel 138 | 139 | .. [*] The top level directory contains ``CONTRIBUTING.rst``, ``LICENSE.txt``, ``requirements.txt``, etc. 140 | 141 | .. _`Reconcile any merge conflicts`: 142 | https://help.github.com/articles/addressing-merge-conflicts/ 143 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | vibration_toolbox 2 | ================== 3 | Copyright (C) 1993 by Joseph C. Slater and Raphael Timbó 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This Makefile was created by transforming the one for oct2py 2 | # The line #------------------ shows where editing to date has been completed 3 | # for the first attempt. 4 | # Note: This is meant for vibration_toolbox developer use only 5 | 6 | .PHONY: all clean test cover release gh-pages docs 7 | 8 | # I don't know what the following line means/does 9 | #export TEST_ARGS=--exe -v --with-doctest 10 | export NAME=vibration_toolbox 11 | 12 | export GHP_MSG="Generated gh-pages for `git log master -1 --pretty=short --abbrev-commit`" 13 | export BDR_MSG="Generated binder branch" 14 | export VERSION=`python -c "import $(NAME); print($(NAME).__version__)"` 15 | 16 | #all: clean 17 | # python setup.py install 18 | 19 | #---------------------------------------------------- 20 | 21 | help: 22 | @echo "Please use \`make ' where is one of:" 23 | @echo " clean to clear build files" 24 | @echo " test to test all docstring examples" 25 | @echo " cover to test coverage (not working yet)" 26 | @echo " release to edit version, build docs and release" 27 | @echo " wheel build wheel file (for local use)" 28 | @echo " wheel-dist build wheel and push to github" 29 | @echo " docs build docs using sphinx" 30 | @echo " html alias for docs" 31 | @echo " gh-pages build and release docs" 32 | @echo " binder make binder branch" 33 | 34 | clean: 35 | rm -rf build 36 | rm -rf dist 37 | find . -name "*.pyc" -o -name "*.py,cover"| xargs rm -f 38 | # killall -9 nosetests; true 39 | 40 | test: 41 | pytest 42 | 43 | cover: clean 44 | pip install nose-cov 45 | nosetests $(TEST_ARGS) --with-cov --cov $(NAME) $(NAME) 46 | coverage annotate 47 | 48 | release: clean 49 | pip install --user readme_renderer 50 | #python setup.py check -r -s 51 | pytest 52 | #python setup.py register 53 | rm -rf dist 54 | python setup.py bdist_wheel 55 | # python setup.py sdist 56 | git tag v$(VERSION) 57 | git push origin --all 58 | git push origin --tags 59 | # printf '\nUpgrade vibration toolbox with release and sha256 sum:' 60 | # printf '\nOK, no sha256 sum yet:' 61 | twine upload dist/* 62 | # shasum -a 256 dist/*.tar.gz 63 | 64 | wheel: 65 | rm -rf dist 66 | python setup.py bdist_wheel 67 | 68 | wheel-dist: gh-pages 69 | rm -rf dist 70 | python setup.py bdist_wheel 71 | 72 | docs: 73 | # Warnings become errors and stop build 74 | export SPHINXOPTS=-W 75 | # pip install sphinx-bootstrap-theme numpydoc sphinx ghp-import 76 | # Run the make file in the docs directory 77 | make -C docs clean 78 | make -C docs html 79 | 80 | html: docs 81 | 82 | gh-pages: 83 | git checkout master 84 | git pull origin master 85 | git commit -a -m "Keep examples in sync"; true 86 | git push origin; true 87 | make docs 88 | ghp-import -n -p -m $(GHP_MSG) docs/_build/html 89 | 90 | binder: 91 | git checkout master 92 | git pull origin master 93 | git commit -a -m "Keep examples in sync"; true 94 | git push origin; true 95 | ghp-import -n -p -b binder -m $(BDR_MSG) docs/tutorial 96 | -------------------------------------------------------------------------------- /build/lib/readme.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | The Engineering Vibration Toolbox for Python 3 | ============================================= 4 | 5 | .. .. include:: 6 | .. image:: https://badge.fury.io/py/vibration-toolbox.png/ 7 | :target: http://badge.fury.io/py/vibration-toolbox 8 | 9 | .. image:: https://travis-ci.org/vibrationtoolbox/vibration_toolbox.svg?branch=master 10 | :target: https://travis-ci.org/vibrationtoolbox/vibration_toolbox 11 | 12 | .. image:: https://zenodo.org/badge/39572419.svg 13 | :target: https://zenodo.org/badge/latestdoi/39572419 14 | 15 | .. image:: https://mybinder.org/badge.svg 16 | :target: https://mybinder.org/v2/gh/vibrationtoolbox/vibration_toolbox/binder 17 | 18 | .. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg 19 | :target: https://saythanks.io/to/josephcslater 20 | 21 | .. image:: https://img.shields.io/badge/patreon-donate-yellow.svg 22 | :target: https://www.patreon.com/josephcslater 23 | 24 | .. .. image:: https://img.shields.io/pypi/v/vibration_toolbox.svg 25 | :target: https://img.shields.io/pypi/v/vibration_toolbox 26 | 27 | .. #image:: https://coveralls.io/repos/vibrationtoolbox/vibration_toolbox/badge.png?branch=master 28 | .. #:target: https://coveralls.io/r/vibrationtoolbox/vibration_toolbox 29 | 30 | .. image:: http://pepy.tech/badge/vibration-toolbox 31 | :target: http://pepy.tech/project/vibration-toolbox 32 | :alt: PyPi Download stats 33 | 34 | 35 | Joseph C. Slater and Raphael Timbó 36 | ---------------------------------- 37 | 38 | Welcome to the `Vibration Toolbox `_. 39 | This `Python `_ version is a completely new design build for modern education. This is an *educational* set of codes intended primarily for 40 | demonstration of vibration analysis and phenomenon. You may find them useful for application, but that isn't the intent of this toolbox. If you have professional-level needs please `contact the authors `_. 41 | 42 | Full `documentation is available `_, but please excuse that it is still under development. Such documentation has never existed for the other ports of the toolbox so this is taking some time. We don't need feedback at this time, but we will take assistance in improving documentation and code. *Please* clone the repository and support use by submitting pull requests fixing typos and clarifying documentation. 43 | 44 | 45 | Try now! 46 | -------- 47 | 48 | You won't get everything, but you can try parts of the toolbox immediately by riunning the `tutorial on mybinder.org `_, right in your browser window! 49 | 50 | 51 | Installation 52 | ------------ 53 | 54 | If you aren't familiar at all with Python, please see `Installing Python `_. 55 | 56 | Installation is made easy with ``pip`` (or ``pip3``), with releases as we have time while we try 57 | to create a full first release. Much of it works already, but we certainly need 58 | issue reports (on `github `_). 59 | 60 | To install type:: 61 | 62 | pip install --user vibration_toolbox 63 | 64 | at your command prompt **(not the python prompt)** where ``--user`` isn't necessary if you are using a locally installed version of Python such as `Anaconda `_. 65 | 66 | To run, I recommend you open a `Jupyter `_ notebook by using ``jupyter notebook`` at your command prompt/terminal prompt/Anaconda prompt and then type:: 67 | 68 | import vibration_toolbox as vtb 69 | 70 | For examples, see the `JupyterNotebooks folder `_. Some of these have interactive capabilities that are only apparent when you run them yourself instead of just looking at them on GitHub. Unfortunately our organization of these still leaves a little to be desired. Help accepted! 71 | 72 | Installation of current code/contributing 73 | _________________________________________ 74 | 75 | The usage documentation is far behind the current code, while the reference is way ahead of the released code due to the `autodoc `_ capability of `Sphinx `_. Especially as of 2017, the code is still in rapid development. So is the documentation. Releases to `pypi `_. 76 | 77 | If you wish to install the current version of the software, and especially contribute, please follow the instructions in `Contributing.rst `_ 78 | 79 | That should be it. Please note issues on the `issues tab `_ on GitHub. 80 | -------------------------------------------------------------------------------- /build/lib/vibration_toolbox/__init__.py: -------------------------------------------------------------------------------- 1 | from .continuous_systems import * 2 | from .vibesystem import * 3 | from .ema import * 4 | from .mdof import * 5 | from .sdof import * 6 | import matplotlib as mpl 7 | import sys 8 | """The Vibration Toolbox, Python Edition. 9 | 10 | Joseph C. Slater and Raphael Timbó 11 | 12 | `import vibration_toolbox as vtb` will keep them inside the `vtb` namespace 13 | 14 | `import vibration_toolbox.sdof as sdof` will keep the sdof functions in the 15 | `sdof` namespace. 16 | """ 17 | 18 | __title__ = 'vibration_toolbox' 19 | # version may have no more then numerical digits after decimal point. 20 | # 1.11 is actually a higher release than 1.2 (confusing) 21 | __version__ = '0.6.10' 22 | __author__ = u'Joseph C. Slater and Raphael Timbó' 23 | __license__ = 'MIT' 24 | __copyright__ = 'Copyright 1991-2019 Joseph C. Slater' 25 | __all__ = ['sdof', 'mdof', 'ema', 'vibesystem', 'continuous_systems', 26 | '__version__'] 27 | 28 | """ 29 | If the __all__ above is commented out, this code will then execute to 30 | completion, as the default behaviour of import * is to import all symbols 31 | that do not begin with an underscore, from the given namespace. 32 | 33 | Reference: 34 | https://docs.python.org/3.5/tutorial/modules.html#importing-from-a-package 35 | """ 36 | 37 | 38 | if 'pytest' in sys.argv[0]: 39 | # print('Setting backend to agg to run tests') 40 | mpl.use('agg') 41 | 42 | 43 | # print options were change inside modules to produce better 44 | # outputs at examples. Here we set the print options to the 45 | # default values after importing the modules to avoid changing 46 | # np default print options when importing the toolbox. 47 | np.set_printoptions(edgeitems=3, infstr='inf', linewidth=75, 48 | nanstr='nan', precision=8, suppress=False, 49 | threshold=1000, formatter=None) 50 | -------------------------------------------------------------------------------- /build/lib/vibration_toolbox/conftest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | np.set_printoptions(precision=2, suppress=True) 4 | print('Running conftest.py') 5 | 6 | -------------------------------------------------------------------------------- /build/lib/vibration_toolbox/data/case1.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/build/lib/vibration_toolbox/data/case1.mat -------------------------------------------------------------------------------- /build/lib/vibration_toolbox/data/case2.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/build/lib/vibration_toolbox/data/case2.mat -------------------------------------------------------------------------------- /build/lib/vibration_toolbox/data/frf_data1.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/build/lib/vibration_toolbox/data/frf_data1.mat -------------------------------------------------------------------------------- /build/lib/vibration_toolbox/ema.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import scipy.linalg as la 4 | 5 | 6 | def frf(x, f, dt): 7 | r"""Return the frequency response function. 8 | 9 | Calculates :math:`H(i\\omega)`, and coherance of the sampled data. 10 | 11 | Parameters 12 | ---------- 13 | x: array 14 | Array with the displacement data 15 | f: array 16 | Array with the force data 17 | dt: float 18 | Time step of the sampled data 19 | n: int 20 | Number of points in the fft 21 | 22 | Returns 23 | ------- 24 | freq: array 25 | Driving frequencies 26 | mag: array 27 | Magnitude of the frequency response function 28 | ang: array 29 | Phase of the frequency response function 30 | coh: array 31 | Coherence function 32 | 33 | Plot with the frf magnitude, phase and 34 | coherence. 35 | 36 | Examples 37 | -------- 38 | >>> # First we need to load the sampled data which in a .mat file 39 | >>> import vibration_toolbox as vtb 40 | >>> import scipy.io as sio 41 | >>> data = sio.loadmat(vtb.__path__[0] + '/data/frf_data1.mat') 42 | >>> #print(data) 43 | >>> # Data is imported as arrays. Modify then to fit our function 44 | >>> x = data['x'] 45 | >>> x = x.reshape(len(x)) 46 | >>> f = data['f'] 47 | >>> f = f.reshape(len(f)) 48 | >>> dt = data['dt'] 49 | >>> dt = float(dt) 50 | >>> # Now we are able to call the function 51 | >>> freq, mag, ang, coh = vtb.frf(x, f, dt) 52 | >>> mag[10] 53 | 1.018394853080... 54 | 55 | """ 56 | w = np.sin(np.pi * np.arange(len(f)) / len(f))**2 # window 57 | # apply window 58 | xw = x * w 59 | fw = f * w 60 | # take ffts 61 | FX = np.fft.fft(xw) 62 | FF = np.fft.fft(fw) 63 | # calculate the spectral densities 64 | SXF = FF * np.conj(FX) 65 | SXX = FX * np.conj(FX) 66 | SFF = FF * np.conj(FF) 67 | # calculate the frequency response functions 68 | TXF = SXX / SXF 69 | 70 | lt = len(TXF) // 2 71 | freq = np.arange(lt) / (2 * lt * dt) 72 | 73 | TXF = TXF[:lt] 74 | mag = np.absolute(TXF) 75 | ang = np.angle(TXF) * 180 / np.pi 76 | 77 | coh = (np.absolute(SXF)**2) / (SXX * SFF) 78 | coh = np.real(coh) 79 | 80 | # plot H(w) 81 | fig = plt.figure(figsize=(8, 6)) 82 | ax1 = fig.add_subplot(311) 83 | ax2 = fig.add_subplot(312, sharex=ax1) 84 | ax3 = fig.add_subplot(313, sharex=ax2) 85 | fig.tight_layout() 86 | 87 | ax1.set_title(r'$H(\omega)$ - Magnitude') 88 | ax2.set_title(r'$H(\omega)$ - Phase') 89 | ax3.set_title(r'$H(\omega)$ - Coherence') 90 | ax3.set_xlabel('Frequency (Hz)') 91 | ax3.set_ylim(0, 2) 92 | 93 | ax1.semilogy(freq, mag) 94 | ax2.plot(freq, ang) 95 | ax3.plot(freq, coh[:lt]) 96 | 97 | plt.show() 98 | 99 | return freq, mag, ang, coh 100 | 101 | 102 | def plot_fft(t, time_response, ax=None, **kwargs): 103 | """Plot fft ot time response. 104 | 105 | Parameters 106 | ---------- 107 | t : array 108 | Time array. 109 | time_response : array 110 | Array with the system's time response. 111 | ax : matplotlib.axes, optional 112 | Matplotlib axes where the amplitude will be plotted. 113 | If None creates a new. 114 | 115 | Returns 116 | ------- 117 | ax : array with matplotlib.axes, optional 118 | Matplotlib axes array created with plt.subplots. 119 | Plot has frequency in rad/s and magnitude in meters peak to peak. 120 | 121 | Examples 122 | -------- 123 | >>> import vibration_toolbox as vtb 124 | >>> t = np.linspace(0, 10, 1000) 125 | >>> time_response = 2 * np.sin(40*t) 126 | >>> vtb.plot_fft(t, time_response) 127 | >> # First we need to load the sampled data which is in a .mat file 185 | >>> import vibration_toolbox as vtb 186 | >>> import scipy.io as sio 187 | >>> data = sio.loadmat(vtb.__path__[0] + '/data/case1.mat') 188 | >>> #print(data) 189 | >>> # Data is imported as arrays. Modify then to fit our function. 190 | >>> TF = data['Hf_chan_2'] 191 | >>> f = data['Freq_domain'] 192 | >>> # Now we are able to call the function 193 | >>> z, nf, a = vtb.sdof_cf(f,TF,500,1000) 194 | >>> nf 195 | 212.092530551... 196 | 197 | """ 198 | # check fmin fmax existance 199 | if Fmin is None: 200 | inlow = 0 201 | else: 202 | inlow = Fmin 203 | 204 | if Fmax is None: 205 | inhigh = np.size(f) 206 | else: 207 | inhigh = Fmax 208 | 209 | if f[inlow] == 0: 210 | inlow = 1 211 | 212 | f = f[inlow:inhigh, :] 213 | TF = TF[inlow:inhigh, :] 214 | 215 | R = TF 216 | y = np.amax(np.abs(TF)) 217 | cin = np.argmax(np.abs(TF)) 218 | 219 | ll = np.size(f) 220 | 221 | w = f * 2 * np.pi * 1j 222 | 223 | w2 = w * 0 224 | R3 = R * 0 225 | 226 | for i in range(1, ll + 1): 227 | R3[i - 1] = np.conj(R[ll - i]) 228 | w2[i - 1] = np.conj(w[ll - i]) 229 | 230 | w = np.vstack((w2, w)) 231 | R = np.vstack((R3, R)) 232 | 233 | N = 2 234 | x, y = np.meshgrid(np.arange(0, N + 1), R) 235 | x, w2d = np.meshgrid(np.arange(0, N + 1), w) 236 | c = -1 * w**N * R 237 | 238 | aa1 = w2d[:, np.arange(0, N)] \ 239 | ** x[:, np.arange(0, N)] \ 240 | * y[:, np.arange(0, N)] 241 | aa2 = -w2d[:, np.arange(0, N + 1)] \ 242 | ** x[:, np.arange(0, N + 1)] 243 | aa = np.hstack((aa1, aa2)) 244 | 245 | aa = np.reshape(aa, [-1, 5]) 246 | 247 | b, _, _, _ = la.lstsq(aa, c) 248 | 249 | b = b.flatten() 250 | rs = np.roots(np.array([1, 251 | b[1], 252 | b[0]])) 253 | omega = np.abs(rs[1]) 254 | z = -1 * np.real(rs[1]) / np.abs(rs[1]) 255 | nf = omega / 2 / np.pi 256 | 257 | XoF1 = np.hstack(([1 / (w - rs[0]), 1 / (w - rs[1])])) 258 | XoF2 = 1 / (w**0) 259 | XoF3 = 1 / w**2 260 | XoF = np.hstack((XoF1, XoF2, XoF3)) 261 | 262 | # check if extra _ needed 263 | 264 | a, _, _, _ = la.lstsq(XoF, R) 265 | XoF = XoF[np.arange(ll, 2 * ll), :].dot(a) 266 | 267 | a = np.sqrt(-2 * np.imag(a[0]) * np.imag(rs[0]) 268 | - 2 * np.real(a[0]) * np.real(rs[0])) 269 | Fmin = np.min(f) 270 | Fmax = np.max(f) 271 | phase = np.unwrap(np.angle(TF), np.pi, 0) * 180 / np.pi 272 | phase2 = np.unwrap(np.angle(XoF), np.pi, 0) * 180 / np.pi 273 | while phase2[cin] > 50: 274 | phase2 = phase2 - 360 275 | phased = phase2[cin] - phase[cin] 276 | phase = phase + np.round(phased / 360) * 360 277 | 278 | fig = plt.figure() 279 | ax1 = fig.add_subplot(2, 1, 1) 280 | ax2 = fig.add_subplot(2, 1, 2) 281 | fig.tight_layout() 282 | 283 | ax1.set_xlabel('Frequency (Hz)') 284 | ax1.set_ylabel('Magnitude (dB)') 285 | ax1.plot(f, 20 * np.log10(np.abs(XoF)), label="Identified FRF") 286 | ax1.plot(f, 20 * np.log10(np.abs(TF)), label="Experimental FRF") 287 | ax1.legend() 288 | 289 | ax2.set_xlabel('Frequency (Hz)') 290 | ax2.set_ylabel('Phase (deg)') 291 | ax2.plot(f, phase2, label="Identified FRF") 292 | ax2.plot(f, phase, label="Experimental FRF") 293 | ax2.legend() 294 | 295 | plt.show() 296 | 297 | a = a[0]**2 / (2 * np.pi * nf)**2 298 | return z, nf, a 299 | 300 | 301 | def mdof_cf(f, TF, Fmin=None, Fmax=None): 302 | """Curve fit to multiple degree of freedom FRF. 303 | 304 | If Fmin and Fmax are not entered, the first and last elements of TF are 305 | used. 306 | 307 | If the first column of TF is a collocated (input and output location are 308 | the same), then the mode shape returned is the mass normalized mode shape. 309 | This can then be used to generate an identified mass, damping, and 310 | stiffness matrix as shown in the following example. 311 | 312 | Parameters 313 | ---------- 314 | f: array 315 | The frequency vector in Hz. Does not have to start at 0 Hz. 316 | TF: array 317 | The complex transfer function 318 | Fmin: int 319 | The minimum frequency to be used for curve fitting in the FRF 320 | Fmax: int 321 | The maximum frequency to be used for curve fitting in the FRF 322 | 323 | Returns 324 | ------- 325 | z: double 326 | The damping ratio 327 | nf: double 328 | Natural frequency (Hz) 329 | u: array 330 | The mode shape 331 | 332 | Notes 333 | ----- 334 | FRF are columns comprised of the FRFs presuming single input, multiple 335 | output z and nf are the damping ratio and natural frequency (Hz) u is the 336 | mode shape. Only one peak may exist in the segment of the FRF passed to 337 | sdofcf. No zeros may exist within this segment. If so, curve fitting 338 | becomes unreliable. 339 | 340 | Examples 341 | -------- 342 | >>> # First we need to load the sampled data which is in a .mat file 343 | >>> import vibration_toolbox as vtb 344 | >>> import scipy.io as sio 345 | >>> data = sio.loadmat(vtb.__path__[0] + '/data/case2.mat') 346 | >>> #print(data) 347 | >>> # Data is imported as arrays. Modify then to fit our function 348 | >>> TF = data['Hf_chan_2'] 349 | >>> f = data['Freq_domain'] 350 | >>> # Now we are able to call the function 351 | >>> z, nf, a = vtb.mdof_cf(f,TF,500,1000) 352 | >>> nf 353 | 192.59382330... 354 | 355 | """ 356 | # check fmin fmax existance 357 | if Fmin is None: 358 | inlow = 0 359 | else: 360 | inlow = Fmin 361 | 362 | if Fmax is None: 363 | inhigh = np.size(f) 364 | else: 365 | inhigh = Fmax 366 | 367 | if f[inlow] == 0: 368 | inlow = 1 369 | 370 | f = f[inlow:inhigh, :] 371 | TF = TF[inlow:inhigh, :] 372 | 373 | R = TF.T 374 | 375 | U, _, _ = np.linalg.svd(R) 376 | T = U[:, 0] 377 | Hp = np.transpose(T).dot(R) 378 | R = np.transpose(Hp) 379 | 380 | ll = np.size(f) 381 | w = f * 2 * np.pi * 1j 382 | 383 | w2 = w * 0 384 | R3 = R * 0 385 | TF2 = TF * 0 386 | for i in range(1, ll + 1): 387 | R3[i - 1] = np.conj(R[ll - i]) 388 | w2[i - 1] = np.conj(w[ll - i]) 389 | TF2[i - 1, :] = np.conj(TF[ll - i, :]) 390 | 391 | w = np.vstack((w2, w)) 392 | R = np.hstack((R3, R)) 393 | 394 | N = 2 395 | x, y = np.meshgrid(np.arange(0, N + 1), R) 396 | 397 | x, w2d = np.meshgrid(np.arange(0, N + 1), w) 398 | 399 | R = np.ndarray.flatten(R) 400 | w = np.ndarray.flatten(w) 401 | c = -1 * w**N * R 402 | 403 | aa1 = w2d[:, np.arange(0, N)] \ 404 | ** x[:, np.arange(0, N)] \ 405 | * y[:, np.arange(0, N)] 406 | aa2 = -w2d[:, np.arange(0, N + 1)] \ 407 | ** x[:, np.arange(0, N + 1)] 408 | aa = np.hstack((aa1, aa2)) 409 | 410 | b, _, _, _ = la.lstsq(aa, c) 411 | 412 | rs = np.roots(np.array([1, 413 | b[1], 414 | b[0]])) 415 | 416 | # irs = np.argsort(np.abs(np.imag(rs))) # necessary? 417 | 418 | omega = np.abs(rs[1]) 419 | z = -1 * np.real(rs[1]) / np.abs(rs[1]) 420 | nf = omega / 2 / np.pi 421 | 422 | XoF1 = 1 / ((rs[0] - w) * (rs[1] - w)) 423 | 424 | XoF2 = 1 / (w**0) 425 | XoF3 = 1 / w**2 426 | 427 | XoF = np.vstack((XoF1, XoF2, XoF3)).T 428 | TF3 = np.vstack((TF2, TF)) 429 | 430 | a, _, _, _ = la.lstsq(XoF, TF3) 431 | 432 | u = np.transpose(a[0, :]) 433 | 434 | u = u / np.sqrt(np.abs(a[0, 0])) 435 | 436 | return z, nf, u 437 | -------------------------------------------------------------------------------- /build/lib/vibration_toolbox/mdof.py: -------------------------------------------------------------------------------- 1 | """Multiple Degree of Freedom Analysis Tools.""" 2 | 3 | import numpy as np 4 | import scipy.linalg as la 5 | import scipy.signal as signal 6 | import matplotlib as mpl 7 | 8 | __all__ = [ 9 | "modes_system", 10 | "modes_system_undamped", 11 | "response_system", 12 | "response_system_undamped", 13 | ] 14 | 15 | 16 | mpl.rcParams["lines.linewidth"] = 2 17 | mpl.rcParams["figure.figsize"] = (10, 6) 18 | 19 | 20 | def _eigen(A, B=None): 21 | """Return sorted eigenvector/eigenvalue pairs. 22 | 23 | e.g. for a given system linalg.eig will return eingenvalues as: 24 | (array([ 0. +89.4j, 0. -89.4j, 0. +89.4j, 0. -89.4j, 0.+983.2j, 25 | 0.-983.2j, 0. +40.7j, 0. -40.7j]) 26 | This function will sort this eigenvalues as: 27 | (array([ 0. +40.7j, 0. +89.4j, 0. +89.4j, 0.+983.2j, 0. -40.7j, 28 | 0. -89.4j, 0. -89.4j, 0.-983.2j]) 29 | Correspondent eigenvectors will follow the same order. 30 | 31 | Note: Works fine for moderately sized models. Does not leverage the 32 | full set of constraints to optimize the solution. See the vibrationtesting 33 | module for a more advanced solver. 34 | 35 | Parameters 36 | ---------- 37 | A: array 38 | A complex or real matrix whose eigenvalues and eigenvectors 39 | will be computed. 40 | B: float or str 41 | Right-hand side matrix in a generalized eigenvalue problem. 42 | Default is None, identity matrix is assumed. 43 | 44 | Returns 45 | ------- 46 | evalues: array 47 | Sorted eigenvalues 48 | evectors: array 49 | Sorted eigenvalues 50 | 51 | Examples 52 | -------- 53 | >>> import vibration_toolbox as vtb 54 | >>> L = np.array([[2, -1, 0], 55 | ... [-4, 8, -4], 56 | ... [0, -4, 4]]) 57 | >>> lam, P = vtb.mdof._eigen(L) 58 | >>> lam 59 | array([ 0.56+0.j, 2.63+0.j, 10.81+0.j]) 60 | 61 | """ 62 | if B is None: 63 | evalues, evectors = la.eig(A) 64 | else: 65 | evalues, evectors = la.eig(A, B) 66 | 67 | if all(eigs == 0 for eigs in evalues.imag): 68 | if all(eigs > 0 for eigs in evalues.real): 69 | idxp = evalues.real.argsort() # positive in increasing order 70 | idxn = np.array([], dtype=int) 71 | else: 72 | # positive in increasing order 73 | idxp = evalues.real.argsort()[int(len(evalues) / 2):] 74 | # negative in decreasing order 75 | idxn = evalues.real.argsort()[int(len(evalues) / 2) - 1:: -1] 76 | 77 | else: 78 | # positive in increasing order 79 | idxp = evalues.imag.argsort()[int(len(evalues) / 2):] 80 | # negative in decreasing order 81 | idxn = evalues.imag.argsort()[int(len(evalues) / 2) - 1:: -1] 82 | 83 | idx = np.hstack([idxp, idxn]) 84 | 85 | return evalues[idx], evectors[:, idx] 86 | 87 | 88 | def _normalize(X, Y): 89 | """ 90 | Return normalized left eigenvectors. 91 | 92 | This function is used to normalize vectors of the matrix 93 | Y with respect to X so that Y.T @ X = I (identity). 94 | This is used to normalize the matrix with the left eigenvectors. 95 | 96 | Parameters 97 | ---------- 98 | X: array 99 | A complex or real matrix 100 | Y: array 101 | A complex or real matrix to be normalized 102 | 103 | Returns 104 | ------- 105 | Yn: array 106 | Normalized matrix 107 | 108 | Examples 109 | -------- 110 | >>> # This test has been moved to tests_mdof 111 | >>> X = np.array([[ 0.84+0.j , 0.14-0.j , 0.84-0.j , 0.14+0.j ], 112 | ... [ 0.01-0.3j , 0.00+0.15j, 0.01+0.3j , 0.00-0.15j], 113 | ... [-0.09+0.42j, -0.01+0.65j, -0.09-0.42j, -0.01-0.65j], 114 | ... [ 0.15+0.04j, -0.74+0.j , 0.15-0.04j, -0.74-0.j ]]) 115 | >>> Y = np.array([[-0.03-0.41j, 0.04+0.1j , -0.03+0.41j, 0.04-0.1j ], 116 | ... [ 0.88+0.j , 0.68+0.j , 0.88-0.j , 0.68-0.j ], 117 | ... [-0.21-0.j , 0.47+0.05j, -0.21+0.j , 0.47-0.05j], 118 | ... [ 0.00-0.08j, 0.05-0.54j, 0.00+0.08j, 0.05+0.54j]]) 119 | >>> Yn = _normalize(X, Y) 120 | >>> Yn # doctest: +SKIP 121 | array([[ 0.58-0.05j, 0.12-0.06j, 0.58+0.05j, 0.12+0.06j], 122 | [ 0.01+1.24j, -0.07-0.82j, 0.01-1.24j, -0.07+0.82j], 123 | [-0. -0.3j , 0.01-0.57j, -0. +0.3j , 0.01+0.57j], 124 | [ 0.11-0.j , -0.66-0.01j, 0.11+0.j , -0.66+0.01j]]) 125 | 126 | """ 127 | Yn = np.zeros_like(X) 128 | YTX = Y.T @ X # normalize y so that Y.T @ X will return I 129 | factors = [1 / a for a in np.diag(YTX)] 130 | # multiply each column in y by a factor in 'factors' 131 | for col in enumerate(Y.T): 132 | Yn[col[0]] = col[1] * factors[col[0]] 133 | Yn = Yn.T 134 | 135 | return Yn 136 | 137 | 138 | def modes_system_undamped(M, K): 139 | r"""Return eigensolution of multiple DOF system. 140 | 141 | Returns the natural frequencies (w), 142 | eigenvectors (P), mode shapes (S) and the modal transformation 143 | matrix S for an undamped system. 144 | 145 | See Notes for explanation of the underlying math. 146 | 147 | Parameters 148 | ---------- 149 | M: float array 150 | Mass matrix 151 | K: float array 152 | Stiffness matrix 153 | 154 | Returns 155 | ------- 156 | w: float array 157 | The natural frequencies of the system 158 | P: float array 159 | The eigenvectors of the system. 160 | S: float array 161 | The mass-normalized mode shapes of the system. 162 | Sinv: float array 163 | The modal transformation matrix S^-1(takes x -> r(modal coordinates)) 164 | 165 | Notes 166 | ----- 167 | Given :math:`M\ddot{x}(t)+Kx(t)=0`, with mode shapes :math:`u`, the matrix 168 | of mode shapes :math:`S=[u_1 u_1 \ldots]` can be created. If the modal 169 | coordinates are the vector :math:`r(t)`. The modal transformation separates 170 | space and time from :math:`x(t)` such that :math:`x(t)=S r(t)`. 171 | Substituting into the governing equation: 172 | 173 | :math:`MS\ddot{r}(t)+KSr(t)=0` 174 | 175 | Premultiplying by :math:`S^T` 176 | 177 | :math:`S^TMS\ddot{r}(t)+S^TKSr(t)=0` 178 | 179 | The matrices :math:`S^TMS` and :math:`S^TKS` will be diagonalized by this 180 | process (:math:`u_i` are the eigenvectors of :math:`M^{-1}K`). 181 | 182 | If scaled properly (mass normalized so :math:`u_i^TMu_i=1`) then 183 | :math:`S^TMS=I` and :math:`S^TKS=\Omega^2` where :math:`\Omega^2` is a 184 | diagonal matrix of the natural frequencies squared in radians per second. 185 | 186 | Further, inverses are unstable so the better way to solve linear equations is with 187 | Gauss elimination. 188 | 189 | :math:`AB=C` given known :math:`A` and :math:`C` 190 | is solved using `la.solve(A, C, assume_a='pos')`. 191 | 192 | :math:`BA=C` given known :math:`A` and :math:`C` is solved by first 193 | transposing the equation to :math:`A^TB^T=C^T`, then solving for 194 | :math:`C^T`. The resulting command is 195 | `la.solve(A.T, C.T, assume_a='pos').T` 196 | 197 | Examples 198 | -------- 199 | >>> M = np.array([[4, 0, 0], 200 | ... [0, 4, 0], 201 | ... [0, 0, 4]]) 202 | >>> K = np.array([[8, -4, 0], 203 | ... [-4, 8, -4], 204 | ... [0, -4, 4]]) 205 | >>> w, P, S, Sinv = modes_system_undamped(M, K) 206 | >>> w # doctest: +SKIP 207 | array([0.45, 1.25, 1.8 ]) 208 | >>> S 209 | array([[ 0.16, -0.37, -0.3 ], 210 | [ 0.3 , -0.16, 0.37], 211 | [ 0.37, 0.3 , -0.16]]) 212 | 213 | """ 214 | L = la.cholesky(M) 215 | lam, P = _eigen(la.solve(L, la.solve(L, K, assume_a='pos').T, 216 | assume_a='pos').T) 217 | w = np.real(np.sqrt(lam)) 218 | S = la.solve(L, P, assume_a="pos") 219 | Sinv = la.solve(L.T, P, assume_a="pos").T 220 | 221 | return w, P, S, Sinv 222 | 223 | 224 | def modes_system(M, K, C=None): 225 | """Natural frequencies, damping ratios, and mode shapes of MDOF system. 226 | This function will return the natural frequencies (wn), the 227 | damped natural frequencies (wd), the damping ratios (zeta), 228 | the right eigenvectors (X) and the left eigenvectors (Y) for a 229 | system defined by M, K and C. 230 | If the dampind matrix 'C' is none or if the damping is proportional, 231 | wd and zeta will be none and X and Y will be equal. 232 | 233 | Parameters 234 | ---------- 235 | M: array 236 | Mass matrix 237 | K: array 238 | Stiffness matrix 239 | C: array 240 | Damping matrix 241 | 242 | Returns 243 | ------- 244 | wn: array 245 | The natural frequencies of the system 246 | wd: array 247 | The damped natural frequencies of the system 248 | zeta: array 249 | The damping ratios 250 | X: array 251 | The right eigenvectors 252 | Y: array 253 | The left eigenvectors 254 | 255 | Examples 256 | -------- 257 | >>> # This test has been moved to tests_mdof 258 | >>> M = np.array([[1, 0], 259 | ... [0, 1]]) 260 | >>> K = np.array([[2, -1], 261 | ... [-1, 6]]) 262 | >>> C = np.array([[0.3, -0.02], 263 | ... [-0.02, 0.1]]) 264 | >>> wn, wd, zeta, X, Y = modes_system(M, K, C) # doctest: +SKIP 265 | Damping is non-proportional, eigenvectors are complex. 266 | >>> wn # doctest: +SKIP 267 | array([1.33, 2.5 , 1.33, 2.5 ]) 268 | >>> wd # doctest: +SKIP 269 | array([1.32, 2.5 , 1.32, 2.5 ]) 270 | >>> zeta # doctest: +SKIP 271 | array([0.11, 0.02, 0.11, 0.02]) 272 | >>> X # doctest: +SKIP 273 | array([[-0.06-0.58j, -0.01+0.08j, -0.06+0.58j, -0.01-0.08j], 274 | [-0. -0.14j, -0.01-0.36j, -0. +0.14j, -0.01+0.36j], 275 | [ 0.78+0.j , -0.21-0.03j, 0.78-0.j , -0.21+0.03j], 276 | [ 0.18+0.01j, 0.9 +0.j , 0.18-0.01j, 0.9 -0.j ]]) 277 | >>> Y # doctest: +SKIP 278 | array([[ 0.02+0.82j, 0.01-0.31j, 0.02-0.82j, 0.01+0.31j], 279 | [-0.05+0.18j, 0.01+1.31j, -0.05-0.18j, 0.01-1.31j], 280 | [ 0.61+0.06j, -0.12-0.02j, 0.61-0.06j, -0.12+0.02j], 281 | [ 0.14+0.03j, 0.53+0.j , 0.14-0.03j, 0.53-0.j ]]) 282 | >>> C = 0.2*K # with proportional damping 283 | >>> wn, wd, zeta, X, Y = modes_system(M, K, C) # doctest: +SKIP 284 | Damping is proportional or zero, eigenvectors are real 285 | >>> X # doctest: +SKIP 286 | array([[-0.97, 0.23], 287 | [-0.23, -0.97]]) 288 | """ 289 | 290 | n = len(M) 291 | 292 | Z = np.zeros((n, n)) 293 | I = np.eye(n) 294 | 295 | if ( 296 | C is None or 297 | np.all(C == 0) or 298 | la.norm( # check if C has only zero entries 299 | la.solve(M, C, assume_a="pos") @ K - \ 300 | la.solve(M, K, assume_a="pos") @ C, 2 301 | ) < 302 | 1e-8 * la.norm(la.solve(M, K, assume_a="pos") @ C, 2) 303 | ): 304 | w, P, S, Sinv = modes_system_undamped(M, K) 305 | wn = w 306 | wd = w 307 | # zeta = None 308 | zeta = np.diag(S.T @ C @ S) / 2 / wn 309 | wd = wn * np.sqrt(1 - zeta ** 2) 310 | X = P 311 | Y = P 312 | print("Damping is proportional or zero, eigenvectors are real") 313 | return wn, wd, zeta, X, Y 314 | 315 | Z = np.zeros((n, n)) 316 | I = np.eye(n) 317 | 318 | # creates the state space matrix 319 | A = np.vstack( 320 | [ 321 | np.hstack([Z, I]), 322 | np.hstack( 323 | [-la.solve(M, K, assume_a="pos"), 324 | - la.solve(M, C, assume_a="pos")] 325 | ), 326 | ] 327 | ) 328 | 329 | w, X = _eigen(A) 330 | _, Y = _eigen(A.T) 331 | 332 | wd = abs(np.imag(w)) 333 | wn = np.absolute(w) 334 | zeta = -np.real(w) / np.absolute(w) 335 | 336 | Y = _normalize(X, Y) 337 | 338 | print("Damping is non-proportional, eigenvectors are complex.") 339 | 340 | return wn, wd, zeta, X, Y 341 | 342 | 343 | def response_system_undamped(M, K, x0, v0, max_time): 344 | """ 345 | This function calculates the time response for an undamped system 346 | and returns the vector (state-space) X. The n first rows contain the 347 | displacement (x) and the n last rows contain velocity (v) for each 348 | coordinate. Each column is related to a time-step. 349 | The time array is also returned. 350 | 351 | Parameters 352 | ---------- 353 | M: array 354 | Mass matrix 355 | K: array 356 | Stiffness matrix 357 | x0: array 358 | Array with displacement initial conditions 359 | v0: array 360 | Array with velocity initial conditions 361 | max_time: float 362 | End time 363 | 364 | Returns 365 | ------- 366 | t: array 367 | Array with the time 368 | X: array 369 | The state-space vector for each time 370 | 371 | Examples 372 | -------- 373 | >>> M = np.array([[1, 0], 374 | ... [0, 4]]) 375 | >>> K = np.array([[12, -2], 376 | ... [-2, 12]]) 377 | >>> x0 = np.array([1, 1]) 378 | >>> v0 = np.array([0, 0]) 379 | >>> max_time = 10 380 | >>> t, X = response_system_undamped(M, K, x0, v0, max_time) 381 | >>> # first column is the initial conditions [x1, x2, v1, v2] 382 | >>> X[:, 0] # doctest: +SKIP 383 | array([1., 1., 0., 0.]) 384 | >>> X[:, 1] # displacement and velocities after delta t 385 | array([ 1. , 1. , -0.04, -0.01]) 386 | """ 387 | 388 | t = np.linspace(0, max_time, int(250 * max_time)) 389 | dt = t[1] - t[0] 390 | 391 | n = len(M) 392 | 393 | Z = np.zeros((n, n)) 394 | I = np.eye(n, n) 395 | 396 | # creates the state space matrix 397 | A = np.vstack([np.hstack([Z, I]), np.hstack( 398 | [-la.solve(M, K, assume_a="pos"), Z])]) 399 | 400 | # creates the x array and set the first line according to the initial 401 | # conditions 402 | X = np.zeros((2 * n, len(t))) 403 | X[:, 0] = np.hstack([x0, v0]) 404 | 405 | Ad = la.expm(A * dt) 406 | for i in range(len(t) - 1): 407 | X[:, i + 1] = Ad @ X[:, i] 408 | 409 | return t, X 410 | 411 | 412 | def response_system(M, C, K, F, x0, v0, t): 413 | """ 414 | Returns system response given the initial 415 | displacement vector 'X0', initial velocity vector 'V0', 416 | the mass matrix 'M', the stiffness matrix 'M', and the damping 417 | matrix 'C' and force 'F'. 418 | T is a row vector of evenly spaced times. 419 | F is a matrix of forces over time, each column corresponding 420 | to the corresponding column of T, each row corresponding to 421 | the same numbered DOF. 422 | 423 | Parameters 424 | ---------- 425 | M: array 426 | Mass matrix 427 | K: array 428 | Stiffness matrix 429 | C: array 430 | Damping matrix 431 | x0: array 432 | Array with displacement initial conditions 433 | v0: array 434 | Array with velocity initial conditions 435 | t: array 436 | Array withe evenly spaced times 437 | 438 | Returns 439 | ------- 440 | T : array 441 | Time values for the output. 442 | yout : array 443 | System response. 444 | xout : array 445 | Time evolution of the state vector. 446 | 447 | Examples 448 | -------- 449 | >>> M = np.array([[9, 0], 450 | ... [0, 1]]) 451 | >>> K = np.array([[27, -3], 452 | ... [-3, 3]]) 453 | >>> C = K/10 454 | >>> x0 = np.array([0, 1]) 455 | >>> v0 = np.array([1, 0]) 456 | >>> t = np.linspace(0, 10, 100) 457 | >>> F = np.vstack([0*t, 458 | ... 3*np.cos(2*t)]) 459 | >>> tou, yout, xout = response_system(M, C, K, F, x0, v0, t) 460 | >>> tou[:10] # doctest: +SKIP 461 | array([0. , 0.1 , 0.2 , 0.3 , 0.4 , 0.51, 0.61, 0.71, 0.81, 0.91]) 462 | 463 | >>> yout[:10] 464 | array([[ 0. , 1. , 1. , 0. ], 465 | [ 0.1 , 1. , 0.99, 0.04], 466 | [ 0.2 , 1.01, 0.95, 0.1 ], 467 | [ 0.29, 1.02, 0.88, 0.15], 468 | [ 0.38, 1.04, 0.79, 0.19], 469 | [ 0.45, 1.06, 0.68, 0.2 ], 470 | [ 0.51, 1.08, 0.55, 0.17], 471 | [ 0.56, 1.09, 0.41, 0.09], 472 | [ 0.59, 1.09, 0.26, -0.04], 473 | [ 0.61, 1.08, 0.11, -0.22]]) 474 | 475 | >>> xout[:10] 476 | array([[ 0. , 1. , 1. , 0. ], 477 | [ 0.1 , 1. , 0.99, 0.04], 478 | [ 0.2 , 1.01, 0.95, 0.1 ], 479 | [ 0.29, 1.02, 0.88, 0.15], 480 | [ 0.38, 1.04, 0.79, 0.19], 481 | [ 0.45, 1.06, 0.68, 0.2 ], 482 | [ 0.51, 1.08, 0.55, 0.17], 483 | [ 0.56, 1.09, 0.41, 0.09], 484 | [ 0.59, 1.09, 0.26, -0.04], 485 | [ 0.61, 1.08, 0.11, -0.22]]) 486 | """ 487 | 488 | n = len(M) 489 | 490 | Z = np.zeros((n, n)) 491 | I = np.eye(n) 492 | 493 | # creates the state space matrix 494 | A = np.vstack( 495 | [ 496 | np.hstack([Z, I]), 497 | np.hstack( 498 | [-la.solve(M, K, assume_a="pos"), 499 | - la.solve(M, C, assume_a="pos")] 500 | ), 501 | ] 502 | ) 503 | B = np.vstack([Z, la.inv(M)]) 504 | C = np.eye(2 * n) 505 | D = 0 * B 506 | 507 | sys = signal.lti(A, B, C, D) 508 | 509 | IC = np.hstack([x0, v0]) 510 | F = F.T 511 | T, yout, xout = signal.lsim(sys, F, t, IC) 512 | 513 | return T, yout, xout 514 | -------------------------------------------------------------------------------- /build/lib/vibration_toolbox/readme.rst: -------------------------------------------------------------------------------- 1 | Please refer to the `Numpy standards for docstrings `_. 2 | 3 | Specifically note: 4 | 5 | 1. Parameters should be listed similarly to: 6 | 7 | | filename : str 8 | | copy : bool 9 | | dtype : data-type 10 | | iterable : iterable object 11 | | shape : int or tuple of int 12 | | files : list of str 13 | | time : array_like 14 | 15 | 2. First line should be inline with the ``"""`` and brief enough to fit on one line. 16 | 17 | 3. There must be a blank line after the first line. 18 | 19 | This is not exhaustive. It just highlights some consistent errors made. 20 | -------------------------------------------------------------------------------- /build/lib/vibration_toolbox/vibesystem.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.linalg as la 3 | import scipy.signal as signal 4 | import matplotlib as mpl 5 | import matplotlib.pyplot as plt 6 | 7 | __all__ = ['VibeSystem'] 8 | 9 | plt.style.use('seaborn-white') 10 | 11 | color_palette = ["#4C72B0", "#55A868", "#C44E52", 12 | "#8172B2", "#CCB974", "#64B5CD"] 13 | 14 | plt.style.use({ 15 | 'lines.linewidth': 2.5, 16 | 'axes.grid': True, 17 | 'axes.linewidth': 0.1, 18 | 'grid.color': '.9', 19 | 'grid.linestyle': '--', 20 | 'legend.frameon': True, 21 | 'legend.framealpha': 0.2 22 | }) 23 | 24 | colors = color_palette + [(.1, .1, .1)] 25 | for code, color in zip('bgrmyck', colors): 26 | rgb = mpl.colors.colorConverter.to_rgb(color) 27 | mpl.colors.colorConverter.colors[code] = rgb 28 | mpl.colors.colorConverter.cache[code] = rgb 29 | 30 | 31 | class VibeSystem(object): 32 | r"""A multiple degrees of freedom system. 33 | 34 | This class will create a multiple degree of 35 | freedom system given M, C and K matrices. 36 | 37 | Parameters 38 | ---------- 39 | M : array 40 | Mass matrix. 41 | C : array 42 | Damping matrix. 43 | K : array 44 | Stiffness matrix. 45 | name : str, optional 46 | Name of the system. 47 | 48 | Attributes 49 | ---------- 50 | evalues : array 51 | System's eigenvalues. 52 | evectors : array 53 | System's eigenvectors. 54 | wn : array 55 | System's natural frequencies in rad/s. 56 | wd : array 57 | System's damped natural frequencies in rad/s. 58 | damping_ratio : array 59 | System's damping factor for each mode. 60 | H : scipy.signal.lti 61 | Continuous-time linear time invariant system 62 | 63 | Examples 64 | -------- 65 | For a system consisting of two masses connected 66 | to each other and both connected to a wall we have 67 | the following matrices: 68 | 69 | >>> m0, m1 = 1, 1 70 | >>> c0, c1, c2 = 5, 5, 5 71 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 72 | 73 | >>> M = np.array([[m0, 0], 74 | ... [0, m1]]) 75 | >>> C = np.array([[c0+c1, -c2], 76 | ... [-c1, c2+c2]]) 77 | >>> K = np.array([[k0+k1, -k2], 78 | ... [-k1, k2+k2]]) 79 | >>> sys = VibeSystem(M, C, K) 80 | >>> sys.wn # doctest: +SKIP 81 | array([31.62, 54.77]) 82 | >>> sys.wd # doctest: +SKIP 83 | array([31.52, 54.26]) 84 | """ 85 | def __init__(self, M, C, K, name=''): 86 | self._M = M 87 | self._C = C 88 | self._K = K 89 | self.name = name 90 | # Values for evalues and evectors will be calculated by self._calc_system 91 | self.evalues = None 92 | self.evectors = None 93 | self.wn = None 94 | self.wd = None 95 | self.lti = None 96 | 97 | self.n = len(M) 98 | self._calc_system() 99 | 100 | @property 101 | def M(self): 102 | return self._M 103 | 104 | @M.setter 105 | def M(self, value): 106 | self._M = value 107 | # if the parameter is changed this will update the system 108 | self._calc_system() 109 | 110 | @property 111 | def C(self): 112 | return self._C 113 | 114 | @C.setter 115 | def C(self, value): 116 | self._C = value 117 | # if the parameter is changed this will update the system 118 | self._calc_system() 119 | 120 | @property 121 | def K(self): 122 | return self._K 123 | 124 | @K.setter 125 | def K(self, value): 126 | self._K = value 127 | # if the parameter is changed this will update the system 128 | self._calc_system() 129 | 130 | def __repr__(self): 131 | M = np.array_str(self.M) 132 | K = np.array_str(self.K) 133 | C = np.array_str(self.C) 134 | return ('Mass Matrix: \n' 135 | '{} \n\n' 136 | 'Stiffness Matrix: \n' 137 | '{} \n\n' 138 | 'Damping Matrix: \n' 139 | '{}'.format(M, K, C)) 140 | 141 | def _calc_system(self): 142 | self.evalues, self.evectors = self._eigen() 143 | self.wn = np.absolute(self.evalues)[:self.n] 144 | self.wd = np.imag(self.evalues)[:self.n] 145 | self.damping_ratio = (-np.real(self.evalues) / 146 | np.absolute(self.evalues))[:self.n] 147 | self.lti = self._lti() 148 | 149 | def A(self): 150 | """State space matrix 151 | 152 | This method will return the state space matrix 153 | of the system. 154 | 155 | Returns 156 | ---------- 157 | A : array 158 | System's state space matrix. 159 | 160 | Examples 161 | -------- 162 | >>> m0, m1 = 1, 1 163 | >>> c0, c1, c2 = 1, 1, 1 164 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 165 | 166 | >>> M = np.array([[m0, 0], 167 | ... [0, m1]]) 168 | >>> C = np.array([[c0+c1, -c2], 169 | ... [-c1, c2+c2]]) 170 | >>> K = np.array([[k0+k1, -k2], 171 | ... [-k1, k2+k2]]) 172 | >>> sys = VibeSystem(M, C, K) # create the system 173 | >>> sys.A() 174 | array([[ 0., 0., 1., 0.], 175 | [ 0., 0., 0., 1.], 176 | [-2000., 1000., -2., 1.], 177 | [ 1000., -2000., 1., -2.]]) 178 | """ 179 | 180 | Z = np.zeros((self.n, self.n)) 181 | I = np.eye(self.n) 182 | 183 | A = np.vstack( 184 | [np.hstack([Z, I]), 185 | np.hstack([la.solve(-self.M, self.K), 186 | la.solve(-self.M, self.C)])]) 187 | return A 188 | 189 | @staticmethod 190 | def _index(eigenvalues): 191 | r"""Function used to generate an index that will sort 192 | eigenvalues and eigenvectors based on the imaginary (wd) 193 | part of the eigenvalues. Positive eigenvalues will be 194 | positioned at the first half of the array. 195 | """ 196 | # avoid float point errors when sorting 197 | evals_truncated = np.around(eigenvalues, decimals=10) 198 | a = np.imag(evals_truncated) # First column 199 | b = np.absolute(evals_truncated) # Second column 200 | ind = np.lexsort((b, a)) # Sort by imag, then by absolute 201 | # Positive eigenvalues first 202 | positive = [i for i in ind[len(a) // 2:]] 203 | negative = [i for i in ind[:len(a) // 2]] 204 | 205 | idx = np.array([positive, negative]).flatten() 206 | 207 | return idx 208 | 209 | def _eigen(self, sorted_=True): 210 | r"""This method will return the eigenvalues and eigenvectors of 211 | the state space matrix A, sorted by the index method which 212 | considers the imaginary part (wd) of the eigenvalues for sorting. 213 | To avoid sorting use sorted_=False 214 | """ 215 | evalues, evectors = la.eig(self.A()) 216 | if sorted_ is False: 217 | return evalues, evectors 218 | 219 | idx = self._index(evalues) 220 | 221 | return evalues[idx], evectors[:, idx] 222 | 223 | def _lti(self): 224 | r"""Continuous-time linear time invariant system. 225 | 226 | This method is used to create a Continuous-time linear 227 | time invariant system for the mdof system. 228 | From this system we can obtain poles, impulse response, 229 | generate a bode, etc. 230 | """ 231 | Z = np.zeros((self.n, self.n)) 232 | I = np.eye(self.n) 233 | 234 | # x' = Ax + Bu 235 | B2 = I 236 | A = self.A() 237 | B = np.vstack([Z, 238 | la.solve(self.M, B2)]) 239 | 240 | # y = Cx + Du 241 | # Observation matrices 242 | Cd = I 243 | Cv = Z 244 | Ca = Z 245 | 246 | C = np.hstack((Cd - Ca @ la.solve(self.M, self.K), 247 | Cv - Ca @ la.solve(self.M, self.C))) 248 | D = Ca @ la.solve(self.M, B2) 249 | 250 | return signal.lti(A, B, C, D) 251 | 252 | def time_response(self, F, t, ic=None): 253 | r"""Time response for a mdof system. 254 | 255 | This method returns the time response for a mdof system 256 | given a force, time and initial conditions. 257 | 258 | Parameters 259 | ---------- 260 | F : array 261 | Force array (needs to have the same length as time array). 262 | t : array 263 | Time array. 264 | ic : array, optional 265 | The initial conditions on the state vector (zero by default). 266 | 267 | Returns 268 | ---------- 269 | t : array 270 | Time values for the output. 271 | yout : array 272 | System response. 273 | xout : array 274 | Time evolution of the state vector. 275 | 276 | 277 | Examples 278 | -------- 279 | >>> m0, m1 = 1, 1 280 | >>> c0, c1, c2 = 1, 1, 1 281 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 282 | 283 | >>> M = np.array([[m0, 0], 284 | ... [0, m1]]) 285 | >>> C = np.array([[c0+c1, -c2], 286 | ... [-c1, c2+c2]]) 287 | >>> K = np.array([[k0+k1, -k2], 288 | ... [-k1, k2+k2]]) 289 | >>> sys = VibeSystem(M, C, K) # create the system 290 | >>> t = np.linspace(0, 25, 1000) # time array 291 | >>> F1 = np.zeros((len(t), 2)) 292 | >>> F1[:, 1] = 1000*np.sin(40*t) # force applied on m1 293 | >>> t, yout, xout = sys.time_response(F1, t) 294 | >>> # response on m0 295 | >>> yout[:5, 0] # doctest: +SKIP 296 | array([0. , 0. , 0.07, 0.32, 0.61]) 297 | >>> # response on m1 298 | >>> yout[:5, 1] # doctest: +SKIP 299 | array([0. , 0.08, 0.46, 0.79, 0.48]) 300 | """ 301 | return signal.lsim(self.lti, F, t, X0=ic) 302 | 303 | def freq_response(self, F=None, omega=None, modes=None): 304 | r"""Frequency response for a mdof system. 305 | 306 | This method returns the frequency response for a mdof system 307 | given a range of frequencies, the force for each frequency 308 | and the modes that will be used. 309 | 310 | Parameters 311 | ---------- 312 | F : array, optional 313 | Force array (needs to have the same length as time array). 314 | If not given the impulse response is calculated. 315 | omega : array, optional 316 | Array with the desired range of frequencies (the default 317 | is 0 to 1.5 x highest damped natural frequency. 318 | modes : list, optional 319 | Modes that will be used to calculate the frequency response 320 | (all modes will be used if a list is not given). 321 | 322 | Returns 323 | ---------- 324 | omega : array 325 | Array with the frequencies 326 | magdb : array 327 | Magnitude (dB) of the frequency response for each pair input/output. 328 | The order of the array is: [output, input, magnitude] 329 | phase : array 330 | Phase of the frequency response for each pair input/output. 331 | The order of the array is: [output, input, phase] 332 | 333 | Examples 334 | -------- 335 | >>> m0, m1 = 1, 1 336 | >>> c0, c1, c2 = 1, 1, 1 337 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 338 | 339 | >>> M = np.array([[m0, 0], 340 | ... [0, m1]]) 341 | >>> C = np.array([[c0+c1, -c2], 342 | ... [-c1, c2+c2]]) 343 | >>> K = np.array([[k0+k1, -k2], 344 | ... [-k1, k2+k2]]) 345 | >>> sys = VibeSystem(M, C, K) # create the system 346 | >>> omega, magdb, phase = sys.freq_response() 347 | >>> # magnitude for output on 0 and input on 1. 348 | >>> magdb[0, 1, :4] 349 | array([-69.54, -69.54, -69.54, -69.54]) 350 | >>> # phase for output on 1 and input on 1. 351 | >>> phase[1, 1, :4] 352 | array([...0. , -0. , -0.01, -0.01]) 353 | """ 354 | rows = self.lti.inputs # inputs (mag and phase) 355 | cols = self.lti.outputs # outputs 356 | 357 | B = self.lti.B 358 | C = self.lti.C 359 | D = self.lti.D 360 | 361 | evals = self.evalues 362 | psi = self.evectors 363 | psi_inv = la.inv(psi) # TODO change to get psi_inv from la.eig 364 | 365 | # if omega is not given, define a range 366 | if omega is None: 367 | omega = np.linspace(0, max(evals.imag) * 1.5, 1000) 368 | 369 | # if modes are selected: 370 | if modes is not None: 371 | n = self.n # n dof -> number of modes 372 | m = len(modes) # -> number of desired modes 373 | # idx to get each evalue/evector and its conjugate 374 | idx = np.zeros((2 * m), int) 375 | idx[0:m] = modes # modes 376 | idx[m:] = range(2 * n)[-m:] # conjugates (see how evalues are ordered) 377 | 378 | evals = evals[np.ix_(idx)] 379 | psi = psi[np.ix_(range(2 * n), idx)] 380 | psi_inv = psi_inv[np.ix_(idx, range(2 * n))] 381 | 382 | magdb = np.empty((cols, rows, len(omega))) 383 | phase = np.empty((cols, rows, len(omega))) 384 | 385 | for wi, w in enumerate(omega): 386 | diag = np.diag([1 / (1j * w - lam) for lam in evals]) 387 | if F is None: 388 | H = C @ psi @ diag @ psi_inv @ B + D 389 | else: 390 | H = (C @ psi @ diag @ psi_inv @ B + D) @ F[wi] 391 | 392 | magh = 20.0 * np.log10(abs(H)) 393 | angh = np.rad2deg((np.angle(H))) 394 | 395 | magdb[:, :, wi] = magh 396 | phase[:, :, wi] = angh 397 | 398 | return omega, magdb, phase 399 | 400 | def plot_freq_response(self, out, inp, modes=None, ax0=None, ax1=None, **kwargs): 401 | """Plot frequency response. 402 | 403 | This method plots the frequency response given 404 | an output and an input. 405 | 406 | Parameters 407 | ---------- 408 | out : int 409 | Output. 410 | input : int 411 | Input. 412 | modes : list, optional 413 | Modes that will be used to calculate the frequency response 414 | (all modes will be used if a list is not given). 415 | 416 | ax0 : matplotlib.axes, optional 417 | Matplotlib axes where the amplitude will be plotted. 418 | If None creates a new. 419 | ax1 : matplotlib.axes, optional 420 | Matplotlib axes where the phase will be plotted. 421 | If None creates a new. 422 | kwargs : optional 423 | Additional key word arguments can be passed to change 424 | the plot (e.g. linestyle='--') 425 | 426 | Returns 427 | ------- 428 | ax0 : matplotlib.axes 429 | Matplotlib axes with amplitude plot. 430 | ax1 : matplotlib.axes 431 | Matplotlib axes with phase plot. 432 | 433 | Examples 434 | -------- 435 | >>> m0, m1 = 1, 1 436 | >>> c0, c1, c2 = 1, 1, 1 437 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 438 | 439 | >>> M = np.array([[m0, 0], 440 | ... [0, m1]]) 441 | >>> C = np.array([[c0+c1, -c2], 442 | ... [-c1, c2+c2]]) 443 | >>> K = np.array([[k0+k1, -k2], 444 | ... [-k1, k2+k2]]) 445 | >>> sys = VibeSystem(M, C, K) # create the system 446 | >>> # plot frequency response for input and output at m0 447 | >>> sys.plot_freq_response(0, 0) 448 | (... 449 | """ 450 | if ax0 is None or ax1 is None: 451 | fig, ax = plt.subplots(2) 452 | if ax0 is not None: 453 | _, ax1 = ax 454 | if ax1 is not None: 455 | ax0, _ = ax 456 | else: 457 | ax0, ax1 = ax 458 | # TODO add option to select plot units 459 | omega, magdb, phase = self.freq_response(modes=modes) 460 | 461 | ax0.plot(omega, magdb[out, inp, :], **kwargs) 462 | ax1.plot(omega, phase[out, inp, :], **kwargs) 463 | for ax in [ax0, ax1]: 464 | ax.set_xlim(0, max(omega)) 465 | ax.yaxis.set_major_locator( 466 | mpl.ticker.MaxNLocator(prune='lower')) 467 | ax.yaxis.set_major_locator( 468 | mpl.ticker.MaxNLocator(prune='upper')) 469 | 470 | ax0.text(.9, .9, 'Output %s' % out, 471 | horizontalalignment='center', 472 | transform=ax0.transAxes) 473 | ax0.text(.9, .7, 'Input %s' % inp, 474 | horizontalalignment='center', 475 | transform=ax0.transAxes) 476 | 477 | ax0.set_ylabel('Magnitude $(dB)$') 478 | ax1.set_ylabel('Phase') 479 | ax1.set_xlabel('Frequency (rad/s)') 480 | 481 | return ax0, ax1 482 | 483 | def plot_freq_response_grid(self, outs, inps, modes=None, ax=None): 484 | """Plot frequency response. 485 | 486 | This method plots the frequency response given 487 | an output and an input. 488 | 489 | Parameters 490 | ---------- 491 | outs : list 492 | List with the desired outputs. 493 | inps : list 494 | List with the desired outputs. 495 | modes : list 496 | List with the modes that will be used to construct 497 | the frequency response plot. 498 | 499 | ax : array with matplotlib.axes, optional 500 | Matplotlib axes array created with plt.subplots. 501 | It needs to have a shape of (2*inputs, outputs). 502 | 503 | Returns 504 | ------- 505 | ax : array with matplotlib.axes, optional 506 | Matplotlib axes array created with plt.subplots. 507 | 508 | Examples 509 | -------- 510 | >>> m0, m1 = 1, 1 511 | >>> c0, c1, c2 = 1, 1, 1 512 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 513 | 514 | >>> M = np.array([[m0, 0], 515 | ... [0, m1]]) 516 | >>> C = np.array([[c0+c1, -c2], 517 | ... [-c1, c2+c2]]) 518 | >>> K = np.array([[k0+k1, -k2], 519 | ... [-k1, k2+k2]]) 520 | >>> sys = VibeSystem(M, C, K) # create the system 521 | >>> # plot frequency response for inputs at [0, 1] 522 | >>> # and outputs at [0, 1] 523 | >>> sys.plot_freq_response_grid(outs=[0, 1], inps=[0, 1]) 524 | array([[ 1: 533 | for i, out in enumerate(outs): 534 | for j, inp in enumerate(inps): 535 | self.plot_freq_response(out, inp, 536 | modes=modes, 537 | ax0=ax[2*i, j], 538 | ax1=ax[2*i + 1, j]) 539 | else: 540 | for i, inp in enumerate(inps): 541 | self.plot_freq_response(outs[0], inp, 542 | modes=modes, 543 | ax0=ax[2*i], 544 | ax1=ax[2*i + 1]) 545 | 546 | return ax 547 | 548 | def plot_time_response(self, F, t, ic=None, out=None, ax=None): 549 | r"""Plot the time response for a mdof system. 550 | 551 | This method returns the time response for a mdof system 552 | given a force, time and initial conditions. 553 | 554 | Parameters 555 | ---------- 556 | F : array 557 | Force array (needs to have the same length as time array). 558 | t : array 559 | Time array. 560 | ic : array, optional 561 | The initial conditions on the state vector (zero by default). 562 | out : list 563 | Desired output for which the time response will be plotted. 564 | ax : array with matplotlib.axes, optional 565 | Matplotlib axes array created with plt.subplots. 566 | It needs to have a shape of (2*inputs, outputs). 567 | 568 | Returns 569 | ------- 570 | ax : array with matplotlib.axes, optional 571 | Matplotlib axes array created with plt.subplots. 572 | t : array 573 | Time values for the output. 574 | yout : array 575 | System response. 576 | xout : array 577 | Time evolution of the state vector. 578 | 579 | Examples 580 | -------- 581 | >>> m0, m1 = 1, 1 582 | >>> c0, c1, c2 = 1, 1, 1 583 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 584 | 585 | >>> M = np.array([[m0, 0], 586 | ... [0, m1]]) 587 | >>> C = np.array([[c0+c1, -c2], 588 | ... [-c1, c2+c2]]) 589 | >>> K = np.array([[k0+k1, -k2], 590 | ... [-k1, k2+k2]]) 591 | >>> sys = VibeSystem(M, C, K) # create the system 592 | >>> t = np.linspace(0, 25, 1000) # time array 593 | >>> F1 = np.zeros((len(t), 2)) 594 | >>> F1[:, 1] = 1000*np.sin(40*t) # force applied on m1 595 | >>> sys.plot_time_response(F1, t) 596 | array([... 597 | """ 598 | if ax is None: 599 | fig, axs = plt.subplots(self.lti.outputs, 1, sharex=True) 600 | 601 | fig.suptitle('Time response ' + self.name, fontsize=12) 602 | plt.subplots_adjust(hspace=0.01) 603 | 604 | if out is not None: 605 | raise NotImplementedError('Not implemented yet for specific outputs.') 606 | 607 | t, yout, xout = self.time_response(F, t, ic=ic) 608 | 609 | for i, ax in enumerate(axs): 610 | ax.plot(t, yout[:, i]) 611 | 612 | # set the same y limits 613 | min_ = min([ax.get_ylim()[0] for ax in axs]) 614 | max_ = max([ax.get_ylim()[1] for ax in axs]) 615 | lim = max(abs(min_), max_) 616 | 617 | for i, ax in enumerate(axs): 618 | ax.set_ylim([-lim, lim]) 619 | ax.set_xlim(t[0], t[-1]) 620 | ax.set_ylabel('Amp. output %s (m)' % i, fontsize=8) 621 | 622 | axs[-1].set_xlabel('Time (s)') 623 | 624 | return axs 625 | -------------------------------------------------------------------------------- /create_distro.rst: -------------------------------------------------------------------------------- 1 | 2 | What I need to do to get this to install properly with pip 3 | https://www.codementor.io/python/tutorial/host-your-python-package-using-github-on-pypi 4 | 5 | http://peterdowns.com/posts/first-time-with-pypi.html 6 | 7 | To test release 8 | --------------- 9 | 10 | .. code-block:: python 11 | 12 | python setup.py register -r pypitest 13 | python setup.py sdist upload -r pypitest 14 | 15 | look at `https://testpypi.python.org/pypi` 16 | 17 | To release 18 | ---------------- 19 | 20 | Edit `__init__.py` to update the version number. 21 | 22 | .. code-block:: python 23 | 24 | python setup.py register -r pypi 25 | python setup.py sdist upload -r pypi 26 | 27 | 28 | https://pypi.python.org/pypi/wheel 29 | -------------------------------------------------------------------------------- /dist/vibration_toolbox-0.6.10-py35-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/dist/vibration_toolbox-0.6.10-py35-none-any.whl -------------------------------------------------------------------------------- /docs/Installing_Python.rst: -------------------------------------------------------------------------------- 1 | .. _installing_python: 2 | 3 | Installing Python 4 | _________________ 5 | 6 | In order to be able to use the Vibration Toolbox you need a working `Scientific Python`_ installation. 7 | 8 | The easiest path to this is to install Python via `Anaconda`_ or `Enthought Canopy`_. **You must install** Python 3.5 or later for the Vibration Toolbox to work. I prefer the `Anaconda`_ distribution myself, but many organizations prefer `Enthought Canopy`_. **Do not install Python 2.7.** The Vibration Toolbox requires a Python 3.5 or later. 9 | 10 | This proceeds as a normal install on your platform (Mac, Windows, Linux...). 11 | 12 | Subsequently you must update some components of Anaconda by using the *conda* command from the *Anaconda Command Prompt*. On Windows, this runs as an actual program. 13 | 14 | To ensure the conda package list is as up to date as possible:: 15 | 16 | conda update conda 17 | 18 | Then update everything else with:: 19 | 20 | conda update --all 21 | 22 | To use the `Jupyter`_ (the notebook), launch a terminal on Mac or Linux, or the Anaconda Terminal on Windows (or similar name for the `Enthought Canopy`_ distribution of Scientific Python) and type: 23 | 24 | .. code-block:: bash 25 | 26 | jupyter notebook 27 | 28 | A Matlab_-like experience is to use Spyder, which is also included. To run Spyder, at your terminal on Mac or Linux, or the Anaconda Terminal on Windows, type:: 29 | 30 | .. code-block:: bash 31 | 32 | spyder 33 | 34 | It's all in the GUI from here. You just need to play around a bit. 35 | 36 | .. _github: http://www.github.com 37 | .. _Anaconda: http://continuum.io/downloads 38 | .. _Jupyter: http://www.jupyter.org 39 | .. _`Enthought Canopy`: https://store.enthought.com/downloads/ 40 | .. _`Scientific Python`: https://www.scipy.org 41 | .. _`Matlab`: http://www.mathworks.com 42 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = VibrationToolbox 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Vibration Toolbox documentation build configuration file, created by 5 | # sphinx-quickstart on Sun Apr 9 10:02:35 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | import os 21 | import sys 22 | import sphinx.environment 23 | from docutils.utils import get_source_line 24 | 25 | # Find modules and their help for inclusion in documentation 26 | sys.path.insert(0, os.path.abspath('.')) 27 | sys.path.append('../vibration_toolbox') 28 | sys.path.append('../JupyterNotebooks') 29 | sys.path.append('..') 30 | 31 | 32 | # Avoid needin to update version here for release 33 | from vibration_toolbox import __version__, __title__, __author__, __license__,\ 34 | __copyright__ 35 | 36 | # print("license is", __version__) 37 | 38 | 39 | # -- General configuration ------------------------------------------------ 40 | 41 | # If your documentation needs a minimal Sphinx version, state it here. 42 | # 43 | # needs_sphinx = '1.0' 44 | 45 | # Add any Sphinx extension module names here, as strings. They can be 46 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 47 | # ones. 48 | extensions = ['sphinx.ext.autodoc', 49 | 'numpydoc', 50 | 'sphinx.ext.doctest', 51 | 'sphinx.ext.todo', 52 | 'sphinx.ext.coverage', 53 | 'sphinx.ext.mathjax', 54 | 'sphinx.ext.ifconfig', 55 | 'sphinx.ext.viewcode', 56 | 'sphinx.ext.githubpages', 57 | 'sphinx.ext.autosectionlabel', 58 | 'matplotlib.sphinxext.plot_directive', 59 | 'nbsphinx', 60 | 'sphinx.ext.mathjax', 61 | 'IPython.sphinxext.ipython_console_highlighting', 62 | 'sphinx.ext.autosummary', 63 | 'jupyter_sphinx.embed_widgets'] 64 | 65 | # Add any paths that contain templates here, relative to this directory. 66 | templates_path = ['_templates'] 67 | 68 | # The suffix(es) of source filenames. 69 | # You can specify multiple suffix as a list of string: 70 | # 71 | # source_suffix = ['.rst', '.md'] 72 | source_suffix = '.rst' 73 | 74 | # The master toctree document. 75 | master_doc = 'index' 76 | 77 | numpydoc_show_class_members = False 78 | 79 | # Whether to produce plot:: directives for Examples sections that contain 80 | # import matplotlib 81 | numpydoc_use_plots = True 82 | 83 | 84 | # General information about the project. 85 | project = 'Vibration Toolbox' 86 | copyright = u'2017, Joseph C. Slater and Raphael Timbó' 87 | author = u'Joseph C. Slater and Raphael Timbó' 88 | 89 | # The version info for the project you're documenting, acts as replacement for 90 | # |version| and |release|, also used in various other places throughout the 91 | # built documents. 92 | # 93 | # The short X.Y version. 94 | version = __version__ 95 | # The full version, including alpha/beta/rc tags. 96 | release = version 97 | 98 | # The language for content autogenerated by Sphinx. Refer to documentation 99 | # for a list of supported languages. 100 | # 101 | # This is also used if you do content translation via gettext catalogs. 102 | # Usually you set "language" from the command line for these cases. 103 | language = None 104 | 105 | # List of patterns, relative to source directory, that match files and 106 | # directories to ignore when looking for source files. 107 | # This patterns also effect to html_static_path and html_extra_path 108 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'readme.rst', 109 | '**.ipynb_checkpoints'] 110 | 111 | # The name of the Pygments (syntax highlighting) style to use. 112 | pygments_style = 'sphinx' 113 | 114 | # If true, `todo` and `todoList` produce output, else they produce nothing. 115 | todo_include_todos = True 116 | 117 | 118 | # Block out warning from nonlocal image in main readme file of repository when 119 | # building docs in sphinx 120 | # http://stackoverflow.com/questions/12772927/specifying-an-online-image-in-sphinx-restructuredtext-format 121 | def _warn_node(self, msg, node, **kwargs): 122 | if not msg.startswith('nonlocal image URI found:'): 123 | self._warnfunc(msg, '%s:%s' % get_source_line(node), **kwargs) 124 | 125 | 126 | sphinx.environment.BuildEnvironment.warn_node = _warn_node 127 | 128 | 129 | # -- Options for HTML output ---------------------------------------------- 130 | 131 | # The theme to use for HTML and HTML Help pages. See the documentation for 132 | # a list of builtin themes. 133 | # 134 | html_theme = 'alabaster' 135 | 136 | 137 | html_sidebars = { 138 | '**': [ 139 | 'about.html', 'navigation.html', 'searchbox.html', 'sourcelink.html', 140 | ] 141 | } 142 | 143 | 144 | # Theme options are theme-specific and customize the look and feel of a theme 145 | # further. For a list of options available for each theme, see the 146 | # documentation. 147 | # 148 | # html_theme_options = {} 149 | 150 | # Add any paths that contain custom static files (such as style sheets) here, 151 | # relative to this directory. They are copied after the builtin static files, 152 | # so a file named "default.css" will overwrite the builtin "default.css". 153 | html_static_path = ['_static'] 154 | 155 | 156 | # -- Options for HTMLHelp output ------------------------------------------ 157 | 158 | # Output file base name for HTML help builder. 159 | htmlhelp_basename = 'VibrationToolboxdoc' 160 | 161 | html_theme_options = { 162 | # 'logo': 'js.jpg', 163 | 'logo_name': 'VTB', 164 | 'description': 'Vibration Education in a Modern Environment', 165 | 'github_button': 'True', 166 | 'github_user': 'vibrationtoolbox', 167 | 'analytics_id': 'UA-62100376-4', 168 | } 169 | 170 | 171 | # -- Options for LaTeX output --------------------------------------------- 172 | 173 | latex_elements = { 174 | # The paper size ('letterpaper' or 'a4paper'). 175 | # 176 | # 'papersize': 'letterpaper', 177 | 178 | # The font size ('10pt', '11pt' or '12pt'). 179 | # 180 | # 'pointsize': '10pt', 181 | 182 | # Additional stuff for the LaTeX preamble. 183 | # 184 | # 'preamble': '', 185 | 186 | # Latex figure (float) alignment 187 | # 188 | # 'figure_align': 'htbp', 189 | } 190 | 191 | # Grouping the document tree into LaTeX files. List of tuples 192 | # (source start file, target name, title, 193 | # author, documentclass [howto, manual, or own class]). 194 | latex_documents = [ 195 | (master_doc, 'VibrationToolbox.tex', 'Vibration Toolbox Documentation', 196 | 'Joseph C. Slater and Raphael Timbó', 'manual'), 197 | ] 198 | 199 | 200 | # -- Options for manual page output --------------------------------------- 201 | 202 | # One entry per manual page. List of tuples 203 | # (source start file, name, description, authors, manual section). 204 | man_pages = [ 205 | (master_doc, 'vibrationtoolbox', 'Vibration Toolbox Documentation', 206 | [author], 1) 207 | ] 208 | 209 | 210 | # -- Options for Texinfo output ------------------------------------------- 211 | 212 | # Grouping the document tree into Texinfo files. List of tuples 213 | # (source start file, target name, title, author, 214 | # dir menu entry, description, category) 215 | texinfo_documents = [ 216 | (master_doc, 'VibrationToolbox', 'Vibration Toolbox Documentation', 217 | author, 'VibrationToolbox', 'One line description of project.', 218 | 'Miscellaneous'), 219 | ] 220 | -------------------------------------------------------------------------------- /docs/contributors.rst: -------------------------------------------------------------------------------- 1 | Contributors 2 | ____________ 3 | 4 | The Vibration Toolbox was created by Joseph C. Slater as companion software to a textbook. In 2016 Raphael Timbó ported of the software to Python in partnership with Joseph C. Slater. While all contributions are visible through the github logs, it's worthwhile to attempt to list them here. 5 | 6 | Other contributors 7 | ~~~~~~~~~~~~~~~~~~ 8 | 9 | | Gabriel Loranger 10 | | Anusha Anisetti 11 | | Daniel Wedlund 12 | -------------------------------------------------------------------------------- /docs/downloads.rst: -------------------------------------------------------------------------------- 1 | Download Notebooks 2 | ___________________________ 3 | 4 | Tutorial Notebooks 5 | ^^^^^^^^^^^^^^^^^^ 6 | 7 | | :download:`Single Degree of Freedom ` 8 | | :download:`Multiple Degree of Freedom ` 9 | | :download:`Continuous Systems ` 10 | | :download:`Vibration System ` 11 | 12 | Examples Notebooks 13 | ^^^^^^^^^^^^^^^^^^ 14 | 15 | | :download:`Vibration Absorber ` 16 | -------------------------------------------------------------------------------- /docs/examples/index.rst: -------------------------------------------------------------------------------- 1 | Examples 2 | -------- 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | Beats.ipynb 8 | working_notebook.ipynb 9 | vibesystem_notebook-damper.ipynb 10 | -------------------------------------------------------------------------------- /docs/examples/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | matplotlib 4 | sphinx 5 | ipython 6 | control 7 | vibration_toolbox 8 | xarray 9 | -------------------------------------------------------------------------------- /docs/examples/system_damp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/docs/examples/system_damp.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Vibration Toolbox documentation master file, created by 2 | sphinx-quickstart on Sun Apr 9 10:02:35 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | ============================================= 7 | The Engineering Vibration Toolbox for Python 8 | ============================================= 9 | 10 | .. image:: https://badge.fury.io/py/vibration-toolbox.png/ 11 | :target: http://badge.fury.io/py/vibration-toolbox 12 | 13 | .. image:: https://travis-ci.org/vibrationtoolbox/vibration_toolbox.svg?branch=master 14 | :target: https://travis-ci.org/vibrationtoolbox/vibration_toolbox 15 | 16 | .. image:: https://zenodo.org/badge/39572419.svg 17 | :target: https://zenodo.org/badge/latestdoi/39572419 18 | 19 | .. image:: https://mybinder.org/badge.svg 20 | :target: https://mybinder.org/v2/gh/vibrationtoolbox/vibration_toolbox/binder 21 | 22 | .. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg 23 | :target: https://saythanks.io/to/josephcslater 24 | 25 | .. image:: https://img.shields.io/badge/patreon-donate-yellow.svg 26 | :target: https://www.patreon.com/josephcslater 27 | 28 | .. image:: http://pepy.tech/badge/vibration-toolbox 29 | :target: http://pepy.tech/project/vibration-toolbox 30 | :alt: PyPi Download stats 31 | 32 | 33 | Joseph C. Slater and Raphael Timbó 34 | 35 | Welcome to `Engineering Vibration Toolbox `_. 36 | Originally written for `Matlab `_\®, this `Python `_ version is a completely new design build for modern education. This is an *educational* set of codes intended primarily for 37 | demonstration of concepts. You may find them useful for application, but that 38 | isn't the intent. If you have professional-level needs please `contact the authors `_. 39 | 40 | For more information, please see the `documentation for the Python version `_ but please excuse that it is still under development. Such documentation has never existed for the other ports of the toolbox so this is taking some time. We don't need feedback at this time, but we will take assistance in improving documentation and code. 41 | 42 | 43 | Try now! 44 | -------- 45 | 46 | You won't get everything, but you can try parts of the toolbox immediately on and `tutorial on mybinder.org `_, right in your browser window. 47 | 48 | 49 | Table of Contents 50 | ----------------- 51 | 52 | .. toctree:: 53 | :maxdepth: 3 54 | 55 | installation 56 | tutorial/index 57 | downloads 58 | reference/index 59 | contributors 60 | 61 | .. toctree:: 62 | :maxdepth: 1 63 | 64 | examples/index 65 | 66 | Indices and tables 67 | __________________ 68 | 69 | * :ref:`genindex` 70 | * :ref:`modindex` 71 | * :ref:`search` 72 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | 4 | Easy Installation 5 | _________________ 6 | 7 | If you aren't familiar at all with Python, please see `Installing Python `_. 8 | 9 | Installation is made easy with ``pip`` (or ``pip3``), with releases as we have time while we try 10 | to create a full first release. Much of it works already, but we certainly need 11 | issue reports (on `github `_). 12 | 13 | To install:: 14 | 15 | pip install --user vibration_toolbox 16 | 17 | where ``--user`` isn't necessary if you are using a locally installed version of Python such as `Anaconda `_. 18 | 19 | To run, I recommend you open a `Jupyter`_ notebook by using ``jupyter notebook`` and then type:: 20 | 21 | import vibration_toolbox as vtb 22 | 23 | For examples, see the `example ipynb notebooks `_. Some of these have interactive capabilities that are only apparent when you load them with `Jupyter`_ instead of just looking at them on github. 24 | 25 | Installation of current development version 26 | ___________________________________________ 27 | 28 | The usage documentation is far behind the current code, while the reference is way ahead of the released code due to the `autodoc `_ capability of `Sphinx `_. Especially as of early 2017, the code is in rapid development. So is the documentation. Releases to `pypi `_ are far behind current status as stopping to deploy would cost more time that it is worth. 29 | 30 | If you wish to install the current version of the software please see `CONTRIBUTING.rst `_. 31 | 32 | That should be it. Please note issues on the `issues tab `_ on github. 33 | 34 | .. _Jupyter: jupyter.org 35 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | This is the folder for building the complete documentation for the Vibration Toolbox. The actual documentation is hosted via `githubpages `_. 2 | 3 | The remainder of this document contains notes for developers. 4 | 5 | To make the doc, in the `docs` folder type:: 6 | 7 | make docs 8 | 9 | To serve the pages, move to the `docs/_build/html` folder and type:: 10 | 11 | python -m http.server 12 | 13 | 14 | 15 | =============== 16 | Title 17 | =============== 18 | 19 | 20 | Section Title 21 | ------------- 22 | 23 | | ------------ 24 | 25 | Subsection Title 26 | ________________ 27 | 28 | | _______________ 29 | 30 | Subsubsection Title 31 | ~~~~~~~~~~~~~~~~~~~ 32 | 33 | | ~~~~~~~~~~~~~~~~~~~ 34 | 35 | Subsubsubsection Title 36 | ^^^^^^^^^^^^^^^^^^^^^^ 37 | 38 | | ^^^^^^^^^^^^^^^^^^^^^^ 39 | 40 | Subsubsubsubection Title 41 | '''''''''''''''''''''''' 42 | 43 | | ''''''''''''''''''''''' 44 | 45 | We shouldn't ever need this many. 46 | 47 | 48 | All documentation will be built with via `sphinx `_ using ``make html`` in the ``docs`` directory. Updating on ``github`` is documented 49 | in the ``developers.rst`` file at the top of the repository. 50 | 51 | Documentation of functions help (uses `autodoc `_): 52 | 53 | 54 | Module and function help needs to then follow the `numpy convention. 55 | `_ 56 | 57 | 58 | For information on how function docstrings are used, see `numpydoc `_ 59 | 60 | For plots: 61 | http://matplotlib.org/sampledoc/extensions.html 62 | -------------------------------------------------------------------------------- /docs/reference/continuous.rst: -------------------------------------------------------------------------------- 1 | Continuous Systems (:mod:`continuous_systems`) 2 | ----------------------------------------------- 3 | 4 | .. module:: continuous_systems 5 | :synopsis: Continuous Systems 6 | 7 | .. .. py:module:: continuous_systems 8 | 9 | .. autofunction:: euler_beam_modes 10 | 11 | .. autofunction:: euler_beam_frf 12 | -------------------------------------------------------------------------------- /docs/reference/ema.rst: -------------------------------------------------------------------------------- 1 | Experimental Modal Analysis (:mod:`vibration_toolbox.ema`) 2 | ------------------------------------------------------------------ 3 | 4 | .. sectionauthor:: Joseph C. Slater 5 | 6 | .. .. currentmodule:: vibration_toolbox.mdof 7 | 8 | .. The :mod:`vibration_toolbox.mdof` 9 | 10 | .. (:mod:`vibration_toolbox.mdof`) 11 | 12 | .. .. literalinclude:: examples/4-1 13 | 14 | :mod:`ema` -- Experimental Modal Analysis 15 | ___________________________________________________ 16 | 17 | .. .. module:: mdof 18 | :synopsis: Multiple Degree of Freedom Systems 19 | 20 | .. .. py:module:: ema 21 | 22 | .. automodule:: ema 23 | :members: 24 | -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | _________ 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | sdof 8 | mdof 9 | continuous 10 | vibe_system 11 | ema 12 | -------------------------------------------------------------------------------- /docs/reference/mdof.rst: -------------------------------------------------------------------------------- 1 | Multiple Degree of Freedom Systems (:mod:`vibration_toolbox.mdof`) 2 | ------------------------------------------------------------------ 3 | 4 | .. sectionauthor:: Joseph C. Slater 5 | 6 | .. .. currentmodule:: vibration_toolbox.mdof 7 | 8 | .. The :mod:`vibration_toolbox.mdof` 9 | 10 | .. (:mod:`vibration_toolbox.mdof`) 11 | 12 | .. .. literalinclude:: examples/4-1 13 | 14 | :mod:`mdof` -- Multiple Degree of Freedom Functions 15 | ___________________________________________________ 16 | 17 | .. .. module:: mdof 18 | :synopsis: Multiple Degree of Freedom Systems 19 | 20 | .. .. py:module:: mdof 21 | 22 | .. automodule:: mdof 23 | :members: 24 | -------------------------------------------------------------------------------- /docs/reference/sdof.rst: -------------------------------------------------------------------------------- 1 | Single Degree of Freedom Systems (:mod:`vibration_toolbox.sdof`) 2 | ---------------------------------------------------------------- 3 | 4 | .. sectionauthor:: Joseph C. Slater 5 | 6 | .. currentmodule:: vibration_toolbox.sdof 7 | 8 | The :mod:`vibration_toolbox.sdof` 9 | 10 | 11 | .. .. literalinclude:: examples/4-1 12 | 13 | :mod:`sdof` -- Single Degree of Freedom Functions 14 | _________________________________________________ 15 | 16 | .. module:: sdof 17 | :synopsis: Single Degree of Freedom Systems 18 | 19 | .. .. py:module:: sdof 20 | 21 | .. autofunction:: free_response 22 | 23 | .. autofunction:: phase_plot 24 | 25 | .. autofunction:: phase_plot_i 26 | 27 | .. autofunction:: time_plot_i 28 | 29 | .. autofunction:: euler 30 | 31 | .. autofunction:: forced_response 32 | 33 | .. autofunction:: transmissibility 34 | 35 | .. autofunction:: rotating_unbalance 36 | 37 | .. autofunction:: impulse_response 38 | 39 | .. autofunction:: step_response 40 | 41 | .. autofunction:: fourier_series 42 | 43 | .. autofunction:: fourier_approximation 44 | 45 | .. autofunction:: response_spectrum 46 | -------------------------------------------------------------------------------- /docs/reference/vibe_system.rst: -------------------------------------------------------------------------------- 1 | Vibration Systems (:mod:`vibesystem`) 2 | ---------------------------------------- 3 | 4 | 5 | .. I can't figure out how to get this to work on modules and classes. 6 | 7 | 8 | .. py:currentmodule:: vibration_toolbox.vibesystem 9 | 10 | .. autoclass:: VibeSystem 11 | :members: 12 | -------------------------------------------------------------------------------- /docs/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | ________ 3 | 4 | 5 | Tutorials with worked examples and background information for the Vibration Toolbox. 6 | 7 | .. sectionauthor:: Joseph C. Slater 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | sdof_notebook.ipynb 13 | mdof_notebook.ipynb 14 | continuous_systems_notebook.ipynb 15 | vibesystem_notebook.ipynb 16 | -------------------------------------------------------------------------------- /docs/tutorial/mdof_notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Multiple Degree of Freedom" 8 | ] 9 | }, 10 | { 11 | "cell_type": "markdown", 12 | "metadata": {}, 13 | "source": [ 14 | "[Download This Notebook](http://vibrationtoolbox.github.io/vibration_toolbox/_downloads/mdof_notebook.ipynb)" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": { 21 | "collapsed": true 22 | }, 23 | "outputs": [], 24 | "source": [] 25 | } 26 | ], 27 | "metadata": { 28 | "kernelspec": { 29 | "display_name": "Python 3", 30 | "language": "python", 31 | "name": "python3" 32 | }, 33 | "language_info": { 34 | "codemirror_mode": { 35 | "name": "ipython", 36 | "version": 3 37 | }, 38 | "file_extension": ".py", 39 | "mimetype": "text/x-python", 40 | "name": "python", 41 | "nbconvert_exporter": "python", 42 | "pygments_lexer": "ipython3", 43 | "version": "3.6.1" 44 | } 45 | }, 46 | "nbformat": 4, 47 | "nbformat_minor": 2 48 | } 49 | -------------------------------------------------------------------------------- /docs/tutorial/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | matplotlib 4 | sphinx 5 | ipython 6 | control 7 | vibration_toolbox 8 | xarray 9 | -------------------------------------------------------------------------------- /docs/tutorial/sdof_characteristic_responses.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "# Select each cell in order and hit \"shift-return\" or \"shift-enter\" to execute\n", 10 | "import vibration_toolbox as vtb" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 2, 16 | "metadata": {}, 17 | "outputs": [ 18 | { 19 | "data": { 20 | "application/vnd.jupyter.widget-view+json": { 21 | "model_id": "3e096b79d10545ebbcc74564689411c1", 22 | "version_major": 2, 23 | "version_minor": 0 24 | }, 25 | "text/plain": [ 26 | "VBox(children=(HBox(children=(VBox(children=(Label(value='Mass'), FloatSlider(value=5.0, continuous_update=Fal…" 27 | ] 28 | }, 29 | "metadata": {}, 30 | "output_type": "display_data" 31 | } 32 | ], 33 | "source": [ 34 | "display(vtb.sdof_interact())" 35 | ] 36 | } 37 | ], 38 | "metadata": { 39 | "kernelspec": { 40 | "display_name": "Python 3", 41 | "language": "python", 42 | "name": "python3" 43 | }, 44 | "language_info": { 45 | "codemirror_mode": { 46 | "name": "ipython", 47 | "version": 3 48 | }, 49 | "file_extension": ".py", 50 | "mimetype": "text/x-python", 51 | "name": "python", 52 | "nbconvert_exporter": "python", 53 | "pygments_lexer": "ipython3", 54 | "version": "3.7.1" 55 | }, 56 | "latex_envs": { 57 | "LaTeX_envs_menu_present": true, 58 | "autoclose": false, 59 | "autocomplete": true, 60 | "bibliofile": "biblio.bib", 61 | "cite_by": "apalike", 62 | "current_citInitial": 1, 63 | "eqLabelWithNumbers": true, 64 | "eqNumInitial": 1, 65 | "hotkeys": { 66 | "equation": "Ctrl-E", 67 | "itemize": "Ctrl-I" 68 | }, 69 | "labels_anchors": false, 70 | "latex_user_defs": false, 71 | "report_style_numbering": false, 72 | "user_envs_cfg": false 73 | }, 74 | "toc": { 75 | "base_numbering": 1, 76 | "nav_menu": {}, 77 | "number_sections": true, 78 | "sideBar": true, 79 | "skip_h1_title": false, 80 | "title_cell": "Table of Contents", 81 | "title_sidebar": "Contents", 82 | "toc_cell": false, 83 | "toc_position": {}, 84 | "toc_section_display": true, 85 | "toc_window_display": false 86 | }, 87 | "varInspector": { 88 | "cols": { 89 | "lenName": 16, 90 | "lenType": 16, 91 | "lenVar": 40 92 | }, 93 | "kernels_config": { 94 | "python": { 95 | "delete_cmd_postfix": "", 96 | "delete_cmd_prefix": "del ", 97 | "library": "var_list.py", 98 | "varRefreshCmd": "print(var_dic_list())" 99 | }, 100 | "r": { 101 | "delete_cmd_postfix": ") ", 102 | "delete_cmd_prefix": "rm(", 103 | "library": "var_list.r", 104 | "varRefreshCmd": "cat(var_dic_list()) " 105 | } 106 | }, 107 | "types_to_exclude": [ 108 | "module", 109 | "function", 110 | "builtin_function_or_method", 111 | "instance", 112 | "_Feature" 113 | ], 114 | "window_display": false 115 | } 116 | }, 117 | "nbformat": 4, 118 | "nbformat_minor": 2 119 | } 120 | -------------------------------------------------------------------------------- /docs/tutorial/system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/docs/tutorial/system.png -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --ignore=setup.py --doctest-modules 3 | doctest_optionflags= NORMALIZE_WHITESPACE ELLIPSIS 4 | -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | ============================================= 2 | The Engineering Vibration Toolbox for Python 3 | ============================================= 4 | 5 | .. .. include:: 6 | .. image:: https://badge.fury.io/py/vibration-toolbox.png/ 7 | :target: http://badge.fury.io/py/vibration-toolbox 8 | 9 | .. image:: https://travis-ci.org/vibrationtoolbox/vibration_toolbox.svg?branch=master 10 | :target: https://travis-ci.org/vibrationtoolbox/vibration_toolbox 11 | 12 | .. image:: https://zenodo.org/badge/39572419.svg 13 | :target: https://zenodo.org/badge/latestdoi/39572419 14 | 15 | .. image:: https://mybinder.org/badge.svg 16 | :target: https://mybinder.org/v2/gh/vibrationtoolbox/vibration_toolbox/binder 17 | 18 | .. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg 19 | :target: https://saythanks.io/to/josephcslater 20 | 21 | .. image:: https://img.shields.io/badge/patreon-donate-yellow.svg 22 | :target: https://www.patreon.com/josephcslater 23 | 24 | .. .. image:: https://img.shields.io/pypi/v/vibration_toolbox.svg 25 | :target: https://img.shields.io/pypi/v/vibration_toolbox 26 | 27 | .. #image:: https://coveralls.io/repos/vibrationtoolbox/vibration_toolbox/badge.png?branch=master 28 | .. #:target: https://coveralls.io/r/vibrationtoolbox/vibration_toolbox 29 | 30 | .. image:: http://pepy.tech/badge/vibration-toolbox 31 | :target: http://pepy.tech/project/vibration-toolbox 32 | :alt: PyPi Download stats 33 | 34 | 35 | Joseph C. Slater and Raphael Timbó 36 | ---------------------------------- 37 | 38 | Welcome to the `Vibration Toolbox `_. 39 | This `Python `_ version is a completely new design build for modern education. This is an *educational* set of codes intended primarily for 40 | demonstration of vibration analysis and phenomenon. You may find them useful for application, but that isn't the intent of this toolbox. If you have professional-level needs please `contact the authors `_. 41 | 42 | Full `documentation is available `_, but please excuse that it is still under development. Such documentation has never existed for the other ports of the toolbox so this is taking some time. We don't need feedback at this time, but we will take assistance in improving documentation and code. *Please* clone the repository and support use by submitting pull requests fixing typos and clarifying documentation. 43 | 44 | 45 | Try now! 46 | -------- 47 | 48 | You won't get everything, but you can try parts of the toolbox immediately by riunning the `tutorial on mybinder.org `_, right in your browser window! 49 | 50 | 51 | Installation 52 | ------------ 53 | 54 | If you aren't familiar at all with Python, please see `Installing Python `_. 55 | 56 | Installation is made easy with ``pip`` (or ``pip3``), with releases as we have time while we try 57 | to create a full first release. Much of it works already, but we certainly need 58 | issue reports (on `github `_). 59 | 60 | To install type:: 61 | 62 | pip install --user vibration_toolbox 63 | 64 | at your command prompt **(not the python prompt)** where ``--user`` isn't necessary if you are using a locally installed version of Python such as `Anaconda `_. 65 | 66 | To run, I recommend you open a `Jupyter `_ notebook by using ``jupyter notebook`` at your command prompt/terminal prompt/Anaconda prompt and then type:: 67 | 68 | import vibration_toolbox as vtb 69 | 70 | For examples, see the `JupyterNotebooks folder `_. Some of these have interactive capabilities that are only apparent when you run them yourself instead of just looking at them on GitHub. Unfortunately our organization of these still leaves a little to be desired. Help accepted! 71 | 72 | Installation of current code/contributing 73 | _________________________________________ 74 | 75 | The usage documentation is far behind the current code, while the reference is way ahead of the released code due to the `autodoc `_ capability of `Sphinx `_. Especially as of 2017, the code is still in rapid development. So is the documentation. Releases to `pypi `_. 76 | 77 | If you wish to install the current version of the software, and especially contribute, please follow the instructions in `Contributing.rst `_ 78 | 79 | That should be it. Please note issues on the `issues tab `_ on GitHub. 80 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy >= 1.12.0 2 | scipy >= 0.18.0 3 | matplotlib >= 2.0.0 4 | sphinx >= 1.6.0 5 | ipython >= 5.8.0 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = readme.rst 3 | 4 | [build_sphinx] 5 | source-dir = docs 6 | build-dir = docs/build 7 | all_files = 1 8 | 9 | [upload_sphinx] 10 | upload-dir = docs/build/html 11 | 12 | [bdist_wheel] 13 | python-tag = py35 14 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | #from distutils.core import setup 4 | from setuptools import setup 5 | import os 6 | import sys 7 | 8 | if sys.version_info < (3, 5): 9 | sys.exit('Sorry, Python < 3.5 is not supported.') 10 | # Utility function to read the README file. 11 | # Used for the long_description. It's nice, because now 1) we have a top level 12 | # README file and 2) it's easier to type in the README file than to put a raw 13 | # string in below ... 14 | def read(fname): 15 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 16 | 17 | with open('vibration_toolbox/__init__.py', 'rb') as fid: 18 | for line in fid: 19 | line = line.decode('utf-8') 20 | if line.startswith('__version__'): 21 | version = line.strip().split()[-1][1:-1] 22 | break 23 | 24 | download_url = ('https://github.com/vibrationtoolbox/vibration_toolbox/\ 25 | blob/master/dist/vibration_toolbox-' + version + '.whl') 26 | 27 | 28 | setup(name='vibration_toolbox', 29 | version=version, 30 | description=('Educational code illustrating fundamentals of vibration \ 31 | for engineers.'), 32 | author=u'Joseph C. Slater and Raphael Timbó', 33 | author_email='joseph.c.slater@gmail.com', 34 | url='https://github.com/vibrationtoolbox/vibration_toolbox', 35 | packages=['vibration_toolbox'], 36 | package_data={'vibration_toolbox': ['../readme.rst', 'data/*.mat'], 37 | '': ['readme.rst']}, 38 | long_description=read('readme.rst'), 39 | keywords=['vibration', 'mechanical engineering', 'civil engineering'], 40 | install_requires=['numpy', 'scipy', 'matplotlib', 'jupyter'], 41 | setup_requires=['pytest-runner'], 42 | tests_require=['pytest'], 43 | include_package_data=True 44 | ) 45 | 46 | # https://docs.python.org/3/distutils/setupscript.html#additional-meta-data 47 | -------------------------------------------------------------------------------- /vibration_toolbox/__init__.py: -------------------------------------------------------------------------------- 1 | from .continuous_systems import * 2 | from .vibesystem import * 3 | from .ema import * 4 | from .mdof import * 5 | from .sdof import * 6 | import matplotlib as mpl 7 | import sys 8 | """The Vibration Toolbox, Python Edition. 9 | 10 | Joseph C. Slater and Raphael Timbó 11 | 12 | `import vibration_toolbox as vtb` will keep them inside the `vtb` namespace 13 | 14 | `import vibration_toolbox.sdof as sdof` will keep the sdof functions in the 15 | `sdof` namespace. 16 | """ 17 | 18 | __title__ = 'vibration_toolbox' 19 | # version may have no more then numerical digits after decimal point. 20 | # 1.11 is actually a higher release than 1.2 (confusing) 21 | __version__ = '0.6.10' 22 | __author__ = u'Joseph C. Slater and Raphael Timbó' 23 | __license__ = 'MIT' 24 | __copyright__ = 'Copyright 1991-2019 Joseph C. Slater' 25 | __all__ = ['sdof', 'mdof', 'ema', 'vibesystem', 'continuous_systems', 26 | '__version__'] 27 | 28 | """ 29 | If the __all__ above is commented out, this code will then execute to 30 | completion, as the default behaviour of import * is to import all symbols 31 | that do not begin with an underscore, from the given namespace. 32 | 33 | Reference: 34 | https://docs.python.org/3.5/tutorial/modules.html#importing-from-a-package 35 | """ 36 | 37 | 38 | if 'pytest' in sys.argv[0]: 39 | # print('Setting backend to agg to run tests') 40 | mpl.use('agg') 41 | 42 | 43 | # print options were change inside modules to produce better 44 | # outputs at examples. Here we set the print options to the 45 | # default values after importing the modules to avoid changing 46 | # np default print options when importing the toolbox. 47 | np.set_printoptions(edgeitems=3, infstr='inf', linewidth=75, 48 | nanstr='nan', precision=8, suppress=False, 49 | threshold=1000, formatter=None) 50 | -------------------------------------------------------------------------------- /vibration_toolbox/conftest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | np.set_printoptions(precision=2, suppress=True) 4 | print('Running conftest.py') 5 | -------------------------------------------------------------------------------- /vibration_toolbox/continuous_systems.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import numpy as np 3 | import scipy as sp 4 | import matplotlib as mpl 5 | from scipy.interpolate import UnivariateSpline 6 | from scipy.optimize import newton_krylov 7 | 8 | try: 9 | from IPython.display import clear_output, display, HTML 10 | from ipywidgets.widgets.interaction import interact, interactive 11 | except ImportError: 12 | print('Interactive iPython tools will not work without IPython.display \ 13 | and ipywidgets installed.') 14 | 15 | 16 | def _in_ipynb(): 17 | try: 18 | cfg = get_ipython().config 19 | if cfg['IPKernelApp']['parent_appname'] == 'ipython-notebook': 20 | return True 21 | else: 22 | return False 23 | except NameError: 24 | return False 25 | 26 | 27 | mpl.rcParams['lines.linewidth'] = 2 28 | mpl.rcParams['figure.figsize'] = (10, 6) 29 | 30 | 31 | def euler_beam_modes(n=10, bctype=3, npoints=2001, 32 | beamparams=np.array([7.31e10, 8.4375e-09, 33 | 2747.0, 4.5e-04, 0.4])): 34 | """Mode shapes and natural frequencies of Euler-Bernoulli beam. 35 | 36 | Parameters 37 | ---------- 38 | n: int, numpy array 39 | highest mode number or array of mode numbers to return 40 | bctype: int 41 | bctype = 1 free-free 42 | bctype = 2 clamped-free 43 | bctype = 3 clamped-pinned 44 | bctype = 4 clamped-sliding 45 | bctype = 5 clamped-clamped 46 | bctype = 6 pinned-pinned 47 | beamparams: numpy array 48 | E, I, rho, A, L, 49 | Young's modulus, second moment of area, density, cross section area, 50 | length of beam 51 | npoints: int 52 | number of points for returned mode shape array 53 | 54 | Returns 55 | ------- 56 | omega_n: numpy array 57 | array of natural frequencies 58 | x: numpy array 59 | x coordinate 60 | U: numpy array 61 | mass normalized mode shape 62 | 63 | Examples 64 | -------- 65 | >>> import matplotlib.pyplot as plt 66 | >>> import vibration_toolbox as vtb 67 | >>> omega_n, x, U = vtb.euler_beam_modes(n=1) 68 | >>> plt.figure() 69 |
70 | >>> plt.plot(x,U) 71 | [] 72 | >>> plt.xlabel('x (m)') 73 | Text(0.5, 0, 'x (m)') 74 | >>> plt.ylabel('Displacement (m)') 75 | Text(0, 0.5, 'Displacement (m)') 76 | >>> plt.title('Mode 1') 77 | Text(0.5, 1.0, 'Mode 1') 78 | >>> plt.grid(True) 79 | """ 80 | 81 | E = beamparams[0] 82 | I = beamparams[1] 83 | rho = beamparams[2] 84 | A = beamparams[3] 85 | L = beamparams[4] 86 | if isinstance(n, int): 87 | ln = n 88 | n = np.arange(n) + 1 89 | else: 90 | ln = len(n) 91 | 92 | # len=[0:(1/(npoints-1)):1]'; %Normalized length of the beam 93 | x_normed = np.linspace(0, 1, npoints, endpoint=True) 94 | x = x_normed * L 95 | # Determine natural frequencies and mode shapes depending on the 96 | # boundary condition. 97 | # Mass simplification. The following was arange_(1,length_(n)).reshape(-1) 98 | mode_num_range = np.arange(0, ln) 99 | Bnl = np.empty(ln) 100 | w = np.empty(ln) 101 | U = np.empty([npoints, ln]) 102 | 103 | if bctype == 1: 104 | desc = 'Free-Free ' 105 | Bnllow = np.array((0, 0, 4.73004074486, 7.8532046241, 106 | 10.995607838, 14.1371654913, 17.2787596574)) 107 | for i in mode_num_range: 108 | if n[i] > 7: 109 | Bnl[i] = (2 * n[i] - 3) * np.pi / 2 110 | else: 111 | Bnl[i] = Bnllow[i] 112 | for i in mode_num_range: 113 | if n[i] == 1: 114 | w[i] = 0 115 | U[:, i] = 1 + x_normed * 0 116 | elif n[i] == 2: 117 | w[i] = 0 118 | U[:, i] = x_normed - 0.5 119 | else: 120 | sig = (np.cosh(Bnl[i]) - np.cos(Bnl[i])) / \ 121 | (np.sinh(Bnl[i]) - np.sin(Bnl[i])) 122 | w[i] = (Bnl[i] ** 2) * np.sqrt(E * I / (rho * A * L ** 4)) 123 | b = Bnl[i] * x_normed 124 | U[:, i] = np.cosh(b) + np.cos(b) - sig * \ 125 | (np.sinh(b) + np.sin(b)) 126 | elif bctype == 2: 127 | desc = 'Clamped-Free ' 128 | Bnllow = np.array((1.88, 4.69, 7.85, 10.99, 14.14)) 129 | for i in mode_num_range: 130 | if n[i] > 4: 131 | Bnl[i] = (2 * n[i] - 1) * np.pi / 2 132 | else: 133 | Bnl[i] = Bnllow[i] 134 | 135 | for i in mode_num_range: 136 | sig = (np.sinh(Bnl[i]) - np.sin(Bnl[i])) / \ 137 | (np.cosh(Bnl[i]) - np.cos(Bnl[i])) 138 | w[i] = (Bnl[i] ** 2) * np.sqrt(E * I / (rho * A * L ** 4)) 139 | b = Bnl[i] * x_normed 140 | # plt.plot(x,(sp.cosh(b) - sp.cos(b) - 141 | # sig * (sp.sinh(b) - sp.sin(b)))) 142 | U[:, i] = np.cosh(b) - np.cos(b) - sig * (np.sinh(b) - np.sin(b)) 143 | 144 | elif bctype == 3: 145 | desc = 'Clamped-Pinned ' 146 | Bnllow = np.array((3.93, 7.07, 10.21, 13.35, 16.49)) 147 | for i in mode_num_range: 148 | if n[i] > 4: 149 | Bnl[i] = (4 * n[i] + 1) * np.pi / 4 150 | else: 151 | Bnl[i] = Bnllow[i] 152 | for i in mode_num_range: 153 | sig = (np.cosh(Bnl[i]) - np.cos(Bnl[i])) / \ 154 | (np.sinh(Bnl[i]) - np.sin(Bnl[i])) 155 | w[i] = (Bnl[i] ** 2) * np.sqrt(E * I / (rho * A * L ** 4)) 156 | b = Bnl[i] * x_normed 157 | U[:, i] = np.cosh(b) - np.cos(b) - sig * (np.sinh(b) - np.sin(b)) 158 | elif bctype == 4: 159 | desc = 'Clamped-Sliding ' 160 | Bnllow = np.array((2.37, 5.5, 8.64, 11.78, 14.92)) 161 | for i in mode_num_range: 162 | if n[i] > 4: 163 | Bnl[i] = (4 * n[i] - 1) * np.pi / 4 164 | else: 165 | Bnl[i] = Bnllow[i] 166 | for i in mode_num_range: 167 | sig = (np.sinh(Bnl[i]) + np.sin(Bnl[i])) / \ 168 | (np.cosh(Bnl[i]) - np.cos(Bnl[i])) 169 | w[i] = (Bnl[i] ** 2) * np.sqrt(E * I / (rho * A * L ** 4)) 170 | b = Bnl[i] * x_normed 171 | U[:, i] = np.cosh(b) - np.cos(b) - sig * (np.sinh(b) - np.sin(b)) 172 | elif bctype == 5: 173 | desc = 'Clamped-Clamped ' 174 | Bnllow = np.array((4.73, 7.85, 11, 14.14, 17.28)) 175 | for i in mode_num_range: 176 | if n[i] > 4: 177 | Bnl[i] = (2 * n[i] + 1) * np.pi / 2 178 | else: 179 | Bnl[i] = Bnllow[i] 180 | for i in mode_num_range: 181 | sig = (np.cosh(Bnl[i]) - np.cos(Bnl[i])) / \ 182 | (np.sinh(Bnl[i]) - np.sin(Bnl[i])) 183 | w[i] = (Bnl[i] ** 2) * np.sqrt(E * I / (rho * A * L ** 4)) 184 | b = Bnl[i] * x_normed 185 | U[:, i] = np.cosh(b) - np.cos(b) - sig * (np.sinh(b) - np.sin(b)) 186 | elif bctype == 6: 187 | desc = 'Pinned-Pinned ' 188 | for i in mode_num_range: 189 | Bnl[i] = n[i] * np.pi 190 | w[i] = (Bnl[i] ** 2) * np.sqrt(E * I / (rho * A * L ** 4)) 191 | U[:, i] = np.sin(Bnl[i] * x_normed) 192 | 193 | # Mass Normalization of mode shapes 194 | for i in mode_num_range: 195 | U[:, i] = U[:, i] / np.sqrt(np.dot(U[:, i], U[:, i]) * rho * A * L) 196 | 197 | omega_n = w 198 | return omega_n, x, U 199 | 200 | 201 | def euler_beam_frf(xin=0.22, xout=0.32, fmin=0.0, fmax=1000.0, zeta=0.02, 202 | bctype=2, npoints=2001, 203 | beamparams=np.array([7.31e10, 1 / 12 * 0.03 * .015 ** 3, 204 | 2747.0, .015 * 0.03, 0.4])): 205 | """Frequency response function fo Euler-Bernoulli beam. 206 | 207 | Parameters 208 | ---------- 209 | xin: float 210 | location of applied force 211 | xout: float 212 | location of displacement sensor 213 | fmin: float 214 | lowest frequency of interest 215 | fmax: float 216 | highest frequency of interest 217 | zeta: float 218 | damping ratio 219 | bctype: int 220 | bctype = 1 free-free 221 | bctype = 2 clamped-free 222 | bctype = 3 clamped-pinned 223 | bctype = 4 clamped-sliding 224 | bctype = 5 clamped-clamped 225 | bctype = 6 pinned-pinned 226 | beamparams: numpy array 227 | E, I, rho, A, L, 228 | Young's modulus, second moment of area, density, cross section area, 229 | length of beam 230 | npoints: int 231 | number of points for returned mode shape array 232 | 233 | Returns 234 | ------- 235 | fout: numpy array 236 | array of driving frequencies (Hz) 237 | H: numpy array 238 | Frequency Response Function 239 | 240 | Examples 241 | -------- 242 | >>> import matplotlib.pyplot as plt 243 | >>> import vibration_toolbox as vtb 244 | >>> _, _ = vtb.euler_beam_frf() 245 | 246 | """ 247 | 248 | E = beamparams[0] 249 | I = beamparams[1] 250 | rho = beamparams[2] 251 | A = beamparams[3] 252 | L = beamparams[4] 253 | npoints = 2001 254 | i = 0 255 | w = np.linspace(fmin, fmax, 2001) * 2 * sp.pi 256 | if min([xin, xout]) < 0 or max([xin, xout]) > L: 257 | print('One or both locations are not on the beam') 258 | return 259 | wn = np.array((0, 0)) 260 | # The number 200 is arbitrarily large and unjustified. 261 | a = np.empty([npoints, 200], dtype=complex) 262 | f = np.empty(100) 263 | 264 | while wn[-1] < 1.3 * (fmax * 2 * sp.pi): 265 | i = i + 1 266 | wn, xx, U = euler_beam_modes(n=i, bctype=bctype, 267 | beamparams=beamparams, npoints=5000) 268 | spl = UnivariateSpline(xx, U[:, i - 1]) 269 | Uin = spl(xin) 270 | Uout = spl(xout) 271 | a[:, i - 1] = rho * A * Uin * Uout / \ 272 | (wn[-1] ** 2 - w ** 2 + 2 * zeta * wn[-1] * w * np.lib.scimath.sqrt(-1)) 273 | f[i] = wn[-1] / 2 / sp.pi 274 | a = a[:, 0:i] 275 | plt.figure() 276 | plt.subplot(211) 277 | plt.plot(w / 2 / sp.pi, 20 * np.lib.scimath.log10(np.absolute(np.sum(a, axis=1))), '-') 278 | # plt.hold(True) 279 | plt.plot(w / 2 / sp.pi, 20 * np.lib.scimath.log10(np.absolute(a)), '-') 280 | plt.grid(True) 281 | plt.xlabel('Frequency (Hz)') 282 | plt.ylabel('FRF (dB)') 283 | axlim = plt.axis() 284 | 285 | plt.axis(axlim + np.array([0, 0, -0.1 * (axlim[3] - axlim[2]), 286 | 0.1 * (axlim[3] - axlim[2])])) 287 | 288 | plt.subplot(212) 289 | plt.plot(w / 2 / sp.pi, np.unwrap(np.angle(np.sum(a, axis=1))) / 290 | sp.pi * 180, '-') 291 | plt.plot(w / 2 / sp.pi, np.unwrap(np.angle(a)) / sp.pi * 180, '-') 292 | plt.grid(True) 293 | plt.xlabel('Frequency (Hz)') 294 | plt.ylabel('Phase (deg)') 295 | plt.tight_layout() 296 | axlim = plt.axis() 297 | plt.axis(axlim + np.array([0, 0, -0.1 * (axlim[3] - axlim[2]), 298 | 0.1 * (axlim[3] - axlim[2])])) 299 | plt.show() 300 | 301 | fout = w / 2 / sp.pi 302 | H = a 303 | return fout, H 304 | 305 | 306 | def ebf(xin, xout, fmin, fmax, zeta): 307 | """Shortcut call to `euler_beam_frf`.""" 308 | _, _ = euler_beam_frf(xin, xout, fmin, fmax, zeta) 309 | return 310 | 311 | 312 | def ebf1(xin, xout): 313 | """Shortcut call to `euler_beam_frf`.""" 314 | _, _ = euler_beam_frf(xin, xout) 315 | return 316 | 317 | 318 | def uniform_bar_modes(n=10, bctype=3, npoints=2001, 319 | barparams=np.array([7.31e10, 2747.0, 0.4]), 320 | kl_over_EA = 1000, m_over_rhoAL = 1000): 321 | """Mode shapes and natural frequencies of Uniform bar/rod. 322 | 323 | Parameters 324 | ---------- 325 | n: int, numpy array 326 | highest mode number or array of mode numbers to return 327 | bctype: int 328 | bctype = 1 free-free 329 | bctype = 2 fixed-free 330 | bctype = 3 fixed-fixed 331 | bctype = 4 fixed-spring 332 | bctype = 5 fixed-mass 333 | barparams: numpy array 334 | E, rho, L 335 | Young's modulus, density, length of bar 336 | npoints: int 337 | number of points for returned mode shape array 338 | kl_over_EA: float 339 | spring stiffness times rod length divided by EA. (for clamped-spring 340 | condition) 341 | m_over_rhoAL: float 342 | end mass divided by rho A L of rod. (for end mass condition) 343 | 344 | Returns 345 | ------- 346 | omega_n: numpy array 347 | array of natural frequencies 348 | x: numpy array 349 | x coordinate 350 | U: numpy array 351 | mass normalized mode shape 352 | 353 | Notes 354 | ----- 355 | For most situations the cross sectional area cancels out and has no effect. It only matters in 356 | the last two cases: and end spring or and mass, and is included with the parameters special to 357 | those cases. 358 | 359 | Examples 360 | -------- 361 | >>> import matplotlib.pyplot as plt 362 | >>> import vibration_toolbox as vtb 363 | >>> omega_n, x, U = vtb.uniform_bar_modes(n=3) 364 | >>> plt.figure() 365 | 366 | >>> plt.plot(x,U) 367 | [] 368 | >>> plt.xlabel('x (m)') 369 | 370 | >>> plt.ylabel('Displacement (m)') 371 | 372 | >>> plt.title('Mode 3') 373 | 374 | """ 375 | E = barparams[0] 376 | rho = barparams[1] 377 | L = barparams[2] 378 | if isinstance(n, int): 379 | ln = n 380 | n = np.arange(n) + 1 381 | else: 382 | ln = len(n) 383 | 384 | # len=[0:(1/(npoints-1)):1]'; %Normalized length of the bar 385 | x_normed = np.linspace(0, 1, npoints, endpoint = True) 386 | x = x_normed * L 387 | # Determine natural frequencies and mode shapes depending on the 388 | # boundary condition. 389 | # Mass simplification. The following was arange_(1,length_(n)).reshape(-1) 390 | mode_num_range = np.arange(1, ln) 391 | w = np.empty(ln) 392 | U = np.empty([npoints, ln]) 393 | 394 | if bctype == 1: 395 | desc = 'Free-Free ' 396 | for i in mode_num_range: 397 | w[i] = i * np.pi * np.sqrt(E/rho) / L 398 | U[:, i] = np.cos(i * np.pi * x_normed) 399 | elif bctype == 2: 400 | desc = 'Fixed-Free ' 401 | for i in mode_num_range: 402 | w[i] = (2*i-1) * np.pi * np.sqrt(E/rho) / (2 * L) 403 | U[:, i] = np.sin((2*i-1) * np.pi * x_normed / 2) 404 | elif bctype == 3: 405 | desc = 'Fixed-Fixed ' 406 | for i in mode_num_range: 407 | w[i] = i * np.pi * np.sqrt(E/rho) / L 408 | U[:, i] = np.sin((i) * np.pi * x_normed) 409 | elif bctype == 4: 410 | desc = 'Fixed-Spring' 411 | def func(lam): 412 | return lam + np.tan(lam)* kl_over_EA 413 | for i in mode_num_range: 414 | lam = newton_krylov(func, (0.25+i/2)*np.pi) 415 | w[i] = lam * np.sqrt(E/rho) / L 416 | U[:, i] = np.sin(lam * x_normed) 417 | elif bctype == 5: 418 | desc = 'Fixed-Mass' 419 | def func(lam): 420 | return np.tan(lam)-1/lam/m_over_rhoAL 421 | for i in mode_num_range: 422 | lam = newton_krylov(func, 0.25+i*np.pi) 423 | w[i] = lam * np.sqrt(E/rho) / L 424 | U[:, i] = np.sin(lam * x_normed) 425 | 426 | 427 | 428 | # Mass Normalization of mode shapes 429 | for i in mode_num_range: 430 | U[:, i] = U[:, i] / np.sqrt(np.dot(U[:, i], U[:, i]) * rho * L) 431 | 432 | omega_n = w 433 | return omega_n, x, U 434 | 435 | """ 436 | def ebf(xin, xout, fmin, fmax, zeta): 437 | _, _ = uniform_bar_frf(xin, xout, fmin, fmax, zeta) 438 | return 439 | """ 440 | 441 | def uniform_bar_modes(n=10, bctype=3, npoints=2001, 442 | barparams=np.array([7.31e10, 2747.0, 0.4])): 443 | """Mode shapes and natural frequencies of Uniform bar/rod. 444 | 445 | Parameters 446 | ---------- 447 | n: int, numpy array 448 | highest mode number or array of mode numbers to return 449 | bctype: int 450 | bctype = 1 free-free 451 | bctype = 2 fixed-free 452 | bctype = 3 fixed-fixed 453 | 454 | barparams: numpy array 455 | E, rho, L 456 | Young's modulus, density, length of bar 457 | npoints: int 458 | number of points for returned mode shape array 459 | 460 | 461 | Returns 462 | ------- 463 | omega_n: numpy array 464 | array of natural frequencies 465 | x: numpy array 466 | x coordinate 467 | U: numpy array 468 | mass normalized mode shape 469 | 470 | 471 | Examples 472 | -------- 473 | >>> import matplotlib.pyplot as plt 474 | >>> import vibration_toolbox as vtb 475 | >>> omega_n, x, U, *_ = vtb.uniform_bar_modes(n=1) 476 | >>> 477 | >>> plt.plot(x,U) 478 | [] 479 | >>> plt.xlabel('x (m)') 480 | Text(0.5, 0, 'x (m)') 481 | >>> plt.ylabel('Displacement (m)') 482 | Text(0, 0.5, 'Displacement (m)') 483 | >>> plt.title('Mode 1') 484 | Text(0.5, 1.0, 'Mode 1') 485 | >>> plt.show() 486 | """ 487 | E = barparams[0] 488 | rho = barparams[1] 489 | L = barparams[2] 490 | if isinstance(n, int): 491 | ln = n 492 | n = np.arange(n) + 1 493 | else: 494 | ln = len(n) 495 | 496 | # len=[0:(1/(npoints-1)):1]'; %Normalized length of the bar 497 | lenth = np.linspace(0, L, npoints) 498 | x = lenth * L 499 | # Determine natural frequencies and mode shapes depending on the 500 | # boundary condition. 501 | # Mass simplification. The following was arange_(1,length_(n)).reshape(-1) 502 | mode_num_range = np.arange(0, ln) 503 | w = np.empty(ln) 504 | U = np.empty([npoints, ln]) 505 | 506 | if bctype == 1: 507 | desc = 'Free-Free ' 508 | for i in mode_num_range: 509 | w[i] = i * np.pi * np.sqrt(E / rho) / L 510 | U[:, i] = np.cos(i * np.pi * lenth / L) 511 | elif bctype == 2: 512 | desc = 'Fixed-Free ' 513 | for i in mode_num_range: 514 | w[i] = (2 * i - 1) * np.pi * np.sqrt(E / rho) / (2 * L) 515 | U[:, i] = np.sin((2 * i - 1) * np.pi * lenth / (2 * L)) 516 | elif bctype == 3: 517 | desc = 'Fixed-Fixed ' 518 | for i in mode_num_range: 519 | w[i] = i * np.pi * np.sqrt(E / rho) / L 520 | U[:, i] = np.sin(i * np.pi * lenth / (2 * L)) 521 | # Mass Normalization of mode shapes 522 | # for i in mode_num_range: 523 | # U[:, i] = U[:, i] / np.sqrt(np.dot(U[:, i], U[:, i]) * rho * L) 524 | 525 | omega_n = w 526 | return omega_n, x, U 527 | 528 | def torsional_bar_modes(n=10, bctype=2, cstype=4, npoints=2001, 529 | tbarparams=np.array([7.8e9, 8.4375e-09, 530 | 2747.0, 0.4]), cspar=np.array([0.7,0.9,.1,.2])): 531 | """Mode shapes and natural frequencies of Torsional bar. 532 | 533 | Parameters 534 | ---------- 535 | n: int, numpy array 536 | highest mode number or array of mode numbers to return 537 | bctype: int 538 | bctype = 1 free-free 539 | bctype = 2 fixed-free 540 | bctype = 3 fixed-fixed 541 | tbarparams: numpy array 542 | G, J, rho, L 543 | Shear modulus, Polar moment of area, density, 544 | length of bar 545 | npoints: int 546 | number of points for returned mode shape array 547 | cspar: float 548 | Cross-section parameters 549 | cstype: numpy array 550 | Cross-section type 551 | cstype =1 is a circular shaft and cspar = R. 552 | cstype = 2 is a hollow circular shaft and cspar = [R1 R2] 553 | cstype = 3 is a square shaft and cspar = a 554 | cstype = 4 is a hollow rectangular shaft and cspar = [a b A B] 555 | 556 | Returns 557 | ------- 558 | omega_n: numpy array 559 | array of natural frequencies 560 | x: numpy array 561 | x coordinate 562 | U: numpy array 563 | mass normalized mode shape 564 | 565 | Examples 566 | -------- 567 | >>> import matplotlib.pyplot as plt 568 | >>> import vibration_toolbox as vtb 569 | >>> omega_n, x, U,*_ = vtb.torsional_bar_modes(n=1) 570 | >>> 571 | >>> plt.plot(x,U) 572 | [] 573 | >>> plt.xlabel('x (m)') 574 | Text(0.5, 0, 'x (m)') 575 | >>> plt.ylabel('Displacement (m)') 576 | Text(0, 0.5, 'Displacement (m)') 577 | >>> plt.title('Mode 1') 578 | Text(0.5, 1.0, 'Mode 1') 579 | >>> plt.grid(True) 580 | """ 581 | 582 | G = tbarparams[0] 583 | J = tbarparams[1] 584 | rho = tbarparams[2] 585 | L = tbarparams[3] 586 | 587 | if cstype == 1: 588 | R = cspar[0] 589 | g = (np.pi * R ** 4) / 2 590 | elif cstype == 2: 591 | R1 = cspar[0] 592 | R2 = cspar[1] 593 | g = (np.pi / 2) * (R2 ** 4 - R1 ** 4) 594 | elif cstype == 3: 595 | a = cspar[1] 596 | g = .1406 * a ** 4 597 | elif cstype == 4: 598 | if len(cspar) != 4: 599 | print("Not enough parameters for cstype 4") 600 | a = cspar[0] 601 | b = cspar[1] 602 | A = cspar[2] 603 | B = cspar[3] 604 | g = (2 * A * B * (a - A) ** 2 * (b - B) ** 2) / (a * A + b * B - A ** 2 - B ** 2) 605 | 606 | if g<=0: 607 | print("The constant gamma is less than or equal to zero.") 608 | 609 | if isinstance(n, int): 610 | ln = n 611 | n = np.arange(n) + 1 612 | else: 613 | ln = len(n) 614 | 615 | lenth = np.linspace(0, 1, npoints) 616 | x = lenth * L 617 | 618 | # Determine natural frequencies and mode shapes depending on the 619 | # boundary condition. 620 | # Mass simplification. The following was arange_(1,length_(n)).reshape(-1) 621 | mode_num_range = np.arange(0, ln) 622 | w = np.empty(ln) 623 | U = np.empty([npoints, ln]) 624 | c = np.sqrt(G*g/(rho*J)) 625 | 626 | if bctype == 1: 627 | desc = 'Free-Free ' 628 | for i in mode_num_range: 629 | w[i] = (i * np.pi * c) / L 630 | U[:, i] = np.cos(i * np.pi * x / L) 631 | elif bctype == 2: 632 | desc = 'Fixed-Free ' 633 | for i in mode_num_range: 634 | w[i] = (2 * i - 1) * np.pi * c / (2 * L) 635 | U[:, i] = np.sin((2 * i - 1) * np.pi * x / (2 * L)) 636 | elif bctype == 3: 637 | desc = 'Fixed-Fixed ' 638 | for i in mode_num_range: 639 | w[i] = (i * np.pi * c) / L 640 | U[:, i] = np.sin(i * np.pi * x / L) 641 | 642 | # Mass Normalization of mode shapes 643 | for i in mode_num_range: 644 | U[:, i] = U[:, i] / np.sqrt(np.dot(U[:, i], U[:, i]) * rho * L) 645 | 646 | omega_n = w 647 | return omega_n, x, U 648 | -------------------------------------------------------------------------------- /vibration_toolbox/data/case1.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/vibration_toolbox/data/case1.mat -------------------------------------------------------------------------------- /vibration_toolbox/data/case2.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/vibration_toolbox/data/case2.mat -------------------------------------------------------------------------------- /vibration_toolbox/data/frf_data1.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibrationtoolbox/vibration_toolbox/1bec7f157912c47c24d1356bba54d9dae2d3ba2f/vibration_toolbox/data/frf_data1.mat -------------------------------------------------------------------------------- /vibration_toolbox/ema.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import scipy.linalg as la 4 | 5 | 6 | def frf(x, f, dt): 7 | r"""Return the frequency response function. 8 | 9 | Calculates :math:`H(i\\omega)`, and coherance of the sampled data. 10 | 11 | Parameters 12 | ---------- 13 | x: array 14 | Array with the displacement data 15 | f: array 16 | Array with the force data 17 | dt: float 18 | Time step of the sampled data 19 | n: int 20 | Number of points in the fft 21 | 22 | Returns 23 | ------- 24 | freq: array 25 | Driving frequencies 26 | mag: array 27 | Magnitude of the frequency response function 28 | ang: array 29 | Phase of the frequency response function 30 | coh: array 31 | Coherence function 32 | 33 | Plot with the frf magnitude, phase and 34 | coherence. 35 | 36 | Examples 37 | -------- 38 | >>> # First we need to load the sampled data which in a .mat file 39 | >>> import vibration_toolbox as vtb 40 | >>> import scipy.io as sio 41 | >>> data = sio.loadmat(vtb.__path__[0] + '/data/frf_data1.mat') 42 | >>> #print(data) 43 | >>> # Data is imported as arrays. Modify then to fit our function 44 | >>> x = data['x'] 45 | >>> x = x.reshape(len(x)) 46 | >>> f = data['f'] 47 | >>> f = f.reshape(len(f)) 48 | >>> dt = data['dt'] 49 | >>> dt = float(dt) 50 | >>> # Now we are able to call the function 51 | >>> freq, mag, ang, coh = vtb.frf(x, f, dt) 52 | >>> mag[10] 53 | 1.018394853080... 54 | 55 | """ 56 | w = np.sin(np.pi * np.arange(len(f)) / len(f))**2 # window 57 | # apply window 58 | xw = x * w 59 | fw = f * w 60 | # take ffts 61 | FX = np.fft.fft(xw) 62 | FF = np.fft.fft(fw) 63 | # calculate the spectral densities 64 | SXF = FF * np.conj(FX) 65 | SXX = FX * np.conj(FX) 66 | SFF = FF * np.conj(FF) 67 | # calculate the frequency response functions 68 | TXF = SXX / SXF 69 | 70 | lt = len(TXF) // 2 71 | freq = np.arange(lt) / (2 * lt * dt) 72 | 73 | TXF = TXF[:lt] 74 | mag = np.absolute(TXF) 75 | ang = np.angle(TXF) * 180 / np.pi 76 | 77 | coh = (np.absolute(SXF)**2) / (SXX * SFF) 78 | coh = np.real(coh) 79 | 80 | # plot H(w) 81 | fig = plt.figure(figsize=(8, 6)) 82 | ax1 = fig.add_subplot(311) 83 | ax2 = fig.add_subplot(312, sharex=ax1) 84 | ax3 = fig.add_subplot(313, sharex=ax2) 85 | fig.tight_layout() 86 | 87 | ax1.set_title(r'$H(\omega)$ - Magnitude') 88 | ax2.set_title(r'$H(\omega)$ - Phase') 89 | ax3.set_title(r'$H(\omega)$ - Coherence') 90 | ax3.set_xlabel('Frequency (Hz)') 91 | ax3.set_ylim(0, 2) 92 | 93 | ax1.semilogy(freq, mag) 94 | ax2.plot(freq, ang) 95 | ax3.plot(freq, coh[:lt]) 96 | 97 | plt.show() 98 | 99 | return freq, mag, ang, coh 100 | 101 | 102 | def plot_fft(t, time_response, ax=None, **kwargs): 103 | """Plot fft ot time response. 104 | 105 | Parameters 106 | ---------- 107 | t : array 108 | Time array. 109 | time_response : array 110 | Array with the system's time response. 111 | ax : matplotlib.axes, optional 112 | Matplotlib axes where the amplitude will be plotted. 113 | If None creates a new. 114 | 115 | Returns 116 | ------- 117 | ax : array with matplotlib.axes, optional 118 | Matplotlib axes array created with plt.subplots. 119 | Plot has frequency in rad/s and magnitude in meters peak to peak. 120 | 121 | Examples 122 | -------- 123 | >>> import vibration_toolbox as vtb 124 | >>> t = np.linspace(0, 10, 1000) 125 | >>> time_response = 2 * np.sin(40*t) 126 | >>> vtb.plot_fft(t, time_response) 127 | >> # First we need to load the sampled data which is in a .mat file 185 | >>> import vibration_toolbox as vtb 186 | >>> import scipy.io as sio 187 | >>> data = sio.loadmat(vtb.__path__[0] + '/data/case1.mat') 188 | >>> #print(data) 189 | >>> # Data is imported as arrays. Modify then to fit our function. 190 | >>> TF = data['Hf_chan_2'] 191 | >>> f = data['Freq_domain'] 192 | >>> # Now we are able to call the function 193 | >>> z, nf, a = vtb.sdof_cf(f,TF,500,1000) 194 | >>> nf 195 | 212.092530551... 196 | 197 | """ 198 | # check fmin fmax existance 199 | if Fmin is None: 200 | inlow = 0 201 | else: 202 | inlow = Fmin 203 | 204 | if Fmax is None: 205 | inhigh = np.size(f) 206 | else: 207 | inhigh = Fmax 208 | 209 | if f[inlow] == 0: 210 | inlow = 1 211 | 212 | f = f[inlow:inhigh, :] 213 | TF = TF[inlow:inhigh, :] 214 | 215 | R = TF 216 | y = np.amax(np.abs(TF)) 217 | cin = np.argmax(np.abs(TF)) 218 | 219 | ll = np.size(f) 220 | 221 | w = f * 2 * np.pi * 1j 222 | 223 | w2 = w * 0 224 | R3 = R * 0 225 | 226 | for i in range(1, ll + 1): 227 | R3[i - 1] = np.conj(R[ll - i]) 228 | w2[i - 1] = np.conj(w[ll - i]) 229 | 230 | w = np.vstack((w2, w)) 231 | R = np.vstack((R3, R)) 232 | 233 | N = 2 234 | x, y = np.meshgrid(np.arange(0, N + 1), R) 235 | x, w2d = np.meshgrid(np.arange(0, N + 1), w) 236 | c = -1 * w**N * R 237 | 238 | aa1 = w2d[:, np.arange(0, N)] \ 239 | ** x[:, np.arange(0, N)] \ 240 | * y[:, np.arange(0, N)] 241 | aa2 = -w2d[:, np.arange(0, N + 1)] \ 242 | ** x[:, np.arange(0, N + 1)] 243 | aa = np.hstack((aa1, aa2)) 244 | 245 | aa = np.reshape(aa, [-1, 5]) 246 | 247 | b, _, _, _ = la.lstsq(aa, c) 248 | 249 | b = b.flatten() 250 | rs = np.roots(np.array([1, 251 | b[1], 252 | b[0]])) 253 | omega = np.abs(rs[1]) 254 | z = -1 * np.real(rs[1]) / np.abs(rs[1]) 255 | nf = omega / 2 / np.pi 256 | 257 | XoF1 = np.hstack(([1 / (w - rs[0]), 1 / (w - rs[1])])) 258 | XoF2 = 1 / (w**0) 259 | XoF3 = 1 / w**2 260 | XoF = np.hstack((XoF1, XoF2, XoF3)) 261 | 262 | # check if extra _ needed 263 | 264 | a, _, _, _ = la.lstsq(XoF, R) 265 | XoF = XoF[np.arange(ll, 2 * ll), :].dot(a) 266 | 267 | a = np.sqrt(-2 * np.imag(a[0]) * np.imag(rs[0]) 268 | - 2 * np.real(a[0]) * np.real(rs[0])) 269 | Fmin = np.min(f) 270 | Fmax = np.max(f) 271 | phase = np.unwrap(np.angle(TF), np.pi, 0) * 180 / np.pi 272 | phase2 = np.unwrap(np.angle(XoF), np.pi, 0) * 180 / np.pi 273 | while phase2[cin] > 50: 274 | phase2 = phase2 - 360 275 | phased = phase2[cin] - phase[cin] 276 | phase = phase + np.round(phased / 360) * 360 277 | 278 | fig = plt.figure() 279 | ax1 = fig.add_subplot(2, 1, 1) 280 | ax2 = fig.add_subplot(2, 1, 2) 281 | fig.tight_layout() 282 | 283 | ax1.set_xlabel('Frequency (Hz)') 284 | ax1.set_ylabel('Magnitude (dB)') 285 | ax1.plot(f, 20 * np.log10(np.abs(XoF)), label="Identified FRF") 286 | ax1.plot(f, 20 * np.log10(np.abs(TF)), label="Experimental FRF") 287 | ax1.legend() 288 | 289 | ax2.set_xlabel('Frequency (Hz)') 290 | ax2.set_ylabel('Phase (deg)') 291 | ax2.plot(f, phase2, label="Identified FRF") 292 | ax2.plot(f, phase, label="Experimental FRF") 293 | ax2.legend() 294 | 295 | plt.show() 296 | 297 | a = a[0]**2 / (2 * np.pi * nf)**2 298 | return z, nf, a 299 | 300 | 301 | def mdof_cf(f, TF, Fmin=None, Fmax=None): 302 | """Curve fit to multiple degree of freedom FRF. 303 | 304 | If Fmin and Fmax are not entered, the first and last elements of TF are 305 | used. 306 | 307 | If the first column of TF is a collocated (input and output location are 308 | the same), then the mode shape returned is the mass normalized mode shape. 309 | This can then be used to generate an identified mass, damping, and 310 | stiffness matrix as shown in the following example. 311 | 312 | Parameters 313 | ---------- 314 | f: array 315 | The frequency vector in Hz. Does not have to start at 0 Hz. 316 | TF: array 317 | The complex transfer function 318 | Fmin: int 319 | The minimum frequency to be used for curve fitting in the FRF 320 | Fmax: int 321 | The maximum frequency to be used for curve fitting in the FRF 322 | 323 | Returns 324 | ------- 325 | z: double 326 | The damping ratio 327 | nf: double 328 | Natural frequency (Hz) 329 | u: array 330 | The mode shape 331 | 332 | Notes 333 | ----- 334 | FRF are columns comprised of the FRFs presuming single input, multiple 335 | output z and nf are the damping ratio and natural frequency (Hz) u is the 336 | mode shape. Only one peak may exist in the segment of the FRF passed to 337 | sdofcf. No zeros may exist within this segment. If so, curve fitting 338 | becomes unreliable. 339 | 340 | Examples 341 | -------- 342 | >>> # First we need to load the sampled data which is in a .mat file 343 | >>> import vibration_toolbox as vtb 344 | >>> import scipy.io as sio 345 | >>> data = sio.loadmat(vtb.__path__[0] + '/data/case2.mat') 346 | >>> #print(data) 347 | >>> # Data is imported as arrays. Modify then to fit our function 348 | >>> TF = data['Hf_chan_2'] 349 | >>> f = data['Freq_domain'] 350 | >>> # Now we are able to call the function 351 | >>> z, nf, a = vtb.mdof_cf(f,TF,500,1000) 352 | >>> nf 353 | 192.59382330... 354 | 355 | """ 356 | # check fmin fmax existance 357 | if Fmin is None: 358 | inlow = 0 359 | else: 360 | inlow = Fmin 361 | 362 | if Fmax is None: 363 | inhigh = np.size(f) 364 | else: 365 | inhigh = Fmax 366 | 367 | if f[inlow] == 0: 368 | inlow = 1 369 | 370 | f = f[inlow:inhigh, :] 371 | TF = TF[inlow:inhigh, :] 372 | 373 | R = TF.T 374 | 375 | U, _, _ = np.linalg.svd(R) 376 | T = U[:, 0] 377 | Hp = np.transpose(T).dot(R) 378 | R = np.transpose(Hp) 379 | 380 | ll = np.size(f) 381 | w = f * 2 * np.pi * 1j 382 | 383 | w2 = w * 0 384 | R3 = R * 0 385 | TF2 = TF * 0 386 | for i in range(1, ll + 1): 387 | R3[i - 1] = np.conj(R[ll - i]) 388 | w2[i - 1] = np.conj(w[ll - i]) 389 | TF2[i - 1, :] = np.conj(TF[ll - i, :]) 390 | 391 | w = np.vstack((w2, w)) 392 | R = np.hstack((R3, R)) 393 | 394 | N = 2 395 | x, y = np.meshgrid(np.arange(0, N + 1), R) 396 | 397 | x, w2d = np.meshgrid(np.arange(0, N + 1), w) 398 | 399 | R = np.ndarray.flatten(R) 400 | w = np.ndarray.flatten(w) 401 | c = -1 * w**N * R 402 | 403 | aa1 = w2d[:, np.arange(0, N)] \ 404 | ** x[:, np.arange(0, N)] \ 405 | * y[:, np.arange(0, N)] 406 | aa2 = -w2d[:, np.arange(0, N + 1)] \ 407 | ** x[:, np.arange(0, N + 1)] 408 | aa = np.hstack((aa1, aa2)) 409 | 410 | b, _, _, _ = la.lstsq(aa, c) 411 | 412 | rs = np.roots(np.array([1, 413 | b[1], 414 | b[0]])) 415 | 416 | # irs = np.argsort(np.abs(np.imag(rs))) # necessary? 417 | 418 | omega = np.abs(rs[1]) 419 | z = -1 * np.real(rs[1]) / np.abs(rs[1]) 420 | nf = omega / 2 / np.pi 421 | 422 | XoF1 = 1 / ((rs[0] - w) * (rs[1] - w)) 423 | 424 | XoF2 = 1 / (w**0) 425 | XoF3 = 1 / w**2 426 | 427 | XoF = np.vstack((XoF1, XoF2, XoF3)).T 428 | TF3 = np.vstack((TF2, TF)) 429 | 430 | a, _, _, _ = la.lstsq(XoF, TF3) 431 | 432 | u = np.transpose(a[0, :]) 433 | 434 | u = u / np.sqrt(np.abs(a[0, 0])) 435 | 436 | return z, nf, u 437 | -------------------------------------------------------------------------------- /vibration_toolbox/mdof.py: -------------------------------------------------------------------------------- 1 | """Multiple Degree of Freedom Analysis Tools.""" 2 | 3 | import numpy as np 4 | import scipy.linalg as la 5 | import scipy.signal as signal 6 | import matplotlib as mpl 7 | 8 | __all__ = [ 9 | "modes_system", 10 | "modes_system_undamped", 11 | "response_system", 12 | "response_system_undamped", 13 | ] 14 | 15 | 16 | mpl.rcParams["lines.linewidth"] = 2 17 | mpl.rcParams["figure.figsize"] = (10, 6) 18 | 19 | 20 | def _eigen(A, B=None): 21 | """Return sorted eigenvector/eigenvalue pairs. 22 | 23 | e.g. for a given system linalg.eig will return eingenvalues as: 24 | (array([ 0. +89.4j, 0. -89.4j, 0. +89.4j, 0. -89.4j, 0.+983.2j, 25 | 0.-983.2j, 0. +40.7j, 0. -40.7j]) 26 | This function will sort this eigenvalues as: 27 | (array([ 0. +40.7j, 0. +89.4j, 0. +89.4j, 0.+983.2j, 0. -40.7j, 28 | 0. -89.4j, 0. -89.4j, 0.-983.2j]) 29 | Correspondent eigenvectors will follow the same order. 30 | 31 | Note: Works fine for moderately sized models. Does not leverage the 32 | full set of constraints to optimize the solution. See the vibrationtesting 33 | module for a more advanced solver. 34 | 35 | Parameters 36 | ---------- 37 | A: array 38 | A complex or real matrix whose eigenvalues and eigenvectors 39 | will be computed. 40 | B: float or str 41 | Right-hand side matrix in a generalized eigenvalue problem. 42 | Default is None, identity matrix is assumed. 43 | 44 | Returns 45 | ------- 46 | evalues: array 47 | Sorted eigenvalues 48 | evectors: array 49 | Sorted eigenvalues 50 | 51 | Examples 52 | -------- 53 | >>> import vibration_toolbox as vtb 54 | >>> L = np.array([[2, -1, 0], 55 | ... [-4, 8, -4], 56 | ... [0, -4, 4]]) 57 | >>> lam, P = vtb.mdof._eigen(L) 58 | >>> lam 59 | array([ 0.56+0.j, 2.63+0.j, 10.81+0.j]) 60 | 61 | """ 62 | if B is None: 63 | evalues, evectors = la.eig(A) 64 | else: 65 | evalues, evectors = la.eig(A, B) 66 | 67 | if all(eigs == 0 for eigs in evalues.imag): 68 | if all(eigs > 0 for eigs in evalues.real): 69 | idxp = evalues.real.argsort() # positive in increasing order 70 | idxn = np.array([], dtype=int) 71 | else: 72 | # positive in increasing order 73 | idxp = evalues.real.argsort()[int(len(evalues) / 2):] 74 | # negative in decreasing order 75 | idxn = evalues.real.argsort()[int(len(evalues) / 2) - 1:: -1] 76 | 77 | else: 78 | # positive in increasing order 79 | idxp = evalues.imag.argsort()[int(len(evalues) / 2):] 80 | # negative in decreasing order 81 | idxn = evalues.imag.argsort()[int(len(evalues) / 2) - 1:: -1] 82 | 83 | idx = np.hstack([idxp, idxn]) 84 | 85 | return evalues[idx], evectors[:, idx] 86 | 87 | 88 | def _normalize(X, Y): 89 | """ 90 | Return normalized left eigenvectors. 91 | 92 | This function is used to normalize vectors of the matrix 93 | Y with respect to X so that Y.T @ X = I (identity). 94 | This is used to normalize the matrix with the left eigenvectors. 95 | 96 | Parameters 97 | ---------- 98 | X: array 99 | A complex or real matrix 100 | Y: array 101 | A complex or real matrix to be normalized 102 | 103 | Returns 104 | ------- 105 | Yn: array 106 | Normalized matrix 107 | 108 | Examples 109 | -------- 110 | >>> # This test has been moved to tests_mdof 111 | >>> X = np.array([[ 0.84+0.j , 0.14-0.j , 0.84-0.j , 0.14+0.j ], 112 | ... [ 0.01-0.3j , 0.00+0.15j, 0.01+0.3j , 0.00-0.15j], 113 | ... [-0.09+0.42j, -0.01+0.65j, -0.09-0.42j, -0.01-0.65j], 114 | ... [ 0.15+0.04j, -0.74+0.j , 0.15-0.04j, -0.74-0.j ]]) 115 | >>> Y = np.array([[-0.03-0.41j, 0.04+0.1j , -0.03+0.41j, 0.04-0.1j ], 116 | ... [ 0.88+0.j , 0.68+0.j , 0.88-0.j , 0.68-0.j ], 117 | ... [-0.21-0.j , 0.47+0.05j, -0.21+0.j , 0.47-0.05j], 118 | ... [ 0.00-0.08j, 0.05-0.54j, 0.00+0.08j, 0.05+0.54j]]) 119 | >>> Yn = _normalize(X, Y) 120 | >>> Yn # doctest: +SKIP 121 | array([[ 0.58-0.05j, 0.12-0.06j, 0.58+0.05j, 0.12+0.06j], 122 | [ 0.01+1.24j, -0.07-0.82j, 0.01-1.24j, -0.07+0.82j], 123 | [-0. -0.3j , 0.01-0.57j, -0. +0.3j , 0.01+0.57j], 124 | [ 0.11-0.j , -0.66-0.01j, 0.11+0.j , -0.66+0.01j]]) 125 | 126 | """ 127 | Yn = np.zeros_like(X) 128 | YTX = Y.T @ X # normalize y so that Y.T @ X will return I 129 | factors = [1 / a for a in np.diag(YTX)] 130 | # multiply each column in y by a factor in 'factors' 131 | for col in enumerate(Y.T): 132 | Yn[col[0]] = col[1] * factors[col[0]] 133 | Yn = Yn.T 134 | 135 | return Yn 136 | 137 | 138 | def modes_system_undamped(M, K): 139 | r"""Return eigensolution of multiple DOF system. 140 | 141 | Returns the natural frequencies (w), 142 | eigenvectors (P), mode shapes (S) and the modal transformation 143 | matrix S for an undamped system. 144 | 145 | See Notes for explanation of the underlying math. 146 | 147 | Parameters 148 | ---------- 149 | M: float array 150 | Mass matrix 151 | K: float array 152 | Stiffness matrix 153 | 154 | Returns 155 | ------- 156 | w: float array 157 | The natural frequencies of the system 158 | P: float array 159 | The eigenvectors of the system. 160 | S: float array 161 | The mass-normalized mode shapes of the system. 162 | Sinv: float array 163 | The modal transformation matrix S^-1(takes x -> r(modal coordinates)) 164 | 165 | Notes 166 | ----- 167 | Given :math:`M\ddot{x}(t)+Kx(t)=0`, with mode shapes :math:`u`, the matrix 168 | of mode shapes :math:`S=[u_1 u_1 \ldots]` can be created. If the modal 169 | coordinates are the vector :math:`r(t)`. The modal transformation separates 170 | space and time from :math:`x(t)` such that :math:`x(t)=S r(t)`. 171 | Substituting into the governing equation: 172 | 173 | :math:`MS\ddot{r}(t)+KSr(t)=0` 174 | 175 | Premultiplying by :math:`S^T` 176 | 177 | :math:`S^TMS\ddot{r}(t)+S^TKSr(t)=0` 178 | 179 | The matrices :math:`S^TMS` and :math:`S^TKS` will be diagonalized by this 180 | process (:math:`u_i` are the eigenvectors of :math:`M^{-1}K`). 181 | 182 | If scaled properly (mass normalized so :math:`u_i^TMu_i=1`) then 183 | :math:`S^TMS=I` and :math:`S^TKS=\Omega^2` where :math:`\Omega^2` is a 184 | diagonal matrix of the natural frequencies squared in radians per second. 185 | 186 | Further, inverses are unstable so the better way to solve linear equations is with 187 | Gauss elimination. 188 | 189 | :math:`AB=C` given known :math:`A` and :math:`C` 190 | is solved using `la.solve(A, C, assume_a='pos')`. 191 | 192 | :math:`BA=C` given known :math:`A` and :math:`C` is solved by first 193 | transposing the equation to :math:`A^TB^T=C^T`, then solving for 194 | :math:`C^T`. The resulting command is 195 | `la.solve(A.T, C.T, assume_a='pos').T` 196 | 197 | Examples 198 | -------- 199 | >>> M = np.array([[4, 0, 0], 200 | ... [0, 4, 0], 201 | ... [0, 0, 4]]) 202 | >>> K = np.array([[8, -4, 0], 203 | ... [-4, 8, -4], 204 | ... [0, -4, 4]]) 205 | >>> w, P, S, Sinv = modes_system_undamped(M, K) 206 | >>> w # doctest: +SKIP 207 | array([0.45, 1.25, 1.8 ]) 208 | >>> S 209 | array([[ 0.16, -0.37, -0.3 ], 210 | [ 0.3 , -0.16, 0.37], 211 | [ 0.37, 0.3 , -0.16]]) 212 | 213 | """ 214 | L = la.cholesky(M) 215 | lam, P = _eigen(la.solve(L, la.solve(L, K, assume_a='pos').T, 216 | assume_a='pos').T) 217 | w = np.real(np.sqrt(lam)) 218 | S = la.solve(L, P, assume_a="pos") 219 | Sinv = la.solve(L.T, P, assume_a="pos").T 220 | 221 | return w, P, S, Sinv 222 | 223 | 224 | def modes_system(M, K, C=None): 225 | """Natural frequencies, damping ratios, and mode shapes of MDOF system. 226 | This function will return the natural frequencies (wn), the 227 | damped natural frequencies (wd), the damping ratios (zeta), 228 | the right eigenvectors (X) and the left eigenvectors (Y) for a 229 | system defined by M, K and C. 230 | If the dampind matrix 'C' is none or if the damping is proportional, 231 | wd and zeta will be none and X and Y will be equal. 232 | 233 | Parameters 234 | ---------- 235 | M: array 236 | Mass matrix 237 | K: array 238 | Stiffness matrix 239 | C: array 240 | Damping matrix 241 | 242 | Returns 243 | ------- 244 | wn: array 245 | The natural frequencies of the system 246 | wd: array 247 | The damped natural frequencies of the system 248 | zeta: array 249 | The damping ratios 250 | X: array 251 | The right eigenvectors 252 | Y: array 253 | The left eigenvectors 254 | 255 | Examples 256 | -------- 257 | >>> # This test has been moved to tests_mdof 258 | >>> M = np.array([[1, 0], 259 | ... [0, 1]]) 260 | >>> K = np.array([[2, -1], 261 | ... [-1, 6]]) 262 | >>> C = np.array([[0.3, -0.02], 263 | ... [-0.02, 0.1]]) 264 | >>> wn, wd, zeta, X, Y = modes_system(M, K, C) # doctest: +SKIP 265 | Damping is non-proportional, eigenvectors are complex. 266 | >>> wn # doctest: +SKIP 267 | array([1.33, 2.5 , 1.33, 2.5 ]) 268 | >>> wd # doctest: +SKIP 269 | array([1.32, 2.5 , 1.32, 2.5 ]) 270 | >>> zeta # doctest: +SKIP 271 | array([0.11, 0.02, 0.11, 0.02]) 272 | >>> X # doctest: +SKIP 273 | array([[-0.06-0.58j, -0.01+0.08j, -0.06+0.58j, -0.01-0.08j], 274 | [-0. -0.14j, -0.01-0.36j, -0. +0.14j, -0.01+0.36j], 275 | [ 0.78+0.j , -0.21-0.03j, 0.78-0.j , -0.21+0.03j], 276 | [ 0.18+0.01j, 0.9 +0.j , 0.18-0.01j, 0.9 -0.j ]]) 277 | >>> Y # doctest: +SKIP 278 | array([[ 0.02+0.82j, 0.01-0.31j, 0.02-0.82j, 0.01+0.31j], 279 | [-0.05+0.18j, 0.01+1.31j, -0.05-0.18j, 0.01-1.31j], 280 | [ 0.61+0.06j, -0.12-0.02j, 0.61-0.06j, -0.12+0.02j], 281 | [ 0.14+0.03j, 0.53+0.j , 0.14-0.03j, 0.53-0.j ]]) 282 | >>> C = 0.2*K # with proportional damping 283 | >>> wn, wd, zeta, X, Y = modes_system(M, K, C) # doctest: +SKIP 284 | Damping is proportional or zero, eigenvectors are real 285 | >>> X # doctest: +SKIP 286 | array([[-0.97, 0.23], 287 | [-0.23, -0.97]]) 288 | """ 289 | 290 | n = len(M) 291 | 292 | Z = np.zeros((n, n)) 293 | I = np.eye(n) 294 | 295 | if ( 296 | C is None or 297 | np.all(C == 0) or 298 | la.norm( # check if C has only zero entries 299 | la.solve(M, C, assume_a="pos") @ K - \ 300 | la.solve(M, K, assume_a="pos") @ C, 2 301 | ) < 302 | 1e-8 * la.norm(la.solve(M, K, assume_a="pos") @ C, 2) 303 | ): 304 | w, P, S, Sinv = modes_system_undamped(M, K) 305 | wn = w 306 | wd = w 307 | # zeta = None 308 | zeta = np.diag(S.T @ C @ S) / 2 / wn 309 | wd = wn * np.sqrt(1 - zeta ** 2) 310 | X = P 311 | Y = P 312 | print("Damping is proportional or zero, eigenvectors are real") 313 | return wn, wd, zeta, X, Y 314 | 315 | Z = np.zeros((n, n)) 316 | I = np.eye(n) 317 | 318 | # creates the state space matrix 319 | A = np.vstack( 320 | [ 321 | np.hstack([Z, I]), 322 | np.hstack( 323 | [-la.solve(M, K, assume_a="pos"), 324 | - la.solve(M, C, assume_a="pos")] 325 | ), 326 | ] 327 | ) 328 | 329 | w, X = _eigen(A) 330 | _, Y = _eigen(A.T) 331 | 332 | wd = abs(np.imag(w)) 333 | wn = np.absolute(w) 334 | zeta = -np.real(w) / np.absolute(w) 335 | 336 | Y = _normalize(X, Y) 337 | 338 | print("Damping is non-proportional, eigenvectors are complex.") 339 | 340 | return wn, wd, zeta, X, Y 341 | 342 | 343 | def response_system_undamped(M, K, x0, v0, max_time): 344 | """ 345 | This function calculates the time response for an undamped system 346 | and returns the vector (state-space) X. The n first rows contain the 347 | displacement (x) and the n last rows contain velocity (v) for each 348 | coordinate. Each column is related to a time-step. 349 | The time array is also returned. 350 | 351 | Parameters 352 | ---------- 353 | M: array 354 | Mass matrix 355 | K: array 356 | Stiffness matrix 357 | x0: array 358 | Array with displacement initial conditions 359 | v0: array 360 | Array with velocity initial conditions 361 | max_time: float 362 | End time 363 | 364 | Returns 365 | ------- 366 | t: array 367 | Array with the time 368 | X: array 369 | The state-space vector for each time 370 | 371 | Examples 372 | -------- 373 | >>> M = np.array([[1, 0], 374 | ... [0, 4]]) 375 | >>> K = np.array([[12, -2], 376 | ... [-2, 12]]) 377 | >>> x0 = np.array([1, 1]) 378 | >>> v0 = np.array([0, 0]) 379 | >>> max_time = 10 380 | >>> t, X = response_system_undamped(M, K, x0, v0, max_time) 381 | >>> # first column is the initial conditions [x1, x2, v1, v2] 382 | >>> X[:, 0] # doctest: +SKIP 383 | array([1., 1., 0., 0.]) 384 | >>> X[:, 1] # displacement and velocities after delta t 385 | array([ 1. , 1. , -0.04, -0.01]) 386 | """ 387 | 388 | t = np.linspace(0, max_time, int(250 * max_time)) 389 | dt = t[1] - t[0] 390 | 391 | n = len(M) 392 | 393 | Z = np.zeros((n, n)) 394 | I = np.eye(n, n) 395 | 396 | # creates the state space matrix 397 | A = np.vstack([np.hstack([Z, I]), np.hstack( 398 | [-la.solve(M, K, assume_a="pos"), Z])]) 399 | 400 | # creates the x array and set the first line according to the initial 401 | # conditions 402 | X = np.zeros((2 * n, len(t))) 403 | X[:, 0] = np.hstack([x0, v0]) 404 | 405 | Ad = la.expm(A * dt) 406 | for i in range(len(t) - 1): 407 | X[:, i + 1] = Ad @ X[:, i] 408 | 409 | return t, X 410 | 411 | 412 | def response_system(M, C, K, F, x0, v0, t): 413 | """ 414 | Returns system response given the initial 415 | displacement vector 'X0', initial velocity vector 'V0', 416 | the mass matrix 'M', the stiffness matrix 'M', and the damping 417 | matrix 'C' and force 'F'. 418 | T is a row vector of evenly spaced times. 419 | F is a matrix of forces over time, each column corresponding 420 | to the corresponding column of T, each row corresponding to 421 | the same numbered DOF. 422 | 423 | Parameters 424 | ---------- 425 | M: array 426 | Mass matrix 427 | K: array 428 | Stiffness matrix 429 | C: array 430 | Damping matrix 431 | x0: array 432 | Array with displacement initial conditions 433 | v0: array 434 | Array with velocity initial conditions 435 | t: array 436 | Array withe evenly spaced times 437 | 438 | Returns 439 | ------- 440 | T : array 441 | Time values for the output. 442 | yout : array 443 | System response. 444 | xout : array 445 | Time evolution of the state vector. 446 | 447 | Examples 448 | -------- 449 | >>> M = np.array([[9, 0], 450 | ... [0, 1]]) 451 | >>> K = np.array([[27, -3], 452 | ... [-3, 3]]) 453 | >>> C = K/10 454 | >>> x0 = np.array([0, 1]) 455 | >>> v0 = np.array([1, 0]) 456 | >>> t = np.linspace(0, 10, 100) 457 | >>> F = np.vstack([0*t, 458 | ... 3*np.cos(2*t)]) 459 | >>> tou, yout, xout = response_system(M, C, K, F, x0, v0, t) 460 | >>> tou[:10] # doctest: +SKIP 461 | array([0. , 0.1 , 0.2 , 0.3 , 0.4 , 0.51, 0.61, 0.71, 0.81, 0.91]) 462 | 463 | >>> yout[:10] 464 | array([[ 0. , 1. , 1. , 0. ], 465 | [ 0.1 , 1. , 0.99, 0.04], 466 | [ 0.2 , 1.01, 0.95, 0.1 ], 467 | [ 0.29, 1.02, 0.88, 0.15], 468 | [ 0.38, 1.04, 0.79, 0.19], 469 | [ 0.45, 1.06, 0.68, 0.2 ], 470 | [ 0.51, 1.08, 0.55, 0.17], 471 | [ 0.56, 1.09, 0.41, 0.09], 472 | [ 0.59, 1.09, 0.26, -0.04], 473 | [ 0.61, 1.08, 0.11, -0.22]]) 474 | 475 | >>> xout[:10] 476 | array([[ 0. , 1. , 1. , 0. ], 477 | [ 0.1 , 1. , 0.99, 0.04], 478 | [ 0.2 , 1.01, 0.95, 0.1 ], 479 | [ 0.29, 1.02, 0.88, 0.15], 480 | [ 0.38, 1.04, 0.79, 0.19], 481 | [ 0.45, 1.06, 0.68, 0.2 ], 482 | [ 0.51, 1.08, 0.55, 0.17], 483 | [ 0.56, 1.09, 0.41, 0.09], 484 | [ 0.59, 1.09, 0.26, -0.04], 485 | [ 0.61, 1.08, 0.11, -0.22]]) 486 | """ 487 | 488 | n = len(M) 489 | 490 | Z = np.zeros((n, n)) 491 | I = np.eye(n) 492 | 493 | # creates the state space matrix 494 | A = np.vstack( 495 | [ 496 | np.hstack([Z, I]), 497 | np.hstack( 498 | [-la.solve(M, K, assume_a="pos"), 499 | - la.solve(M, C, assume_a="pos")] 500 | ), 501 | ] 502 | ) 503 | B = np.vstack([Z, la.inv(M)]) 504 | C = np.eye(2 * n) 505 | D = 0 * B 506 | 507 | sys = signal.lti(A, B, C, D) 508 | 509 | IC = np.hstack([x0, v0]) 510 | F = F.T 511 | T, yout, xout = signal.lsim(sys, F, t, IC) 512 | 513 | return T, yout, xout 514 | -------------------------------------------------------------------------------- /vibration_toolbox/readme.rst: -------------------------------------------------------------------------------- 1 | Please refer to the `Numpy standards for docstrings `_. 2 | 3 | Specifically note: 4 | 5 | 1. Parameters should be listed similarly to: 6 | 7 | | filename : str 8 | | copy : bool 9 | | dtype : data-type 10 | | iterable : iterable object 11 | | shape : int or tuple of int 12 | | files : list of str 13 | | time : array_like 14 | 15 | 2. First line should be inline with the ``"""`` and brief enough to fit on one line. 16 | 17 | 3. There must be a blank line after the first line. 18 | 19 | This is not exhaustive. It just highlights some consistent errors made. 20 | -------------------------------------------------------------------------------- /vibration_toolbox/tests/test_continuous.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | import vibration_toolbox as vtb 4 | from numpy.testing import assert_allclose 5 | 6 | 7 | @pytest.fixture 8 | def _normalize_vect(): 9 | X = np.array([[0.84 + 0.j, 0.14 - 0.j, 0.84 - 0.j, 0.14 + 0.j], 10 | [0.01 - 0.3j, 0.00 + 0.15j, 0.01 + 0.3j, 0.00 - 0.15j], 11 | [-0.09 + 0.42j, -0.01 + 0.65j, -0.09 - 0.42j, -0.01 - 0.65j], 12 | [0.15 + 0.04j, -0.74 + 0.j, 0.15 - 0.04j, -0.74 - 0.j]]) 13 | Y = np.array([[-0.03 - 0.41j, 0.04 + 0.1j, -0.03 + 0.41j, 0.04 - 0.1j], 14 | [0.88 + 0.j, 0.68 + 0.j, 0.88 - 0.j, 0.68 - 0.j], 15 | [-0.21 - 0.j, 0.47 + 0.05j, -0.21 + 0.j, 0.47 - 0.05j], 16 | [0.00 - 0.08j, 0.05 - 0.54j, 0.00 + 0.08j, 0.05 + 0.54j]]) 17 | Yn = vtb.mdof._normalize(X, Y) 18 | print(Yn) 19 | return Yn 20 | 21 | 22 | def test_normalise(_normalize_vect): 23 | Yn = np.array([[0.57822773 - 4.69882840e-02j, 0.11696967 - 5.85231773e-02j, 24 | 0.57822773 + 4.69882840e-02j, 0.11696967 + 5.85231773e-02j], 25 | [0.00998912 + 1.24180506e+00j, -0.0687932 - 8.22911025e-01j, 26 | 0.00998912 - 1.24180506e+00j, -0.0687932 + 8.22911025e-01j], 27 | [-0.00238377 - 2.96339843e-01j, 0.01295993 - 5.73835061e-01j, 28 | -0.00238377 + 2.96339843e-01j, 0.01295993 + 5.73835061e-01j], 29 | [0.11289137 - 9.08101611e-04j, -0.65854649 - 5.87827297e-03j, 30 | 0.11289137 + 9.08101611e-04j, -0.65854649 + 5.87827297e-03j]]) 31 | assert_allclose(_normalize_vect, Yn) 32 | 33 | 34 | def modes_sys(): 35 | M = np.array([[1, 0], [0, 1]]) 36 | K = np.array([[2, -1], [-1, 6]]) 37 | C = np.array([[0.3, -0.02], [-0.02, 0.1]]) 38 | wn, wd, zeta, X, Y = vtb.modes_system(M, K, C) 39 | return wn, wd, zeta, X, Y 40 | 41 | 42 | def test_modes_sys_np(): 43 | wn = np.array([1.328707, 2.49613, 1.328707, 2.49613]) 44 | wd = np.array([1.321268, 2.495419, 1.321268, 2.495419]) 45 | zeta = np.array([0.105665, 0.023878, 0.105665, 0.023878]) 46 | X = np.array([[-0.06187175 - 0.58226568j, -0.01117454 + 0.08415145j, 47 | -0.06187175 + 0.58226568j, 48 | -0.01117454 - 0.08415145j], 49 | [-0.0031966 - 0.13686382j, -0.00864532 - 0.36196512j, 50 | -0.0031966 + 0.13686382j, 51 | -0.00864532 + 0.36196512j], 52 | [0.77801575 + 0.0j, -0.20932709 - 0.03290071j, 53 | 0.77801575 - 0.0j, 54 | -0.20932709 + 0.03290071j], 55 | [0.1812826 + 0.0149919j, 0.90376982 + 0.0j, 56 | 0.1812826 - 0.0149919j, 57 | 0.90376982 - 0.0j]]) 58 | Y = np.array([[0.01582041 + 0.81605587j, 0.01036355 - 0.30817433j, 59 | 0.01582041 - 0.81605587j, 60 | 0.01036355 + 0.30817433j], 61 | [-0.05184151 + 0.18429687j, 0.01345577 + 1.31152703j, 62 | -0.05184151 - 0.18429687j, 63 | 0.01345577 - 1.31152703j], 64 | [0.61081141 + 0.05967192j, -0.12152971 - 0.02007119j, 65 | 0.61081141 - 0.05967192j, 66 | -0.12152971 + 0.02007119j], 67 | [0.14117309 + 0.02567391j, 0.5253469 + 0.00408669j, 68 | 0.14117309 - 0.02567391j, 69 | 0.5253469 - 0.00408669j]]) 70 | 71 | wn_func, wd_func, zeta_func, X_func, Y_func = modes_sys() 72 | 73 | assert_allclose(wn_func, wn, rtol=1e-05) 74 | assert_allclose(wd_func, wd, rtol=1e-05) 75 | assert_allclose(zeta_func, zeta, rtol=1e-04) 76 | np.set_printoptions(precision=8) 77 | print(Y_func) 78 | assert_allclose(X_func, X, rtol=1e-05) 79 | assert_allclose(Y_func, Y, rtol=1e-05) 80 | 81 | 82 | def modes_sys_prop(): 83 | M = np.array([[1, 0], [0, 1]]) 84 | K = np.array([[2, -1], [-1, 6]]) 85 | C = 0.2 * K 86 | wn, wd, zeta, X, Y = vtb.modes_system(M, K, C) 87 | return (wn, wd, zeta, X, Y) 88 | 89 | 90 | def test_modes_sys_prop(): 91 | wn, wd, zeta, X, Y = modes_sys_prop() 92 | assert_allclose(X, np.array([[-0.973249, 0.229753], 93 | [-0.229753, -0.973249]]), rtol=1e-05) 94 | -------------------------------------------------------------------------------- /vibration_toolbox/tests/test_ema.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | import vibration_toolbox as vtb 4 | from numpy.testing import assert_allclose 5 | 6 | 7 | @pytest.fixture 8 | def _normalize_vect(): 9 | X = np.array([[0.84 + 0.j, 0.14 - 0.j, 0.84 - 0.j, 0.14 + 0.j], 10 | [0.01 - 0.3j, 0.00 + 0.15j, 0.01 + 0.3j, 0.00 - 0.15j], 11 | [-0.09 + 0.42j, -0.01 + 0.65j, -0.09 - 0.42j, -0.01 - 0.65j], 12 | [0.15 + 0.04j, -0.74 + 0.j, 0.15 - 0.04j, -0.74 - 0.j]]) 13 | Y = np.array([[-0.03 - 0.41j, 0.04 + 0.1j, -0.03 + 0.41j, 0.04 - 0.1j], 14 | [0.88 + 0.j, 0.68 + 0.j, 0.88 - 0.j, 0.68 - 0.j], 15 | [-0.21 - 0.j, 0.47 + 0.05j, -0.21 + 0.j, 0.47 - 0.05j], 16 | [0.00 - 0.08j, 0.05 - 0.54j, 0.00 + 0.08j, 0.05 + 0.54j]]) 17 | Yn = vtb.mdof._normalize(X, Y) 18 | print(Yn) 19 | return Yn 20 | 21 | 22 | def test_normalise(_normalize_vect): 23 | Yn = np.array([[0.57822773 - 4.69882840e-02j, 0.11696967 - 5.85231773e-02j, 24 | 0.57822773 + 4.69882840e-02j, 0.11696967 + 5.85231773e-02j], 25 | [0.00998912 + 1.24180506e+00j, -0.0687932 - 8.22911025e-01j, 26 | 0.00998912 - 1.24180506e+00j, -0.0687932 + 8.22911025e-01j], 27 | [-0.00238377 - 2.96339843e-01j, 0.01295993 - 5.73835061e-01j, 28 | -0.00238377 + 2.96339843e-01j, 0.01295993 + 5.73835061e-01j], 29 | [0.11289137 - 9.08101611e-04j, -0.65854649 - 5.87827297e-03j, 30 | 0.11289137 + 9.08101611e-04j, -0.65854649 + 5.87827297e-03j]]) 31 | assert_allclose(_normalize_vect, Yn) 32 | 33 | 34 | def modes_sys(): 35 | M = np.array([[1, 0], [0, 1]]) 36 | K = np.array([[2, -1], [-1, 6]]) 37 | C = np.array([[0.3, -0.02], [-0.02, 0.1]]) 38 | wn, wd, zeta, X, Y = vtb.modes_system(M, K, C) 39 | return wn, wd, zeta, X, Y 40 | 41 | 42 | def test_modes_sys_np(): 43 | wn = np.array([1.328707, 2.49613, 1.328707, 2.49613]) 44 | wd = np.array([1.321268, 2.495419, 1.321268, 2.495419]) 45 | zeta = np.array([0.105665, 0.023878, 0.105665, 0.023878]) 46 | X = np.array([[-0.06187175 - 0.58226568j, -0.01117454 + 0.08415145j, 47 | -0.06187175 + 0.58226568j, 48 | -0.01117454 - 0.08415145j], 49 | [-0.0031966 - 0.13686382j, -0.00864532 - 0.36196512j, 50 | -0.0031966 + 0.13686382j, 51 | -0.00864532 + 0.36196512j], 52 | [0.77801575 + 0.0j, -0.20932709 - 0.03290071j, 53 | 0.77801575 - 0.0j, 54 | -0.20932709 + 0.03290071j], 55 | [0.1812826 + 0.0149919j, 0.90376982 + 0.0j, 56 | 0.1812826 - 0.0149919j, 57 | 0.90376982 - 0.0j]]) 58 | Y = np.array([[0.01582041 + 0.81605587j, 0.01036355 - 0.30817433j, 59 | 0.01582041 - 0.81605587j, 60 | 0.01036355 + 0.30817433j], 61 | [-0.05184151 + 0.18429687j, 0.01345577 + 1.31152703j, 62 | -0.05184151 - 0.18429687j, 63 | 0.01345577 - 1.31152703j], 64 | [0.61081141 + 0.05967192j, -0.12152971 - 0.02007119j, 65 | 0.61081141 - 0.05967192j, 66 | -0.12152971 + 0.02007119j], 67 | [0.14117309 + 0.02567391j, 0.5253469 + 0.00408669j, 68 | 0.14117309 - 0.02567391j, 69 | 0.5253469 - 0.00408669j]]) 70 | 71 | wn_func, wd_func, zeta_func, X_func, Y_func = modes_sys() 72 | 73 | assert_allclose(wn_func, wn, rtol=1e-05) 74 | assert_allclose(wd_func, wd, rtol=1e-05) 75 | assert_allclose(zeta_func, zeta, rtol=1e-04) 76 | np.set_printoptions(precision=8) 77 | print(Y_func) 78 | assert_allclose(X_func, X, rtol=1e-05) 79 | assert_allclose(Y_func, Y, rtol=1e-05) 80 | 81 | 82 | def modes_sys_prop(): 83 | M = np.array([[1, 0], [0, 1]]) 84 | K = np.array([[2, -1], [-1, 6]]) 85 | C = 0.2 * K 86 | wn, wd, zeta, X, Y = vtb.modes_system(M, K, C) 87 | return (wn, wd, zeta, X, Y) 88 | 89 | 90 | def test_modes_sys_prop(): 91 | wn, wd, zeta, X, Y = modes_sys_prop() 92 | assert_allclose(X, np.array([[-0.973249, 0.229753], 93 | [-0.229753, -0.973249]]), rtol=1e-05) 94 | -------------------------------------------------------------------------------- /vibration_toolbox/tests/test_mdof.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | import vibration_toolbox as vtb 4 | from numpy.testing import assert_allclose 5 | 6 | 7 | @pytest.fixture 8 | def _normalize_vect(): 9 | X = np.array([[0.84 + 0.j, 0.14 - 0.j, 0.84 - 0.j, 0.14 + 0.j], 10 | [0.01 - 0.3j, 0.00 + 0.15j, 0.01 + 0.3j, 0.00 - 0.15j], 11 | [-0.09 + 0.42j, -0.01 + 0.65j, -0.09 - 0.42j, -0.01 - 0.65j], 12 | [0.15 + 0.04j, -0.74 + 0.j, 0.15 - 0.04j, -0.74 - 0.j]]) 13 | Y = np.array([[-0.03 - 0.41j, 0.04 + 0.1j, -0.03 + 0.41j, 0.04 - 0.1j], 14 | [0.88 + 0.j, 0.68 + 0.j, 0.88 - 0.j, 0.68 - 0.j], 15 | [-0.21 - 0.j, 0.47 + 0.05j, -0.21 + 0.j, 0.47 - 0.05j], 16 | [0.00 - 0.08j, 0.05 - 0.54j, 0.00 + 0.08j, 0.05 + 0.54j]]) 17 | Yn = vtb.mdof._normalize(X, Y) 18 | print(Yn) 19 | return Yn 20 | 21 | 22 | def test_normalise(_normalize_vect): 23 | Yn = np.array([[0.57822773 - 4.69882840e-02j, 0.11696967 - 5.85231773e-02j, 24 | 0.57822773 + 4.69882840e-02j, 0.11696967 + 5.85231773e-02j], 25 | [0.00998912 + 1.24180506e+00j, -0.0687932 - 8.22911025e-01j, 26 | 0.00998912 - 1.24180506e+00j, -0.0687932 + 8.22911025e-01j], 27 | [-0.00238377 - 2.96339843e-01j, 0.01295993 - 5.73835061e-01j, 28 | -0.00238377 + 2.96339843e-01j, 0.01295993 + 5.73835061e-01j], 29 | [0.11289137 - 9.08101611e-04j, -0.65854649 - 5.87827297e-03j, 30 | 0.11289137 + 9.08101611e-04j, -0.65854649 + 5.87827297e-03j]]) 31 | assert_allclose(_normalize_vect, Yn) 32 | 33 | 34 | def modes_sys(): 35 | M = np.array([[1, 0], [0, 1]]) 36 | K = np.array([[2, -1], [-1, 6]]) 37 | C = np.array([[0.3, -0.02], [-0.02, 0.1]]) 38 | wn, wd, zeta, X, Y = vtb.modes_system(M, K, C) 39 | return wn, wd, zeta, X, Y 40 | 41 | 42 | def test_modes_sys_np(): 43 | wn = np.array([1.328707, 2.49613, 1.328707, 2.49613]) 44 | wd = np.array([1.321268, 2.495419, 1.321268, 2.495419]) 45 | zeta = np.array([0.105665, 0.023878, 0.105665, 0.023878]) 46 | X = np.array([[-0.06187175 - 0.58226568j, -0.01117454 + 0.08415145j, 47 | -0.06187175 + 0.58226568j, 48 | -0.01117454 - 0.08415145j], 49 | [-0.0031966 - 0.13686382j, -0.00864532 - 0.36196512j, 50 | -0.0031966 + 0.13686382j, 51 | -0.00864532 + 0.36196512j], 52 | [0.77801575 + 0.0j, -0.20932709 - 0.03290071j, 53 | 0.77801575 - 0.0j, 54 | -0.20932709 + 0.03290071j], 55 | [0.1812826 + 0.0149919j, 0.90376982 + 0.0j, 56 | 0.1812826 - 0.0149919j, 57 | 0.90376982 - 0.0j]]) 58 | Y = np.array([[0.01582041 + 0.81605587j, 0.01036355 - 0.30817433j, 59 | 0.01582041 - 0.81605587j, 60 | 0.01036355 + 0.30817433j], 61 | [-0.05184151 + 0.18429687j, 0.01345577 + 1.31152703j, 62 | -0.05184151 - 0.18429687j, 63 | 0.01345577 - 1.31152703j], 64 | [0.61081141 + 0.05967192j, -0.12152971 - 0.02007119j, 65 | 0.61081141 - 0.05967192j, 66 | -0.12152971 + 0.02007119j], 67 | [0.14117309 + 0.02567391j, 0.5253469 + 0.00408669j, 68 | 0.14117309 - 0.02567391j, 69 | 0.5253469 - 0.00408669j]]) 70 | 71 | wn_func, wd_func, zeta_func, X_func, Y_func = modes_sys() 72 | 73 | assert_allclose(wn_func, wn, rtol=1e-05) 74 | assert_allclose(wd_func, wd, rtol=1e-05) 75 | assert_allclose(zeta_func, zeta, rtol=1e-04) 76 | np.set_printoptions(precision=8) 77 | print(Y_func) 78 | assert_allclose(X_func, X, rtol=1e-05) 79 | assert_allclose(Y_func, Y, rtol=1e-05) 80 | 81 | 82 | def modes_sys_prop(): 83 | M = np.array([[1, 0], [0, 1]]) 84 | K = np.array([[2, -1], [-1, 6]]) 85 | C = 0.2 * K 86 | wn, wd, zeta, X, Y = vtb.modes_system(M, K, C) 87 | return (wn, wd, zeta, X, Y) 88 | 89 | 90 | def test_modes_sys_prop(): 91 | wn, wd, zeta, X, Y = modes_sys_prop() 92 | assert_allclose(X, np.array([[-0.973249, 0.229753], 93 | [-0.229753, -0.973249]]), rtol=1e-05) 94 | -------------------------------------------------------------------------------- /vibration_toolbox/tests/test_sdof.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | import vibration_toolbox as vtb 4 | from numpy.testing import assert_allclose 5 | 6 | 7 | def test_free_response(): 8 | response = np.array([[1.00000000], 9 | [0.99591926], 10 | [0.99168070], 11 | [0.98728508], 12 | [0.98273317]]) 13 | assert_allclose(vtb.free_response()[1][:5], response) 14 | 15 | 16 | def test_fourier_series(): 17 | f = np.hstack((np.arange(-1, 1, .04), np.arange(1, -1, -.04))) 18 | f += 1 19 | t = np.arange(0, len(f)) / len(f) 20 | a, b = vtb.fourier_series(f, t, 5) 21 | assert_allclose(a[:4], 22 | np.array([2.00000000e+00, 23 | -8.10836188e-01, 24 | 7.01998169e-17, 25 | -9.03304154e-02]), 26 | atol=1e-7, 27 | rtol=1e-7) 28 | assert_allclose(b[:4], 29 | np.array([1.13686838e-15, 30 | 4.45501585e-17, 31 | 3.37507799e-16, 32 | 6.09825913e-17]), 33 | atol=1e-7, 34 | rtol=1e-7) 35 | 36 | 37 | def test_transmissibility(): 38 | _, D, _ = vtb.transmissibility(zs=[0.05, 0.1, 0.25, 0.5, 0.7], 39 | rmin=0, 40 | rmax=2) 41 | assert_allclose(D[:10], 42 | np.array([1., 43 | 1.00001115, 44 | 1.00004459, 45 | 1.00010032, 46 | 1.00017834, 47 | 1.00027863, 48 | 1.00040118, 49 | 1.00054598, 50 | 1.000713, 51 | 1.00090222]), 52 | rtol=1e-7, 53 | atol=1e-7) 54 | 55 | 56 | def test_rotating_unbalance(): 57 | _, Xn = vtb.rotating_unbalance(m=1, m0=0.5, e=0.1, zs=[0.1], rmin=0, 58 | rmax=3.5, normalized=True) 59 | assert_allclose(Xn[0, :5], 60 | np.array([0. +0.j , 61 | 0.035396-0.000251j, 62 | 0.071048-0.00101j, 63 | 0.107218-0.0023j, 64 | 0.14418 -0.004161j]), 65 | atol=1e-4) 66 | -------------------------------------------------------------------------------- /vibration_toolbox/tests/test_vibesystem.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | import vibration_toolbox as vtb 4 | from numpy.testing import assert_allclose 5 | 6 | 7 | @pytest.fixture 8 | def _normalize_vect(): 9 | X = np.array([[0.84 + 0.j, 0.14 - 0.j, 0.84 - 0.j, 0.14 + 0.j], 10 | [0.01 - 0.3j, 0.00 + 0.15j, 0.01 + 0.3j, 0.00 - 0.15j], 11 | [-0.09 + 0.42j, -0.01 + 0.65j, -0.09 - 0.42j, -0.01 - 0.65j], 12 | [0.15 + 0.04j, -0.74 + 0.j, 0.15 - 0.04j, -0.74 - 0.j]]) 13 | Y = np.array([[-0.03 - 0.41j, 0.04 + 0.1j, -0.03 + 0.41j, 0.04 - 0.1j], 14 | [0.88 + 0.j, 0.68 + 0.j, 0.88 - 0.j, 0.68 - 0.j], 15 | [-0.21 - 0.j, 0.47 + 0.05j, -0.21 + 0.j, 0.47 - 0.05j], 16 | [0.00 - 0.08j, 0.05 - 0.54j, 0.00 + 0.08j, 0.05 + 0.54j]]) 17 | Yn = vtb.mdof._normalize(X, Y) 18 | print(Yn) 19 | return Yn 20 | 21 | 22 | def test_normalise(_normalize_vect): 23 | Yn = np.array([[0.57822773 - 4.69882840e-02j, 0.11696967 - 5.85231773e-02j, 24 | 0.57822773 + 4.69882840e-02j, 0.11696967 + 5.85231773e-02j], 25 | [0.00998912 + 1.24180506e+00j, -0.0687932 - 8.22911025e-01j, 26 | 0.00998912 - 1.24180506e+00j, -0.0687932 + 8.22911025e-01j], 27 | [-0.00238377 - 2.96339843e-01j, 0.01295993 - 5.73835061e-01j, 28 | -0.00238377 + 2.96339843e-01j, 0.01295993 + 5.73835061e-01j], 29 | [0.11289137 - 9.08101611e-04j, -0.65854649 - 5.87827297e-03j, 30 | 0.11289137 + 9.08101611e-04j, -0.65854649 + 5.87827297e-03j]]) 31 | assert_allclose(_normalize_vect, Yn) 32 | 33 | 34 | def modes_sys(): 35 | M = np.array([[1, 0], [0, 1]]) 36 | K = np.array([[2, -1], [-1, 6]]) 37 | C = np.array([[0.3, -0.02], [-0.02, 0.1]]) 38 | wn, wd, zeta, X, Y = vtb.modes_system(M, K, C) 39 | return wn, wd, zeta, X, Y 40 | 41 | 42 | def test_modes_sys_np(): 43 | wn = np.array([1.328707, 2.49613, 1.328707, 2.49613]) 44 | wd = np.array([1.321268, 2.495419, 1.321268, 2.495419]) 45 | zeta = np.array([0.105665, 0.023878, 0.105665, 0.023878]) 46 | X = np.array([[-0.06187175 - 0.58226568j, -0.01117454 + 0.08415145j, 47 | -0.06187175 + 0.58226568j, 48 | -0.01117454 - 0.08415145j], 49 | [-0.0031966 - 0.13686382j, -0.00864532 - 0.36196512j, 50 | -0.0031966 + 0.13686382j, 51 | -0.00864532 + 0.36196512j], 52 | [0.77801575 + 0.0j, -0.20932709 - 0.03290071j, 53 | 0.77801575 - 0.0j, 54 | -0.20932709 + 0.03290071j], 55 | [0.1812826 + 0.0149919j, 0.90376982 + 0.0j, 56 | 0.1812826 - 0.0149919j, 57 | 0.90376982 - 0.0j]]) 58 | Y = np.array([[0.01582041 + 0.81605587j, 0.01036355 - 0.30817433j, 59 | 0.01582041 - 0.81605587j, 60 | 0.01036355 + 0.30817433j], 61 | [-0.05184151 + 0.18429687j, 0.01345577 + 1.31152703j, 62 | -0.05184151 - 0.18429687j, 63 | 0.01345577 - 1.31152703j], 64 | [0.61081141 + 0.05967192j, -0.12152971 - 0.02007119j, 65 | 0.61081141 - 0.05967192j, 66 | -0.12152971 + 0.02007119j], 67 | [0.14117309 + 0.02567391j, 0.5253469 + 0.00408669j, 68 | 0.14117309 - 0.02567391j, 69 | 0.5253469 - 0.00408669j]]) 70 | 71 | wn_func, wd_func, zeta_func, X_func, Y_func = modes_sys() 72 | 73 | assert_allclose(wn_func, wn, rtol=1e-05) 74 | assert_allclose(wd_func, wd, rtol=1e-05) 75 | assert_allclose(zeta_func, zeta, rtol=1e-04) 76 | np.set_printoptions(precision=8) 77 | print(Y_func) 78 | assert_allclose(X_func, X, rtol=1e-05) 79 | assert_allclose(Y_func, Y, rtol=1e-05) 80 | 81 | 82 | def modes_sys_prop(): 83 | M = np.array([[1, 0], [0, 1]]) 84 | K = np.array([[2, -1], [-1, 6]]) 85 | C = 0.2 * K 86 | wn, wd, zeta, X, Y = vtb.modes_system(M, K, C) 87 | return (wn, wd, zeta, X, Y) 88 | 89 | 90 | def test_modes_sys_prop(): 91 | wn, wd, zeta, X, _ = modes_sys_prop() 92 | assert_allclose(X, np.array([[-0.973249, 0.229753], 93 | [-0.229753, -0.973249]]), rtol=1e-05) 94 | -------------------------------------------------------------------------------- /vibration_toolbox/vibesystem.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import scipy.linalg as la 3 | import scipy.signal as signal 4 | import matplotlib as mpl 5 | import matplotlib.pyplot as plt 6 | 7 | __all__ = ['VibeSystem'] 8 | 9 | plt.style.use('seaborn-white') 10 | 11 | color_palette = ["#4C72B0", "#55A868", "#C44E52", 12 | "#8172B2", "#CCB974", "#64B5CD"] 13 | 14 | plt.style.use({ 15 | 'lines.linewidth': 2.5, 16 | 'axes.grid': True, 17 | 'axes.linewidth': 0.1, 18 | 'grid.color': '.9', 19 | 'grid.linestyle': '--', 20 | 'legend.frameon': True, 21 | 'legend.framealpha': 0.2 22 | }) 23 | 24 | colors = color_palette + [(.1, .1, .1)] 25 | for code, color in zip('bgrmyck', colors): 26 | rgb = mpl.colors.colorConverter.to_rgb(color) 27 | mpl.colors.colorConverter.colors[code] = rgb 28 | mpl.colors.colorConverter.cache[code] = rgb 29 | 30 | 31 | class VibeSystem(object): 32 | r"""A multiple degrees of freedom system. 33 | 34 | This class will create a multiple degree of 35 | freedom system given M, C and K matrices. 36 | 37 | Parameters 38 | ---------- 39 | M : array 40 | Mass matrix. 41 | C : array 42 | Damping matrix. 43 | K : array 44 | Stiffness matrix. 45 | name : str, optional 46 | Name of the system. 47 | 48 | Attributes 49 | ---------- 50 | evalues : array 51 | System's eigenvalues. 52 | evectors : array 53 | System's eigenvectors. 54 | wn : array 55 | System's natural frequencies in rad/s. 56 | wd : array 57 | System's damped natural frequencies in rad/s. 58 | damping_ratio : array 59 | System's damping factor for each mode. 60 | H : scipy.signal.lti 61 | Continuous-time linear time invariant system 62 | 63 | Examples 64 | -------- 65 | For a system consisting of two masses connected 66 | to each other and both connected to a wall we have 67 | the following matrices: 68 | 69 | >>> m0, m1 = 1, 1 70 | >>> c0, c1, c2 = 5, 5, 5 71 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 72 | 73 | >>> M = np.array([[m0, 0], 74 | ... [0, m1]]) 75 | >>> C = np.array([[c0+c1, -c2], 76 | ... [-c1, c2+c2]]) 77 | >>> K = np.array([[k0+k1, -k2], 78 | ... [-k1, k2+k2]]) 79 | >>> sys = VibeSystem(M, C, K) 80 | >>> sys.wn # doctest: +SKIP 81 | array([31.62, 54.77]) 82 | >>> sys.wd # doctest: +SKIP 83 | array([31.52, 54.26]) 84 | """ 85 | def __init__(self, M, C, K, name=''): 86 | self._M = M 87 | self._C = C 88 | self._K = K 89 | self.name = name 90 | # Values for evalues and evectors will be calculated by self._calc_system 91 | self.evalues = None 92 | self.evectors = None 93 | self.wn = None 94 | self.wd = None 95 | self.lti = None 96 | 97 | self.n = len(M) 98 | self._calc_system() 99 | 100 | @property 101 | def M(self): 102 | return self._M 103 | 104 | @M.setter 105 | def M(self, value): 106 | self._M = value 107 | # if the parameter is changed this will update the system 108 | self._calc_system() 109 | 110 | @property 111 | def C(self): 112 | return self._C 113 | 114 | @C.setter 115 | def C(self, value): 116 | self._C = value 117 | # if the parameter is changed this will update the system 118 | self._calc_system() 119 | 120 | @property 121 | def K(self): 122 | return self._K 123 | 124 | @K.setter 125 | def K(self, value): 126 | self._K = value 127 | # if the parameter is changed this will update the system 128 | self._calc_system() 129 | 130 | def __repr__(self): 131 | M = np.array_str(self.M) 132 | K = np.array_str(self.K) 133 | C = np.array_str(self.C) 134 | return ('Mass Matrix: \n' 135 | '{} \n\n' 136 | 'Stiffness Matrix: \n' 137 | '{} \n\n' 138 | 'Damping Matrix: \n' 139 | '{}'.format(M, K, C)) 140 | 141 | def _calc_system(self): 142 | self.evalues, self.evectors = self._eigen() 143 | self.wn = np.absolute(self.evalues)[:self.n] 144 | self.wd = np.imag(self.evalues)[:self.n] 145 | self.damping_ratio = (-np.real(self.evalues) / 146 | np.absolute(self.evalues))[:self.n] 147 | self.lti = self._lti() 148 | 149 | def A(self): 150 | """State space matrix 151 | 152 | This method will return the state space matrix 153 | of the system. 154 | 155 | Returns 156 | ---------- 157 | A : array 158 | System's state space matrix. 159 | 160 | Examples 161 | -------- 162 | >>> m0, m1 = 1, 1 163 | >>> c0, c1, c2 = 1, 1, 1 164 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 165 | 166 | >>> M = np.array([[m0, 0], 167 | ... [0, m1]]) 168 | >>> C = np.array([[c0+c1, -c2], 169 | ... [-c1, c2+c2]]) 170 | >>> K = np.array([[k0+k1, -k2], 171 | ... [-k1, k2+k2]]) 172 | >>> sys = VibeSystem(M, C, K) # create the system 173 | >>> sys.A() 174 | array([[ 0., 0., 1., 0.], 175 | [ 0., 0., 0., 1.], 176 | [-2000., 1000., -2., 1.], 177 | [ 1000., -2000., 1., -2.]]) 178 | """ 179 | 180 | Z = np.zeros((self.n, self.n)) 181 | I = np.eye(self.n) 182 | 183 | A = np.vstack( 184 | [np.hstack([Z, I]), 185 | np.hstack([la.solve(-self.M, self.K), 186 | la.solve(-self.M, self.C)])]) 187 | return A 188 | 189 | @staticmethod 190 | def _index(eigenvalues): 191 | r"""Function used to generate an index that will sort 192 | eigenvalues and eigenvectors based on the imaginary (wd) 193 | part of the eigenvalues. Positive eigenvalues will be 194 | positioned at the first half of the array. 195 | """ 196 | # avoid float point errors when sorting 197 | evals_truncated = np.around(eigenvalues, decimals=10) 198 | a = np.imag(evals_truncated) # First column 199 | b = np.absolute(evals_truncated) # Second column 200 | ind = np.lexsort((b, a)) # Sort by imag, then by absolute 201 | # Positive eigenvalues first 202 | positive = [i for i in ind[len(a) // 2:]] 203 | negative = [i for i in ind[:len(a) // 2]] 204 | 205 | idx = np.array([positive, negative]).flatten() 206 | 207 | return idx 208 | 209 | def _eigen(self, sorted_=True): 210 | r"""This method will return the eigenvalues and eigenvectors of 211 | the state space matrix A, sorted by the index method which 212 | considers the imaginary part (wd) of the eigenvalues for sorting. 213 | To avoid sorting use sorted_=False 214 | """ 215 | evalues, evectors = la.eig(self.A()) 216 | if sorted_ is False: 217 | return evalues, evectors 218 | 219 | idx = self._index(evalues) 220 | 221 | return evalues[idx], evectors[:, idx] 222 | 223 | def _lti(self): 224 | r"""Continuous-time linear time invariant system. 225 | 226 | This method is used to create a Continuous-time linear 227 | time invariant system for the mdof system. 228 | From this system we can obtain poles, impulse response, 229 | generate a bode, etc. 230 | """ 231 | Z = np.zeros((self.n, self.n)) 232 | I = np.eye(self.n) 233 | 234 | # x' = Ax + Bu 235 | B2 = I 236 | A = self.A() 237 | B = np.vstack([Z, 238 | la.solve(self.M, B2)]) 239 | 240 | # y = Cx + Du 241 | # Observation matrices 242 | Cd = I 243 | Cv = Z 244 | Ca = Z 245 | 246 | C = np.hstack((Cd - Ca @ la.solve(self.M, self.K), 247 | Cv - Ca @ la.solve(self.M, self.C))) 248 | D = Ca @ la.solve(self.M, B2) 249 | 250 | return signal.lti(A, B, C, D) 251 | 252 | def time_response(self, F, t, ic=None): 253 | r"""Time response for a mdof system. 254 | 255 | This method returns the time response for a mdof system 256 | given a force, time and initial conditions. 257 | 258 | Parameters 259 | ---------- 260 | F : array 261 | Force array (needs to have the same length as time array). 262 | t : array 263 | Time array. 264 | ic : array, optional 265 | The initial conditions on the state vector (zero by default). 266 | 267 | Returns 268 | ---------- 269 | t : array 270 | Time values for the output. 271 | yout : array 272 | System response. 273 | xout : array 274 | Time evolution of the state vector. 275 | 276 | 277 | Examples 278 | -------- 279 | >>> m0, m1 = 1, 1 280 | >>> c0, c1, c2 = 1, 1, 1 281 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 282 | 283 | >>> M = np.array([[m0, 0], 284 | ... [0, m1]]) 285 | >>> C = np.array([[c0+c1, -c2], 286 | ... [-c1, c2+c2]]) 287 | >>> K = np.array([[k0+k1, -k2], 288 | ... [-k1, k2+k2]]) 289 | >>> sys = VibeSystem(M, C, K) # create the system 290 | >>> t = np.linspace(0, 25, 1000) # time array 291 | >>> F1 = np.zeros((len(t), 2)) 292 | >>> F1[:, 1] = 1000*np.sin(40*t) # force applied on m1 293 | >>> t, yout, xout = sys.time_response(F1, t) 294 | >>> # response on m0 295 | >>> yout[:5, 0] # doctest: +SKIP 296 | array([0. , 0. , 0.07, 0.32, 0.61]) 297 | >>> # response on m1 298 | >>> yout[:5, 1] # doctest: +SKIP 299 | array([0. , 0.08, 0.46, 0.79, 0.48]) 300 | """ 301 | return signal.lsim(self.lti, F, t, X0=ic) 302 | 303 | def freq_response(self, F=None, omega=None, modes=None): 304 | r"""Frequency response for a mdof system. 305 | 306 | This method returns the frequency response for a mdof system 307 | given a range of frequencies, the force for each frequency 308 | and the modes that will be used. 309 | 310 | Parameters 311 | ---------- 312 | F : array, optional 313 | Force array (needs to have the same length as time array). 314 | If not given the impulse response is calculated. 315 | omega : array, optional 316 | Array with the desired range of frequencies (the default 317 | is 0 to 1.5 x highest damped natural frequency. 318 | modes : list, optional 319 | Modes that will be used to calculate the frequency response 320 | (all modes will be used if a list is not given). 321 | 322 | Returns 323 | ---------- 324 | omega : array 325 | Array with the frequencies 326 | magdb : array 327 | Magnitude (dB) of the frequency response for each pair input/output. 328 | The order of the array is: [output, input, magnitude] 329 | phase : array 330 | Phase of the frequency response for each pair input/output. 331 | The order of the array is: [output, input, phase] 332 | 333 | Examples 334 | -------- 335 | >>> m0, m1 = 1, 1 336 | >>> c0, c1, c2 = 1, 1, 1 337 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 338 | 339 | >>> M = np.array([[m0, 0], 340 | ... [0, m1]]) 341 | >>> C = np.array([[c0+c1, -c2], 342 | ... [-c1, c2+c2]]) 343 | >>> K = np.array([[k0+k1, -k2], 344 | ... [-k1, k2+k2]]) 345 | >>> sys = VibeSystem(M, C, K) # create the system 346 | >>> omega, magdb, phase = sys.freq_response() 347 | >>> # magnitude for output on 0 and input on 1. 348 | >>> magdb[0, 1, :4] 349 | array([-69.54, -69.54, -69.54, -69.54]) 350 | >>> # phase for output on 1 and input on 1. 351 | >>> phase[1, 1, :4] 352 | array([...0. , -0. , -0.01, -0.01]) 353 | """ 354 | rows = self.lti.inputs # inputs (mag and phase) 355 | cols = self.lti.outputs # outputs 356 | 357 | B = self.lti.B 358 | C = self.lti.C 359 | D = self.lti.D 360 | 361 | evals = self.evalues 362 | psi = self.evectors 363 | psi_inv = la.inv(psi) # TODO change to get psi_inv from la.eig 364 | 365 | # if omega is not given, define a range 366 | if omega is None: 367 | omega = np.linspace(0, max(evals.imag) * 1.5, 1000) 368 | 369 | # if modes are selected: 370 | if modes is not None: 371 | n = self.n # n dof -> number of modes 372 | m = len(modes) # -> number of desired modes 373 | # idx to get each evalue/evector and its conjugate 374 | idx = np.zeros((2 * m), int) 375 | idx[0:m] = modes # modes 376 | idx[m:] = range(2 * n)[-m:] # conjugates (see how evalues are ordered) 377 | 378 | evals = evals[np.ix_(idx)] 379 | psi = psi[np.ix_(range(2 * n), idx)] 380 | psi_inv = psi_inv[np.ix_(idx, range(2 * n))] 381 | 382 | magdb = np.empty((cols, rows, len(omega))) 383 | phase = np.empty((cols, rows, len(omega))) 384 | 385 | for wi, w in enumerate(omega): 386 | diag = np.diag([1 / (1j * w - lam) for lam in evals]) 387 | if F is None: 388 | H = C @ psi @ diag @ psi_inv @ B + D 389 | else: 390 | H = (C @ psi @ diag @ psi_inv @ B + D) @ F[wi] 391 | 392 | magh = 20.0 * np.log10(abs(H)) 393 | angh = np.rad2deg((np.angle(H))) 394 | 395 | magdb[:, :, wi] = magh 396 | phase[:, :, wi] = angh 397 | 398 | return omega, magdb, phase 399 | 400 | def plot_freq_response(self, out, inp, modes=None, ax0=None, ax1=None, **kwargs): 401 | """Plot frequency response. 402 | 403 | This method plots the frequency response given 404 | an output and an input. 405 | 406 | Parameters 407 | ---------- 408 | out : int 409 | Output. 410 | input : int 411 | Input. 412 | modes : list, optional 413 | Modes that will be used to calculate the frequency response 414 | (all modes will be used if a list is not given). 415 | 416 | ax0 : matplotlib.axes, optional 417 | Matplotlib axes where the amplitude will be plotted. 418 | If None creates a new. 419 | ax1 : matplotlib.axes, optional 420 | Matplotlib axes where the phase will be plotted. 421 | If None creates a new. 422 | kwargs : optional 423 | Additional key word arguments can be passed to change 424 | the plot (e.g. linestyle='--') 425 | 426 | Returns 427 | ------- 428 | ax0 : matplotlib.axes 429 | Matplotlib axes with amplitude plot. 430 | ax1 : matplotlib.axes 431 | Matplotlib axes with phase plot. 432 | 433 | Examples 434 | -------- 435 | >>> m0, m1 = 1, 1 436 | >>> c0, c1, c2 = 1, 1, 1 437 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 438 | 439 | >>> M = np.array([[m0, 0], 440 | ... [0, m1]]) 441 | >>> C = np.array([[c0+c1, -c2], 442 | ... [-c1, c2+c2]]) 443 | >>> K = np.array([[k0+k1, -k2], 444 | ... [-k1, k2+k2]]) 445 | >>> sys = VibeSystem(M, C, K) # create the system 446 | >>> # plot frequency response for input and output at m0 447 | >>> sys.plot_freq_response(0, 0) 448 | (>> m0, m1 = 1, 1 512 | >>> c0, c1, c2 = 1, 1, 1 513 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 514 | 515 | >>> M = np.array([[m0, 0], 516 | ... [0, m1]]) 517 | >>> C = np.array([[c0+c1, -c2], 518 | ... [-c1, c2+c2]]) 519 | >>> K = np.array([[k0+k1, -k2], 520 | ... [-k1, k2+k2]]) 521 | >>> sys = VibeSystem(M, C, K) # create the system 522 | >>> # plot frequency response for inputs at [0, 1] 523 | >>> # and outputs at [0, 1] 524 | >>> sys.plot_freq_response_grid(outs=[0, 1], inps=[0, 1]) 525 | array([[ 1: 535 | for i, out in enumerate(outs): 536 | for j, inp in enumerate(inps): 537 | self.plot_freq_response(out, inp, 538 | modes=modes, 539 | ax0=ax[2*i, j], 540 | ax1=ax[2*i + 1, j]) 541 | else: 542 | for i, inp in enumerate(inps): 543 | self.plot_freq_response(outs[0], inp, 544 | modes=modes, 545 | ax0=ax[2*i], 546 | ax1=ax[2*i + 1]) 547 | 548 | return ax 549 | 550 | def plot_time_response(self, F, t, ic=None, out=None, ax=None): 551 | r"""Plot the time response for a mdof system. 552 | 553 | This method returns the time response for a mdof system 554 | given a force, time and initial conditions. 555 | 556 | Parameters 557 | ---------- 558 | F : array 559 | Force array (needs to have the same length as time array). 560 | t : array 561 | Time array. 562 | ic : array, optional 563 | The initial conditions on the state vector (zero by default). 564 | out : list 565 | Desired output for which the time response will be plotted. 566 | ax : array with matplotlib.axes, optional 567 | Matplotlib axes array created with plt.subplots. 568 | It needs to have a shape of (2*inputs, outputs). 569 | 570 | Returns 571 | ------- 572 | ax : array with matplotlib.axes, optional 573 | Matplotlib axes array created with plt.subplots. 574 | t : array 575 | Time values for the output. 576 | yout : array 577 | System response. 578 | xout : array 579 | Time evolution of the state vector. 580 | 581 | Examples 582 | -------- 583 | >>> m0, m1 = 1, 1 584 | >>> c0, c1, c2 = 1, 1, 1 585 | >>> k0, k1, k2 = 1e3, 1e3, 1e3 586 | 587 | >>> M = np.array([[m0, 0], 588 | ... [0, m1]]) 589 | >>> C = np.array([[c0+c1, -c2], 590 | ... [-c1, c2+c2]]) 591 | >>> K = np.array([[k0+k1, -k2], 592 | ... [-k1, k2+k2]]) 593 | >>> sys = VibeSystem(M, C, K) # create the system 594 | >>> t = np.linspace(0, 25, 1000) # time array 595 | >>> F1 = np.zeros((len(t), 2)) 596 | >>> F1[:, 1] = 1000*np.sin(40*t) # force applied on m1 597 | >>> sys.plot_time_response(F1, t) 598 | array([