├── .nojekyll ├── .gitattributes ├── vibrationtesting ├── tests │ ├── __init__.py │ ├── test_sparsematriceshandler.py │ └── test_vibrationtesting.py ├── data │ ├── case1.mat │ ├── case2.mat │ ├── mckiddat.mat │ ├── testdata.mat │ ├── frf_data1.mat │ ├── WingBeamforMAC.mat │ └── Modeshapedata │ │ ├── 1stNaturalFreq15.mat │ │ ├── 1stNaturalFreq18.mat │ │ ├── 2ndNaturalFreq15.mat │ │ ├── 2ndNaturalFreq18.mat │ │ ├── 3rdNaturalFreq15.mat │ │ ├── 3rdNaturalFreq18.mat │ │ ├── 5thNaturalFreq15.mat │ │ ├── 5thNaturalFreq18.mat │ │ ├── 6thNaturalFreq15.mat │ │ ├── 6thNaturalFreq18.mat │ │ ├── 4rthNaturalFreq15.mat │ │ └── 4rthNaturalFreq18.mat ├── conftest.py ├── __init__.py ├── .ropeproject │ └── config.py ├── sparsematriceshandler.py ├── identification.py └── system.py ├── pytest.ini ├── requirements.txt ├── docs ├── reference │ ├── index.rst │ ├── signal.rst │ ├── system.rst │ └── identification.rst ├── tutorial │ ├── index.rst │ └── Notebooks │ │ └── Ho-Kalman-Demo.ipynb ├── Makefile ├── Installing_Python.rst ├── readme.rst ├── installation.rst ├── index.rst └── conf.py ├── setup.cfg ├── .travis.yml ├── LICENSE.txt ├── .gitignore ├── setup.py ├── Makefile ├── readme.rst ├── JupyterNotebooks ├── secondorderstatespacematlab.ipynb ├── Final_Solution.ipynb ├── Examples.ipynb └── Ho-Kalman-Demo.ipynb ├── .ropeproject └── config.py ├── CONTRIBUTING.rst └── old └── conf.py-old /.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vibrationtesting/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --ignore=setup.py 3 | doctest_optionflags= NORMALIZE_WHITESPACE ELLIPSIS 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | matplotlib 4 | sphinx 5 | ipython 6 | control 7 | vibration_toolbox 8 | xarray 9 | -------------------------------------------------------------------------------- /vibrationtesting/data/case1.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/case1.mat -------------------------------------------------------------------------------- /vibrationtesting/data/case2.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/case2.mat -------------------------------------------------------------------------------- /vibrationtesting/data/mckiddat.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/mckiddat.mat -------------------------------------------------------------------------------- /vibrationtesting/data/testdata.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/testdata.mat -------------------------------------------------------------------------------- /vibrationtesting/conftest.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | np.set_printoptions(precision=4, suppress=True) 4 | print('Running conftest.py') 5 | -------------------------------------------------------------------------------- /vibrationtesting/data/frf_data1.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/frf_data1.mat -------------------------------------------------------------------------------- /vibrationtesting/data/WingBeamforMAC.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/WingBeamforMAC.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/1stNaturalFreq15.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/1stNaturalFreq15.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/1stNaturalFreq18.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/1stNaturalFreq18.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/2ndNaturalFreq15.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/2ndNaturalFreq15.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/2ndNaturalFreq18.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/2ndNaturalFreq18.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/3rdNaturalFreq15.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/3rdNaturalFreq15.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/3rdNaturalFreq18.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/3rdNaturalFreq18.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/5thNaturalFreq15.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/5thNaturalFreq15.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/5thNaturalFreq18.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/5thNaturalFreq18.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/6thNaturalFreq15.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/6thNaturalFreq15.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/6thNaturalFreq18.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/6thNaturalFreq18.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/4rthNaturalFreq15.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/4rthNaturalFreq15.mat -------------------------------------------------------------------------------- /vibrationtesting/data/Modeshapedata/4rthNaturalFreq18.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vibration-Testing/vibrationtesting/HEAD/vibrationtesting/data/Modeshapedata/4rthNaturalFreq18.mat -------------------------------------------------------------------------------- /docs/reference/index.rst: -------------------------------------------------------------------------------- 1 | Reference 2 | --------- 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | signal 8 | system 9 | identification 10 | 11 | .. systemidentification 12 | .. modelupdating 13 | -------------------------------------------------------------------------------- /docs/reference/signal.rst: -------------------------------------------------------------------------------- 1 | Signal Processing and Generation (:mod:`signals`) 2 | ------------------------------------------------- 3 | 4 | .. sectionauthor:: Joseph C. Slater 5 | 6 | .. automodule:: signals 7 | :members: 8 | -------------------------------------------------------------------------------- /docs/reference/system.rst: -------------------------------------------------------------------------------- 1 | System Manipulation, Reduction, and Correction (:mod:`system`) 2 | -------------------------------------------------------------- 3 | 4 | .. sectionauthor:: Joseph C. Slater 5 | 6 | .. automodule:: system 7 | :members: 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/reference/identification.rst: -------------------------------------------------------------------------------- 1 | Modal Analysis and System Identification (:mod:`identification`) 2 | ---------------------------------------------------------------- 3 | 4 | .. sectionauthor:: Joseph C. Slater 5 | 6 | .. automodule:: identification 7 | :members: 8 | -------------------------------------------------------------------------------- /.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 | 21 | -------------------------------------------------------------------------------- /docs/tutorial/index.rst: -------------------------------------------------------------------------------- 1 | Tutorial 2 | -------- 3 | 4 | Tutorials with worked examples using the Vibration Testing module. 5 | 6 | Please excuse the mess while this is brought online. 7 | 8 | .. sectionauthor:: Joseph C. Slater 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | 13 | Notebooks/Signal.ipynb 14 | Notebooks/Ho-Kalman-Demo.ipynb 15 | -------------------------------------------------------------------------------- /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 = VibrationTesting 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 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | vibrationtesting 2 | ================== 3 | Copyright (C) 2002 by Joseph C. Slater 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 | -------------------------------------------------------------------------------- /docs/Installing_Python.rst: -------------------------------------------------------------------------------- 1 | .. _installing_python: 2 | 3 | Installing Python 4 | _________________ 5 | 6 | In order to be able to use the Vibration Testing module you need a working scientific python installation. 7 | 8 | The easiest path to this is to install Python via `Anaconda`_. **You must install** Python 3.5 or later for the Vibration Testing module to work. **Do note install Python 2.7.** 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 | Then, 23 | 24 | .. code-block:: bash 25 | 26 | conda install jupyter 27 | 28 | To use `Jupyter`_ (the notebook), launch a terminal on Mac or Linux, or the Anaconda Terminal on Windows (or similar name) and type: 29 | 30 | .. code-block:: bash 31 | 32 | jupyter notebook 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | .ipynb_check* 21 | .ipynb*check*/* 22 | .#.* 23 | .ipynb_checkpoints/** 24 | .ipynb* 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # Misc 33 | pncr* 34 | Untitled* 35 | 36 | # Sphinx 37 | _build* 38 | latex* 39 | html* 40 | *~ 41 | *.html 42 | 43 | # Mac 44 | aquamacs 45 | 46 | # PyInstaller 47 | # Usually these files are written by a python script from a template 48 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 49 | *.manifest 50 | *.spec 51 | 52 | # Installer logs 53 | pip-log.txt 54 | pip-delete-this-directory.txt 55 | 56 | # Unit test / coverage reports 57 | htmlcov/ 58 | .tox/ 59 | .coverage 60 | .coverage.* 61 | .cache 62 | nosetests.xml 63 | coverage.xml 64 | *,cover 65 | .hypothesis/ 66 | 67 | # Translations 68 | *.mo 69 | *.pot 70 | 71 | # Django stuff: 72 | *.log 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | target/ 79 | vibrationtesting/.DS_Store 80 | .DS_Store 81 | *.lock 82 | .pytest_cache/v/cache/lastfailed 83 | -------------------------------------------------------------------------------- /vibrationtesting/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Vibration Testing module 3 | ------------------------ 4 | 5 | This module is a companion to the manuscript Vibration Testing with Health 6 | Monitoring by Joseph C. Slater. It includes functions for: 7 | 8 | - Signal processing 9 | - System manipulation and solution 10 | - System identification 11 | 12 | """ 13 | from __future__ import division, print_function, absolute_import 14 | 15 | __title__ = 'vibrationtesting' 16 | __version__ = '0.24' 17 | __author__ = u'Joseph C. Slater' 18 | __license__ = 'MIT' 19 | __copyright__ = 'Copyright 2002-2017 Joseph C. Slater' 20 | __all__ = ['identification', 'signals', 'system', 21 | '__version__'] 22 | 23 | import sys 24 | import matplotlib as mpl 25 | import numpy as np 26 | 27 | if 'pytest' in sys.argv[0]: 28 | # print('Setting backend to agg to run tests') 29 | _ = mpl.use('agg') 30 | np.set_printoptions(precision=4, suppress=True) 31 | 32 | from .signals import * 33 | from .system import * 34 | from .identification import * 35 | from .sparsematriceshandler import * 36 | 37 | # Signal processing (:mod:`vibrationtesting.signal`) 38 | 39 | # print options were change inside modules to produce better 40 | # outputs at examples. Here we set the print options to the 41 | # default values after importing the modules to avoid changing 42 | # np default print options when importing the module. 43 | -------------------------------------------------------------------------------- /docs/readme.rst: -------------------------------------------------------------------------------- 1 | This is the folder for building the complete documentation for the Vibration Testing module. 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 | Subsection Title 24 | ________________ 25 | 26 | Subsubsection Title 27 | ~~~~~~~~~~~~~~~~~~~ 28 | 29 | Subsubsubsection Title 30 | `````````````````````` 31 | 32 | Subsubsubsubection Title 33 | '''''''''''''''''''''''' 34 | 35 | We shouldn't ever need this many. 36 | 37 | 38 | All documentation will be built with via `sphinx `_ using ``make html`` in the ``docs`` directory. Updating on ``github`` is documented 39 | in the ``developers.rst`` file at the top of the repository. 40 | 41 | Documentation of functions help (uses `autodoc `_): 42 | 43 | 44 | Module and function help needs to then follow the `numpy convention. 45 | `_ 46 | 47 | 48 | For information on how function docstrings are used, see `numpydoc `_ 49 | 50 | For plots: 51 | http://matplotlib.org/sampledoc/extensions.html 52 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | import os 5 | import sys 6 | 7 | if sys.version_info < (3, 5): 8 | sys.exit('Sorry, Python < 3.5 is not supported.') 9 | # Utility function to read the README file. 10 | # Used for the long_description. It's nice, because now 1) we have a top level 11 | # README file and 2) it's easier to type in the README file than to put a raw 12 | # string in below ... 13 | 14 | 15 | def read(fname): 16 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 17 | 18 | 19 | with open('vibrationtesting/__init__.py', 'rb') as fid: 20 | for line in fid: 21 | line = line.decode('utf-8') 22 | if line.startswith('__version__'): 23 | version = line.strip().split()[-1][1:-1] 24 | break 25 | 26 | download_url = ('https://github.com/Vibration-Testing/vibrationtesting/\ 27 | blob/master/dist/vibrationtesting-' + version + '.whl') 28 | 29 | setup(name='vibrationtesting', 30 | version=version, 31 | description=('Signal processing, modal analysis, plotting, and system\ 32 | identification for vibrating systems'), 33 | author=u'Joseph C. Slater', 34 | author_email='joseph.c.slater@gmail.com', 35 | url='https://github.com/Vibration-Testing/vibrationtesting', 36 | packages=['vibrationtesting'], 37 | package_data={'vibrationtesting': ['../readme.rst', 'data/*.mat'], 38 | '': ['readme.rst']}, 39 | long_description=read('readme.rst'), 40 | keywords=['vibration', 'mechanical engineering', 'testing', 41 | 'civil engineering', 'modal analysis'], 42 | install_requires=['numpy', 'scipy', 'matplotlib'], 43 | setup_requires=['pytest-runner'], 44 | tests_require=['pytest'] 45 | ) 46 | 47 | # https://docs.python.org/3/distutils/setupscript.html#additional-meta-data 48 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | 7 | Installing_Python 8 | 9 | Easy Installation 10 | _________________ 11 | 12 | If you aren't familiar at all with Python, please see `Installing Python `_. 13 | 14 | Installation is made easy with ``pip`` (or ``pip3``), with releases as we have time while we try 15 | to create a full first release. Much of it works already, but we certainly need 16 | issue reports (on `github `_). 17 | 18 | To install:: 19 | 20 | pip install --user vibrationtesting 21 | 22 | where ``--user`` isn't necessary if you are using a locally installed version of Python such as `Anaconda `_. 23 | 24 | To run, I recommend you open a `Jupyter`_ notebook by using ``jupyter notebook`` and then type:: 25 | 26 | import vibrationtesting as vt 27 | 28 | 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. Sorry- these are very rough right now. You can see the `less rough notebooks `_ used to make the manual as well. 29 | 30 | Installation of current development version 31 | ___________________________________________ 32 | 33 | 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 in rapid development. So is the documentation. To get the most current version and stay up to date see the file `CONTRIBUTING.rst `_ in the github repository. 34 | 35 | .. _Jupyter: jupyter.org 36 | -------------------------------------------------------------------------------- /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 vibrationtesting 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=vibrationtesting 11 | 12 | export GHP_MSG="Generated gh-pages for `git log master -1 --pretty=short --abbrev-commit`" 13 | export VERSION=`python -c "import $(NAME); print($(NAME).__version__)"` 14 | 15 | #all: clean 16 | # python setup.py install 17 | 18 | #---------------------------------------------------- 19 | 20 | help: 21 | @echo "Please use \`make ' where is one of:" 22 | @echo " clean to clear build files" 23 | @echo " test to test all docstring examples" 24 | @echo " cover to test coverage (not working yet)" 25 | @echo " release to edit version, build docs and release" 26 | @echo " wheel build wheel file (for local use)" 27 | @echo " wheel-dist build wheel and push to github" 28 | @echo " docs build docs using sphin" 29 | @echo " html alias for docs" 30 | @echo " gh-pages build and release docs" 31 | 32 | clean: 33 | rm -rf build 34 | rm -rf dist 35 | find . -name "*.pyc" -o -name "*.py,cover"| xargs rm -f 36 | # killall -9 nosetests; true 37 | 38 | test: 39 | pytest 40 | 41 | cover: clean 42 | pip install nose-cov 43 | nosetests $(TEST_ARGS) --with-cov --cov $(NAME) $(NAME) 44 | coverage annotate 45 | 46 | release: clean 47 | pip install --user readme_renderer 48 | #python setup.py check -r -s 49 | pytest 50 | #python setup.py register 51 | rm -rf dist 52 | python setup.py bdist_wheel 53 | # python setup.py sdist 54 | git tag v$(VERSION) 55 | git push origin --all 56 | git push origin --tags 57 | printf '\nUpgrade vibration testing with release and sha256 sum:' 58 | printf '\nOK, no sha256 sum yet:' 59 | twine upload dist/* 60 | shasum -a 256 dist/*.whl 61 | 62 | wheel: 63 | rm -rf dist 64 | python setup.py bdist_wheel 65 | 66 | wheel-dist: gh-pages 67 | rm -rf dist 68 | python setup.py bdist_wheel 69 | 70 | docs: 71 | # Warnings become errors and stop build 72 | export SPHINXOPTS=-W 73 | # pip install sphinx-bootstrap-theme numpydoc sphinx ghp-import 74 | # Run the make file in the docs directory 75 | make -C docs clean 76 | make -C docs html 77 | 78 | html: docs 79 | 80 | gh-pages: 81 | git checkout master 82 | git pull origin master 83 | git commit -a -m "Keep examples in sync"; true 84 | git push origin; true 85 | make docs 86 | ghp-import -n -p -m $(GHP_MSG) docs/_build/html 87 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Vibration Testing 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 Vibration Testing Module 8 | ============================================= 9 | 10 | .. .. include:: 11 | .. image:: https://badge.fury.io/py/vibrationtesting.png/ 12 | :target: http://badge.fury.io/py/vibrationtesting 13 | 14 | .. image:: https://travis-ci.org/vibrationtesting/vibrationtesting.svg?branch=master 15 | :target: https://travis-ci.org/vibrationtesting/vibrationtesting 16 | 17 | .. image:: https://img.shields.io/pypi/v/vibrationtesting.svg 18 | :target: https://img.shields.io/pypi/v/vibrationtesting 19 | 20 | .. #image:: https://coveralls.io/repos/vibrationtesting/vibrationtesting/badge.png?branch=master 21 | .. #:target: https://coveralls.io/r/vibrationtesting/vibrationtesting 22 | 23 | 24 | Joseph C. Slater 25 | 26 | Welcome to the `Vibration Testing` module. This module has three separate pieces in varying degrees of completion: 27 | 28 | - Signal processing 29 | - Model identification (Modal Analysis) 30 | - Model updating 31 | 32 | If you are looking for a `Matlab version `_, one does exist, but is not being actively developed by me. You'll find more features hear. Volunteering to support Open Source is much more rewarding than volunteering to support Matlab. 33 | 34 | This software is nominally provided without support, etc., but I will certainly answer a question or two. If you have professional-level needs please `contact the authors `_. Further, please also `submit bug reports `_ with code indicating the error. 35 | 36 | 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 module so this is taking some time. We don't need feedback at this time, but we will take assistance in improving documentation and code. 37 | 38 | Table of Contents 39 | ----------------- 40 | 41 | .. toctree:: 42 | :maxdepth: 3 43 | 44 | installation 45 | tutorial/index 46 | reference/index 47 | 48 | Download Tutorial Notebooks 49 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 50 | 51 | .. :download:`Single Degree of Freedom ` 52 | 53 | .. :download:`Multiple Degree of Freedom ` 54 | 55 | .. :download:`Continuous Systems ` 56 | 57 | .. :download:`Vibration System ` 58 | 59 | 60 | Indices and tables 61 | __________________ 62 | 63 | * :ref:`genindex` 64 | * :ref:`modindex` 65 | * :ref:`search` 66 | -------------------------------------------------------------------------------- /readme.rst: -------------------------------------------------------------------------------- 1 | Vibration Testing: 2 | ================== 3 | 4 | A package for signal processing, modal analysis, and model reduction and model updating 5 | 6 | .. .. include:: 7 | .. image:: https://badge.fury.io/py/vibrationtesting.png/ 8 | :target: http://badge.fury.io/py/vibrationtesting 9 | 10 | .. image:: https://travis-ci.org/Vibration-Testing/vibrationtesting.svg?branch=master 11 | :target: https://travis-ci.org/Vibration-Testing/vibrationtesting 12 | 13 | .. image:: https://zenodo.org/badge/50037940.svg 14 | :target: https://zenodo.org/badge/latestdoi/50037940 15 | 16 | .. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg 17 | :target: https://saythanks.io/to/josephcslater 18 | 19 | .. image:: https://img.shields.io/badge/patreon-donate-yellow.svg 20 | :target: https://www.patreon.com/josephcslater 21 | 22 | .. image:: http://pepy.tech/badge/vibrationtesting 23 | :target: http://pepy.tech/project/vibrationtesting 24 | :alt: PyPi Download stats 25 | 26 | Joseph C. Slater 27 | ---------------- 28 | 29 | Welcome to `Vibration Testing `_. 30 | 31 | For more information, please see the `documentation for the Python version `_. 32 | 33 | Installation 34 | ------------ 35 | 36 | If you aren't familiar at all with Python, please see `Installing Python `_. 37 | 38 | Installation is made easy with ``pip`` (or ``pip3``), with releases as we have time while we try 39 | to create a full first release. Much of it works already, but I certainly need 40 | issue reports (on `github `_) when something is not working as it should. 41 | 42 | To install type:: 43 | 44 | pip install --user vibrationtesting 45 | 46 | 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 `_. 47 | 48 | To run, I recommend you open a `Jupyter `_ notebook by using ``jupyter notebook`` and then type:: 49 | 50 | import vibrationtesting as vt 51 | 52 | For examples, see the `JupyterNotebooks folder `_. (In flux- also look in doc/Tutorials for now) 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. 53 | 54 | Installation of current code 55 | ____________________________ 56 | 57 | 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 in flux. So is the documentation. Releases to `PyPI `_ are far behind current status as stopping to deploy would cost more time that it is worth. I have the objective of releasing a first non-beta version at the end of May 2018, but even this cannot be promised. 58 | 59 | If you wish to install the current version of the software, read the instructions in `Contributing.rst `_ 60 | 61 | That should be it. Please note issues on the `issues tab `_ on GitHub. 62 | 63 | Like this module, `buy me a coffee! `_ 64 | 65 | Quick notes: 66 | ------------- 67 | 68 | The `convention used for the signal processing `_ is that of the `python controls module `_. 69 | -------------------------------------------------------------------------------- /vibrationtesting/tests/test_sparsematriceshandler.py: -------------------------------------------------------------------------------- 1 | """pytest unit tests for vibrationtesting""" 2 | 3 | import numpy as np 4 | import vibrationtesting as vt 5 | import numpy.testing as nt 6 | import scipy.io as sio 7 | 8 | def test_sos_modal_forsparse(): 9 | mat_contents=sio.loadmat('vibrationtesting/data/WingBeamforMAC.mat') # WFEM generated .mat file 10 | K = (mat_contents['K']) 11 | M = (mat_contents['M']) 12 | ## Mr and Kr are WFEM outputs after Guyan reduction 13 | Kr = (mat_contents['Kr']) 14 | Mr = (mat_contents['Mr']) 15 | master = np.array([[ 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 20, 16 | 21, 22, 23, 24, 26, 27, 28, 29, 30, 32, 33, 34, 35, 36, 38, 39, 17 | 40, 41, 42, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 56, 57, 58, 18 | 59, 60, 62, 63, 64, 65, 66, 68, 69, 70, 71, 72, 74, 75, 76, 77, 19 | 78, 80, 81, 82, 83, 84, 86, 87, 88, 89, 90]]) 20 | ## Mred and Kred are from guyan_forsparse 21 | [Mred, Kred, master] = vt.guyan_forsparse(M, K, master=master, fraction=None) 22 | [omega_sp,zeta_sp,Psi_sp] = vt.sos_modal_forsparse(Mred,Kred) 23 | Kbm = Kr.todense() 24 | Mbm = Mr.todense() 25 | omega, zeta, Psi = vt.sos_modal(Mbm, Kbm) 26 | ## The below compares sparsematriceshandler.py vs system.py results 27 | nt.assert_array_almost_equal(omega_sp,omega) 28 | nt.assert_array_almost_equal(zeta_sp,zeta) 29 | nt.assert_array_almost_equal(Psi_sp,Psi) 30 | 31 | def test_guyan_forsparse(): 32 | mat_contents=sio.loadmat('vibrationtesting/data/WingBeamforMAC.mat') # WFEM generated .mat file 33 | K = (mat_contents['K']) 34 | M = (mat_contents['M']) 35 | ## Mr and Kr are WFEM outputs after Guyan reduction 36 | Kr = (mat_contents['Kr']) 37 | Mr = (mat_contents['Mr']) 38 | master = np.array([[ 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 14, 15, 16, 17, 18, 20, 39 | 21, 22, 23, 24, 26, 27, 28, 29, 30, 32, 33, 34, 35, 36, 38, 39, 40 | 40, 41, 42, 44, 45, 46, 47, 48, 50, 51, 52, 53, 54, 56, 57, 58, 41 | 59, 60, 62, 63, 64, 65, 66, 68, 69, 70, 71, 72, 74, 75, 76, 77, 42 | 78, 80, 81, 82, 83, 84, 86, 87, 88, 89, 90]]) 43 | ## Mred and Kred are from guyan_forsparse 44 | [Mred, Kred, master] = vt.guyan_forsparse(M, K, master=master, fraction=None) 45 | Kred = Kred.todense() 46 | Mred = Mred.todense() 47 | Kr = Kr.todense() 48 | Mr = Mr.todense() 49 | ## The below compares the two outputs guyanWFEM vs guyanVibrationtesting 50 | nt.assert_array_almost_equal(Mred,Mr) 51 | nt.assert_array_almost_equal(Kred,Kr) 52 | 53 | def test_mode_expansion_from_model_forsparse(): 54 | mat_contents=sio.loadmat('vibrationtesting/data/Modeshapedata/1stNaturalFreq15.mat') 55 | U1 = mat_contents['U1'] 56 | mat_contents=sio.loadmat('vibrationtesting/data/Modeshapedata/2ndNaturalFreq15.mat') 57 | U2 = mat_contents['U2'] 58 | mat_contents=sio.loadmat('vibrationtesting/data/Modeshapedata/3rdNaturalFreq15.mat') 59 | U3 = mat_contents['U3'] 60 | mat_contents=sio.loadmat('vibrationtesting/data/Modeshapedata/4rthNaturalFreq15.mat') 61 | U4 = mat_contents['U4'] 62 | mat_contents=sio.loadmat('vibrationtesting/data/Modeshapedata/5thNaturalFreq15.mat') 63 | U5 = mat_contents['U5'] 64 | mat_contents=sio.loadmat('vibrationtesting/data/Modeshapedata/6thNaturalFreq15.mat') 65 | U6 = mat_contents['U6'] 66 | Psi_1 = np.array(U1) 67 | Psi_2 = np.array(U2) 68 | Psi_3 = np.array(U3) 69 | Psi_4 = np.array(U4) 70 | Psi_5 = np.array(U5) 71 | Psi_6 = np.array(U6) 72 | Psi_1=np.column_stack((Psi_1,Psi_2,Psi_3,Psi_4,Psi_5,Psi_6)) 73 | Psi_abs = np.abs(Psi_1)*np.real(np.sign(Psi_1)) 74 | Psi_1 = Psi_abs 75 | mat_contents=sio.loadmat('vibrationtesting/data/WingBeamforMAC.mat') # WFEM generated .mat file 76 | K = (mat_contents['Kr']) 77 | M = (mat_contents['Mr']) 78 | Kbm = K.todense() 79 | Mbm = M.todense() 80 | measured = np.array([[1,6,11,16,21,26,31,36,41,46,51,56,61,66,71]]) 81 | omega=np.array([13.54, 51.81, 125.19, 167.76, 228.05, 328.41]) 82 | Psi_fullBM_1=vt.mode_expansion_from_model(Psi_1, omega, Mbm, Kbm, measured) 83 | Psi_fullBM_2=vt.mode_expansion_from_model_forsparse(Psi_1, omega, M, K, measured) 84 | 85 | ## The below compares sparsematriceshandler.py vs system.py results 86 | nt.assert_array_almost_equal(Psi_fullBM_1,Psi_fullBM_2) 87 | -------------------------------------------------------------------------------- /JupyterNotebooks/secondorderstatespacematlab.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 16, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "m =\n", 15 | " 2\n", 16 | "c =\n", 17 | " 0.0100\n", 18 | "k =\n", 19 | " 100\n", 20 | "f =\n", 21 | " 7\n", 22 | "A =\n", 23 | " 0 1.0000\n", 24 | " -50.0000 -0.0050\n", 25 | "B =\n", 26 | " 0\n", 27 | " 3.5000\n", 28 | "C =\n", 29 | " 1 0\n", 30 | "D =\n", 31 | " 0\n", 32 | "v =\n", 33 | " -0.0000 - 0.1400i -0.0000 + 0.1400i\n", 34 | " 0.9901 + 0.0000i 0.9901 + 0.0000i\n", 35 | "d =\n", 36 | " -0.0025 + 7.0711i 0.0000 + 0.0000i\n", 37 | " 0.0000 + 0.0000i -0.0025 - 7.0711i\n", 38 | "Bm =\n", 39 | " 1.7674 + 0.0006i\n", 40 | " 1.7674 - 0.0006i\n", 41 | "Cm =\n", 42 | " -0.0000 - 0.1400i -0.0000 + 0.1400i\n", 43 | "Am =\n", 44 | " -0.0025 + 7.0711i -0.0000 + 0.0000i\n", 45 | " -0.0000 - 0.0000i -0.0025 - 7.0711i\n", 46 | "lam1 =\n", 47 | " -0.0025 + 7.0711i\n", 48 | "ans =\n", 49 | " -0.0025 + 7.0711i\n", 50 | " -0.0025 - 7.0711i\n", 51 | "ans =\n", 52 | " 0.0000 + 3.5707i 0.5050 + 0.0002i\n", 53 | " 0.0000 - 3.5707i 0.5050 - 0.0002i\n", 54 | "P2 =\n", 55 | " 0.0025 + 7.0711i 1.0000 + 0.0000i\n", 56 | " -0.0025 - 7.0711i 1.0000 - 0.0007i\n", 57 | "Aso =\n", 58 | " 0.0000 + 0.0000i 1.0000 - 0.0000i\n", 59 | " -50.0000 - 0.0000i -0.0050 - 0.0000i\n", 60 | "Bso =\n", 61 | " -0.0000 + 0.0000i\n", 62 | " 1.7674 + 0.0006i\n", 63 | "Cso =\n", 64 | " 1.9803 - 0.0007i 0.0000 + 0.0000i\n" 65 | ] 66 | } 67 | ], 68 | "source": [ 69 | "m = 2, c = .01, k = 100 \n", 70 | "%roots([m c k]) \n", 71 | "f = 7 \n", 72 | "A = [0 1;-m\\k -m\\c] \n", 73 | "B = [0;f/m] \n", 74 | "C = [1 0]\n", 75 | "D=0 \n", 76 | "[v,d]=eig(A) \n", 77 | "P = v;Bm = P\\B, Cm = C*P \n", 78 | "Am=v\\A*v\n", 79 | "lam1=d(1,1)\n", 80 | "roots([m c k]) \n", 81 | "inv(v)\n", 82 | "P2=[-conj(lam1) 1;conj(lam1) exp(j*(pi-2*angle(lam1)))]\n", 83 | "Aso = P2\\Am*P2\n", 84 | "Bso = P2\\Bm\n", 85 | "Cso = Cm * P2" 86 | ] 87 | }, 88 | { 89 | "cell_type": "code", 90 | "execution_count": null, 91 | "metadata": { 92 | "collapsed": true 93 | }, 94 | "outputs": [], 95 | "source": [] 96 | } 97 | ], 98 | "metadata": { 99 | "hide_input": false, 100 | "kernelspec": { 101 | "display_name": "Matlab", 102 | "language": "matlab", 103 | "name": "matlab" 104 | }, 105 | "language_info": { 106 | "codemirror_mode": "octave", 107 | "file_extension": ".m", 108 | "help_links": [ 109 | { 110 | "text": "MetaKernel Magics", 111 | "url": "https://github.com/calysto/metakernel/blob/master/metakernel/magics/README.md" 112 | } 113 | ], 114 | "mimetype": "text/x-matlab", 115 | "name": "matlab", 116 | "version": "0.8.0" 117 | }, 118 | "latex_envs": { 119 | "bibliofile": "biblio.bib", 120 | "cite_by": "apalike", 121 | "current_citInitial": 1, 122 | "eqLabelWithNumbers": true, 123 | "eqNumInitial": 0 124 | } 125 | }, 126 | "nbformat": 4, 127 | "nbformat_minor": 0 128 | } 129 | -------------------------------------------------------------------------------- /.ropeproject/config.py: -------------------------------------------------------------------------------- 1 | # The default ``config.py`` 2 | # flake8: noqa 3 | 4 | 5 | def set_prefs(prefs): 6 | """This function is called before opening the project""" 7 | 8 | # Specify which files and folders to ignore in the project. 9 | # Changes to ignored resources are not added to the history and 10 | # VCSs. Also they are not returned in `Project.get_files()`. 11 | # Note that ``?`` and ``*`` match all characters but slashes. 12 | # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' 13 | # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' 14 | # '.svn': matches 'pkg/.svn' and all of its children 15 | # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' 16 | # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' 17 | prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', 18 | '.hg', '.svn', '_svn', '.git', '.tox'] 19 | 20 | # Specifies which files should be considered python files. It is 21 | # useful when you have scripts inside your project. Only files 22 | # ending with ``.py`` are considered to be python files by 23 | # default. 24 | #prefs['python_files'] = ['*.py'] 25 | 26 | # Custom source folders: By default rope searches the project 27 | # for finding source folders (folders that should be searched 28 | # for finding modules). You can add paths to that list. Note 29 | # that rope guesses project source folders correctly most of the 30 | # time; use this if you have any problems. 31 | # The folders should be relative to project root and use '/' for 32 | # separating folders regardless of the platform rope is running on. 33 | # 'src/my_source_folder' for instance. 34 | #prefs.add('source_folders', 'src') 35 | 36 | # You can extend python path for looking up modules 37 | #prefs.add('python_path', '~/python/') 38 | 39 | # Should rope save object information or not. 40 | prefs['save_objectdb'] = True 41 | prefs['compress_objectdb'] = False 42 | 43 | # If `True`, rope analyzes each module when it is being saved. 44 | prefs['automatic_soa'] = True 45 | # The depth of calls to follow in static object analysis 46 | prefs['soa_followed_calls'] = 0 47 | 48 | # If `False` when running modules or unit tests "dynamic object 49 | # analysis" is turned off. This makes them much faster. 50 | prefs['perform_doa'] = True 51 | 52 | # Rope can check the validity of its object DB when running. 53 | prefs['validate_objectdb'] = True 54 | 55 | # How many undos to hold? 56 | prefs['max_history_items'] = 32 57 | 58 | # Shows whether to save history across sessions. 59 | prefs['save_history'] = True 60 | prefs['compress_history'] = False 61 | 62 | # Set the number spaces used for indenting. According to 63 | # :PEP:`8`, it is best to use 4 spaces. Since most of rope's 64 | # unit-tests use 4 spaces it is more reliable, too. 65 | prefs['indent_size'] = 4 66 | 67 | # Builtin and c-extension modules that are allowed to be imported 68 | # and inspected by rope. 69 | prefs['extension_modules'] = [] 70 | 71 | # Add all standard c-extensions to extension_modules list. 72 | prefs['import_dynload_stdmods'] = True 73 | 74 | # If `True` modules with syntax errors are considered to be empty. 75 | # The default value is `False`; When `False` syntax errors raise 76 | # `rope.base.exceptions.ModuleSyntaxError` exception. 77 | prefs['ignore_syntax_errors'] = False 78 | 79 | # If `True`, rope ignores unresolvable imports. Otherwise, they 80 | # appear in the importing namespace. 81 | prefs['ignore_bad_imports'] = False 82 | 83 | # If `True`, rope will insert new module imports as 84 | # `from import ` by default. 85 | prefs['prefer_module_from_imports'] = False 86 | 87 | # If `True`, rope will transform a comma list of imports into 88 | # multiple separate import statements when organizing 89 | # imports. 90 | prefs['split_imports'] = False 91 | 92 | # If `True`, rope will remove all top-level import statements and 93 | # reinsert them at the top of the module when making changes. 94 | prefs['pull_imports_to_top'] = True 95 | 96 | # If `True`, rope will sort imports alphabetically by module name instead of 97 | # alphabetically by import statement, with from imports after normal 98 | # imports. 99 | prefs['sort_imports_alphabetically'] = False 100 | 101 | # Location of implementation of rope.base.oi.type_hinting.interfaces.ITypeHintingFactory 102 | # In general case, you don't have to change this value, unless you're an rope expert. 103 | # Change this value to inject you own implementations of interfaces 104 | # listed in module rope.base.oi.type_hinting.providers.interfaces 105 | # For example, you can add you own providers for Django Models, or disable the search 106 | # type-hinting in a class hierarchy, etc. 107 | prefs['type_hinting_factory'] = 'rope.base.oi.type_hinting.factory.default_type_hinting_factory' 108 | 109 | 110 | def project_opened(project): 111 | """This function is called after opening the project""" 112 | # Do whatever you like here! 113 | -------------------------------------------------------------------------------- /vibrationtesting/.ropeproject/config.py: -------------------------------------------------------------------------------- 1 | # The default ``config.py`` 2 | # flake8: noqa 3 | 4 | 5 | def set_prefs(prefs): 6 | """This function is called before opening the project""" 7 | 8 | # Specify which files and folders to ignore in the project. 9 | # Changes to ignored resources are not added to the history and 10 | # VCSs. Also they are not returned in `Project.get_files()`. 11 | # Note that ``?`` and ``*`` match all characters but slashes. 12 | # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' 13 | # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' 14 | # '.svn': matches 'pkg/.svn' and all of its children 15 | # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' 16 | # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' 17 | prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', 18 | '.hg', '.svn', '_svn', '.git', '.tox'] 19 | 20 | # Specifies which files should be considered python files. It is 21 | # useful when you have scripts inside your project. Only files 22 | # ending with ``.py`` are considered to be python files by 23 | # default. 24 | #prefs['python_files'] = ['*.py'] 25 | 26 | # Custom source folders: By default rope searches the project 27 | # for finding source folders (folders that should be searched 28 | # for finding modules). You can add paths to that list. Note 29 | # that rope guesses project source folders correctly most of the 30 | # time; use this if you have any problems. 31 | # The folders should be relative to project root and use '/' for 32 | # separating folders regardless of the platform rope is running on. 33 | # 'src/my_source_folder' for instance. 34 | #prefs.add('source_folders', 'src') 35 | 36 | # You can extend python path for looking up modules 37 | #prefs.add('python_path', '~/python/') 38 | 39 | # Should rope save object information or not. 40 | prefs['save_objectdb'] = True 41 | prefs['compress_objectdb'] = False 42 | 43 | # If `True`, rope analyzes each module when it is being saved. 44 | prefs['automatic_soa'] = True 45 | # The depth of calls to follow in static object analysis 46 | prefs['soa_followed_calls'] = 0 47 | 48 | # If `False` when running modules or unit tests "dynamic object 49 | # analysis" is turned off. This makes them much faster. 50 | prefs['perform_doa'] = True 51 | 52 | # Rope can check the validity of its object DB when running. 53 | prefs['validate_objectdb'] = True 54 | 55 | # How many undos to hold? 56 | prefs['max_history_items'] = 32 57 | 58 | # Shows whether to save history across sessions. 59 | prefs['save_history'] = True 60 | prefs['compress_history'] = False 61 | 62 | # Set the number spaces used for indenting. According to 63 | # :PEP:`8`, it is best to use 4 spaces. Since most of rope's 64 | # unit-tests use 4 spaces it is more reliable, too. 65 | prefs['indent_size'] = 4 66 | 67 | # Builtin and c-extension modules that are allowed to be imported 68 | # and inspected by rope. 69 | prefs['extension_modules'] = [] 70 | 71 | # Add all standard c-extensions to extension_modules list. 72 | prefs['import_dynload_stdmods'] = True 73 | 74 | # If `True` modules with syntax errors are considered to be empty. 75 | # The default value is `False`; When `False` syntax errors raise 76 | # `rope.base.exceptions.ModuleSyntaxError` exception. 77 | prefs['ignore_syntax_errors'] = False 78 | 79 | # If `True`, rope ignores unresolvable imports. Otherwise, they 80 | # appear in the importing namespace. 81 | prefs['ignore_bad_imports'] = False 82 | 83 | # If `True`, rope will insert new module imports as 84 | # `from import ` by default. 85 | prefs['prefer_module_from_imports'] = False 86 | 87 | # If `True`, rope will transform a comma list of imports into 88 | # multiple separate import statements when organizing 89 | # imports. 90 | prefs['split_imports'] = False 91 | 92 | # If `True`, rope will remove all top-level import statements and 93 | # reinsert them at the top of the module when making changes. 94 | prefs['pull_imports_to_top'] = True 95 | 96 | # If `True`, rope will sort imports alphabetically by module name instead of 97 | # alphabetically by import statement, with from imports after normal 98 | # imports. 99 | prefs['sort_imports_alphabetically'] = False 100 | 101 | # Location of implementation of rope.base.oi.type_hinting.interfaces.ITypeHintingFactory 102 | # In general case, you don't have to change this value, unless you're an rope expert. 103 | # Change this value to inject you own implementations of interfaces 104 | # listed in module rope.base.oi.type_hinting.providers.interfaces 105 | # For example, you can add you own providers for Django Models, or disable the search 106 | # type-hinting in a class hierarchy, etc. 107 | prefs['type_hinting_factory'] = 'rope.base.oi.type_hinting.factory.default_type_hinting_factory' 108 | 109 | 110 | def project_opened(project): 111 | """This function is called after opening the project""" 112 | # Do whatever you like here! 113 | -------------------------------------------------------------------------------- /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 | To contribute code we recommend you follow these steps: 11 | 12 | #. Fork the repository on github 13 | 14 | #. Set up travis-ci for your branch. This is actually pretty quick and easy: 15 | 16 | #. Go to travis-ci.org and Sign in with GitHub. 17 | 18 | #. Account page will show all the repositories attached to GitHub. 19 | 20 | #. Find the right repository and enable Travis CI. 21 | 22 | #. Once this is done, Travis CI will be turned-on in GitHub fork. 23 | 24 | #. Go back to the fork on GitHub, click 25 | 26 | ``Settings`` -> ``Webhooks`` -> ``updated travis-ci.org link``. 27 | 28 | #. Default or customize the options based on needs and click 29 | 30 | ``Update webhook``. 31 | 32 | #. Clone the repository to your favorite location on your drive where you want to work on it. 33 | 34 | #. To work in `developer mode `_, from a terminal (python enabled) at the top level directory inside the ``vibration testing module`` 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 testing module`` you may have a conflict. You must uninstall it and install your development version with the command above. 39 | 40 | #. If a new function is added 41 | please provide docstrings following the `Numpy standards for docstrings `_. 42 | The docstrings should contain examples to be tested. 43 | 44 | Specifically note: 45 | 46 | 1. Parameters should be listed similarly to: 47 | 48 | | filename : str 49 | | copy : bool 50 | | dtype : data-type 51 | | iterable : iterable object 52 | | shape : int or tuple of int 53 | | files : list of str 54 | | time : array_like 55 | 56 | 2. First line should be inline with the ``"""`` and brief enough to fit on one line. 57 | 58 | 3. There must be a blank line after the first line. 59 | 60 | This is not exhaustive. It just highlights some consistent errors made. 61 | 62 | #. Run the doctests regularly when you make edits. 63 | 64 | To run the doctests ``_ is needed and can be installed with ``pip install -U pytest``. 65 | 66 | To run the tests from the shell you can `cd` to the project's root directory and type:: 67 | 68 | $ pytest 69 | 70 | 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. 71 | 72 | 2. To run the tests from ``spyder`` see `spyder-unittest `_ before submitting a pull request. To do this, as your terminal within your clone ofthe repository:: 75 | 76 | $ git pull origin 77 | 78 | #. Commit and check `travis-ci `_ 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. 79 | 80 | #. If the tests are passing, make a git pull (in your GitHub app) to assure that your code is up to date with the 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. 81 | 82 | #. Please provide feedback and corrections to these instructions. 83 | 84 | Instructions bellow are directed to main developers 85 | =================================================== 86 | 87 | To make distribution and release 88 | -------------------------------- 89 | 90 | 1) Edit the version number in ``vibrationtesting/__init__.py`` 91 | 2) Use the Makefile, ``make release`` 92 | 93 | The ``conf.py`` file for the documentation pulls the version from ``__init__.py`` 94 | 95 | To make a distribution (for testing or posting to github) 96 | ----------------------------------------------------------- 97 | 98 | .. code-block:: bash 99 | 100 | >> make wheel 101 | 102 | To test before release 103 | ---------------------- 104 | 105 | TravisCI does this for us. 106 | 107 | See `notes `_ on working in development mode. 108 | 109 | To test distribution installabilty 110 | ----------------------------------- 111 | Note: these are out of date. 112 | 113 | python setup.py register -r pypitest 114 | python setup.py sdist upload -r pypitest 115 | 116 | look at https://testpypi.python.org/pypi 117 | 118 | Other information sites 119 | ------------------------ 120 | 121 | `twine notes `_ 122 | 123 | https://pypi.python.org/pypi/wheel 124 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Vibration Testing 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('../vibrationtesting') 28 | sys.path.append('../JupyterNotebooks') 29 | sys.path.append('..') 30 | sys.path.append(os.path.join(os.path.dirname(__name__), '..')) 31 | 32 | # Avoid needin to update version here for release 33 | from vibrationtesting 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 | 64 | # Add any paths that contain templates here, relative to this directory. 65 | templates_path = ['_templates'] 66 | 67 | # The suffix(es) of source filenames. 68 | # You can specify multiple suffix as a list of string: 69 | # 70 | # source_suffix = ['.rst', '.md'] 71 | source_suffix = '.rst' 72 | 73 | # The master toctree document. 74 | master_doc = 'index' 75 | 76 | numpydoc_show_class_members = False 77 | 78 | # Whether to produce plot:: directives for Examples sections that contain 79 | # import matplotlib 80 | numpydoc_use_plots = True 81 | 82 | 83 | # General information about the project. 84 | project = 'Vibration Testing' 85 | copyright = u'2017, Joseph C. Slater' 86 | author = u'Joseph C. Slater' 87 | 88 | # The version info for the project you're documenting, acts as replacement for 89 | # |version| and |release|, also used in various other places throughout the 90 | # built documents. 91 | # 92 | # The short X.Y version. 93 | version = __version__ 94 | # The full version, including alpha/beta/rc tags. 95 | release = version 96 | 97 | # The language for content autogenerated by Sphinx. Refer to documentation 98 | # for a list of supported languages. 99 | # 100 | # This is also used if you do content translation via gettext catalogs. 101 | # Usually you set "language" from the command line for these cases. 102 | language = None 103 | 104 | # List of patterns, relative to source directory, that match files and 105 | # directories to ignore when looking for source files. 106 | # This patterns also effect to html_static_path and html_extra_path 107 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'readme.rst', 108 | '**.ipynb_checkpoints'] 109 | 110 | # The name of the Pygments (syntax highlighting) style to use. 111 | pygments_style = 'sphinx' 112 | 113 | # If true, `todo` and `todoList` produce output, else they produce nothing. 114 | todo_include_todos = True 115 | 116 | 117 | # Block out warning from nonlocal image in main readme file of repository when 118 | # building docs in sphinx 119 | # http://stackoverflow.com/questions/12772927/specifying-an-online-image-in-sphinx-restructuredtext-format 120 | def _warn_node(self, msg, node, **kwargs): 121 | if not msg.startswith('nonlocal image URI found:'): 122 | self._warnfunc(msg, '%s:%s' % get_source_line(node), **kwargs) 123 | 124 | 125 | sphinx.environment.BuildEnvironment.warn_node = _warn_node 126 | 127 | 128 | # -- Options for HTML output ---------------------------------------------- 129 | 130 | # The theme to use for HTML and HTML Help pages. See the documentation for 131 | # a list of builtin themes. 132 | # 133 | html_theme = 'alabaster' 134 | 135 | 136 | html_sidebars = { 137 | '**': [ 138 | 'about.html', 'navigation.html', 'searchbox.html', 'sourcelink.html', 139 | ] 140 | } 141 | 142 | 143 | # Theme options are theme-specific and customize the look and feel of a theme 144 | # further. For a list of options available for each theme, see the 145 | # documentation. 146 | # 147 | # html_theme_options = {} 148 | 149 | # Add any paths that contain custom static files (such as style sheets) here, 150 | # relative to this directory. They are copied after the builtin static files, 151 | # so a file named "default.css" will overwrite the builtin "default.css". 152 | html_static_path = ['_static'] 153 | 154 | 155 | # -- Options for HTMLHelp output ------------------------------------------ 156 | 157 | # Output file base name for HTML help builder. 158 | htmlhelp_basename = 'VibrationTesting' 159 | 160 | html_theme_options = { 161 | # 'logo': 'js.jpg', 162 | 'logo_name': 'VTest', 163 | 'description': 'Vibration Testing Analysis', 164 | 'github_button': 'True', 165 | 'github_user': 'vibrationtesting', 166 | 'analytics_id': 'UA-62100376-6', 167 | } 168 | 169 | 170 | # -- Options for LaTeX output --------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | # 175 | # 'papersize': 'letterpaper', 176 | 177 | # The font size ('10pt', '11pt' or '12pt'). 178 | # 179 | # 'pointsize': '10pt', 180 | 181 | # Additional stuff for the LaTeX preamble. 182 | # 183 | # 'preamble': '', 184 | 185 | # Latex figure (float) alignment 186 | # 187 | # 'figure_align': 'htbp', 188 | } 189 | 190 | # Grouping the document tree into LaTeX files. List of tuples 191 | # (source start file, target name, title, 192 | # author, documentclass [howto, manual, or own class]). 193 | latex_documents = [ 194 | (master_doc, 'VibrationTesting.tex', 'Vibration Testing Documentation', 195 | 'Joseph C. Slater', 'manual'), 196 | ] 197 | 198 | 199 | # -- Options for manual page output --------------------------------------- 200 | 201 | # One entry per manual page. List of tuples 202 | # (source start file, name, description, authors, manual section). 203 | man_pages = [ 204 | (master_doc, 'vibrationtesting', 'Vibration Testing Documentation', 205 | [author], 1) 206 | ] 207 | 208 | 209 | # -- Options for Texinfo output ------------------------------------------- 210 | 211 | # Grouping the document tree into Texinfo files. List of tuples 212 | # (source start file, target name, title, author, 213 | # dir menu entry, description, category) 214 | texinfo_documents = [ 215 | (master_doc, 'VibrationTesting', 'Vibration Testing Documentation', 216 | author, 'VibrationTesting', 'One line description of project.', 217 | 'Miscellaneous'), 218 | ] 219 | -------------------------------------------------------------------------------- /vibrationtesting/tests/test_vibrationtesting.py: -------------------------------------------------------------------------------- 1 | """pytest unit tests for vibrationtesting""" 2 | 3 | import numpy as np 4 | import vibrationtesting as vt 5 | import numpy.testing as nt 6 | 7 | np.set_printoptions(precision=4, suppress=True) 8 | 9 | M = np.array([[4, 0, 0], 10 | [0, 4, 0], 11 | [0, 0, 4]]) 12 | Cso = np.array([[.1, 0, 0], 13 | [0, 0, 0], 14 | [0, 0, 0]]) 15 | K = np.array([[8, -4, 0], 16 | [-4, 8, -4], 17 | [0, -4, 4]]) 18 | omega, zeta, Psi = vt.sos_modal(M, K) 19 | 20 | 21 | ######################################################### 22 | # system.py tests 23 | ######################################################### 24 | 25 | def test_d2c(): 26 | Ad = np.array([[0.9999, 0.0001, 0.01, 0.], 27 | [0., 0.9999, 0., 0.01], 28 | [-0.014, 0.012, 0.9999, 0.0001], 29 | [0.008, -0.014, 0.0001, 0.9999]]) 30 | Bd = np.array([[0.], 31 | [0.], 32 | [0.], 33 | [0.01]]) 34 | C = np.array([[-1.4, 1.2, -0.0058, 0.0014]]) 35 | D = np.array([[-0.2]]) 36 | A, B, *_ = vt.d2c(Ad, Bd, C, D, 0.01) 37 | nt.assert_array_almost_equal(A, 38 | np.array([[-0.002999, 0.003999, 1.000053, 39 | -0.00006], 40 | [-0.004001, -0.002999, 41 | - 0.000023, 1.000053], 42 | [-1.400083, 1.200148, 43 | - 0.003, 0.003999], 44 | [0.800075, -1.400143, 0.006, 45 | -0.002999]])) 46 | 47 | # def test_c2d(): 48 | 49 | 50 | # def test_ssfrf(): 51 | 52 | 53 | # def test_sos_frf(): 54 | 55 | 56 | # def test_so2ss(): 57 | 58 | # Damp- no test 59 | 60 | def test_sos_modal(): 61 | M = np.array([[4, 0, 0], 62 | [0, 4, 0], 63 | [0, 0, 4]]) 64 | K = np.array([[8, -4, 0], 65 | [-4, 8, -4], 66 | [0, -4, 4]]) 67 | omega, zeta, Psi = vt.sos_modal(M, K, K / 10) 68 | nt.assert_array_almost_equal( 69 | omega, np.array([0.445042, 1.24698, 1.801938])) 70 | K_diag = np.array([[0.198062, 0., -0.], 71 | [0., 1.554958, -0.], 72 | [-0., -0., 3.24698]]) 73 | 74 | nt.assert_array_almost_equal(Psi.T@K@Psi, K_diag) 75 | 76 | K2 = K - np.eye(K.shape[0])@M * (Psi.T@K@Psi)[0, 0] 77 | omega, zeta, Psi = vt.sos_modal(M, K2) 78 | nt.assert_array_almost_equal(omega, np.array([0., 1.164859, 1.746115])) 79 | Psi_true = np.array([[-0.163993, 0.368488, -0.295505], 80 | [-0.295505, 0.163993, 0.368488], 81 | [-0.368488, -0.295505, -0.163993]]) 82 | nt.assert_array_almost_equal(Psi_true, Psi) 83 | 84 | # Diagonalizes? 85 | Psi_true = np.array([[0., 0., -0.], 86 | [-0., 1.356896, 0.], 87 | [-0., 0., 3.048917]]) 88 | nt.assert_array_almost_equal(Psi.T@K2@Psi, Psi_true) 89 | 90 | # How about non-proportional damping 91 | 92 | C = K / 10 93 | C[0, 0] = 2 * C[0, 0] 94 | omega, zeta, Psi = vt.sos_modal(M, K2, C) 95 | 96 | # Damping matrix cannot be completely diagonalized. 97 | 98 | nt.assert_array_almost_equal(omega, np.array([0., 1.164859, 1.746115])) 99 | 100 | nt.assert_array_almost_equal(zeta, np.array([0., 0.113371, 0.112981])) 101 | C_diag = np.array([[0.041321, -0.048343, 0.038768], 102 | [-0.048343, 0.264123, -0.087112], 103 | [0.038768, -0.087112, 0.394556]]) 104 | nt.assert_array_almost_equal(C_diag, Psi.T@C@Psi) 105 | 106 | 107 | def test_serep(): 108 | M = np.array([[4, 0, 0], 109 | [0, 4, 0], 110 | [0, 0, 4]]) 111 | K = np.array([[8, -4, 0], 112 | [-4, 8, -4], 113 | [0, -4, 4]]) 114 | retained = np.array([[1, 2]]) 115 | Mred, Kred, T, truncated_dofs = vt.serep(M, K, retained) 116 | Mr_soln = np.array([[16.98791841, -16.19566936], 117 | [-16.19566936, 24.19566936]]) 118 | Kr_soln = np.array([[20.98791841, -12.98791841], 119 | [-12.98791841, 10.21983253]]) 120 | nt.assert_array_almost_equal(Mred, Mr_soln) 121 | nt.assert_array_almost_equal(Kred, Kr_soln) 122 | 123 | 124 | # def test_guyan(): 125 | 126 | 127 | # def test_mode_expansion_from_model(): 128 | 129 | 130 | # def improved_reduction(when written): 131 | 132 | 133 | # def test_model_correction_direct(): 134 | 135 | 136 | # slice- no test 137 | 138 | 139 | # def test_rsolve(): 140 | 141 | 142 | # def test_real_modes(): 143 | 144 | 145 | def test_ss_modal(): 146 | Bt = np.array([[1], [0], [0]]) 147 | Ca = np.array([[1, 0, 0]]) 148 | Cd = Cv = np.zeros_like(Ca) 149 | A, B, C, D = vt.so2ss(M, Cso, K, Bt, Cd, Cv, Ca) 150 | Am, Bm, Cm, Dm, eigenvalues, modes = vt.ss_modal(A, B, C, D) 151 | nt.assert_array_almost_equal(Am, np.array( 152 | [[-0.0013 + 0.445j, 0.0000 + 0.j, 0.0000 + 0.j, 0.0000 + 0.j, 153 | 0.0000 + 0.j, 0.0000 + 0.j], 154 | [0.0000 + 0.j, -0.0013 - 0.445j, 0.0000 + 0.j, 0.0000 + 0.j, 155 | 0.0000 + 0.j, 0.0000 + 0.j], 156 | [0.0000 + 0.j, 0.0000 + 0.j, -0.0068 + 1.247j, 0.0000 + 0.j, 157 | 0.0000 + 0.j, 0.0000 + 0.j], 158 | [0.0000 + 0.j, 0.0000 + 0.j, 0.0000 + 0.j, -0.0068 - 1.247j, 159 | 0.0000 + 0.j, 0.0000 + 0.j], 160 | [0.0000 + 0.j, 0.0000 + 0.j, 0.0000 + 0.j, 0.0000 + 0.j, 161 | -0.0044 + 1.8019j, 0.0000 + 0.j], 162 | [0.0000 + 0.j, 0.0000 + 0.j, 0.0000 + 0.j, 0.0000 + 0.j, 163 | 0.0000 + 0.j, -0.0044 - 1.8019j]]), decimal=3) 164 | nt.assert_array_almost_equal(Cm, np.array( 165 | [[0.0594 - 0.0001j, 0.0594 + 0.0001j, 0.0039 - 0.717j, 0.0039 + 0.717j, 166 | 0.0241 - 0.9307j, 0.0241 + 0.9307j]]), decimal=3) 167 | 168 | 169 | def test_sos_modal_non_rigid(): 170 | omega, zeta, Psi = vt.sos_modal(M, K, K / 10) 171 | nt.assert_array_almost_equal(omega, np.array( 172 | [0.445, 1.247, 1.8019]), decimal=3) 173 | 174 | 175 | def test_sos_modal_non_rigid_diag(): 176 | omega, zeta, Psi = vt.sos_modal(M, K, K / 10) 177 | nt.assert_array_almost_equal(Psi.T@K@Psi, 178 | np.array([[0.1981, 0., -0.], 179 | [0., 1.555, -0.], 180 | [-0., -0., 3.247]]), 181 | decimal=3) 182 | 183 | 184 | def test_sos_modal_rigid(): 185 | omega, zeta, Psi = vt.sos_modal(M, K) 186 | K2 = K - np.eye(K.shape[0])@M * (Psi.T@K@Psi)[0, 0] 187 | omega, zeta, Psi = vt.sos_modal(M, K2) 188 | nt.assert_array_almost_equal(omega, np.array( 189 | [0., 1.1649, 1.7461]), decimal=3) 190 | 191 | 192 | def test_sos_modal_rigid_diag(): 193 | omega, zeta, Psi = vt.sos_modal(M, K) 194 | K2 = K - np.eye(K.shape[0])@M * (Psi.T@K@Psi)[0, 0] 195 | omega, zeta, Psi = vt.sos_modal(M, K2) 196 | nt.assert_array_almost_equal(Psi, 197 | np.array([[-0.164, 0.3685, -0.2955], 198 | [-0.2955, 0.164, 0.3685], 199 | [-0.3685, -0.2955, -0.164]]), 200 | decimal=3) 201 | 202 | 203 | def test_sos_modal_non_proportional(): 204 | omega, zeta, Psi = vt.sos_modal(M, K) 205 | K2 = K - np.eye(K.shape[0])@M * (Psi.T@K@Psi)[0, 0] 206 | C = K / 10 207 | C[0, 0] = 2 * C[0, 0] 208 | omega, zeta, Psi = vt.sos_modal(M, K2, C) 209 | nt.assert_array_almost_equal(omega, 210 | np.array([0., 1.1649, 1.7461]), decimal=3) 211 | nt.assert_array_almost_equal(zeta, 212 | np.array([0., 0.1134, 0.113]), decimal=3) 213 | nt.assert_array_almost_equal(Psi.T@C@Psi, 214 | np.array([[0.0413, -0.0483, 0.0388], 215 | [-0.0483, 0.2641, -0.0871], 216 | [0.0388, -0.0871, 0.3946]]), 217 | decimal=3) 218 | -------------------------------------------------------------------------------- /old/conf.py-old: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Vibration Testing Tools documentation build configuration file, created by 5 | # sphinx-quickstart on Thu May 14 17:13:49 2015. 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 | import sys 17 | import os 18 | import shlex 19 | 20 | # If extensions (or modules to document with autodoc) are in another directory, 21 | # add these directories to sys.path here. If the directory is relative to the 22 | # documentation root, use os.path.abspath to make it absolute, like shown here. 23 | #sys.path.insert(0, os.path.abspath('.')) 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.doctest', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.mathjax', 39 | 'sphinx.ext.viewcode', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | #source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = 'Vibration Testing Tools' 58 | copyright = '2015, Joseph C. Slater' 59 | author = 'Joseph C. Slater' 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = '0.1' 67 | # The full version, including alpha/beta/rc tags. 68 | release = '0.1' 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | #today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | #today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = ['_build'] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | #default_role = None 90 | 91 | # If true, '()' will be appended to :func: etc. cross-reference text. 92 | #add_function_parentheses = True 93 | 94 | # If true, the current module name will be prepended to all description 95 | # unit titles (such as .. function::). 96 | #add_module_names = True 97 | 98 | # If true, sectionauthor and moduleauthor directives will be shown in the 99 | # output. They are ignored by default. 100 | #show_authors = False 101 | 102 | # The name of the Pygments (syntax highlighting) style to use. 103 | pygments_style = 'sphinx' 104 | 105 | # A list of ignored prefixes for module index sorting. 106 | #modindex_common_prefix = [] 107 | 108 | # If true, keep warnings as "system message" paragraphs in the built documents. 109 | #keep_warnings = False 110 | 111 | # If true, `todo` and `todoList` produce output, else they produce nothing. 112 | todo_include_todos = True 113 | 114 | 115 | # -- Options for HTML output ---------------------------------------------- 116 | 117 | # The theme to use for HTML and HTML Help pages. See the documentation for 118 | # a list of builtin themes. 119 | html_theme = 'alabaster' 120 | 121 | # Theme options are theme-specific and customize the look and feel of a theme 122 | # further. For a list of options available for each theme, see the 123 | # documentation. 124 | #html_theme_options = {} 125 | 126 | # Add any paths that contain custom themes here, relative to this directory. 127 | #html_theme_path = [] 128 | 129 | # The name for this set of Sphinx documents. If None, it defaults to 130 | # " v documentation". 131 | #html_title = None 132 | 133 | # A shorter title for the navigation bar. Default is the same as html_title. 134 | #html_short_title = None 135 | 136 | # The name of an image file (relative to this directory) to place at the top 137 | # of the sidebar. 138 | #html_logo = None 139 | 140 | # The name of an image file (within the static path) to use as favicon of the 141 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 142 | # pixels large. 143 | #html_favicon = None 144 | 145 | # Add any paths that contain custom static files (such as style sheets) here, 146 | # relative to this directory. They are copied after the builtin static files, 147 | # so a file named "default.css" will overwrite the builtin "default.css". 148 | html_static_path = ['_static'] 149 | 150 | # Add any extra paths that contain custom files (such as robots.txt or 151 | # .htaccess) here, relative to this directory. These files are copied 152 | # directly to the root of the documentation. 153 | #html_extra_path = [] 154 | 155 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 156 | # using the given strftime format. 157 | #html_last_updated_fmt = '%b %d, %Y' 158 | 159 | # If true, SmartyPants will be used to convert quotes and dashes to 160 | # typographically correct entities. 161 | #html_use_smartypants = True 162 | 163 | # Custom sidebar templates, maps document names to template names. 164 | #html_sidebars = {} 165 | 166 | # Additional templates that should be rendered to pages, maps page names to 167 | # template names. 168 | #html_additional_pages = {} 169 | 170 | # If false, no module index is generated. 171 | #html_domain_indices = True 172 | 173 | # If false, no index is generated. 174 | #html_use_index = True 175 | 176 | # If true, the index is split into individual pages for each letter. 177 | #html_split_index = False 178 | 179 | # If true, links to the reST sources are added to the pages. 180 | #html_show_sourcelink = True 181 | 182 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 183 | #html_show_sphinx = True 184 | 185 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 186 | #html_show_copyright = True 187 | 188 | # If true, an OpenSearch description file will be output, and all pages will 189 | # contain a tag referring to it. The value of this option must be the 190 | # base URL from which the finished HTML is served. 191 | #html_use_opensearch = '' 192 | 193 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 194 | #html_file_suffix = None 195 | 196 | # Language to be used for generating the HTML full-text search index. 197 | # Sphinx supports the following languages: 198 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 199 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' 200 | #html_search_language = 'en' 201 | 202 | # A dictionary with options for the search language support, empty by default. 203 | # Now only 'ja' uses this config value 204 | #html_search_options = {'type': 'default'} 205 | 206 | # The name of a javascript file (relative to the configuration directory) that 207 | # implements a search results scorer. If empty, the default will be used. 208 | #html_search_scorer = 'scorer.js' 209 | 210 | # Output file base name for HTML help builder. 211 | htmlhelp_basename = 'VibrationTestingToolsdoc' 212 | 213 | # -- Options for LaTeX output --------------------------------------------- 214 | 215 | latex_elements = { 216 | # The paper size ('letterpaper' or 'a4paper'). 217 | #'papersize': 'letterpaper', 218 | 219 | # The font size ('10pt', '11pt' or '12pt'). 220 | #'pointsize': '10pt', 221 | 222 | # Additional stuff for the LaTeX preamble. 223 | #'preamble': '', 224 | 225 | # Latex figure (float) alignment 226 | #'figure_align': 'htbp', 227 | } 228 | 229 | # Grouping the document tree into LaTeX files. List of tuples 230 | # (source start file, target name, title, 231 | # author, documentclass [howto, manual, or own class]). 232 | latex_documents = [ 233 | (master_doc, 'VibrationTestingTools.tex', 'Vibration Testing Tools Documentation', 234 | 'Joseph C. Slater', 'manual'), 235 | ] 236 | 237 | # The name of an image file (relative to this directory) to place at the top of 238 | # the title page. 239 | #latex_logo = None 240 | 241 | # For "manual" documents, if this is true, then toplevel headings are parts, 242 | # not chapters. 243 | #latex_use_parts = False 244 | 245 | # If true, show page references after internal links. 246 | #latex_show_pagerefs = False 247 | 248 | # If true, show URL addresses after external links. 249 | #latex_show_urls = False 250 | 251 | # Documents to append as an appendix to all manuals. 252 | #latex_appendices = [] 253 | 254 | # If false, no module index is generated. 255 | #latex_domain_indices = True 256 | 257 | 258 | # -- Options for manual page output --------------------------------------- 259 | 260 | # One entry per manual page. List of tuples 261 | # (source start file, name, description, authors, manual section). 262 | man_pages = [ 263 | (master_doc, 'vibrationtestingtools', 'Vibration Testing Tools Documentation', 264 | [author], 1) 265 | ] 266 | 267 | # If true, show URL addresses after external links. 268 | #man_show_urls = False 269 | 270 | 271 | # -- Options for Texinfo output ------------------------------------------- 272 | 273 | # Grouping the document tree into Texinfo files. List of tuples 274 | # (source start file, target name, title, author, 275 | # dir menu entry, description, category) 276 | texinfo_documents = [ 277 | (master_doc, 'VibrationTestingTools', 'Vibration Testing Tools Documentation', 278 | author, 'VibrationTestingTools', 'One line description of project.', 279 | 'Miscellaneous'), 280 | ] 281 | 282 | # Documents to append as an appendix to all manuals. 283 | #texinfo_appendices = [] 284 | 285 | # If false, no module index is generated. 286 | #texinfo_domain_indices = True 287 | 288 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 289 | #texinfo_show_urls = 'footnote' 290 | 291 | # If true, do not generate a @detailmenu in the "Top" node's menu. 292 | #texinfo_no_detailmenu = False 293 | -------------------------------------------------------------------------------- /vibrationtesting/sparsematriceshandler.py: -------------------------------------------------------------------------------- 1 | """ 2 | System expansion, reduction, and corrections functions for Sparse systems. 3 | 4 | @author: Joseph C. Slater and Sainag Immidisetty 5 | """ 6 | __license__ = "Joseph C. Slater" 7 | 8 | __docformat__ = 'reStructuredText' 9 | 10 | 11 | #import math 12 | import numpy as np 13 | #import scipy.signal as sig 14 | import scipy.linalg as la 15 | import scipy.sparse.linalg as spla 16 | from scipy.sparse import lil_matrix 17 | #from scipy.sparse import csr_matrix 18 | import scipy.sparse as sps 19 | 20 | 21 | def sos_modal_forsparse(M, K, C=False, damp_diag=0.03, shift=1): 22 | r"""Eigen analysis of proportionally damped system. 23 | 24 | Optimally find mass normalized mode shapes and natural frequencies 25 | of a system modelled by :math:`M\ddot{x}+Kx=0`. 26 | 27 | If provided, obtain damping ratios presuming :math:`C` can be decoupled. 28 | 29 | Provides a warning if diagonalization of damping matrix fails worse than 30 | relative error of `damp_diag`. 31 | 32 | Parameters 33 | ---------- 34 | M, K : float arrays 35 | Mass and stiffness matrices 36 | C : float array, optional 37 | Damping matrix 38 | damp_diag : float, optional 39 | Maximum amount of off-diagonal error allowed in assuming C can be 40 | diagonalized 41 | shift : float, optional 42 | Shift used in eigensolution. Should be approximately equal to the first 43 | non-zero eigenvalue. 44 | 45 | Returns 46 | ------- 47 | omega : float array (1xN) 48 | Vector of natural frequencies (rad/sec) 49 | zeta : float array (1xN) 50 | Vector of damping ratios 51 | Psi : float array (NxN) 52 | Matrix of mass normalized mode shapes by column 53 | 54 | 55 | Examples 56 | -------- 57 | 58 | 59 | """ 60 | 61 | #Kdiag = K.diagonal().reshape(-1,1) 62 | 63 | #Mdiag = M.diagonal().reshape(-1,1) 64 | 65 | #minvals = np.sort((Kdiag/Mdiag),axis=0) 66 | 67 | #shift = minvals[min(7,len(minvals))] 68 | 69 | #shift = shift[0] 70 | 71 | #K = ((K.tocsr() + (K.T).tocsr()).tolil()) * 0.5 + shift * ((M.tocsr() + (M.T).tocsr()).tolil()) * 0.5 72 | 73 | #M = ((M.tocsr() + (M.T).tocsr()).tolil()) * 0.5 74 | 75 | K = lil_matrix(K) 76 | 77 | M = lil_matrix(M) 78 | 79 | K = K + M * shift 80 | 81 | [lam, Psi] = la.eigh(K.toarray(), M.toarray()) 82 | 83 | lam = lam.reshape(-1,1) 84 | 85 | omega = np.sqrt(np.abs(lam - shift)) 86 | 87 | omega = omega.reshape(-1,) 88 | 89 | norms = np.diag(1.0 / np.sqrt(np.diag(Psi.T@M@Psi))) 90 | 91 | Psi = Psi @ norms 92 | 93 | zeta = np.zeros_like(omega) 94 | 95 | if C is not False: 96 | diagonalized_C = Psi.T@C@Psi 97 | 98 | diagonal_C = np.diag(diagonalized_C) 99 | 100 | if min(omega) > 1e-5: 101 | zeta = diagonal_C / 2 / omega # error if omega = 0 102 | max_off_diagonals = np.amax(np.abs(diagonalized_C 103 | - np.diag(diagonal_C)), axis=0) 104 | # error if no damping 105 | damp_error = np.max(max_off_diagonals / diagonal_C) 106 | else: 107 | zeta = np.zeros_like(omega) 108 | damp_error = np.zeros_like(omega) 109 | de_diag_C = diagonalized_C - np.diag(diagonal_C) 110 | for mode_num, omega_i in enumerate(omega): 111 | if omega[mode_num] > 1e-5: 112 | zeta[mode_num] = diagonal_C[mode_num] / 2 / omega_i 113 | damp_error = (np.max(np.abs(de_diag_C[:, mode_num])) 114 | / diagonal_C[mode_num]) 115 | 116 | if damp_error > damp_diag: 117 | print('Damping matrix cannot be completely diagonalized.') 118 | print('Off diagonal error of {:4.0%}.'.format(damp_error)) 119 | 120 | return omega, zeta, Psi 121 | 122 | 123 | def guyan_forsparse(M, K, master=None, fraction=None): 124 | r"""Guyan reduced model. 125 | 126 | Applies Guyan Reductions to second order system of equations of the form 127 | 128 | .. math:: M \ddot{x} + K x = 0 129 | 130 | which are reduced to the form 131 | 132 | .. math:: M_r \ddot{x}_m + K_r x_m = 0 133 | 134 | where 135 | 136 | .. math:: 137 | 138 | x = T x_m 139 | 140 | M_r= T^T M T 141 | 142 | K_r= T^T K T 143 | 144 | Parameters 145 | ---------- 146 | M, K : float arrays 147 | Mass and Stiffness matrices 148 | master : float array or list, optional 149 | List of retained degrees of freedom (0 indexing) 150 | fraction : float, optional 151 | Fraction of degrees of freedom (0< `fraction` <1.0) to retain in model. 152 | If both master and fraction are neglected, fraction is set to 0.25. 153 | 154 | Returns 155 | ------- 156 | Mred, Kred, T : float arrays 157 | Reduced Mass matric, Reduced Stiffness matrix, Transformation matrix 158 | master_dofs : int list 159 | List of master degrees of freedom (0 indexing) 160 | truncated_dofs : int list 161 | List of truncated degrees of freedom (0 indexing) 162 | 163 | Examples 164 | -------- 165 | 166 | Notes 167 | ----- 168 | Reduced coordinate system forces can be obtained by `Fr = T.T @ F`. 169 | 170 | Reduced damping matrix can be obtained using `Cr = T.T @ C @ T`. 171 | 172 | If mode shapes are obtained for the reduced system, full system mode shapes 173 | are `phi = T @ phi_r`. 174 | 175 | Code is not as efficient as possible. Using block submatrices would be 176 | more efficient. 177 | 178 | """ 179 | 180 | 181 | if master is None: 182 | if fraction is None: 183 | fraction = 0.25 184 | 185 | ratios = np.diag(M) / np.diag(K) 186 | ranked = [i[0] for i in sorted(enumerate(ratios), key=lambda x: x[1])] 187 | thresh = int(fraction * ratios.size) 188 | if (thresh >= ratios.size) or thresh == 0: 189 | print("Can't keep", thresh, 'DOFs.') 190 | print("Fraction of", fraction, "is too low or too high.") 191 | return 0, 0, 0, 0, 0 192 | 193 | master = ranked[-thresh:] 194 | 195 | master = np.array(master) 196 | 197 | ncoord = M.shape[0] 198 | 199 | i = np.arange(0, ncoord) 200 | 201 | i = i.reshape(1,-1) 202 | 203 | i = i + np.ones((1,i.shape[1]),int) 204 | 205 | lmaster = master.shape[1] 206 | 207 | i[0,master-1] = np.transpose(np.zeros((lmaster,1))) 208 | 209 | i = np.sort((i), axis =1) 210 | 211 | slave = i[0,lmaster + 0:ncoord] 212 | 213 | K= lil_matrix(K) 214 | 215 | slave = slave.reshape(1,-1) 216 | 217 | master = master-np.ones((1,master.shape[0]),int) 218 | 219 | master = master.ravel() 220 | 221 | slave = slave - np.ones((1,slave.shape[0]),int) 222 | 223 | slave = slave.ravel() 224 | 225 | kss = slice_forSparse(K, slave, slave) 226 | 227 | ksm = slice_forSparse(K, slave, master) 228 | 229 | T= np.zeros((len(master)+len(slave), len(master))) 230 | 231 | T= lil_matrix(T) 232 | 233 | T[master,:lmaster] = sps.eye(lmaster,lmaster) 234 | 235 | T[slave,0:lmaster]=spla.spsolve(-kss,ksm) 236 | 237 | Mred = T.T * M * T 238 | 239 | Kred = T.T * K * T 240 | 241 | return Mred, Kred, master 242 | 243 | 244 | def mode_expansion_from_model_forsparse(Psi, omega, M, K, measured): 245 | r"""Deflection extrapolation to full FEM model coordinates, matrix method. 246 | 247 | Provided an equation of the form: 248 | 249 | :math:`\begin{pmatrix}-\begin{bmatrix}M_{mm}&M_{mu}\\ M_{um}&M_{uu} 250 | \end{bmatrix} \omega_i^2 251 | +\begin{bmatrix}K_{mm}&K_{mu}\\ K_{um}&K_{uu}\end{bmatrix}\end{pmatrix}` 252 | :math:`\begin{bmatrix}\Psi_{i_m}\\ \Psi_{i_u}\end{bmatrix}= 0` 253 | 254 | Where: 255 | 256 | - :math:`M` and :math:`K` are the mass and stiffness matrices, likely from 257 | a finite element model 258 | - :math:`\Psi_i` and :math:`\omega_i` represent a mode/frequency pair 259 | - subscripts :math:`m` and :math:`u` represent measure and unmeasured 260 | of the mode 261 | 262 | Determines the unknown portion of the mode (or operational deflection) 263 | shape, :math:`\Psi_{i_u}` by 264 | direct algebraic solution, aka 265 | 266 | :math:`\Psi_{i_u} = - (K_{uu}- M_{ss} \omega_i^2) ^{-1} 267 | (K_{um}-M_{um}\omega_i^2)\Psi_{i_m}` 268 | 269 | Parameters 270 | ---------- 271 | Psi : float array 272 | mode shape or shapes, 2-D array columns of which are mode shapes 273 | omega : float or 1-D float array 274 | natural (or driving) frequencies 275 | M, K : float arrays 276 | Mass and Stiffness matrices 277 | measured : float or integer array or list 278 | List of measured degrees of freedom (0 indexed) 279 | 280 | Returns 281 | ------- 282 | Psi_full : float array 283 | Complete mode shape 284 | 285 | Examples 286 | -------- 287 | >>> import vibrationtesting as vt 288 | >>> M = np.array([[4, 0, 0], 289 | ... [0, 4, 0], 290 | ... [0, 0, 4]]) 291 | >>> K = np.array([[8, -4, 0], 292 | ... [-4, 8, -4], 293 | ... [0, -4, 4]]) 294 | >>> measured = np.array([[0, 2]]) 295 | >>> omega, zeta, Psi = vt.sos_modal(M, K) 296 | >>> Psi_measured = np.array([[-0.15], [-0.37]]) 297 | >>> Psi_full = vt.mode_expansion_from_model(Psi_measured, omega[0], M, K, 298 | ... measured) 299 | >>> print(np.hstack((Psi[:,0].reshape(-1,1), Psi_full))) 300 | [[-0.164 -0.15 ] 301 | [-0.2955 0.2886] 302 | [-0.3685 -0.37 ]] 303 | 304 | Notes 305 | ----- 306 | .. seealso:: incomplete multi-mode update. Would require each at a 307 | different frequency. 308 | 309 | """ 310 | 311 | 312 | measured = measured.reshape(-1) # retained dofs 313 | num_measured = len(measured) 314 | ndof = int(M.shape[0]) # length(M); 315 | unmeasured_dofs = list(set(np.arange(ndof)) - set(measured)) 316 | num_unmeasured = len(unmeasured_dofs) 317 | 318 | M= lil_matrix(M) 319 | 320 | K= lil_matrix(K) 321 | 322 | Muu = slice_forSparse(M, unmeasured_dofs, unmeasured_dofs) 323 | 324 | Kuu = slice_forSparse(K, unmeasured_dofs, unmeasured_dofs) 325 | 326 | Mum = slice_forSparse(M, unmeasured_dofs, measured) 327 | 328 | Kum = slice_forSparse(K, unmeasured_dofs, measured) 329 | 330 | if isinstance(omega, float): 331 | omega = np.array(omega).reshape(1) 332 | 333 | Psi_full = np.zeros((num_measured + num_unmeasured, Psi.shape[1])) 334 | Psi_full[measured] = Psi 335 | 336 | for i, omega_n in enumerate(omega): 337 | Psi_i = Psi[:, i].reshape(-1, 1) 338 | Psi_unmeasured = la.solve((Kuu - Muu * omega_n**2), 339 | (Kum - Mum * omega_n**2)@Psi_i) 340 | Psi_unmeasured = Psi_unmeasured.reshape(-1, ) 341 | Psi_full[unmeasured_dofs, i] = Psi_unmeasured 342 | # Psi_full = Psi_full.reshape(-1, 1) 343 | return Psi_full 344 | 345 | def slice_forSparse(Matrix, a, b): 346 | """ 347 | 348 | Parameters 349 | ---------- 350 | Matrix : float array 351 | Arbitrary array 352 | a, b : int lists or arrays 353 | list of rows and columns to be selected from `Matrix` 354 | 355 | Returns 356 | ------- 357 | Matrix : float array 358 | 359 | """ 360 | Maa = Matrix[a,:].toarray() 361 | 362 | Maa = Maa[:, a] 363 | 364 | Mab = Matrix[a,:].toarray() 365 | 366 | Mab = Mab[:, b] 367 | 368 | 369 | if(np.array_equiv(a,b)): 370 | return Maa 371 | 372 | else: 373 | return Mab 374 | -------------------------------------------------------------------------------- /JupyterNotebooks/Final_Solution.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 99, 6 | "metadata": { 7 | "collapsed": false 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "The autoreload extension is already loaded. To reload it, use:\n", 15 | " %reload_ext autoreload\n" 16 | ] 17 | } 18 | ], 19 | "source": [ 20 | "%load_ext autoreload\n", 21 | "%autoreload 2\n", 22 | "%matplotlib inline\n", 23 | "sp.set_printoptions(precision=3)\n", 24 | "\n", 25 | "import control as ctrl\n", 26 | "import matplotlib.pyplot as plt\n", 27 | "import scipy as sp\n", 28 | "import scipy.linalg as la\n", 29 | "import vibrationtesting as vt\n", 30 | "import numpy.linalg as lanp" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 4, 36 | "metadata": { 37 | "collapsed": false 38 | }, 39 | "outputs": [ 40 | { 41 | "name": "stdout", 42 | "output_type": "stream", 43 | "text": [ 44 | "Help on function so2ss in module vibrationtesting.signal:\n", 45 | "\n", 46 | "so2ss(M, C, K, Bt, Cd, Cv, Ca)\n", 47 | " returns A, B, C, D\n", 48 | " Given second order linear matrix equation of the form \n", 49 | " :math:`M\\ddot{x} + C \\dot{x} + K x= Bt u`\n", 50 | " and\n", 51 | " :math:`y = Cd x + + Cv \\dot{x} + Ca\\ddot{x}`\n", 52 | " returns the state space form equations\n", 53 | " :math:`\\dot{z} = A z + B u`\n", 54 | " :math:`y = C z + D u`\n", 55 | "\n" 56 | ] 57 | } 58 | ], 59 | "source": [ 60 | "help(vt.so2ss)" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 59, 66 | "metadata": { 67 | "collapsed": true 68 | }, 69 | "outputs": [], 70 | "source": [ 71 | "M = np.array([[1,0],[0,1]])\n", 72 | "C = np.array([[0.4, -.1],[-.1,.2]])\n", 73 | "K = np.array([[2,-1],[-1,2]])\n", 74 | "Cd = sp.eye(2)\n", 75 | "Cv = Cd*0\n", 76 | "Ca = Cv\n", 77 | "Bt = np.array([[1],[0]])" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": 62, 83 | "metadata": { 84 | "collapsed": false 85 | }, 86 | "outputs": [], 87 | "source": [ 88 | "A, B, Css, D = vt.so2ss(M, C, K, Bt, Cd, Cv, Ca)" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 63, 94 | "metadata": { 95 | "collapsed": false 96 | }, 97 | "outputs": [ 98 | { 99 | "name": "stdout", 100 | "output_type": "stream", 101 | "text": [ 102 | "A = \n", 103 | "[[ 0. 0. 1. 0. ]\n", 104 | " [ 0. 0. 0. 1. ]\n", 105 | " [-2. 1. -0.4 0.1]\n", 106 | " [ 1. -2. 0.1 -0.2]],\n", 107 | " B = \n", 108 | "[[ 0.]\n", 109 | " [ 0.]\n", 110 | " [ 1.]\n", 111 | " [ 0.]],\n", 112 | " C = \n", 113 | "[[ 1. 0. 0. 0.]\n", 114 | " [ 0. 1. 0. 0.]],\n", 115 | " D = \n", 116 | "[[ 0.]\n", 117 | " [ 0.]]\n" 118 | ] 119 | } 120 | ], 121 | "source": [ 122 | "print('A = \\n{},\\n B = \\n{},\\n C = \\n{},\\n D = \\n{}'.format(A, B, Css, D))" 123 | ] 124 | }, 125 | { 126 | "cell_type": "code", 127 | "execution_count": 64, 128 | "metadata": { 129 | "collapsed": true 130 | }, 131 | "outputs": [], 132 | "source": [ 133 | "e, vec = la.eig(A)" 134 | ] 135 | }, 136 | { 137 | "cell_type": "code", 138 | "execution_count": 65, 139 | "metadata": { 140 | "collapsed": false 141 | }, 142 | "outputs": [ 143 | { 144 | "name": "stdout", 145 | "output_type": "stream", 146 | "text": [ 147 | "............... Eigenvalue ........... Damping Frequency\n", 148 | "--------[re]---------[im]--------[abs]----------------------[Hz]\n", 149 | " -0.200 +1.716 1.728 0.116 0.275\n", 150 | " -0.200 -1.716 1.728 0.116 0.275\n", 151 | " -0.100 +0.998 1.003 0.100 0.160\n", 152 | " -0.100 -0.998 1.003 0.100 0.160\n" 153 | ] 154 | } 155 | ], 156 | "source": [ 157 | "vt.damp(A)" 158 | ] 159 | }, 160 | { 161 | "cell_type": "code", 162 | "execution_count": 66, 163 | "metadata": { 164 | "collapsed": false, 165 | "scrolled": true 166 | }, 167 | "outputs": [ 168 | { 169 | "name": "stdout", 170 | "output_type": "stream", 171 | "text": [ 172 | "[[-0.041-0.354j]\n", 173 | " [ 0.101+0.338j]\n", 174 | " [ 0.615+0.j ]\n", 175 | " [-0.600+0.106j]]\n" 176 | ] 177 | } 178 | ], 179 | "source": [ 180 | "print(vec[:,0:1])" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 67, 186 | "metadata": { 187 | "collapsed": false 188 | }, 189 | "outputs": [ 190 | { 191 | "name": "stdout", 192 | "output_type": "stream", 193 | "text": [ 194 | "[[-0.041-0.354j -0.041+0.354j -0.100-0.489j -0.100+0.489j]\n", 195 | " [ 0.101+0.338j 0.101-0.338j -0.050-0.497j -0.050+0.497j]\n", 196 | " [ 0.615+0.j 0.615-0.j 0.498-0.05j 0.498+0.05j ]\n", 197 | " [-0.600+0.106j -0.600-0.106j 0.501+0.j 0.501-0.j ]]\n" 198 | ] 199 | } 200 | ], 201 | "source": [ 202 | "sp.set_printoptions(precision=3)\n", 203 | "print('{}'.format(vec))" 204 | ] 205 | }, 206 | { 207 | "cell_type": "code", 208 | "execution_count": 88, 209 | "metadata": { 210 | "collapsed": true 211 | }, 212 | "outputs": [], 213 | "source": [ 214 | "Ca = np.array([[1, 0]])\n", 215 | "Cd = Ca*0\n", 216 | "Cv = Cd\n", 217 | "Bt = np.array([[1],[0]])\n", 218 | "A, B, Css, D = vt.so2ss(M, C, K, Bt, Cd, Cv, Ca)" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": 89, 224 | "metadata": { 225 | "collapsed": true 226 | }, 227 | "outputs": [], 228 | "source": [ 229 | "Ad, Bd, _, _ = vt.c2d(A, B, C, D, .1)" 230 | ] 231 | }, 232 | { 233 | "cell_type": "code", 234 | "execution_count": 90, 235 | "metadata": { 236 | "collapsed": false 237 | }, 238 | "outputs": [ 239 | { 240 | "data": { 241 | "text/plain": [ 242 | "array([[ 9.902e-01, 4.885e-03, 9.770e-02, 6.523e-04],\n", 243 | " [ 4.918e-03, 9.901e-01, 6.523e-04, 9.868e-02],\n", 244 | " [ -1.948e-01, 9.640e-02, 9.512e-01, 1.452e-02],\n", 245 | " [ 9.737e-02, -1.967e-01, 1.452e-02, 9.704e-01]])" 246 | ] 247 | }, 248 | "execution_count": 90, 249 | "metadata": {}, 250 | "output_type": "execute_result" 251 | } 252 | ], 253 | "source": [ 254 | "Ad" 255 | ] 256 | }, 257 | { 258 | "cell_type": "code", 259 | "execution_count": 91, 260 | "metadata": { 261 | "collapsed": false 262 | }, 263 | "outputs": [ 264 | { 265 | "data": { 266 | "text/plain": [ 267 | "array([[ 4.926e-03],\n", 268 | " [ 2.050e-05],\n", 269 | " [ 9.770e-02],\n", 270 | " [ 6.523e-04]])" 271 | ] 272 | }, 273 | "execution_count": 91, 274 | "metadata": {}, 275 | "output_type": "execute_result" 276 | } 277 | ], 278 | "source": [ 279 | "Bd" 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": 92, 285 | "metadata": { 286 | "collapsed": false 287 | }, 288 | "outputs": [ 289 | { 290 | "data": { 291 | "text/plain": [ 292 | "array([[-2. , 1. , -0.4, 0.1]])" 293 | ] 294 | }, 295 | "execution_count": 92, 296 | "metadata": {}, 297 | "output_type": "execute_result" 298 | } 299 | ], 300 | "source": [ 301 | "Css" 302 | ] 303 | }, 304 | { 305 | "cell_type": "code", 306 | "execution_count": 93, 307 | "metadata": { 308 | "collapsed": false 309 | }, 310 | "outputs": [ 311 | { 312 | "data": { 313 | "text/plain": [ 314 | "array([[ 1.]])" 315 | ] 316 | }, 317 | "execution_count": 93, 318 | "metadata": {}, 319 | "output_type": "execute_result" 320 | } 321 | ], 322 | "source": [ 323 | "D" 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": 94, 329 | "metadata": { 330 | "collapsed": false 331 | }, 332 | "outputs": [ 333 | { 334 | "name": "stdout", 335 | "output_type": "stream", 336 | "text": [ 337 | "Help on function ctrb in module control.statefbk:\n", 338 | "\n", 339 | "ctrb(A, B)\n", 340 | " Controllabilty matrix\n", 341 | " \n", 342 | " Parameters\n", 343 | " ----------\n", 344 | " A, B: array_like or string\n", 345 | " Dynamics and input matrix of the system\n", 346 | " \n", 347 | " Returns\n", 348 | " -------\n", 349 | " C: matrix\n", 350 | " Controllability matrix\n", 351 | " \n", 352 | " Examples\n", 353 | " --------\n", 354 | " >>> C = ctrb(A, B)\n", 355 | "\n" 356 | ] 357 | } 358 | ], 359 | "source": [ 360 | "help(ctrl.ctrb)" 361 | ] 362 | }, 363 | { 364 | "cell_type": "code", 365 | "execution_count": 95, 366 | "metadata": { 367 | "collapsed": false 368 | }, 369 | "outputs": [ 370 | { 371 | "data": { 372 | "text/plain": [ 373 | "matrix([[ 4.926e-03, 1.442e-02, 2.327e-02, 3.133e-02],\n", 374 | " [ 2.050e-05, 1.726e-04, 5.513e-04, 1.225e-03],\n", 375 | " [ 9.770e-02, 9.198e-02, 8.473e-02, 7.619e-02],\n", 376 | " [ 6.523e-04, 2.528e-03, 5.160e-03, 8.395e-03]])" 377 | ] 378 | }, 379 | "execution_count": 95, 380 | "metadata": {}, 381 | "output_type": "execute_result" 382 | } 383 | ], 384 | "source": [ 385 | "CC = ctrl.ctrb(Ad, Bd)\n", 386 | "CC" 387 | ] 388 | }, 389 | { 390 | "cell_type": "code", 391 | "execution_count": 100, 392 | "metadata": { 393 | "collapsed": false 394 | }, 395 | "outputs": [ 396 | { 397 | "data": { 398 | "text/plain": [ 399 | "4" 400 | ] 401 | }, 402 | "execution_count": 100, 403 | "metadata": {}, 404 | "output_type": "execute_result" 405 | } 406 | ], 407 | "source": [ 408 | "lanp.matrix_rank(CC)" 409 | ] 410 | }, 411 | { 412 | "cell_type": "code", 413 | "execution_count": 101, 414 | "metadata": { 415 | "collapsed": false 416 | }, 417 | "outputs": [ 418 | { 419 | "data": { 420 | "text/plain": [ 421 | "matrix([[-2. , 1. , -0.4 , 0.1 ],\n", 422 | " [-1.888, 0.922, -0.574, 0.189],\n", 423 | " [-1.735, 0.811, -0.727, 0.264],\n", 424 | " [-1.546, 0.673, -0.856, 0.325]])" 425 | ] 426 | }, 427 | "execution_count": 101, 428 | "metadata": {}, 429 | "output_type": "execute_result" 430 | } 431 | ], 432 | "source": [ 433 | "OO = ctrl.obsv(Ad, Css)\n", 434 | "OO" 435 | ] 436 | }, 437 | { 438 | "cell_type": "code", 439 | "execution_count": 102, 440 | "metadata": { 441 | "collapsed": false 442 | }, 443 | "outputs": [ 444 | { 445 | "data": { 446 | "text/plain": [ 447 | "4" 448 | ] 449 | }, 450 | "execution_count": 102, 451 | "metadata": {}, 452 | "output_type": "execute_result" 453 | } 454 | ], 455 | "source": [ 456 | "lanp.matrix_rank(OO)" 457 | ] 458 | }, 459 | { 460 | "cell_type": "code", 461 | "execution_count": null, 462 | "metadata": { 463 | "collapsed": true 464 | }, 465 | "outputs": [], 466 | "source": [] 467 | } 468 | ], 469 | "metadata": { 470 | "hide_input": false, 471 | "kernelspec": { 472 | "display_name": "Python 3", 473 | "language": "python", 474 | "name": "python3" 475 | }, 476 | "language_info": { 477 | "codemirror_mode": { 478 | "name": "ipython", 479 | "version": 3 480 | }, 481 | "file_extension": ".py", 482 | "mimetype": "text/x-python", 483 | "name": "python", 484 | "nbconvert_exporter": "python", 485 | "pygments_lexer": "ipython3", 486 | "version": "3.5.1" 487 | }, 488 | "latex_envs": { 489 | "bibliofile": "biblio.bib", 490 | "cite_by": "apalike", 491 | "current_citInitial": 1, 492 | "eqLabelWithNumbers": true, 493 | "eqNumInitial": 0 494 | } 495 | }, 496 | "nbformat": 4, 497 | "nbformat_minor": 0 498 | } 499 | -------------------------------------------------------------------------------- /JupyterNotebooks/Examples.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 3, 6 | "metadata": { 7 | "init_cell": true 8 | }, 9 | "outputs": [ 10 | { 11 | "name": "stdout", 12 | "output_type": "stream", 13 | "text": [ 14 | "The autoreload extension is already loaded. To reload it, use:\n", 15 | " %reload_ext autoreload\n" 16 | ] 17 | } 18 | ], 19 | "source": [ 20 | "%load_ext autoreload\n", 21 | "%autoreload 2\n", 22 | "%matplotlib inline\n", 23 | "import vibrationtesting as vt\n", 24 | "import matplotlib.pyplot as plt\n", 25 | "import numpy as np\n", 26 | "import array_to_latex as a2t\n", 27 | "np.set_printoptions(precision = 9, linewidth = 220, suppress = True)" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "Convert complex modes to real modes" 35 | ] 36 | }, 37 | { 38 | "cell_type": "code", 39 | "execution_count": 4, 40 | "metadata": {}, 41 | "outputs": [], 42 | "source": [ 43 | "M = np.array([[4, 0, 0],\n", 44 | " [0, 4, 0],\n", 45 | " [0, 0, 4]])\n", 46 | "Cso = np.array([[1,0,0],\n", 47 | " [0,0,0],\n", 48 | " [0,0,0]])\n", 49 | "K = np.array([[8, -4, 0],\n", 50 | " [-4, 8, -4],\n", 51 | " [0, -4, 4]])\n", 52 | "omega, _, Psi_true = vt.sos_modal(M, K)\n", 53 | "Bt = np.array([[1],[0],[0]])\n", 54 | "Ca = np.array([[1,0,0]])\n", 55 | "Cd = Cv = np.zeros_like(Ca)\n", 56 | "A, B, C, D = vt.so2ss(M, Cso, K, Bt, Cd, Cv, Ca)\n", 57 | "Am, Bm, Cm, Dm, eigenvalues, modes = vt.ss_modal(A, B, C, D)\n", 58 | "complex_modes = modes[:3,0::2]\n", 59 | "complex_modes = complex_modes@np.linalg.inv(np.diag(np.diag(complex_modes)))\n", 60 | "Psi_corrected = vt.real_modes(complex_modes)\n" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 5, 66 | "metadata": { 67 | "scrolled": true 68 | }, 69 | "outputs": [ 70 | { 71 | "data": { 72 | "text/plain": [ 73 | "array([[-0.163992639, 0.368488115, -0.295504524],\n", 74 | " [-0.295504524, 0.163992639, 0.368488115],\n", 75 | " [-0.368488115, -0.295504524, -0.163992639]])" 76 | ] 77 | }, 78 | "execution_count": 5, 79 | "metadata": {}, 80 | "output_type": "execute_result" 81 | } 82 | ], 83 | "source": [ 84 | "Psi_true" 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 6, 90 | "metadata": {}, 91 | "outputs": [ 92 | { 93 | "data": { 94 | "text/plain": [ 95 | "array([[ 1. +0.j , 2.099234839-0.684531423j, 1.678352494+0.52790572j ],\n", 96 | " [ 1.798247408+0.099411477j, 1. -0.j , -2.218125195-0.153628421j],\n", 97 | " [ 2.240934713+0.157537198j, -1.651470818+0.512991869j, 1. -0.j ]])" 98 | ] 99 | }, 100 | "execution_count": 6, 101 | "metadata": {}, 102 | "output_type": "execute_result" 103 | } 104 | ], 105 | "source": [ 106 | "complex_modes" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 7, 112 | "metadata": {}, 113 | "outputs": [ 114 | { 115 | "name": "stdout", 116 | "output_type": "stream", 117 | "text": [ 118 | "\\begin{bmatrix}\n", 119 | " 1.00000+0.00000j & 2.09923-0.68453j & 1.67835+0.52791j\\\\\n", 120 | " 1.79825+0.09941j & 1.00000-0.00000j & -2.21813-0.15363j\\\\\n", 121 | " 2.24093+0.15754j & -1.65147+0.51299j & 1.00000-0.00000j\n", 122 | "\\end{bmatrix}\n" 123 | ] 124 | } 125 | ], 126 | "source": [ 127 | "a2t.to_ltx(complex_modes, frmt = '{:3.5f}', imstring='j')" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 8, 133 | "metadata": {}, 134 | "outputs": [ 135 | { 136 | "data": { 137 | "text/plain": [ 138 | "array([[ 1. , 2.099234839, 1.678352494],\n", 139 | " [ 1.798247408, 1. , -2.218125195],\n", 140 | " [ 2.240934713, -1.651470818, 1. ]])" 141 | ] 142 | }, 143 | "execution_count": 8, 144 | "metadata": {}, 145 | "output_type": "execute_result" 146 | } 147 | ], 148 | "source": [ 149 | "Psi_real = np.real(complex_modes)\n", 150 | "Psi_real" 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 9, 156 | "metadata": {}, 157 | "outputs": [ 158 | { 159 | "name": "stdout", 160 | "output_type": "stream", 161 | "text": [ 162 | "\\begin{bmatrix}\n", 163 | " -0.1639 & 0.3684 & -0.2955\\\\\n", 164 | " -0.2955 & 0.1639 & 0.3684\\\\\n", 165 | " -0.3684 & -0.2955 & -0.1639\n", 166 | "\\end{bmatrix}\n" 167 | ] 168 | } 169 | ], 170 | "source": [ 171 | "a2t.to_ltx(Psi_true, frmt = '{:3.5f}')" 172 | ] 173 | }, 174 | { 175 | "cell_type": "code", 176 | "execution_count": 10, 177 | "metadata": {}, 178 | "outputs": [], 179 | "source": [ 180 | "?a2t.to_clp" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 11, 186 | "metadata": { 187 | "scrolled": true 188 | }, 189 | "outputs": [ 190 | { 191 | "data": { 192 | "text/plain": [ 193 | "array([[ 1. , 2.208024044, 1.759417956],\n", 194 | " [ 1.800993165, 1. , -2.223439019],\n", 195 | " [ 2.246465302, -1.729310996, 1. ]])" 196 | ] 197 | }, 198 | "execution_count": 11, 199 | "metadata": {}, 200 | "output_type": "execute_result" 201 | } 202 | ], 203 | "source": [ 204 | "Psi_abs = np.abs(complex_modes)*np.real(np.sign(complex_modes))\n", 205 | "Psi_abs" 206 | ] 207 | }, 208 | { 209 | "cell_type": "code", 210 | "execution_count": 12, 211 | "metadata": {}, 212 | "outputs": [ 213 | { 214 | "data": { 215 | "text/plain": [ 216 | "array([[ 1. , 2.208024044, 1.759417956],\n", 217 | " [ 1.80253425 , 0.974920375, -2.116079409],\n", 218 | " [ 2.229245067, -1.760956913, 0.902996771]])" 219 | ] 220 | }, 221 | "execution_count": 12, 222 | "metadata": {}, 223 | "output_type": "execute_result" 224 | } 225 | ], 226 | "source": [ 227 | "Psi_corrected" 228 | ] 229 | }, 230 | { 231 | "cell_type": "code", 232 | "execution_count": 13, 233 | "metadata": {}, 234 | "outputs": [ 235 | { 236 | "data": { 237 | "text/plain": [ 238 | "array([[0.999999338, 0.000479191, 0.000063106],\n", 239 | " [0.000000603, 0.999343589, 0.000762406],\n", 240 | " [0.000000059, 0.00017722 , 0.999174488]])" 241 | ] 242 | }, 243 | "execution_count": 13, 244 | "metadata": {}, 245 | "output_type": "execute_result" 246 | } 247 | ], 248 | "source": [ 249 | "vt.mac(Psi_true,Psi_real)" 250 | ] 251 | }, 252 | { 253 | "cell_type": "code", 254 | "execution_count": 14, 255 | "metadata": {}, 256 | "outputs": [ 257 | { 258 | "name": "stdout", 259 | "output_type": "stream", 260 | "text": [ 261 | "\\begin{bmatrix}\n", 262 | " 1.0000 & 0.0004 & 0.0000\\\\\n", 263 | " 0.0000 & 0.9993 & 0.0007\\\\\n", 264 | " 0.0000 & 0.0001 & 0.9991\n", 265 | "\\end{bmatrix}\n" 266 | ] 267 | } 268 | ], 269 | "source": [ 270 | "a2t.to_ltx(vt.mac(Psi_true,Psi_real), frmt = '{:3.5f}')" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": 15, 276 | "metadata": {}, 277 | "outputs": [ 278 | { 279 | "data": { 280 | "text/plain": [ 281 | "array([[0.99999997 , 0.000187273, 0. ],\n", 282 | " [0. , 0.999812655, 0.000061695],\n", 283 | " [0.00000003 , 0.000000072, 0.999938305]])" 284 | ] 285 | }, 286 | "execution_count": 15, 287 | "metadata": {}, 288 | "output_type": "execute_result" 289 | } 290 | ], 291 | "source": [ 292 | "vt.mac(Psi_true,Psi_abs)" 293 | ] 294 | }, 295 | { 296 | "cell_type": "code", 297 | "execution_count": 16, 298 | "metadata": {}, 299 | "outputs": [ 300 | { 301 | "name": "stdout", 302 | "output_type": "stream", 303 | "text": [ 304 | "\\begin{bmatrix}\n", 305 | " 1.0000 & 0.0001 & 0.0000\\\\\n", 306 | " 0.0000 & 0.9998 & 0.0000\\\\\n", 307 | " 0.0000 & 0.0000 & 0.9999\n", 308 | "\\end{bmatrix}\n" 309 | ] 310 | } 311 | ], 312 | "source": [ 313 | "a2t.to_ltx(vt.mac(Psi_true,Psi_abs), frmt = '{:3.5f}')" 314 | ] 315 | }, 316 | { 317 | "cell_type": "code", 318 | "execution_count": 17, 319 | "metadata": {}, 320 | "outputs": [ 321 | { 322 | "data": { 323 | "text/plain": [ 324 | "array([[0.999983388, 0.000000759, 0.000007767],\n", 325 | " [0.000012366, 0.999990366, 0.000566347],\n", 326 | " [0.000004246, 0.000008875, 0.999425887]])" 327 | ] 328 | }, 329 | "execution_count": 17, 330 | "metadata": {}, 331 | "output_type": "execute_result" 332 | } 333 | ], 334 | "source": [ 335 | "vt.mac(Psi_true,Psi_corrected)" 336 | ] 337 | }, 338 | { 339 | "cell_type": "code", 340 | "execution_count": 18, 341 | "metadata": {}, 342 | "outputs": [ 343 | { 344 | "name": "stdout", 345 | "output_type": "stream", 346 | "text": [ 347 | "\\begin{bmatrix}\n", 348 | " 0.9999 & 0.0000 & 0.0000\\\\\n", 349 | " 0.0000 & 0.9999 & 0.0005\\\\\n", 350 | " 0.0000 & 0.0000 & 0.9994\n", 351 | "\\end{bmatrix}\n" 352 | ] 353 | } 354 | ], 355 | "source": [ 356 | "a2t.to_ltx(vt.mac(Psi_true,Psi_corrected), frmt = '{:3.5f}')" 357 | ] 358 | }, 359 | { 360 | "cell_type": "code", 361 | "execution_count": 19, 362 | "metadata": {}, 363 | "outputs": [], 364 | "source": [ 365 | "Psi_real = vt.mass_normalize(Psi_real, M)" 366 | ] 367 | }, 368 | { 369 | "cell_type": "code", 370 | "execution_count": 20, 371 | "metadata": {}, 372 | "outputs": [ 373 | { 374 | "data": { 375 | "text/plain": [ 376 | "array([[ 1. , 0.022663407, -0.007723197],\n", 377 | " [ 0.022663407, 1. , -0.041083449],\n", 378 | " [-0.007723197, -0.041083449, 1. ]])" 379 | ] 380 | }, 381 | "execution_count": 20, 382 | "metadata": {}, 383 | "output_type": "execute_result" 384 | } 385 | ], 386 | "source": [ 387 | "Psi_real.T@M@Psi_real" 388 | ] 389 | }, 390 | { 391 | "cell_type": "code", 392 | "execution_count": 21, 393 | "metadata": { 394 | "scrolled": false 395 | }, 396 | "outputs": [ 397 | { 398 | "name": "stdout", 399 | "output_type": "stream", 400 | "text": [ 401 | "\\begin{bmatrix}\n", 402 | " 1.0 & 0.0 & -0.0\\\\\n", 403 | " 0.0 & 1.0 & -0.0\\\\\n", 404 | " -0.0 & -0.0 & 1.0\n", 405 | "\\end{bmatrix}\n" 406 | ] 407 | } 408 | ], 409 | "source": [ 410 | "a2t.to_ltx(Psi_real.T@M@Psi_real)" 411 | ] 412 | }, 413 | { 414 | "cell_type": "code", 415 | "execution_count": 22, 416 | "metadata": {}, 417 | "outputs": [ 418 | { 419 | "name": "stdout", 420 | "output_type": "stream", 421 | "text": [ 422 | "\\begin{bmatrix}\n", 423 | " 8.0 & -4.0 & 0.0\\\\\n", 424 | " -4.0 & 8.0 & -4.0\\\\\n", 425 | " 0.0 & -4.0 & 4.0\n", 426 | "\\end{bmatrix}\n" 427 | ] 428 | } 429 | ], 430 | "source": [ 431 | "a2t.to_ltx(K)" 432 | ] 433 | }, 434 | { 435 | "cell_type": "code", 436 | "execution_count": null, 437 | "metadata": {}, 438 | "outputs": [], 439 | "source": [] 440 | } 441 | ], 442 | "metadata": { 443 | "hide_input": false, 444 | "kernelspec": { 445 | "display_name": "Python 3", 446 | "language": "python", 447 | "name": "python3" 448 | }, 449 | "language_info": { 450 | "codemirror_mode": { 451 | "name": "ipython", 452 | "version": 3 453 | }, 454 | "file_extension": ".py", 455 | "mimetype": "text/x-python", 456 | "name": "python", 457 | "nbconvert_exporter": "python", 458 | "pygments_lexer": "ipython3", 459 | "version": "3.6.4" 460 | }, 461 | "nbTranslate": { 462 | "displayLangs": [ 463 | "*" 464 | ], 465 | "hotkey": "alt-t", 466 | "langInMainMenu": true, 467 | "sourceLang": "en", 468 | "targetLang": "fr", 469 | "useGoogleTranslate": true 470 | }, 471 | "toc": { 472 | "nav_menu": {}, 473 | "number_sections": true, 474 | "sideBar": true, 475 | "skip_h1_title": false, 476 | "title_cell": "Table of Contents", 477 | "title_sidebar": "Contents", 478 | "toc_cell": false, 479 | "toc_position": {}, 480 | "toc_section_display": true, 481 | "toc_window_display": false 482 | }, 483 | "varInspector": { 484 | "cols": { 485 | "lenName": 16, 486 | "lenType": 16, 487 | "lenVar": 40 488 | }, 489 | "kernels_config": { 490 | "python": { 491 | "delete_cmd_postfix": "", 492 | "delete_cmd_prefix": "del ", 493 | "library": "var_list.py", 494 | "varRefreshCmd": "print(var_dic_list())" 495 | }, 496 | "r": { 497 | "delete_cmd_postfix": ") ", 498 | "delete_cmd_prefix": "rm(", 499 | "library": "var_list.r", 500 | "varRefreshCmd": "cat(var_dic_list()) " 501 | } 502 | }, 503 | "types_to_exclude": [ 504 | "module", 505 | "function", 506 | "builtin_function_or_method", 507 | "instance", 508 | "_Feature" 509 | ], 510 | "window_display": false 511 | } 512 | }, 513 | "nbformat": 4, 514 | "nbformat_minor": 2 515 | } 516 | -------------------------------------------------------------------------------- /vibrationtesting/identification.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on Nov. 26, 2017 3 | @author: Joseph C. Slater 4 | """ 5 | __license__ = "Joseph C. Slater" 6 | 7 | __docformat__ = 'reStructuredText' 8 | 9 | import numpy as np 10 | import scipy.linalg as la 11 | import matplotlib.pyplot as plt 12 | 13 | ''' 14 | ====================== 15 | System Identification 16 | ====================== 17 | 18 | 19 | Array convention for data/FRFs: 20 | 21 | Frequency Response Functions 22 | ---------------------------- 23 | | 0 dimension is the output 24 | | 1 dimension is the frequency index 25 | | 2 dimension is the input 26 | 27 | Time Histories 28 | -------------- 29 | | 0 dimension is the output 30 | | 1 dimension is the time 31 | | 2 dimension is the instance index 32 | | 3 dimension is the input 33 | 34 | Spectrum Densities 35 | ------------------ 36 | | 0 dimension is the output (Cross Spectrum) or channel (Auto Spectrums) 37 | | 1 dimension is the frequency index 38 | | 2 dimension is the instance index 39 | | 3 dimension is the input (Cross Spectrum Densities) 40 | 41 | ''' 42 | 43 | 44 | def sdof_cf(f, TF, Fmin=None, Fmax=None): 45 | """Curve fit to a single degree of freedom FRF. 46 | 47 | Only one peak may exist in the segment of the FRF passed to sdofcf. No 48 | zeros may exist within this segment. If so, curve fitting becomes 49 | unreliable. 50 | 51 | If Fmin and Fmax are not entered, the first and last elements of TF are 52 | used. 53 | 54 | Parameters 55 | ---------- 56 | f: array 57 | The frequency vector in Hz. Does not have to start at 0 Hz. 58 | TF: array 59 | The complex transfer function 60 | Fmin: int 61 | The minimum frequency to be used for curve fitting in the FRF 62 | Fmax: int 63 | The maximum frequency to be used for curve fitting in the FRF 64 | 65 | Returns 66 | ------- 67 | z: double 68 | The damping ratio 69 | nf: double 70 | Natural frequency (Hz) 71 | a: double 72 | The numerator of the identified transfer functions 73 | 74 | Plot of the FRF magnitude and phase. 75 | 76 | Examples 77 | -------- 78 | >>> # First we need to load the sampled data which is in a .mat file 79 | >>> import vibrationtesting as vt 80 | >>> import scipy.io as sio 81 | >>> data = sio.loadmat(vt.__path__[0] + '/data/case1.mat') 82 | >>> #print(data) 83 | >>> # Data is imported as arrays. Modify then to fit our function. 84 | >>> TF = data['Hf_chan_2'] 85 | >>> f = data['Freq_domain'] 86 | >>> # Now we are able to call the function 87 | >>> z, nf, a = vt.sdof_cf(f,TF,500,1000) 88 | >>> nf 89 | 212.092530551... 90 | 91 | Notes 92 | ----- 93 | Author: Original Joseph C. Slater. Python version, Gabriel Loranger 94 | """ 95 | 96 | # check fmin fmax existance 97 | if Fmin is None: 98 | inlow = 0 99 | else: 100 | inlow = Fmin 101 | 102 | if Fmax is None: 103 | inhigh = np.size(f) 104 | else: 105 | inhigh = Fmax 106 | 107 | if f[inlow] == 0: 108 | inlow = 1 109 | 110 | f = f[inlow:inhigh, :] 111 | TF = TF[inlow:inhigh, :] 112 | 113 | R = TF 114 | y = np.amax(np.abs(TF)) 115 | cin = np.argmax(np.abs(TF)) 116 | 117 | ll = np.size(f) 118 | 119 | w = f * 2 * np.pi * 1j 120 | 121 | w2 = w * 0 122 | R3 = R * 0 123 | 124 | for i in range(1, ll + 1): 125 | R3[i - 1] = np.conj(R[ll - i]) 126 | w2[i - 1] = np.conj(w[ll - i]) 127 | 128 | w = np.vstack((w2, w)) 129 | R = np.vstack((R3, R)) 130 | 131 | N = 2 132 | x, y = np.meshgrid(np.arange(0, N + 1), R) 133 | x, w2d = np.meshgrid(np.arange(0, N + 1), w) 134 | c = -1 * w**N * R 135 | 136 | aa1 = w2d[:, np.arange(0, N)] \ 137 | ** x[:, np.arange(0, N)] \ 138 | * y[:, np.arange(0, N)] 139 | aa2 = -w2d[:, np.arange(0, N + 1)] \ 140 | ** x[:, np.arange(0, N + 1)] 141 | aa = np.hstack((aa1, aa2)) 142 | 143 | aa = np.reshape(aa, [-1, 5]) 144 | 145 | b, _, _, _ = la.lstsq(aa, c) 146 | 147 | rs = np.roots(np.array([1, 148 | b[1], 149 | b[0]])) 150 | omega = np.abs(rs[1]) 151 | z = -1 * np.real(rs[1]) / np.abs(rs[1]) 152 | nf = omega / 2 / np.pi 153 | 154 | XoF1 = np.hstack(([1 / (w - rs[0]), 1 / (w - rs[1])])) 155 | XoF2 = 1 / (w**0) 156 | XoF3 = 1 / w**2 157 | XoF = np.hstack((XoF1, XoF2, XoF3)) 158 | 159 | # check if extra _ needed 160 | 161 | a, _, _, _ = la.lstsq(XoF, R) 162 | XoF = XoF[np.arange(ll, 2 * ll), :].dot(a) 163 | 164 | a = np.sqrt(-2 * np.imag(a[0]) * np.imag(rs[0]) - 165 | 2 * np.real(a[0]) * np.real(rs[0])) 166 | Fmin = np.min(f) 167 | Fmax = np.max(f) 168 | phase = np.unwrap(np.angle(TF), np.pi, 0) * 180 / np.pi 169 | phase2 = np.unwrap(np.angle(XoF), np.pi, 0) * 180 / np.pi 170 | while phase2[cin] > 50: 171 | phase2 = phase2 - 360 172 | phased = phase2[cin] - phase[cin] 173 | phase = phase + np.round(phased / 360) * 360 174 | 175 | fig = plt.figure() 176 | ax1 = fig.add_subplot(2, 1, 1) 177 | ax2 = fig.add_subplot(2, 1, 2) 178 | fig.tight_layout() 179 | 180 | ax1.set_xlabel('Frequency (Hz)') 181 | ax1.set_ylabel('Magnitude (dB)') 182 | ax1.plot(f, 20 * np.log10(np.abs(XoF)), label="Identified FRF") 183 | ax1.plot(f, 20 * np.log10(np.abs(TF)), label="Experimental FRF") 184 | ax1.legend() 185 | 186 | ax2.set_xlabel('Frequency (Hz)') 187 | ax2.set_ylabel('Phase (deg)') 188 | ax2.plot(f, phase2, label="Identified FRF") 189 | ax2.plot(f, phase, label="Experimental FRF") 190 | ax2.legend() 191 | 192 | plt.show() 193 | 194 | a = a[0]**2 / (2 * np.pi * nf)**2 195 | return z, nf, a 196 | 197 | 198 | def mdof_cf(f, TF, Fmin=None, Fmax=None): 199 | """ 200 | Curve fit to multiple degree of freedom FRF 201 | 202 | If Fmin and Fmax are not entered, the first and last elements of TF are 203 | used. 204 | 205 | If the first column of TF is a collocated (input and output location are 206 | the same), then the mode shape returned is the mass normalized mode shape. 207 | This can then be used to generate an identified mass, damping, and 208 | stiffness matrix as shown in the following example. 209 | 210 | Parameters 211 | ---------- 212 | f: array 213 | The frequency vector in Hz. Does not have to start at 0 Hz. 214 | TF: array 215 | The complex transfer function 216 | Fmin: int 217 | The minimum frequency to be used for curve fitting in the FRF 218 | Fmax: int 219 | The maximum frequency to be used for curve fitting in the FRF 220 | 221 | Returns 222 | ------- 223 | z: double 224 | The damping ratio 225 | nf: double 226 | Natural frequency (Hz) 227 | u: array 228 | The mode shape 229 | 230 | Notes 231 | ----- 232 | FRF are columns comprised of the FRFs presuming single input, multiple 233 | output z and nf are the damping ratio and natural frequency (Hz) u is the 234 | mode shape. Only one peak may exist in the segment of the FRF passed to 235 | sdofcf. No zeros may exist within this segment. If so, curve fitting 236 | becomes unreliable. 237 | 238 | Author: Original Joseph C. Slater. Python version, Gabriel Loranger 239 | 240 | Examples 241 | -------- 242 | >>> # First we need to load the sampled data which is in a .mat file 243 | >>> import vibrationtesting as vt 244 | >>> import scipy.io as sio 245 | >>> data = sio.loadmat(vt.__path__[0] + '/data/case2.mat') 246 | >>> #print(data) 247 | >>> # Data is imported as arrays. Modify then to fit our function 248 | >>> TF = data['Hf_chan_2'] 249 | >>> f = data['Freq_domain'] 250 | >>> # Now we are able to call the function 251 | >>> z, nf, a = vt.mdof_cf(f,TF,500,1000) 252 | >>> nf 253 | 192.59382330... 254 | """ 255 | 256 | # check fmin fmax existance 257 | if Fmin is None: 258 | inlow = 0 259 | else: 260 | inlow = Fmin 261 | 262 | if Fmax is None: 263 | inhigh = np.size(f) 264 | else: 265 | inhigh = Fmax 266 | 267 | if f[inlow] == 0: 268 | inlow = 1 269 | 270 | f = f[inlow:inhigh, :] 271 | TF = TF[inlow:inhigh, :] 272 | 273 | R = TF.T 274 | 275 | U, _, _ = np.linalg.svd(R) 276 | T = U[:, 0] 277 | Hp = np.transpose(T).dot(R) 278 | R = np.transpose(Hp) 279 | 280 | ll = np.size(f) 281 | w = f * 2 * np.pi * 1j 282 | 283 | w2 = w * 0 284 | R3 = R * 0 285 | TF2 = TF * 0 286 | for i in range(1, ll + 1): 287 | R3[i - 1] = np.conj(R[ll - i]) 288 | w2[i - 1] = np.conj(w[ll - i]) 289 | TF2[i - 1, :] = np.conj(TF[ll - i, :]) 290 | 291 | w = np.vstack((w2, w)) 292 | R = np.hstack((R3, R)) 293 | 294 | N = 2 295 | x, y = np.meshgrid(np.arange(0, N + 1), R) 296 | 297 | x, w2d = np.meshgrid(np.arange(0, N + 1), w) 298 | 299 | R = np.ndarray.flatten(R) 300 | w = np.ndarray.flatten(w) 301 | c = -1 * w**N * R 302 | 303 | aa1 = w2d[:, np.arange(0, N)] \ 304 | ** x[:, np.arange(0, N)] \ 305 | * y[:, np.arange(0, N)] 306 | aa2 = -w2d[:, np.arange(0, N + 1)] \ 307 | ** x[:, np.arange(0, N + 1)] 308 | aa = np.hstack((aa1, aa2)) 309 | 310 | b, _, _, _ = la.lstsq(aa, c) 311 | 312 | rs = np.roots(np.array([1, 313 | b[1], 314 | b[0]])) 315 | 316 | omega = np.abs(rs[1]) 317 | z = -1 * np.real(rs[1]) / np.abs(rs[1]) 318 | nf = omega / 2 / np.pi 319 | 320 | XoF1 = 1 / ((rs[0] - w) * (rs[1] - w)) 321 | 322 | XoF2 = 1 / (w**0) 323 | XoF3 = 1 / w**2 324 | 325 | XoF = np.vstack((XoF1, XoF2, XoF3)).T 326 | TF3 = np.vstack((TF2, TF)) 327 | 328 | a, _, _, _ = la.lstsq(XoF, TF3) 329 | 330 | u = np.transpose(a[0, :]) 331 | 332 | u = u / np.sqrt(np.abs(a[0, 0])) 333 | 334 | return z, nf, u 335 | 336 | 337 | def cmif(freq, H, freq_min=None, freq_max=None, plot=True): 338 | """Complex mode indicator function. 339 | 340 | Plots the complex mode indicator function 341 | 342 | Parameters 343 | ---------- 344 | freq : array 345 | The frequency vector in Hz. Does not have to start at 0 Hz. 346 | H : array 347 | The complex frequency response function 348 | freq_min : float, optional 349 | The minimum frequency to be plotted 350 | freq_max : float, optional 351 | The maximum frequency to be plotted 352 | plot : boolean, optional (True) 353 | Whether to also plot mode indicator functions 354 | 355 | Returns 356 | ------- 357 | cmifs : complex mode indicator functions 358 | 359 | Examples 360 | -------- 361 | >>> import vibrationtesting as vt 362 | >>> M = np.diag([1,1,1]) 363 | >>> K = K = np.array([[3.03, -1, -1],[-1, 2.98, -1],[-1, -1, 3]]) 364 | >>> Damping = K/100 365 | >>> Cd = np.eye(3) 366 | >>> Cv = Ca = np.zeros_like(Cd) 367 | >>> Bt = np.eye(3) 368 | >>> H_all = np.zeros((3,1000,3), dtype = 'complex128') 369 | >>> for i in np.arange(1, 4): 370 | ... for j in np.arange(1, 4): 371 | ... omega, H_all[i-1,:,j-1] = vt.sos_frf(M, Damping/10, K, Bt, 372 | ... Cd, Cv, Ca, 0, 3, i, 373 | ... j, num_freqs = 1000) 374 | >>> _ = vt.cmif(omega, H_all) 375 | 376 | Notes 377 | ----- 378 | .. note:: Allemang, R. and Brown, D., “A Complete Review of the Complex 379 | Mode Indicator Function (CMIF) With Applications,” Proceedings of ISMA 380 | International Conference on Noise and Vibration Engineering, Katholieke 381 | Universiteit Leuven, Belgium, 2006. 382 | 383 | """ 384 | if freq_max is None: 385 | freq_max = np.max(freq) 386 | # print(str(freq_max)) 387 | 388 | if freq_min is None: 389 | freq_min = 0 390 | 391 | if freq_min < np.min(freq): 392 | freq_min = np.min(freq) 393 | 394 | if freq_min > freq_max: 395 | raise ValueError('freq_min must be less than freq_max.') 396 | 397 | # print(str(np.amin(freq))) 398 | # lenF = freq.shape[1] 399 | 400 | cmifs = np.zeros((max([H.shape[0], H.shape[2]]), max(freq.shape))) 401 | for i, freq_i in enumerate(freq.reshape(-1)): 402 | _, vals, _ = np.linalg.svd(H[:, i, :]) 403 | cmifs[:, i] = np.sort(vals).T 404 | 405 | if plot is True: 406 | fig, ax = plt.subplots(1, 1) 407 | ax.plot(freq.T, np.log10(cmifs.T)) 408 | ax.grid('on') 409 | ax.set_ylabel('Maginitudes') 410 | ax.set_title('Complex Mode Indicator Functions') 411 | ax.set_xlabel('Frequency') 412 | ax.set_xlim(xmax=freq_max, xmin=freq_min) 413 | return cmifs 414 | 415 | 416 | def mac(Psi_1, Psi_2): 417 | """Modal Assurance Criterion. 418 | 419 | Parameters 420 | ---------- 421 | Psi_1, Psi_2 : float arrays 422 | Mode shape matrices to be compared. 423 | 424 | Returns 425 | ------- 426 | mac : float array 427 | 428 | Examples 429 | -------- 430 | 431 | """ 432 | nummodes = Psi_1.shape[1] 433 | MAC = np.zeros((nummodes, nummodes)) 434 | if Psi_1.shape == Psi_2.shape: 435 | for i in np.arange(nummodes): 436 | for j in np.arange(nummodes): 437 | MAC[i, j] = (abs(np.conj(Psi_1[:, i]) @ Psi_2[:, j])**2 / 438 | (np.conj(Psi_1[:, i]) @ Psi_1[:, i] * 439 | np.conj(Psi_2[:, j]) @ Psi_2[:, j])) 440 | else: 441 | print('Mode shape arrays must have the same size.') 442 | return MAC 443 | 444 | 445 | def comac(Psi_1, Psi_2, dof): 446 | """Co-modal assurance criterion 447 | 448 | """ 449 | comac = 1 450 | return comac 451 | 452 | 453 | def mass_normalize(Psi, M): 454 | """Mass normalize mode shapes. 455 | 456 | Parameters 457 | ---------- 458 | Psi : float array 459 | 1-dimensional (single mode) or 2-dimensional array of mode shapes 460 | 461 | Returns 462 | ------- 463 | Psi : float array 464 | 2-dimensional array of mass normalized mode shapes 465 | 466 | """ 467 | if len(Psi.shape) is 1: 468 | Psi = Psi.reshape((-1, 1)) 469 | 470 | for i in np.arange(Psi.shape[1]): 471 | alpha_sqr = Psi[:, i].T @ M @ Psi[:, i] 472 | Psi[:, i] = Psi[:, i] / np.sqrt(alpha_sqr) 473 | 474 | return Psi 475 | -------------------------------------------------------------------------------- /docs/tutorial/Notebooks/Ho-Kalman-Demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Ho-Kalman Identification Example" 8 | ] 9 | }, 10 | { 11 | "cell_type": "code", 12 | "execution_count": 1, 13 | "metadata": { 14 | "init_cell": true 15 | }, 16 | "outputs": [], 17 | "source": [ 18 | "%load_ext autoreload\n", 19 | "%autoreload 2\n", 20 | "%matplotlib inline\n", 21 | "\n", 22 | "import control as ctrl\n", 23 | "import matplotlib.pyplot as plt\n", 24 | "import scipy as sp\n", 25 | "import scipy.linalg as la\n", 26 | "import vibrationtesting as vt\n", 27 | "import scipy.signal as sig" 28 | ] 29 | }, 30 | { 31 | "cell_type": "markdown", 32 | "metadata": {}, 33 | "source": [ 34 | "Considers a system \n", 35 | "\n", 36 | "$$\\ddot{x} + 2 \\dot{x} + 100 x = f(t)$$\n", 37 | "with a displacement sensor. \n", 38 | "\n", 39 | "The state space representation is \n", 40 | "\n", 41 | "$$\\dot{\\mathbf{z}} = A \\mathbf{z} + B u$$\n", 42 | "\n", 43 | "$$y = C \\mathbf{z} + D u$$ \n", 44 | "\n", 45 | "where \n", 46 | "\n", 47 | "$$A = \n", 48 | "\\begin{bmatrix}\n", 49 | "0&1\\\\\n", 50 | "-100&-2\n", 51 | "\\end{bmatrix}\n", 52 | ",\\quad\n", 53 | "B = \n", 54 | "\\begin{bmatrix}0\\\\1\\end{bmatrix}\n", 55 | ",\\quad\n", 56 | "C = \\begin{bmatrix}1&0\\end{bmatrix}\n", 57 | ", \\text{ and }\n", 58 | "D = [0]$$" 59 | ] 60 | }, 61 | { 62 | "cell_type": "code", 63 | "execution_count": 2, 64 | "metadata": {}, 65 | "outputs": [], 66 | "source": [ 67 | "sample_freq = 1e3\n", 68 | "A = np.array([[0, 1],\\\n", 69 | " [-100, 2]])\n", 70 | "B = np.array([[0], [1]])\n", 71 | "C = np.array([[1, 0]])\n", 72 | "D = np.array([[0]])\n", 73 | "sys = ctrl.ss(A, B, C, D)\n" 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "For this example, we will generate an impulse response for only 4 discrete times." 81 | ] 82 | }, 83 | { 84 | "cell_type": "code", 85 | "execution_count": 3, 86 | "metadata": {}, 87 | "outputs": [ 88 | { 89 | "data": { 90 | "text/plain": [ 91 | "array([ 0. , 0.16666667, 0.33333333, 0.5 , 0.66666667,\n", 92 | " 0.83333333])" 93 | ] 94 | }, 95 | "execution_count": 3, 96 | "metadata": {}, 97 | "output_type": "execute_result" 98 | } 99 | ], 100 | "source": [ 101 | "t = np.linspace(0, 1, num = 6, endpoint = False)\n", 102 | "dt = t[1]\n", 103 | "t" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "The impulse response for a second order underdamped system is known to be\n", 111 | "\n", 112 | "$$h(t) = \\frac{1}{\\omega_d}e^{-\\zeta \\omega_n t}\\sin\\left(\\omega_d t\\right)$$" 113 | ] 114 | }, 115 | { 116 | "cell_type": "code", 117 | "execution_count": 4, 118 | "metadata": {}, 119 | "outputs": [], 120 | "source": [ 121 | "omega_n = sp.sqrt(100)\n", 122 | "zeta = 2/(2*omega_n)\n", 123 | "omega_d = omega_n * sp.sqrt(1 - zeta**2)" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 5, 129 | "metadata": {}, 130 | "outputs": [ 131 | { 132 | "data": { 133 | "text/plain": [ 134 | "array([ 0. , 0.08474903, -0.01254052, -0.05886968, 0.01769677,\n", 135 | " 0.03956333])" 136 | ] 137 | }, 138 | "execution_count": 5, 139 | "metadata": {}, 140 | "output_type": "execute_result" 141 | } 142 | ], 143 | "source": [ 144 | "h = 1/omega_d * sp.exp(-zeta*omega_n*t)*sp.sin(omega_d*t)\n", 145 | "h" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": 6, 151 | "metadata": {}, 152 | "outputs": [ 153 | { 154 | "data": { 155 | "image/png": "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\n", 156 | "text/plain": [ 157 | "" 158 | ] 159 | }, 160 | "metadata": {}, 161 | "output_type": "display_data" 162 | } 163 | ], 164 | "source": [ 165 | "plt.plot(t,h,'.')\n", 166 | "plt.axis([0,1,-0.1,0.1])\n", 167 | "plt.grid()" 168 | ] 169 | }, 170 | { 171 | "cell_type": "code", 172 | "execution_count": 6, 173 | "metadata": {}, 174 | "outputs": [ 175 | { 176 | "data": { 177 | "text/plain": [ 178 | "array([[ 0. , 0.08474903],\n", 179 | " [ 0.08474903, -0.01254052],\n", 180 | " [-0.01254052, -0.05886968],\n", 181 | " [-0.05886968, 0.01769677]])" 182 | ] 183 | }, 184 | "execution_count": 6, 185 | "metadata": {}, 186 | "output_type": "execute_result" 187 | } 188 | ], 189 | "source": [ 190 | "Hankel_0 = sp.vstack((h[0:-2],h[1:-1]))\n", 191 | "Hankel_0 = Hankel_0.T\n", 192 | "Hankel_0" 193 | ] 194 | }, 195 | { 196 | "cell_type": "code", 197 | "execution_count": 7, 198 | "metadata": {}, 199 | "outputs": [ 200 | { 201 | "data": { 202 | "text/plain": [ 203 | "array([ 0.08474903, -0.01254052, -0.05886968, 0.01769677])" 204 | ] 205 | }, 206 | "execution_count": 7, 207 | "metadata": {}, 208 | "output_type": "execute_result" 209 | } 210 | ], 211 | "source": [ 212 | "h[1:-1]" 213 | ] 214 | }, 215 | { 216 | "cell_type": "code", 217 | "execution_count": 8, 218 | "metadata": {}, 219 | "outputs": [ 220 | { 221 | "data": { 222 | "text/plain": [ 223 | "array([-0.01254052, -0.05886968, 0.01769677, 0.03956333])" 224 | ] 225 | }, 226 | "execution_count": 8, 227 | "metadata": {}, 228 | "output_type": "execute_result" 229 | } 230 | ], 231 | "source": [ 232 | "h[2:]" 233 | ] 234 | }, 235 | { 236 | "cell_type": "code", 237 | "execution_count": 9, 238 | "metadata": { 239 | "scrolled": true 240 | }, 241 | "outputs": [ 242 | { 243 | "data": { 244 | "text/plain": [ 245 | "array([[ 0.08474903, -0.01254052],\n", 246 | " [-0.01254052, -0.05886968],\n", 247 | " [-0.05886968, 0.01769677],\n", 248 | " [ 0.01769677, 0.03956333]])" 249 | ] 250 | }, 251 | "execution_count": 9, 252 | "metadata": {}, 253 | "output_type": "execute_result" 254 | } 255 | ], 256 | "source": [ 257 | "Hankel_1 = sp.vstack((h[1:-1],h[2:]))\n", 258 | "Hankel_1 = Hankel_1.T\n", 259 | "Hankel_1" 260 | ] 261 | }, 262 | { 263 | "cell_type": "code", 264 | "execution_count": 10, 265 | "metadata": {}, 266 | "outputs": [ 267 | { 268 | "name": "stdout", 269 | "output_type": "stream", 270 | "text": [ 271 | "[[ 0.56941225 0.57615451]\n", 272 | " [-0.59214005 0.56070011]\n", 273 | " [-0.32038129 -0.49580091]\n", 274 | " [ 0.47169449 -0.32839431]]\n", 275 | "[[-0.66563569 0.74627684]\n", 276 | " [ 0.74627684 0.66563569]]\n" 277 | ] 278 | } 279 | ], 280 | "source": [ 281 | "U, sig, Vt = la.svd(Hankel_0)\n", 282 | "V = Vt.T\n", 283 | "U = U[:,:2]\n", 284 | "print(U)\n", 285 | "V = V[:,:2]\n", 286 | "print(V)" 287 | ] 288 | }, 289 | { 290 | "cell_type": "code", 291 | "execution_count": 11, 292 | "metadata": {}, 293 | "outputs": [ 294 | { 295 | "name": "stdout", 296 | "output_type": "stream", 297 | "text": [ 298 | "[[ 0.11107284 0. ]\n", 299 | " [ 0. 0.0979112 ]]\n" 300 | ] 301 | } 302 | ], 303 | "source": [ 304 | "sig = sp.diag(sig)\n", 305 | "print(sig)" 306 | ] 307 | }, 308 | { 309 | "cell_type": "code", 310 | "execution_count": 12, 311 | "metadata": {}, 312 | "outputs": [ 313 | { 314 | "name": "stdout", 315 | "output_type": "stream", 316 | "text": [ 317 | "[[-0.22322281 0.85303151]\n", 318 | " [-0.85967388 0.07525037]]\n" 319 | ] 320 | } 321 | ], 322 | "source": [ 323 | "A_d = la.inv(sp.sqrt(sig))@U.T@Hankel_1@V@la.inv(sp.sqrt(sig))\n", 324 | "print(A_d)" 325 | ] 326 | }, 327 | { 328 | "cell_type": "code", 329 | "execution_count": 13, 330 | "metadata": {}, 331 | "outputs": [ 332 | { 333 | "name": "stdout", 334 | "output_type": "stream", 335 | "text": [ 336 | "[-0.07398622+0.84324217j -0.07398622-0.84324217j]\n", 337 | "[[ 0.12298924-0.69493489j 0.12298924+0.69493489j]\n", 338 | " [ 0.70847664+0.j 0.70847664-0.j ]]\n" 339 | ] 340 | } 341 | ], 342 | "source": [ 343 | "lam_d, vec = la.eig(A_d)\n", 344 | "print(lam_d)\n", 345 | "print(vec)" 346 | ] 347 | }, 348 | { 349 | "cell_type": "code", 350 | "execution_count": 14, 351 | "metadata": {}, 352 | "outputs": [ 353 | { 354 | "name": "stdout", 355 | "output_type": "stream", 356 | "text": [ 357 | "[[-0.22322281 0.85303151]\n", 358 | " [-0.85967388 0.07525037]]\n", 359 | "[[-0.22322281 +0.00000000e+00j 0.85303151 +2.77555756e-17j]\n", 360 | " [-0.85967388 -6.93889390e-18j 0.07525037 +0.00000000e+00j]]\n" 361 | ] 362 | } 363 | ], 364 | "source": [ 365 | "# This should be the same as A_d\n", 366 | "print(A_d)\n", 367 | "print(vec@sp.diag(lam_d)@la.inv(vec))" 368 | ] 369 | }, 370 | { 371 | "cell_type": "code", 372 | "execution_count": 15, 373 | "metadata": {}, 374 | "outputs": [ 375 | { 376 | "data": { 377 | "text/plain": [ 378 | "array([-1.+9.94987437j, -1.-9.94987437j])" 379 | ] 380 | }, 381 | "execution_count": 15, 382 | "metadata": {}, 383 | "output_type": "execute_result" 384 | } 385 | ], 386 | "source": [ 387 | "lam = sp.log(lam_d)/dt\n", 388 | "lam" 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": 16, 394 | "metadata": {}, 395 | "outputs": [ 396 | { 397 | "name": "stdout", 398 | "output_type": "stream", 399 | "text": [ 400 | "The undamped natural frequency is 10.0 rad/sec.\n", 401 | "The damping ratio is 0.09999999999999998.\n" 402 | ] 403 | } 404 | ], 405 | "source": [ 406 | "# These are the continuous time eigenvalues\n", 407 | "print('The undamped natural frequency is {} rad/sec.'.format(abs(lam[0])))\n", 408 | "print('The damping ratio is {}.'.format(-sp.real(lam[0])/abs(lam[0])))\n" 409 | ] 410 | }, 411 | { 412 | "cell_type": "markdown", 413 | "metadata": {}, 414 | "source": [ 415 | "The identified state matrix is" 416 | ] 417 | }, 418 | { 419 | "cell_type": "code", 420 | "execution_count": 17, 421 | "metadata": {}, 422 | "outputs": [ 423 | { 424 | "name": "stdout", 425 | "output_type": "stream", 426 | "text": [ 427 | "[[ -2.76092394 10.06538424]\n", 428 | " [-10.1437611 0.76092394]]\n" 429 | ] 430 | } 431 | ], 432 | "source": [ 433 | "A = la.logm(A_d)/dt\n", 434 | "print(A)" 435 | ] 436 | }, 437 | { 438 | "cell_type": "markdown", 439 | "metadata": {}, 440 | "source": [ 441 | "which is the system the result in a balanced realization form. " 442 | ] 443 | }, 444 | { 445 | "cell_type": "code", 446 | "execution_count": 18, 447 | "metadata": {}, 448 | "outputs": [ 449 | { 450 | "name": "stdout", 451 | "output_type": "stream", 452 | "text": [ 453 | "[[-0.22184035]\n", 454 | " [ 0.23351573]]\n" 455 | ] 456 | } 457 | ], 458 | "source": [ 459 | "# The discrete input matrix is\n", 460 | "B_d = sp.sqrt(sig)@V.T[:,0].T.reshape((2,1))\n", 461 | "print(B_d)" 462 | ] 463 | }, 464 | { 465 | "cell_type": "code", 466 | "execution_count": 19, 467 | "metadata": {}, 468 | "outputs": [ 469 | { 470 | "name": "stdout", 471 | "output_type": "stream", 472 | "text": [ 473 | "[[-2.58036274]\n", 474 | " [-0.22677789]]\n" 475 | ] 476 | } 477 | ], 478 | "source": [ 479 | "# The continuous input matrix is\n", 480 | "B = la.solve((A_d - sp.eye(2)), A) @ B_d\n", 481 | "print(B)" 482 | ] 483 | }, 484 | { 485 | "cell_type": "code", 486 | "execution_count": 20, 487 | "metadata": {}, 488 | "outputs": [ 489 | { 490 | "name": "stdout", 491 | "output_type": "stream", 492 | "text": [ 493 | "[ 0.18977139 0.18028315]\n" 494 | ] 495 | } 496 | ], 497 | "source": [ 498 | "C = (U @ sp.sqrt(sig))[0,:]\n", 499 | "print(C)" 500 | ] 501 | }, 502 | { 503 | "cell_type": "code", 504 | "execution_count": 21, 505 | "metadata": {}, 506 | "outputs": [ 507 | { 508 | "data": { 509 | "text/plain": [ 510 | "0.0" 511 | ] 512 | }, 513 | "execution_count": 21, 514 | "metadata": {}, 515 | "output_type": "execute_result" 516 | } 517 | ], 518 | "source": [ 519 | "# Of course, D is\n", 520 | "D = h[0]\n", 521 | "D" 522 | ] 523 | }, 524 | { 525 | "cell_type": "code", 526 | "execution_count": null, 527 | "metadata": {}, 528 | "outputs": [], 529 | "source": [] 530 | } 531 | ], 532 | "metadata": { 533 | "hide_input": false, 534 | "kernelspec": { 535 | "display_name": "Python 3", 536 | "language": "python", 537 | "name": "python3" 538 | }, 539 | "language_info": { 540 | "codemirror_mode": { 541 | "name": "ipython", 542 | "version": 3 543 | }, 544 | "file_extension": ".py", 545 | "mimetype": "text/x-python", 546 | "name": "python", 547 | "nbconvert_exporter": "python", 548 | "pygments_lexer": "ipython3", 549 | "version": "3.6.3" 550 | }, 551 | "latex_envs": { 552 | "bibliofile": "biblio.bib", 553 | "cite_by": "apalike", 554 | "current_citInitial": 1, 555 | "eqLabelWithNumbers": true, 556 | "eqNumInitial": 0 557 | }, 558 | "toc": { 559 | "nav_menu": {}, 560 | "number_sections": true, 561 | "sideBar": true, 562 | "skip_h1_title": false, 563 | "toc_cell": false, 564 | "toc_position": {}, 565 | "toc_section_display": "block", 566 | "toc_window_display": false 567 | }, 568 | "varInspector": { 569 | "cols": { 570 | "lenName": 16, 571 | "lenType": 16, 572 | "lenVar": 40 573 | }, 574 | "kernels_config": { 575 | "python": { 576 | "delete_cmd_postfix": "", 577 | "delete_cmd_prefix": "del ", 578 | "library": "var_list.py", 579 | "varRefreshCmd": "print(var_dic_list())" 580 | }, 581 | "r": { 582 | "delete_cmd_postfix": ") ", 583 | "delete_cmd_prefix": "rm(", 584 | "library": "var_list.r", 585 | "varRefreshCmd": "cat(var_dic_list()) " 586 | } 587 | }, 588 | "types_to_exclude": [ 589 | "module", 590 | "function", 591 | "builtin_function_or_method", 592 | "instance", 593 | "_Feature" 594 | ], 595 | "window_display": false 596 | } 597 | }, 598 | "nbformat": 4, 599 | "nbformat_minor": 1 600 | } 601 | -------------------------------------------------------------------------------- /JupyterNotebooks/Ho-Kalman-Demo.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "metadata": { 7 | "init_cell": true 8 | }, 9 | "outputs": [], 10 | "source": [ 11 | "%load_ext autoreload\n", 12 | "%autoreload 2\n", 13 | "%matplotlib inline\n", 14 | "\n", 15 | "import control as ctrl\n", 16 | "import matplotlib.pyplot as plt\n", 17 | "import numpy as np\n", 18 | "\n", 19 | "import scipy.linalg as la\n", 20 | "import vibrationtesting as vt\n", 21 | "import scipy.signal as sig" 22 | ] 23 | }, 24 | { 25 | "cell_type": "markdown", 26 | "metadata": {}, 27 | "source": [ 28 | "Considers a system \n", 29 | "\n", 30 | "$$\\ddot{x} + 2 \\dot{x} + 100 x = f(t)$$\n", 31 | "with a displacement sensor. \n", 32 | "\n", 33 | "The state space representation is \n", 34 | "\n", 35 | "$$\\dot{\\mathbf{z}} = A \\mathbf{z} + B \\mathbf{u}$$\n", 36 | "\n", 37 | "$$\\mathbf{y} = C \\mathbf{z} + D \\mathbf{u}$$ \n", 38 | "\n", 39 | "where \n", 40 | "\n", 41 | "$$A = \n", 42 | "\\begin{bmatrix}\n", 43 | "0&1\\\\\n", 44 | "-100&-2\n", 45 | "\\end{bmatrix}\n", 46 | ",\\quad\n", 47 | "B = \n", 48 | "\\begin{bmatrix}0\\\\1\\end{bmatrix}\n", 49 | ",\\quad\n", 50 | "C = \\begin{bmatrix}1&0\\end{bmatrix}\n", 51 | ", \\text{ and }\n", 52 | "D = [0]$$" 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 3, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "sample_freq = 1e3\n", 62 | "A = np.array([[0, 1],\\\n", 63 | " [-100, -2]])\n", 64 | "B = np.array([[0], [1]])\n", 65 | "C = np.array([[1, 0]])\n", 66 | "D = np.array([[0]])\n", 67 | "sys = ctrl.ss(A, B, C, D)\n" 68 | ] 69 | }, 70 | { 71 | "cell_type": "markdown", 72 | "metadata": {}, 73 | "source": [ 74 | "For this example, we will generate an impulse response for only 4 discrete times." 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 4, 80 | "metadata": {}, 81 | "outputs": [ 82 | { 83 | "data": { 84 | "text/plain": [ 85 | "array([ 0. , 0.16666667, 0.33333333, 0.5 , 0.66666667,\n", 86 | " 0.83333333])" 87 | ] 88 | }, 89 | "execution_count": 4, 90 | "metadata": {}, 91 | "output_type": "execute_result" 92 | } 93 | ], 94 | "source": [ 95 | "t = np.linspace(0, 1, num = 6, endpoint = False)\n", 96 | "dt = t[1]\n", 97 | "t" 98 | ] 99 | }, 100 | { 101 | "cell_type": "markdown", 102 | "metadata": {}, 103 | "source": [ 104 | "The impulse response for a second order underdamped system is known to be\n", 105 | "\n", 106 | "$$h(t) = \\frac{1}{\\omega_d}e^{-\\zeta \\omega_n t}\\sin\\left(\\omega_d t\\right)$$" 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 5, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "omega_n = np.sqrt(100)\n", 116 | "zeta = 2/(2*omega_n)\n", 117 | "omega_d = omega_n * np.sqrt(1 - zeta**2)" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 6, 123 | "metadata": {}, 124 | "outputs": [ 125 | { 126 | "data": { 127 | "text/plain": [ 128 | "array([ 0. , 0.08474903, -0.01254052, -0.05886968, 0.01769677,\n", 129 | " 0.03956333])" 130 | ] 131 | }, 132 | "execution_count": 6, 133 | "metadata": {}, 134 | "output_type": "execute_result" 135 | } 136 | ], 137 | "source": [ 138 | "h = 1/omega_d * np.exp(-zeta*omega_n*t)*np.sin(omega_d*t)\n", 139 | "h" 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": 7, 145 | "metadata": {}, 146 | "outputs": [ 147 | { 148 | "data": { 149 | "image/png": "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\n", 150 | "text/plain": [ 151 | "" 152 | ] 153 | }, 154 | "metadata": {}, 155 | "output_type": "display_data" 156 | } 157 | ], 158 | "source": [ 159 | "plt.plot(t,h,'.')\n", 160 | "plt.axis([0,1,-0.1,0.1])\n", 161 | "plt.grid()" 162 | ] 163 | }, 164 | { 165 | "cell_type": "code", 166 | "execution_count": 8, 167 | "metadata": {}, 168 | "outputs": [ 169 | { 170 | "data": { 171 | "text/plain": [ 172 | "array([[ 0. , 0.08474903],\n", 173 | " [ 0.08474903, -0.01254052],\n", 174 | " [-0.01254052, -0.05886968],\n", 175 | " [-0.05886968, 0.01769677]])" 176 | ] 177 | }, 178 | "execution_count": 8, 179 | "metadata": {}, 180 | "output_type": "execute_result" 181 | } 182 | ], 183 | "source": [ 184 | "Hankel_0 = np.vstack((h[0:-2],h[1:-1]))\n", 185 | "Hankel_0 = Hankel_0.T\n", 186 | "Hankel_0" 187 | ] 188 | }, 189 | { 190 | "cell_type": "code", 191 | "execution_count": 9, 192 | "metadata": {}, 193 | "outputs": [ 194 | { 195 | "data": { 196 | "text/plain": [ 197 | "array([ 0.08474903, -0.01254052, -0.05886968, 0.01769677])" 198 | ] 199 | }, 200 | "execution_count": 9, 201 | "metadata": {}, 202 | "output_type": "execute_result" 203 | } 204 | ], 205 | "source": [ 206 | "h[1:-1]" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": 10, 212 | "metadata": {}, 213 | "outputs": [ 214 | { 215 | "data": { 216 | "text/plain": [ 217 | "array([-0.01254052, -0.05886968, 0.01769677, 0.03956333])" 218 | ] 219 | }, 220 | "execution_count": 10, 221 | "metadata": {}, 222 | "output_type": "execute_result" 223 | } 224 | ], 225 | "source": [ 226 | "h[2:]" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": 11, 232 | "metadata": { 233 | "scrolled": true 234 | }, 235 | "outputs": [ 236 | { 237 | "data": { 238 | "text/plain": [ 239 | "array([[ 0.08474903, -0.01254052],\n", 240 | " [-0.01254052, -0.05886968],\n", 241 | " [-0.05886968, 0.01769677],\n", 242 | " [ 0.01769677, 0.03956333]])" 243 | ] 244 | }, 245 | "execution_count": 11, 246 | "metadata": {}, 247 | "output_type": "execute_result" 248 | } 249 | ], 250 | "source": [ 251 | "Hankel_1 = np.vstack((h[1:-1],h[2:]))\n", 252 | "Hankel_1 = Hankel_1.T\n", 253 | "Hankel_1" 254 | ] 255 | }, 256 | { 257 | "cell_type": "code", 258 | "execution_count": 12, 259 | "metadata": {}, 260 | "outputs": [ 261 | { 262 | "name": "stdout", 263 | "output_type": "stream", 264 | "text": [ 265 | "[[ 0.56941225 0.57615451]\n", 266 | " [-0.59214005 0.56070011]\n", 267 | " [-0.32038129 -0.49580091]\n", 268 | " [ 0.47169449 -0.32839431]]\n", 269 | "[[-0.66563569 0.74627684]\n", 270 | " [ 0.74627684 0.66563569]]\n" 271 | ] 272 | } 273 | ], 274 | "source": [ 275 | "U, sig, Vt = la.svd(Hankel_0)\n", 276 | "V = Vt.T\n", 277 | "U = U[:,:2]\n", 278 | "print(U)\n", 279 | "V = V[:,:2]\n", 280 | "print(V)" 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": 13, 286 | "metadata": {}, 287 | "outputs": [ 288 | { 289 | "name": "stdout", 290 | "output_type": "stream", 291 | "text": [ 292 | "[[ 0.11107284 0. ]\n", 293 | " [ 0. 0.0979112 ]]\n" 294 | ] 295 | } 296 | ], 297 | "source": [ 298 | "sig = np.diag(sig)\n", 299 | "print(sig)" 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": 14, 305 | "metadata": {}, 306 | "outputs": [ 307 | { 308 | "name": "stdout", 309 | "output_type": "stream", 310 | "text": [ 311 | "[[-0.22322281 0.85303151]\n", 312 | " [-0.85967388 0.07525037]]\n" 313 | ] 314 | } 315 | ], 316 | "source": [ 317 | "A_d = la.inv(np.sqrt(sig))@U.T@Hankel_1@V@la.inv(np.sqrt(sig))\n", 318 | "print(A_d)" 319 | ] 320 | }, 321 | { 322 | "cell_type": "code", 323 | "execution_count": 15, 324 | "metadata": {}, 325 | "outputs": [ 326 | { 327 | "name": "stdout", 328 | "output_type": "stream", 329 | "text": [ 330 | "[-0.07398622+0.84324217j -0.07398622-0.84324217j]\n", 331 | "[[ 0.12298924-0.69493489j 0.12298924+0.69493489j]\n", 332 | " [ 0.70847664+0.j 0.70847664-0.j ]]\n" 333 | ] 334 | } 335 | ], 336 | "source": [ 337 | "lam_d, vec = la.eig(A_d)\n", 338 | "print(lam_d)\n", 339 | "print(vec)" 340 | ] 341 | }, 342 | { 343 | "cell_type": "code", 344 | "execution_count": 16, 345 | "metadata": {}, 346 | "outputs": [ 347 | { 348 | "name": "stdout", 349 | "output_type": "stream", 350 | "text": [ 351 | "[[-0.22322281 0.85303151]\n", 352 | " [-0.85967388 0.07525037]]\n", 353 | "[[-0.22322281 -6.06518177e-18j 0.85303151 -3.08660016e-18j]\n", 354 | " [-0.85967388 -5.89516371e-18j 0.07525037 -2.24128429e-17j]]\n" 355 | ] 356 | } 357 | ], 358 | "source": [ 359 | "# This should be the same as A_d\n", 360 | "print(A_d)\n", 361 | "print(vec@np.diag(lam_d)@la.inv(vec))" 362 | ] 363 | }, 364 | { 365 | "cell_type": "code", 366 | "execution_count": 17, 367 | "metadata": {}, 368 | "outputs": [ 369 | { 370 | "data": { 371 | "text/plain": [ 372 | "array([-1.+9.94987437j, -1.-9.94987437j])" 373 | ] 374 | }, 375 | "execution_count": 17, 376 | "metadata": {}, 377 | "output_type": "execute_result" 378 | } 379 | ], 380 | "source": [ 381 | "lam = np.log(lam_d)/dt\n", 382 | "lam" 383 | ] 384 | }, 385 | { 386 | "cell_type": "code", 387 | "execution_count": 18, 388 | "metadata": {}, 389 | "outputs": [ 390 | { 391 | "name": "stdout", 392 | "output_type": "stream", 393 | "text": [ 394 | "The undamped natural frequency is 10.0 rad/sec.\n", 395 | "The damping ratio is 0.09999999999999991.\n" 396 | ] 397 | } 398 | ], 399 | "source": [ 400 | "# These are the continuous time eigenvalues\n", 401 | "print('The undamped natural frequency is {} rad/sec.'.format(abs(lam[0])))\n", 402 | "print('The damping ratio is {}.'.format(-np.real(lam[0])/abs(lam[0])))\n" 403 | ] 404 | }, 405 | { 406 | "cell_type": "markdown", 407 | "metadata": {}, 408 | "source": [ 409 | "The identified state matrix is" 410 | ] 411 | }, 412 | { 413 | "cell_type": "code", 414 | "execution_count": 19, 415 | "metadata": {}, 416 | "outputs": [ 417 | { 418 | "name": "stdout", 419 | "output_type": "stream", 420 | "text": [ 421 | "[[ -2.76092394 10.06538424]\n", 422 | " [-10.1437611 0.76092394]]\n" 423 | ] 424 | } 425 | ], 426 | "source": [ 427 | "A = la.logm(A_d)/dt\n", 428 | "print(A)" 429 | ] 430 | }, 431 | { 432 | "cell_type": "markdown", 433 | "metadata": {}, 434 | "source": [ 435 | "which is the system the result in a balanced realization form. " 436 | ] 437 | }, 438 | { 439 | "cell_type": "code", 440 | "execution_count": 20, 441 | "metadata": {}, 442 | "outputs": [ 443 | { 444 | "name": "stdout", 445 | "output_type": "stream", 446 | "text": [ 447 | "[[-0.22184035]\n", 448 | " [ 0.23351573]]\n" 449 | ] 450 | } 451 | ], 452 | "source": [ 453 | "# The discrete input matrix is\n", 454 | "B_d = np.sqrt(sig)@V.T[:,0].T.reshape((2,1))\n", 455 | "print(B_d)" 456 | ] 457 | }, 458 | { 459 | "cell_type": "code", 460 | "execution_count": 21, 461 | "metadata": {}, 462 | "outputs": [ 463 | { 464 | "name": "stdout", 465 | "output_type": "stream", 466 | "text": [ 467 | "[[-2.58036274]\n", 468 | " [-0.22677789]]\n" 469 | ] 470 | } 471 | ], 472 | "source": [ 473 | "# The continuous input matrix is\n", 474 | "B = la.solve((A_d - np.eye(2)), A) @ B_d\n", 475 | "print(B)" 476 | ] 477 | }, 478 | { 479 | "cell_type": "code", 480 | "execution_count": 22, 481 | "metadata": {}, 482 | "outputs": [ 483 | { 484 | "name": "stdout", 485 | "output_type": "stream", 486 | "text": [ 487 | "[ 0.18977139 0.18028315]\n" 488 | ] 489 | } 490 | ], 491 | "source": [ 492 | "C = (U @ np.sqrt(sig))[0,:]\n", 493 | "print(C)" 494 | ] 495 | }, 496 | { 497 | "cell_type": "code", 498 | "execution_count": 23, 499 | "metadata": {}, 500 | "outputs": [ 501 | { 502 | "data": { 503 | "text/plain": [ 504 | "0.0" 505 | ] 506 | }, 507 | "execution_count": 23, 508 | "metadata": {}, 509 | "output_type": "execute_result" 510 | } 511 | ], 512 | "source": [ 513 | "# Of course, D is\n", 514 | "D = h[0]\n", 515 | "D" 516 | ] 517 | }, 518 | { 519 | "cell_type": "markdown", 520 | "metadata": {}, 521 | "source": [ 522 | "# New functions available" 523 | ] 524 | }, 525 | { 526 | "cell_type": "code", 527 | "execution_count": 24, 528 | "metadata": { 529 | "scrolled": true 530 | }, 531 | "outputs": [ 532 | { 533 | "data": { 534 | "text/plain": [ 535 | "(array([[ -2.76092394, 10.06538424],\n", 536 | " [-10.1437611 , 0.76092394]]), array([[-2.58036274],\n", 537 | " [-0.22677789]]), array([ 0.18977139, 0.18028315]), 0.0)" 538 | ] 539 | }, 540 | "execution_count": 24, 541 | "metadata": {}, 542 | "output_type": "execute_result" 543 | } 544 | ], 545 | "source": [ 546 | "vt.d2c(A_d, B_d, C, D, dt)" 547 | ] 548 | }, 549 | { 550 | "cell_type": "code", 551 | "execution_count": 25, 552 | "metadata": {}, 553 | "outputs": [], 554 | "source": [ 555 | "C = np.array([[1, 0]])" 556 | ] 557 | }, 558 | { 559 | "cell_type": "code", 560 | "execution_count": 26, 561 | "metadata": {}, 562 | "outputs": [ 563 | { 564 | "data": { 565 | "text/plain": [ 566 | "(1, 2)" 567 | ] 568 | }, 569 | "execution_count": 26, 570 | "metadata": {}, 571 | "output_type": "execute_result" 572 | } 573 | ], 574 | "source": [ 575 | "C.shape" 576 | ] 577 | }, 578 | { 579 | "cell_type": "code", 580 | "execution_count": 27, 581 | "metadata": {}, 582 | "outputs": [ 583 | { 584 | "data": { 585 | "text/plain": [ 586 | "(2, 2)" 587 | ] 588 | }, 589 | "execution_count": 27, 590 | "metadata": {}, 591 | "output_type": "execute_result" 592 | } 593 | ], 594 | "source": [ 595 | "A.shape" 596 | ] 597 | }, 598 | { 599 | "cell_type": "code", 600 | "execution_count": 28, 601 | "metadata": {}, 602 | "outputs": [ 603 | { 604 | "name": "stdout", 605 | "output_type": "stream", 606 | "text": [ 607 | "[[-0.22322281 0.85303151]\n", 608 | " [-0.85967388 0.07525037]]\n", 609 | "[[-0.22184035]\n", 610 | " [ 0.23351573]]\n" 611 | ] 612 | } 613 | ], 614 | "source": [ 615 | "Ad, Bd, _, _ = vt.c2d(A, B, C, D, dt)\n", 616 | "print(Ad)\n", 617 | "print(Bd)" 618 | ] 619 | }, 620 | { 621 | "cell_type": "code", 622 | "execution_count": 29, 623 | "metadata": { 624 | "scrolled": true 625 | }, 626 | "outputs": [ 627 | { 628 | "name": "stdout", 629 | "output_type": "stream", 630 | "text": [ 631 | "............... Eigenvalue ........... Damping Frequency\n", 632 | "--------[re]---------[im]--------[abs]----------------------[Hz]\n", 633 | " -1.000 +9.950 10.000 0.100 1.592\n", 634 | " -1.000 -9.950 10.000 0.100 1.592\n" 635 | ] 636 | } 637 | ], 638 | "source": [ 639 | "vt.damp(A)" 640 | ] 641 | }, 642 | { 643 | "cell_type": "code", 644 | "execution_count": 30, 645 | "metadata": {}, 646 | "outputs": [ 647 | { 648 | "data": { 649 | "text/plain": [ 650 | "array([[ 0.06375786],\n", 651 | " [ 0.36686755]])" 652 | ] 653 | }, 654 | "execution_count": 30, 655 | "metadata": {}, 656 | "output_type": "execute_result" 657 | } 658 | ], 659 | "source": [ 660 | "(A_d*dt)@B" 661 | ] 662 | }, 663 | { 664 | "cell_type": "code", 665 | "execution_count": null, 666 | "metadata": {}, 667 | "outputs": [], 668 | "source": [] 669 | } 670 | ], 671 | "metadata": { 672 | "hide_input": false, 673 | "kernelspec": { 674 | "display_name": "Python 3", 675 | "language": "python", 676 | "name": "python3" 677 | }, 678 | "language_info": { 679 | "codemirror_mode": { 680 | "name": "ipython", 681 | "version": 3 682 | }, 683 | "file_extension": ".py", 684 | "mimetype": "text/x-python", 685 | "name": "python", 686 | "nbconvert_exporter": "python", 687 | "pygments_lexer": "ipython3", 688 | "version": "3.6.3" 689 | }, 690 | "latex_envs": { 691 | "bibliofile": "biblio.bib", 692 | "cite_by": "apalike", 693 | "current_citInitial": 1, 694 | "eqLabelWithNumbers": true, 695 | "eqNumInitial": 0 696 | }, 697 | "toc": { 698 | "nav_menu": {}, 699 | "number_sections": true, 700 | "sideBar": true, 701 | "skip_h1_title": false, 702 | "toc_cell": false, 703 | "toc_position": {}, 704 | "toc_section_display": "block", 705 | "toc_window_display": false 706 | }, 707 | "varInspector": { 708 | "cols": { 709 | "lenName": 16, 710 | "lenType": 16, 711 | "lenVar": 40 712 | }, 713 | "kernels_config": { 714 | "python": { 715 | "delete_cmd_postfix": "", 716 | "delete_cmd_prefix": "del ", 717 | "library": "var_list.py", 718 | "varRefreshCmd": "print(var_dic_list())" 719 | }, 720 | "r": { 721 | "delete_cmd_postfix": ") ", 722 | "delete_cmd_prefix": "rm(", 723 | "library": "var_list.r", 724 | "varRefreshCmd": "cat(var_dic_list()) " 725 | } 726 | }, 727 | "types_to_exclude": [ 728 | "module", 729 | "function", 730 | "builtin_function_or_method", 731 | "instance", 732 | "_Feature" 733 | ], 734 | "window_display": false 735 | } 736 | }, 737 | "nbformat": 4, 738 | "nbformat_minor": 1 739 | } 740 | -------------------------------------------------------------------------------- /vibrationtesting/system.py: -------------------------------------------------------------------------------- 1 | """ 2 | System manipulation, reduction, and corrections functions. 3 | 4 | @author: Joseph C. Slater 5 | """ 6 | __license__ = "Joseph C. Slater" 7 | 8 | __docformat__ = 'reStructuredText' 9 | 10 | 11 | import math 12 | 13 | import numpy as np 14 | import scipy.signal as sig 15 | import scipy.linalg as la 16 | 17 | 18 | def d2c(Ad, Bd, C, D, dt): 19 | r"""Return continuous A, B, C, D from discrete form. 20 | 21 | Converts a set of digital state space system matrices to their 22 | continuous counterpart. 23 | 24 | Parameters 25 | ---------- 26 | Ad, Bd, C, D : float arrays 27 | Discrete state space system matrices 28 | dt : float 29 | Time step of discrete system 30 | 31 | Returns 32 | ------- 33 | A, B, C, D : float arrays 34 | State space system matrices 35 | 36 | Examples 37 | -------- 38 | >>> import vibrationtesting as vt 39 | >>> Ad = np.array([[ 0.9999,0.0001,0.01,0.], 40 | ... [ 0.,0.9999,0.,0.01], 41 | ... [-0.014,0.012,0.9999,0.0001], 42 | ... [ 0.008, -0.014,0.0001,0.9999]]) 43 | >>> Bd = np.array([[ 0. ], 44 | ... [ 0. ], 45 | ... [ 0. ], 46 | ... [ 0.01]]) 47 | >>> C = np.array([[-1.4, 1.2, -0.0058, 0.0014]]) 48 | >>> D = np.array([[-0.2]]) 49 | >>> A, B, *_ = vt.d2c(Ad, Bd, C, D, 0.01) 50 | >>> print(A) 51 | [[-0.003 0.004 1.0001 -0.0001] 52 | [-0.004 -0.003 -0. 1.0001] 53 | [-1.4001 1.2001 -0.003 0.004 ] 54 | [ 0.8001 -1.4001 0.006 -0.003 ]] 55 | 56 | Notes 57 | ----- 58 | .. note:: Zero-order hold solution 59 | .. note:: discrepancies between :func:`c2d` and :func:`d2c` are due to \ 60 | typing truncation errors. 61 | .. seealso:: :func:`c2d`. 62 | 63 | """ 64 | # Old school (very old) 65 | # A = la.logm(Ad) / dt 66 | # B = la.solve((Ad - np.eye(A.shape[0])), A) @ Bd 67 | sa = Ad.shape[0] 68 | sb = Bd.shape[1] 69 | AAd = np.vstack((np.hstack((Ad, Bd)), 70 | np.hstack((np.zeros((sb, sa)), np.eye(sb))))) 71 | AA = la.logm(AAd) / dt 72 | A = AA[0:sa, 0:sa] 73 | B = AA[0:sa, sa:] 74 | return A, B, C, D 75 | 76 | 77 | def c2d(A, B, C, D, dt): 78 | """Convert continuous state system to discrete time. 79 | 80 | Converts a set of continuous state space system matrices to their 81 | discrete counterpart. 82 | 83 | Parameters 84 | ---------- 85 | A, B, C, D : float arrays 86 | State space system matrices 87 | dt : float 88 | Time step of discrete system 89 | 90 | Returns 91 | ------- 92 | Ad, Bd, C, D : float arrays 93 | Discrete state space system matrices 94 | 95 | Examples 96 | -------- 97 | >>> import numpy as np 98 | >>> np.set_printoptions(precision=4, suppress=True) 99 | >>> import vibrationtesting as vt 100 | >>> A1 = np.array([[ 0., 0. , 1. , 0. ]]) 101 | >>> A2 = np.array([[ 0., 0. , 0. , 1. ]]) 102 | >>> A3 = np.array([[-1.4, 1.2, -0.0058, 0.0014]]) 103 | >>> A4 = np.array([[ 0.8, -1.4, 0.0016, -0.0038]]) 104 | >>> A = np.array([[ 0., 0. , 1. , 0. ], 105 | ... [ 0., 0. , 0. , 1. ], 106 | ... [-1.4, 1.2, -0.0058, 0.0014], 107 | ... [ 0.8, -1.4, 0.0016, -0.0038]]) 108 | >>> B = np.array([[ 0.], 109 | ... [ 0.], 110 | ... [ 0.], 111 | ... [ 1.]]) 112 | >>> C = np.array([[-1.4, 1.2, -0.0058, 0.0014]]) 113 | >>> D = np.array([[-0.2]]) 114 | >>> Ad, Bd, *_ = vt.c2d(A, B, C, D, 0.01) 115 | 116 | Notes 117 | ----- 118 | .. note:: Zero-order hold solution 119 | .. seealso:: :func:`d2c`. 120 | 121 | """ 122 | Ad, Bd, _, _, _ = sig.cont2discrete((A, B, C, D), dt) 123 | # Ad = la.expm(A * dt) 124 | # Bd = la.solve(A, (Ad - np.eye(A.shape[0]))) @ B 125 | return Ad, Bd, C, D 126 | 127 | 128 | def ssfrf(A, B, C, D, omega_low, omega_high, in_index, out_index, 129 | num_freqs=1000): 130 | """Return FRF of state space system. 131 | 132 | Obtains the computed FRF of a state space system between selected input 133 | and output over frequency range of interest. 134 | 135 | Parameters 136 | ---------- 137 | A, B, C, D : float arrays 138 | state system matrices 139 | omega_low, omega_high : floats 140 | low and high frequencies for evaluation 141 | in_index, out_index : ints 142 | input and output numbers (starting at 1) 143 | num_freqs : int 144 | number of frequencies at which to return FRF 145 | 146 | Returns 147 | ------- 148 | omega : float array 149 | frequency vector 150 | H : float array 151 | frequency response function 152 | 153 | Examples 154 | -------- 155 | >>> import vibrationtesting as vt 156 | >>> A = np.array([[ 0., 0. , 1. , 0. ], 157 | ... [ 0., 0. , 0. , 1. ], 158 | ... [-1.4, 1.2, -0.0058, 0.0014], 159 | ... [ 0.8, -1.4, 0.0016, -0.0038]]) 160 | >>> B = np.array([[ 0.], 161 | ... [ 0.], 162 | ... [ 0.], 163 | ... [ 1.]]) 164 | >>> C = np.array([[-1.4, 1.2, -0.0058, 0.0014]]) 165 | >>> D = np.array([[-0.2]]) 166 | >>> omega, H = vt.ssfrf(A, B, C, D, 0, 3.5, 1, 1) 167 | >>> vt.frfplot(omega, H) # doctest: +SKIP 168 | 169 | """ 170 | # A, B, C, D = ctrl.ssdata(sys) 171 | if 0 < in_index < (B.shape[1] + 1) and 0 < out_index < (C.shape[0] + 1): 172 | sa = A.shape[0] 173 | omega = np.linspace(omega_low, omega_high, num_freqs) 174 | H = omega * 1j 175 | i = 0 176 | for i, w in enumerate(omega): 177 | H[i] = (C@la.solve(w * 1j * np.eye(sa) - A, B) + D)[out_index - 1, 178 | in_index - 1] 179 | else: 180 | raise ValueError( 181 | 'Input {} or output {} infeasible.'.format(in_index, out_index)) 182 | return omega.reshape(1, -1), H.reshape(1, -1) 183 | 184 | 185 | def sos_frf(M, C, K, Bt, Cd, Cv, Ca, omega_low, omega_high, 186 | in_index, out_index, num_freqs=1000): 187 | r"""Return FRF of second order system. 188 | 189 | Given second order linear matrix equation of the form 190 | :math:`M\\ddot{x} + C \\dot{x} + K x= \\tilde{B} u` 191 | and 192 | :math:`y = C_d x + C_v \\dot{x} + C_a\\ddot{x}` 193 | converts to state space form and returns the requested frequency response 194 | function 195 | 196 | Parameters 197 | ---------- 198 | M, C, K, Bt, Cd, Cv, Cd : float arrays 199 | Mass, damping, stiffness, input, displacement sensor, velocimeter, 200 | and accelerometer matrices 201 | omega_low, omega_high : floats 202 | low and high frequencies for evaluation 203 | in_index, out_index : ints 204 | input and output numbers (starting at 1) 205 | num_freqs : int 206 | number of frequencies at which to return FRF 207 | 208 | Returns 209 | ------- 210 | omega : float array 211 | frequency vector 212 | H : float array 213 | frequency response function 214 | 215 | Examples not working for second order system 216 | 217 | Need to make one for second order expansion 218 | 219 | Examples 220 | -------- 221 | >>> import vibrationtesting as vt 222 | >>> A = np.array([[ 0., 0. , 1. , 0. ], 223 | ... [ 0., 0. , 0. , 1. ], 224 | ... [-1.4, 1.2, -0.0058, 0.0014], 225 | ... [ 0.8, -1.4, 0.0016, -0.0038]]) 226 | >>> B = np.array([[ 0.], 227 | ... [ 0.], 228 | ... [ 0.], 229 | ... [ 1.]]) 230 | >>> C = np.array([[-1.4, 1.2, -0.0058, 0.0014]]) 231 | >>> D = np.array([[-0.2]]) 232 | >>> omega, H = vt.ssfrf(A, B, C, D, 0, 3.5, 1, 1) 233 | >>> vt.frfplot(omega, H) # doctest: +SKIP 234 | 235 | """ 236 | A, B, C, D = so2ss(M, C, K, Bt, Cd, Cv, Ca) 237 | 238 | omega, H = ssfrf(A, B, C, D, omega_low, omega_high, in_index, out_index, 239 | num_freqs) 240 | 241 | return omega, H 242 | 243 | 244 | def so2ss(M, C, K, Bt, Cd, Cv, Ca): 245 | r"""Convert second order system to state space. 246 | 247 | Given second order linear matrix equation of the form 248 | :math:`M\\ddot{x} + C \\dot{x} + K x= \\tilde{B} u` 249 | and 250 | :math:`y = C_d x + C_v \\dot{x} + C_a\\ddot{x}` 251 | returns the state space form equations 252 | :math:`\\dot{z} = A z + B u`, 253 | :math:`y = C z + D u` 254 | 255 | Parameters 256 | ---------- 257 | M, C, K, Bt, Cd, Cv, Cd : float arrays 258 | Mass , damping, stiffness, input, displacement sensor, velocimeter, 259 | and accelerometer matrices 260 | 261 | Returns 262 | ------- 263 | A, B, C, D : float arrays 264 | State matrices 265 | 266 | Examples 267 | -------- 268 | >>> import vibrationtesting as vt 269 | >>> M = np.array([[2, 1], 270 | ... [1, 3]]) 271 | >>> K = np.array([[2, -1], 272 | ... [-1, 3]]) 273 | >>> C = np.array([[0.01, 0.001], 274 | ... [0.001, 0.01]]) 275 | >>> Bt = np.array([[0], [1]]) 276 | >>> Cd = Cv = np.zeros((1,2)) 277 | >>> Ca = np.array([[1, 0]]) 278 | >>> A, B, Css, D = vt.so2ss(M, C, K, Bt, Cd, Cv, Ca) 279 | >>> print('A: {}'.format(A)) 280 | A: [[ 0. 0. 1. 0. ] 281 | [ 0. 0. 0. 1. ] 282 | [-1.4 1.2 -0.0058 0.0014] 283 | [ 0.8 -1.4 0.0016 -0.0038]] 284 | >>> print('B: ', B) 285 | B: [[ 0. ] 286 | [ 0. ] 287 | [-0.2] 288 | [ 0.4]] 289 | >>> print('C: ', Css) 290 | C: [[-1.4 1.2 -0.0058 0.0014]] 291 | >>> print('D: ', D) 292 | D: [[-0.2]] 293 | 294 | """ 295 | A = np.vstack((np.hstack((np.zeros_like(M), np.eye(M.shape[0]))), 296 | np.hstack((-la.solve(M, K), -la.solve(M, C))))) 297 | B = np.vstack((np.zeros_like(Bt), la.solve(M, Bt))) 298 | C_ss = np.hstack((Cd - Ca@la.solve(M, K), Cv - Ca@la.solve(M, C))) 299 | D = Ca@la.solve(M, Bt) 300 | 301 | return A, B, C_ss, D 302 | 303 | 304 | def damp(A): 305 | """Display natural frequencies and damping ratios of state matrix.""" 306 | # Original Author: Kai P. Mueller for Octave 307 | # Created: September 29, 1997. 308 | 309 | print("............... Eigenvalue ........... Damping Frequency") 310 | print("--------[re]---------[im]--------[abs]----------------------[Hz]") 311 | e, _ = la.eig(A) 312 | 313 | for i in range(len(e)): 314 | pole = e[i] 315 | 316 | d0 = -np.cos(math.atan2(np.imag(pole), np.real(pole))) 317 | f0 = 0.5 / np.pi * abs(pole) 318 | if (abs(np.imag(pole)) < abs(np.real(pole))): 319 | print(' {:.3f} {:.3f} \ 320 | {:.3f} {:.3f}'.format(float(np.real(pole)), 321 | float(abs(pole)), float(d0), 322 | float(f0))) 323 | 324 | else: 325 | print(' {:.3f} {:+.3f} {:.3f} \ 326 | {:.3f} {:.3f}'.format(float(np.real(pole)), 327 | float(np.imag(pole)), float( 328 | abs(pole)), float(d0), 329 | float(f0))) 330 | 331 | 332 | def sos_modal(M, K, C=False, damp_diag=0.03, shift=1): 333 | r"""Eigen analysis of proportionally damped system. 334 | 335 | Optimally find mass normalized mode shapes and natural frequencies 336 | of a system modelled by :math:`M\ddot{x}+Kx=0`. 337 | 338 | If provided, obtain damping ratios presuming :math:`C` can be decoupled. 339 | 340 | Provides a warning if diagonalization of damping matrix fails worse than 341 | relative error of `damp_diag`. 342 | 343 | Parameters 344 | ---------- 345 | M, K : float arrays 346 | Mass and stiffness matrices 347 | C : float array, optional 348 | Damping matrix 349 | damp_diag : float, optional 350 | Maximum amount of off-diagonal error allowed in assuming C can be 351 | diagonalized 352 | shift : float, optional 353 | Shift used in eigensolution. Should be approximately equal to the first 354 | non-zero eigenvalue. 355 | 356 | Returns 357 | ------- 358 | omega : float array (1xN) 359 | Vector of natural frequencies (rad/sec) 360 | zeta : float array (1xN) 361 | Vector of damping ratios 362 | Psi : float array (NxN) 363 | Matrix of mass normalized mode shapes by column 364 | 365 | 366 | Examples 367 | -------- 368 | >>> import numpy as np 369 | >>> import vibrationtesting as vt 370 | >>> M = np.array([[4, 0, 0], 371 | ... [0, 4, 0], 372 | ... [0, 0, 4]]) 373 | >>> K = np.array([[8, -4, 0], 374 | ... [-4, 8, -4], 375 | ... [0, -4, 4]]) 376 | >>> omega, zeta, Psi = vt.sos_modal(M, K, K/10) 377 | 378 | """ 379 | K = K + M * shift 380 | 381 | lam, Psi = la.eigh(K, M) 382 | 383 | omega = np.sqrt(np.abs(lam - shift)) # round to zero 384 | 385 | norms = np.diag(1.0 / np.sqrt(np.diag(Psi.T@M@Psi))) 386 | 387 | Psi = Psi @ norms 388 | 389 | zeta = np.zeros_like(omega) 390 | 391 | if C is not False: 392 | diagonalized_C = Psi.T@C@Psi 393 | 394 | diagonal_C = np.diag(diagonalized_C) 395 | 396 | if min(omega) > 1e-5: 397 | zeta = diagonal_C / 2 / omega # error if omega = 0 398 | max_off_diagonals = np.amax(np.abs(diagonalized_C 399 | - np.diag(diagonal_C)), axis=0) 400 | # error if no damping 401 | damp_error = np.max(max_off_diagonals / diagonal_C) 402 | else: 403 | zeta = np.zeros_like(omega) 404 | damp_error = np.zeros_like(omega) 405 | de_diag_C = diagonalized_C - np.diag(diagonal_C) 406 | for mode_num, omega_i in enumerate(omega): 407 | if omega[mode_num] > 1e-5: 408 | zeta[mode_num] = diagonal_C[mode_num] / 2 / omega_i 409 | damp_error = (np.max(np.abs(de_diag_C[:, mode_num])) 410 | / diagonal_C[mode_num]) 411 | 412 | if damp_error > damp_diag: 413 | print('Damping matrix cannot be completely diagonalized.') 414 | print('Off diagonal error of {:4.0%}.'.format(damp_error)) 415 | 416 | return omega, zeta, Psi 417 | 418 | 419 | def serep(M, K, master): 420 | r"""System Equivalent Reduction Expansion Process reduced model. 421 | 422 | Reduce size of second order system of equations by SEREP processs while 423 | returning expansion matrix 424 | 425 | Equation of the form: 426 | :math:`M \ddot{x} + K x = 0` 427 | is reduced to the form 428 | :math:`M_r \ddot{x}_m + Kr x_m = 0` 429 | where :math:`x = T x_m`, :math:`M_r= T^T M T`, :math:`K_r= T^T K T` 430 | 431 | Parameters 432 | ---------- 433 | M, K : float arrays 434 | Mass and Stiffness matrices 435 | master : float array or list 436 | List of retained degrees of freedom 437 | 438 | Returns 439 | ------- 440 | Mred, Kred, T : float arrays 441 | Reduced Mass matric, reduced stiffness matrix, Transformation matrix 442 | truncated_dofs : int list 443 | List of truncated degrees of freedom, zero indexed 444 | 445 | Examples 446 | -------- 447 | >>> import vibrationtesting as vt 448 | >>> M = np.array([[4, 0, 0], 449 | ... [0, 4, 0], 450 | ... [0, 0, 4]]) 451 | >>> K = np.array([[8, -4, 0], 452 | ... [-4, 8, -4], 453 | ... [0, -4, 4]]) 454 | >>> retained = np.array([[1, 2]]) 455 | >>> Mred, Kred, T, truncated_dofs = vt.serep(M, K, retained) 456 | 457 | Notes 458 | ----- 459 | Reduced coordinate system forces can be obtained by 460 | `Fr = T.T @ F` 461 | 462 | Reduced damping matrix can be obtained using `Cr = T.T*@ C @ T`. 463 | 464 | If mode shapes are obtained for the reduced system, full system mode shapes 465 | are `phi = T @ phi_r` 466 | 467 | .. seealso:: :func:`guyan` 468 | 469 | """ 470 | nm = int(max(master.shape)) # number of modes to keep; 471 | master = master.reshape(-1) - 1 # retained dofs 472 | 473 | ndof = int(M.shape[0]) # length(M); 474 | 475 | omega, _, Psi = sos_modal(M, K) 476 | Psi_tr = Psi[nm:, :nm] # Truncated modes 477 | Psi_rr = Psi[:nm, :nm] # Retained modes 478 | 479 | truncated_dofs = list(set(np.arange(ndof)) - set(master)) 480 | 481 | T = np.zeros((ndof, nm)) 482 | T[master, :nm] = np.eye(nm) 483 | T[truncated_dofs, :nm] = la.solve(Psi_rr.T, Psi_tr.T).T 484 | Mred = T.T @ M @T 485 | Kred = T.T @ K @T 486 | 487 | return Mred, Kred, T, np.array(truncated_dofs) 488 | 489 | 490 | def guyan(M, K, master=None, fraction=None): 491 | r"""Guyan reduced model. 492 | 493 | Applies Guyan Reductions to second order system of equations of the form 494 | 495 | .. math:: M \ddot{x} + K x = 0 496 | 497 | which are reduced to the form 498 | 499 | .. math:: M_r \ddot{x}_m + K_r x_m = 0 500 | 501 | where 502 | 503 | .. math:: 504 | 505 | x = T x_m 506 | 507 | M_r= T^T M T 508 | 509 | K_r= T^T K T 510 | 511 | Parameters 512 | ---------- 513 | M, K : float arrays 514 | Mass and Stiffness matrices 515 | master : float array or list, optional 516 | List of retained degrees of freedom (0 indexing) 517 | fraction : float, optional 518 | Fraction of degrees of freedom (0< `fraction` <1.0) to retain in model. 519 | If both master and fraction are neglected, fraction is set to 0.25. 520 | 521 | Returns 522 | ------- 523 | Mred, Kred, T : float arrays 524 | Reduced Mass matric, Reduced Stiffness matrix, Transformation matrix 525 | master_dofs : int list 526 | List of master degrees of freedom (0 indexing) 527 | truncated_dofs : int list 528 | List of truncated degrees of freedom (0 indexing) 529 | 530 | Examples 531 | -------- 532 | >>> import vibrationtesting as vt 533 | >>> M = np.array([[4, 0, 0], 534 | ... [0, 4, 0], 535 | ... [0, 0, 4]]) 536 | >>> K = np.array([[8, -4, 0], 537 | ... [-4, 8, -4], 538 | ... [0, -4, 4]]) 539 | >>> Mred, Kred, T, master, truncated_dofs = vt.guyan(M, K, fraction = 0.5) 540 | 541 | 542 | Notes 543 | ----- 544 | Reduced coordinate system forces can be obtained by `Fr = T.T @ F`. 545 | 546 | Reduced damping matrix can be obtained using `Cr = T.T @ C @ T`. 547 | 548 | If mode shapes are obtained for the reduced system, full system mode shapes 549 | are `phi = T @ phi_r`. 550 | 551 | Code is not as efficient as possible. Using block submatrices would be 552 | more efficient. 553 | 554 | """ 555 | if master is None: 556 | if fraction is None: 557 | fraction = 0.25 558 | 559 | ratios = np.diag(M) / np.diag(K) 560 | ranked = [i[0] for i in sorted(enumerate(ratios), key=lambda x: x[1])] 561 | thresh = int(fraction * ratios.size) 562 | if (thresh >= ratios.size) or thresh == 0: 563 | print("Can't keep", thresh, 'DOFs.') 564 | print("Fraction of", fraction, "is too low or too high.") 565 | return 0, 0, 0, 0, 0 566 | 567 | master = ranked[-thresh:] 568 | 569 | master = np.array(master) 570 | nm = master.size # number of dofs to keep; 571 | master = master.reshape(-1) # retained dofs 572 | 573 | ndof = int(M.shape[0]) # length(M); 574 | 575 | truncated_dofs = list(set(np.arange(ndof)) - set(master)) 576 | # truncated_dofs = np.array(set(np.arange(ndof)) - set(master)) 577 | """ 578 | Mmm = M[master].T[master].T 579 | Kmm = K[master].T[master].T 580 | Mtm = M[truncated_dofs].T[master].T 581 | Ktm = K[truncated_dofs].T[master].T 582 | Mtt = M[truncated_dofs].T[truncated_dofs].T 583 | Ktt = K[truncated_dofs].T[truncated_dofs].T""" 584 | 585 | # Mmm = slice(M, master, master) 586 | # Kmm = slice(K, master, master) 587 | # Mtm = slice(M, truncated_dofs, master) 588 | Ktm = slice(K, truncated_dofs, master) 589 | # Mtt = slice(M, truncated_dofs, truncated_dofs) 590 | Ktt = slice(K, truncated_dofs, truncated_dofs) 591 | 592 | T = np.zeros((ndof, nm)) 593 | T[master, :nm] = np.eye(nm) 594 | T[truncated_dofs, :nm] = la.solve(-Ktt, Ktm) 595 | Mred = T.T @ M @ T 596 | Kred = T.T @ K @ T 597 | return Mred, Kred, T, master, truncated_dofs 598 | 599 | 600 | def mode_expansion_from_model(Psi, omega, M, K, measured): 601 | r"""Deflection extrapolation to full FEM model coordinates, matrix method. 602 | 603 | Provided an equation of the form: 604 | 605 | :math:`\begin{pmatrix}-\begin{bmatrix}M_{mm}&M_{mu}\\ M_{um}&M_{uu} 606 | \end{bmatrix} \omega_i^2 607 | +\begin{bmatrix}K_{mm}&K_{mu}\\ K_{um}&K_{uu}\end{bmatrix}\end{pmatrix}` 608 | :math:`\begin{bmatrix}\Psi_{i_m}\\ \Psi_{i_u}\end{bmatrix}= 0` 609 | 610 | Where: 611 | 612 | - :math:`M` and :math:`K` are the mass and stiffness matrices, likely from 613 | a finite element model 614 | - :math:`\Psi_i` and :math:`\omega_i` represent a mode/frequency pair 615 | - subscripts :math:`m` and :math:`u` represent measure and unmeasured 616 | of the mode 617 | 618 | Determines the unknown portion of the mode (or operational deflection) 619 | shape, :math:`\Psi_{i_u}` by 620 | direct algebraic solution, aka 621 | 622 | :math:`\Psi_{i_u} = - (K_{uu}- M_{ss} \omega_i^2) ^{-1} 623 | (K_{um}-M_{um}\omega_i^2)\Psi_{i_m}` 624 | 625 | Parameters 626 | ---------- 627 | Psi : float array 628 | mode shape or shapes, 2-D array columns of which are mode shapes 629 | omega : float or 1-D float array 630 | natural (or driving) frequencies 631 | M, K : float arrays 632 | Mass and Stiffness matrices 633 | measured : float or integer array or list 634 | List of measured degrees of freedom (0 indexed) 635 | 636 | Returns 637 | ------- 638 | Psi_full : float array 639 | Complete mode shape 640 | 641 | Examples 642 | -------- 643 | >>> import vibrationtesting as vt 644 | >>> M = np.array([[4, 0, 0], 645 | ... [0, 4, 0], 646 | ... [0, 0, 4]]) 647 | >>> K = np.array([[8, -4, 0], 648 | ... [-4, 8, -4], 649 | ... [0, -4, 4]]) 650 | >>> measured = np.array([[0, 2]]) 651 | >>> omega, zeta, Psi = vt.sos_modal(M, K) 652 | >>> Psi_measured = np.array([[-0.15], [-0.37]]) 653 | >>> Psi_full = vt.mode_expansion_from_model(Psi_measured, omega[0], M, K, 654 | ... measured) 655 | >>> print(np.hstack((Psi[:,0].reshape(-1,1), Psi_full))) 656 | [[-0.164 -0.15 ] 657 | [-0.2955 0.2886] 658 | [-0.3685 -0.37 ]] 659 | 660 | Notes 661 | ----- 662 | .. seealso:: incomplete multi-mode update. Would require each at a 663 | different frequency. 664 | 665 | """ 666 | measured = measured.reshape(-1) # retained dofs 667 | num_measured = len(measured) 668 | ndof = int(M.shape[0]) # length(M); 669 | unmeasured_dofs = list(set(np.arange(ndof)) - set(measured)) 670 | num_unmeasured = len(unmeasured_dofs) 671 | 672 | # Code from before my slicing code 673 | """ 674 | Muu = np.array(M[unmeasured_dofs].T[unmeasured_dofs].T). 675 | reshape(num_unmeasured, num_unmeasured) 676 | 677 | Kuu = np.array(K[unmeasured_dofs].T[unmeasured_dofs].T) 678 | .reshape(num_unmeasured, num_unmeasured) 679 | Mum = np.array(M[unmeasured_dofs].T[measured].T).reshape(num_unmeasured, 680 | num_measured) 681 | Kum = np.array(K[unmeasured_dofs].T[measured].T).reshape(num_unmeasured, 682 | num_measured) 683 | """ 684 | 685 | Muu = slice(M, unmeasured_dofs, unmeasured_dofs) 686 | Kuu = slice(K, unmeasured_dofs, unmeasured_dofs) 687 | Mum = slice(M, unmeasured_dofs, measured) 688 | Kum = slice(K, unmeasured_dofs, measured) 689 | 690 | if isinstance(omega, float): 691 | omega = np.array(omega).reshape(1) 692 | 693 | Psi_full = np.zeros((num_measured + num_unmeasured, Psi.shape[1])) 694 | Psi_full[measured] = Psi 695 | 696 | for i, omega_n in enumerate(omega): 697 | Psi_i = Psi[:, i].reshape(-1, 1) 698 | Psi_unmeasured = la.solve((Kuu - Muu * omega_n**2), 699 | (Kum - Mum * omega_n**2)@Psi_i) 700 | Psi_unmeasured = Psi_unmeasured.reshape(-1, ) 701 | Psi_full[unmeasured_dofs, i] = Psi_unmeasured 702 | # Psi_full = Psi_full.reshape(-1, 1) 703 | return Psi_full 704 | 705 | 706 | def improved_reduction(M, K, master=None, fraction=None): 707 | """Incomplete. 708 | 709 | 4.14 Friswell 710 | 711 | """ 712 | print('not written yet') 713 | return 714 | 715 | 716 | def model_correction_direct(Psi, omega, M, K, method='Baruch'): 717 | """Direct model updating using model data. 718 | 719 | Parameters 720 | ---------- 721 | Psi : float array 722 | Expanded mode shapes from experimental measurements. Must be 723 | real-valued. See `mode_expansion_from_model`. 724 | omega : float array 725 | Natural frequencies identified from modal analysis (diagonal matrix or 726 | vector). 727 | M, K : float arrays 728 | Analytical mass and stiffness matrices 729 | method : string, optional 730 | `Baruch` and Bar-Itzhack [1]_ or `Berman` and Nagy [2]_ 731 | (default Baruch) 732 | 733 | Returns 734 | ------- 735 | Mc, Kc : float arrays 736 | Corrected mass and stiffness matrices. 737 | 738 | Notes 739 | ----- 740 | .. [1] Baruch, M. and Bar-Itzhack, I.Y., "Optimal Weighted 741 | Orthogonalization of Measured Modes," *AIAA Journal*, 16(4), 1978, pp. 742 | 346-351. 743 | .. [2] Berman, A. and Nagy, E.J., 1983, "Improvements of a Large Analytical 744 | Model using Test Data," *AIAA Journal*, 21(8), 1983, pp. 1168-1173. 745 | 746 | """ 747 | if len(omega.shape) == 1: 748 | omega = np.diag(omega) 749 | elif omega.shape[0] != omega.shape[1]: 750 | omega = np.diag(omega) 751 | 752 | lam = omega @ omega 753 | 754 | if method is 'Berman': 755 | 756 | Mdiag = Psi.T@M@Psi 757 | eye_size = Mdiag.shape[0] 758 | Mc = (M + M @ Psi @ la.solve(Mdiag, np.eye(eye_size) - Mdiag) 759 | @ la.solve(Mdiag, Psi.T) @ M) 760 | 761 | Kc = (K - K @ Psi @ Psi.T @ M 762 | - M @ Psi @ Psi.T @ K 763 | + M @Psi@ Psi.T @ K @ Psi @ Psi.T @ M 764 | + M @ Psi @ lam @ Psi.T @ M) 765 | 766 | else: # Defaults to Baruch method. 767 | Phi = rsolve(la.sqrtm(Psi.T @ M @ Psi), Psi, assume_a='pos') 768 | 769 | PhiPhiT = Phi@Phi.T 770 | 771 | sec_term = K @ PhiPhiT @ M 772 | 773 | Kc = K - sec_term - sec_term.T \ 774 | + M @ PhiPhiT @ K @ PhiPhiT @ M\ 775 | + M @ Phi @ lam @ Phi.T @ M 776 | 777 | Mc = M 778 | 779 | return Mc, Kc 780 | 781 | 782 | def slice(Matrix, a, b): 783 | """Slice a matrix properly- like Octave. 784 | 785 | Addresses the confounding inconsistency that `M[a,b]` acts differently if 786 | `a` and `b` are the same length or different lengths. 787 | 788 | Parameters 789 | ---------- 790 | Matrix : float array 791 | Arbitrary array 792 | a, b : int lists or arrays 793 | list of rows and columns to be selected from `Matrix` 794 | 795 | Returns 796 | ------- 797 | Matrix : float array 798 | Properly sliced matrix- no casting allowed. 799 | 800 | """ 801 | # a = a.reshape(-1) 802 | # b = b.reshape(-1) 803 | 804 | return (Matrix[np.array(a).reshape(-1, 1), b] 805 | .reshape(np.array(a).shape[0], np.array(b).shape[0])) 806 | 807 | 808 | def rsolve(B, C, **kwargs): 809 | """Solve right Gauss elimination equation. 810 | 811 | Given :math:`A B = C` return :math:`A = C B^{-1}` 812 | 813 | This uses `scipy.linalg.solve` with a little matrix manipulation first. 814 | All keyword arguments of `scipy.linalg.solve` may be used. 815 | 816 | Parameters 817 | ---------- 818 | B, C : float arrays 819 | 820 | Returns 821 | ------- 822 | A : float array 823 | 824 | Examples 825 | -------- 826 | >>> import numpy as np 827 | >>> import vibrationtesting as vt 828 | >>> B = np.array([[ 8, -4, 0], 829 | ... [-4, 8, -4], 830 | ... [ 0, -4, 4]]) 831 | >>> C = np.array([[ 32, -16, 0], 832 | ... [-16, 36, -20], 833 | ... [ 4, -24, 20]]) 834 | >>> A = vt.rsolve(B, C) 835 | >>> print(np.round(rsolve(B, C))) 836 | [[ 4. 0. 0.] 837 | [-0. 4. -1.] 838 | [ 0. -1. 4.]] 839 | 840 | Notes 841 | ----- 842 | .. seealso:: `scipy.linalg.solve` 843 | 844 | """ 845 | return la.solve(B.T, C.T, **kwargs).T 846 | 847 | 848 | def real_modes(Psi, autorotate=True): 849 | r"""Real modes from complex modes. 850 | 851 | Assuming a transformation 852 | 853 | .. math:: Psi_{real} = Psi_{complex} T 854 | 855 | exists, where :math:`T` is a complex transformation matrix, find 856 | :math:`Psi_{real}` using linear algebra. 857 | 858 | Parameters 859 | ---------- 860 | Psi : complex float array 861 | Complex mode shapes (displacement) 862 | autorotate : Boolean, optional 863 | Attempt to rotate to near-real first 864 | 865 | Returns 866 | ------- 867 | Psi : float array 868 | Real modes 869 | 870 | Examples 871 | -------- 872 | >>> import vibrationtesting as vt 873 | >>> M = np.array([[4, 0, 0], 874 | ... [0, 4, 0], 875 | ... [0, 0, 4]]) 876 | >>> Cso = np.array([[.1,0,0], 877 | ... [0,0,0], 878 | ... [0,0,0]]) 879 | >>> K = np.array([[8, -4, 0], 880 | ... [-4, 8, -4], 881 | ... [0, -4, 4]]) 882 | >>> Bt = np.array([[1],[0],[0]]) 883 | >>> Ca = np.array([[1,0,0]]) 884 | >>> Cd = Cv = np.zeros_like(Ca) 885 | >>> A, B, C, D = vt.so2ss(M, Cso, K, Bt, Cd, Cv, Ca) 886 | >>> Am, Bm, Cm, Dm, eigenvalues, modes = vt.ss_modal(A, B, C, D) 887 | >>> Psi = vt.real_modes(modes[:,0::2]) 888 | 889 | Notes 890 | ----- 891 | .. note:: Rotation of modes should be performed to get them as close to 892 | real as possible first. 893 | .. warning:: Current autorotate bases the rotation on de-rotating the first 894 | element of each vector. User can use their own pre-process by doing to 895 | and setting `autorotate` to False. 896 | 897 | """ 898 | if autorotate is True: 899 | Psi = Psi@np.diag(np.exp(np.angle(Psi[0, :]) * -1j)) 900 | Psi_real = np.real(Psi) 901 | Psi_im = np.imag(Psi) 902 | 903 | Psi = Psi_real - Psi_im @ la.lstsq(Psi_real, Psi_im)[0] 904 | return Psi 905 | 906 | 907 | def ss_modal(A, B=None, C=None, D=None): 908 | r"""State space modes, frequencies, damping ratios, and modal matrices. 909 | 910 | Parameters 911 | ---------- 912 | A, B, C, D : float arrays 913 | State space system matrices 914 | 915 | Returns 916 | ------- 917 | Am, Bm, Cm, Dm : float arrays 918 | Modal state space system matrices 919 | 920 | Examples 921 | -------- 922 | >>> import vibrationtesting as vt 923 | >>> M = np.array([[4, 0, 0], 924 | ... [0, 4, 0], 925 | ... [0, 0, 4]]) 926 | >>> Cso = np.array([[.1,0,0], 927 | ... [0,0,0], 928 | ... [0,0,0]]) 929 | >>> K = np.array([[8, -4, 0], 930 | ... [-4, 8, -4], 931 | ... [0, -4, 4]]) 932 | >>> Bt = np.array([[1],[0],[0]]) 933 | >>> Ca = np.array([[1,0,0]]) 934 | >>> Cd = Cv = np.zeros_like(Ca) 935 | >>> A, B, C, D = vt.so2ss(M, Cso, K, Bt, Cd, Cv, Ca) 936 | >>> Am, Bm, Cm, Dm, eigenvalues, modes = vt.ss_modal(A, B, C, D) 937 | >>> np.allclose(Am,np.array( 938 | ... [[-0.0013+0.445j, 0.0000+0.j, 0.0000+0.j, 0.0000+0.j, 939 | ... 0.0000+0.j, 0.0000+0.j ], 940 | ... [ 0.0000+0.j, -0.0013-0.445j, 0.0000+0.j, 0.0000+0.j, 941 | ... 0.0000+0.j, 0.0000+0.j ], 942 | ... [ 0.0000+0.j, 0.0000+0.j, -0.0068+1.247j, 0.0000+0.j, 943 | ... 0.0000+0.j, 0.0000+0.j ], 944 | ... [ 0.0000+0.j, 0.0000+0.j, 0.0000+0.j, -0.0068-1.247j, 945 | ... 0.0000+0.j, 0.0000+0.j ], 946 | ... [ 0.0000+0.j, 0.0000+0.j, 0.0000+0.j, 0.0000+0.j, 947 | ... -0.0044+1.8019j, 0.0000+0.j ], 948 | ... [ 0.0000+0.j, 0.0000+0.j, 0.0000+0.j, 0.0000+0.j, 949 | ... 0.0000+0.j, -0.0044-1.8019j]]), atol=0.001) 950 | True 951 | >>> Cm 952 | [[ 0.0594-0.0001j 0.0594+0.0001j 0.0039-0.717j 0.0039+0.717j 953 | 0.0241-0.9307j 0.0241+0.9307j]] 954 | 955 | 956 | """ 957 | if B is None: 958 | B = np.zeros_like(A) 959 | 960 | if C is None: 961 | C = np.zeros_like(A) 962 | 963 | if D is None: 964 | D = np.zeros_like(A) 965 | 966 | eigenvalues, vectors = la.eig(A) 967 | 968 | idxp = abs(eigenvalues).argsort() 969 | eigenvalues = eigenvalues[idxp] 970 | vectors = vectors[:, idxp] 971 | 972 | A_modal = la.solve(vectors, A)@vectors 973 | B_modal = la.solve(vectors, B) 974 | C_modal = C@vectors 975 | # D_modal = D wasted CPUs 976 | return A_modal, B_modal, C_modal, D, eigenvalues, vectors 977 | --------------------------------------------------------------------------------