├── .envrc ├── .gitignore ├── .readthedocs.yml ├── COPYING ├── README.org ├── TODO.org ├── docs ├── .nojekyll ├── Makefile ├── conf.py ├── contributing.rst ├── guide │ ├── index.rst │ └── pnlss.rst ├── index.rst ├── install.rst ├── modules │ ├── index.rst │ ├── modules.rst │ ├── pyvib.common.rst │ ├── pyvib.data.rst │ ├── pyvib.filter.rst │ ├── pyvib.fnsi.rst │ ├── pyvib.forcing.rst │ ├── pyvib.frf.rst │ ├── pyvib.frf_test.rst │ ├── pyvib.interpolate.rst │ ├── pyvib.modal.rst │ ├── pyvib.morletWT.rst │ ├── pyvib.newmark.rst │ ├── pyvib.nlforce.rst │ ├── pyvib.nnm.rst │ ├── pyvib.pnlss.rst │ ├── pyvib.rfs.rst │ ├── pyvib.rst │ ├── pyvib.signal.rst │ ├── pyvib.spline.rst │ ├── pyvib.statespace.rst │ ├── pyvib.subspace.rst │ ├── pyvib.test.rst │ └── pyvib.test2.rst └── user-guide.rst ├── examples ├── 2dof │ ├── 2dof_identification.py │ ├── 2dof_simulation.py │ ├── fnsi.py │ ├── hb.py │ ├── hb_plot.py │ ├── newmark.py │ ├── nnm.py │ ├── nnm_plot.py │ ├── ode.py │ ├── parameters.py │ ├── pyds.py │ ├── pyds_stepped.py │ ├── pyds_stepped_plot.py │ ├── rfs.py │ └── wt.py ├── README.txt ├── contact │ ├── fnsi.py │ ├── hb.py │ ├── hb_plot.py │ ├── pyds.py │ ├── rfs.py │ ├── test.py │ └── wt.py ├── nlbeam │ ├── fnsi.py │ ├── hb.py │ ├── hb_plot.py │ ├── newmark.py │ ├── nnm.py │ ├── nnm_plot.py │ ├── rfs.py │ └── wt.py ├── nnm │ ├── nnm.py │ ├── nnm_periodic.py │ └── similar_nnm.mw └── pnlss │ ├── boucwen.py │ ├── boucwen_plot.py │ ├── fnsi_test.py │ ├── hammerstein_x0u0.py │ ├── multisine.py │ ├── nlbeam_pnlss.py │ ├── nlss.py │ ├── nlss_comparison.py │ ├── silverbox_benchmark.py │ ├── silverbox_jp.py │ ├── silverbox_jp_fnsi.py │ ├── silverbox_process.py │ ├── test.py │ └── tutorial.py ├── flake.lock ├── flake.nix ├── push_documentation.py ├── pylint.cfg ├── pyvib ├── __init__.py ├── common.py ├── filter.py ├── fnsi.py ├── forcing.py ├── frf.py ├── hb │ ├── __init__.py │ ├── bifurcation.py │ ├── hb.py │ ├── hbcommon.py │ └── stability.py ├── helper │ ├── __init__.py │ ├── modal_plotting.py │ └── plotting.py ├── interpolate.py ├── lti_conversion.py ├── modal.py ├── morletWT.py ├── newmark.py ├── nlforce.py ├── nlss.py ├── nnm.py ├── nonlinear_elements.py ├── nonlinear_elements_newmark.py ├── pnlss.py ├── polynomial.py ├── rfs.py ├── signal.py ├── spline.py ├── statespace.py ├── subspace.py └── utils │ ├── __init__.py │ ├── config.py │ ├── decorators.py │ ├── misc.py │ ├── pyvibrc │ ├── sample.py │ └── sysinfo.py ├── requirements.txt ├── setup.cfg ├── setup.py └── test ├── forcing_test.py ├── multisine_test.py └── plot_helper.py /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | !/examples/nlbeam/data/NLBeam.mat 2 | dop853_temp/ 3 | radau5_temp/ 4 | __pycache__/ 5 | *.vtu 6 | data/ 7 | *.png 8 | *.pdf 9 | *.tex 10 | driver/ 11 | meshes/ 12 | plots/ 13 | *.pyc 14 | *.pkl 15 | *.npz 16 | !/examples/nlbeam/data/*.mat 17 | !/examples/pnlss/data/ 18 | docs/_build/ 19 | *.mat 20 | /.direnv/ 21 | /result 22 | /_build/ 23 | /pyvib.egg-info/ 24 | build/ 25 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | 3 | build: 4 | image: latest 5 | 6 | python: 7 | version: 3.6 8 | use_system_site_packages: true 9 | setup_py_install: true 10 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | The principal copyright owner for pyvib is Paw Møller 2 | 3 | Copyright (c) 2017-2019 Paw Møller 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions 8 | are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright 11 | notice, this list of conditions and the following disclaimer. 12 | 2. Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 | ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 22 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 23 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 25 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | .. image:: images/logo_small.svg 2 | 3 | * Pyvib 4 | 5 | This repository contains the pyvib packages. A python library for nonlinear 6 | state space modeling using white-, gray- and blackbox models. See the 7 | [[https://pyvib.readthedocs.io][documentation]] for further info. 8 | 9 | 10 | ** Resources 11 | 12 | - [[https://pyvib.readthedocs.io][API documentation]] - technical details on all functions 13 | - [[https://github.com/pawsen/pyvib/tree/master/doc/Tutorial/Getting%20Started.ipynb][Getting-started 14 | guide]] - tutorial page (run with [[http://jupyter.org/][jupyter]] to get interactive 15 | features) 16 | - [[https://github.com/pawsen/pyvib/tree/master/examples][Examples page]] - 17 | stand-alone executables of different applications 18 | 19 | ** Dependencies 20 | 21 | *** Required 22 | 23 | This library requires numpy, scipy and python3.7. E.g. on Linux: 24 | 25 | #+BEGIN_SRC sh 26 | pip install numpy, scipy 27 | #+END_SRC 28 | 29 | ** nixos 30 | 31 | Use =flake.nix= to setup a development env on nix. 32 | 33 | Use either 34 | : nix develop 35 | to an environment with black/isort/pyright 36 | 37 | or 38 | : nix build 39 | to get a link to the package in =./result= 40 | 41 | ** Useful Links 42 | Although this package does not have anything to do with control, state space 43 | models are often used for this. See these packages for more(taken from harold) 44 | 45 | - There is already an almost-matured control toolbox which is led by 46 | Richard Murray et al. and it can perform 47 | already most of the essential tasks. Hence, if you want to have 48 | something that resembles the basics of matlab control toolbox, you should give 49 | it a try. However, it is somewhat limited to SISO tools and also relies on 50 | SLICOT library which can lead to installation hassle and/or licensing 51 | problems for nontrivial tasks. 52 | https://github.com/python-control/python-control 53 | 54 | - You can also use the tools available in SciPy ``signal`` module for basics 55 | of LTI system manipulations. SciPy is a powerful all-purpose scientific 56 | package. This makes it extremely useful however admittedly every discipline 57 | has a limited presence hence the limited functionality. If you are looking 58 | for a quick LTI system manipulation and don't want to install yet another 59 | package, then it might be the tool for you. 60 | https://docs.scipy.org/doc/scipy/reference/signal.html 61 | 62 | - Instead, if you are interested in robust control you probably would 63 | appreciate the `Skogestad-Python`_ project. They are replicating the 64 | code parts of the now-classic book completely in Python. Awesome! 65 | https://github.com/alchemyst/Skogestad-Python 66 | 67 | - harold 68 | MIMO systems and without dependency for SLICOT 69 | https://github.com/ilayn/harold/ 70 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawsen/pyvib/3fd96436ef9f27c2743b11aa55f2fd1b713fbe10/docs/.nojekyll -------------------------------------------------------------------------------- /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 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Configuration file for the Sphinx documentation builder. 5 | # 6 | # This file does only contain a selection of the most common options. For a 7 | # full list see the documentation: 8 | # http://www.sphinx-doc.org/en/master/config 9 | 10 | # -- Path setup -------------------------------------------------------------- 11 | 12 | # If extensions (or modules to document with autodoc) are in another directory, 13 | # add these directories to sys.path here. If the directory is relative to the 14 | # documentation root, use os.path.abspath to make it absolute, like shown here. 15 | # 16 | from datetime import datetime 17 | import os 18 | import sys 19 | import pyvib 20 | 21 | sys.path.insert(0, os.path.abspath('../')) 22 | 23 | # -- Project information ----------------------------------------------------- 24 | 25 | project = u'pyvib' 26 | author = u'Paw' 27 | copyright = u'2017–{0}, '.format(datetime.utcnow().year) + author 28 | 29 | # The short X.Y version. 30 | version = '.'.join(pyvib.__version__.split('.', 2)[:2]) 31 | # The full version, including alpha/beta/rc tags. 32 | release = pyvib.__version__ 33 | 34 | 35 | # -- General configuration --------------------------------------------------- 36 | 37 | # If your documentation needs a minimal Sphinx version, state it here. 38 | # 39 | # needs_sphinx = '1.0' 40 | 41 | # Add any Sphinx extension module names here, as strings. They can be 42 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 43 | # ones. 44 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 45 | 'sphinx.ext.mathjax', 'sphinx.ext.coverage', 46 | 'sphinx.ext.linkcode', 'sphinx.ext.doctest'] 47 | # napoleon: alternative to numpydoc -- looks a bit worse. 48 | # linkcode: link to github, see linkcode_resolve() below] 49 | napoleon_google_docstring = False 50 | napoleon_use_param = False 51 | napoleon_use_ivar = True 52 | 53 | # See https://github.com/rtfd/readthedocs.org/issues/283 54 | mathjax_path = ('https://cdn.mathjax.org/mathjax/latest/MathJax.js?' 55 | 'config=TeX-AMS-MML_HTMLorMML') 56 | 57 | 58 | # Add any paths that contain templates here, relative to this directory. 59 | templates_path = ['_templates'] 60 | 61 | # The suffix(es) of source filenames. 62 | # You can specify multiple suffix as a list of string: 63 | # 64 | source_suffix = ['.rst'] 65 | 66 | # The master toctree document. 67 | master_doc = 'index' 68 | 69 | # The language for content autogenerated by Sphinx. Refer to documentation 70 | # for a list of supported languages. 71 | # 72 | # This is also used if you do content translation via gettext catalogs. 73 | # Usually you set "language" from the command line for these cases. 74 | language = None 75 | 76 | # List of patterns, relative to source directory, that match files and 77 | # directories to ignore when looking for source files. 78 | # This pattern also affects html_static_path and html_extra_path. 79 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 80 | 81 | # The name of the Pygments (syntax highlighting) style to use. 82 | pygments_style = 'sphinx' 83 | 84 | # -- Options for HTML output ------------------------------------------------- 85 | 86 | # The theme to use for HTML and HTML Help pages. See the documentation for 87 | # a list of builtin themes. 88 | # 89 | html_theme = 'sphinx_rtd_theme' # 'alabaster' 90 | #html_theme = 'alabaster' 91 | 92 | # Theme options are theme-specific and customize the look and feel of a theme 93 | # further. For a list of options available for each theme, see the 94 | # documentation. 95 | 96 | html_theme_options = {} 97 | # html_theme_options = { 98 | # "extra_nav_links": { 99 | # "🚀 Github": "https://github.com/pawsen/pyvib", 100 | # "💾 Download Releases": "https://pypi.python.org/pypi/pyvib", 101 | # }, 102 | # 'github_user': 'pawsen', 103 | # 'github_repo': 'pyvib', 104 | # 'github_button': False, 105 | # 'github_banner': True, 106 | # } 107 | 108 | # Add any paths that contain custom static files (such as style sheets) here, 109 | # relative to this directory. They are copied after the builtin static files, 110 | # so a file named "default.css" will overwrite the builtin "default.css". 111 | html_static_path = ['_static'] 112 | 113 | # Custom sidebar templates, must be a dictionary that maps document names 114 | # to template names. 115 | # 116 | # The default sidebars (for documents that don't match any pattern) are 117 | # defined by theme itself. Builtin themes are using these templates by 118 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 119 | # 'searchbox.html']``. 120 | # 121 | # html_sidebars = {} 122 | 123 | 124 | # -- Options for HTMLHelp output --------------------------------------------- 125 | 126 | # Output file base name for HTML help builder. 127 | htmlhelp_basename = 'pyvibdoc' 128 | 129 | # https://stackoverflow.com/a/42513684/1121523 130 | # https://gist.github.com/bskinn/0e164963428d4b51017cebdb6cda5209 131 | intersphinx_mapping = { 132 | 'python': ('https://docs.python.org/3.5', None), 133 | 'sphinx': ('http://www.sphinx-doc.org/en/stable/', None), 134 | 'numpy': ('http://docs.scipy.org/doc/numpy/', None), 135 | 'scipy': ('http://docs.scipy.org/doc/scipy/reference/', None), 136 | 'matplotlib': ('http://matplotlib.sourceforge.net/', None) 137 | } 138 | 139 | # ----------------------------------------------------------------------------- 140 | # Source code links 141 | # ----------------------------------------------------------------------------- 142 | # https://github.com/numpy/numpy/blob/master/doc/source/conf.py#L286 143 | import inspect 144 | from os.path import relpath, dirname 145 | 146 | for name in ['sphinx.ext.linkcode']: #, 'numpydoc.linkcode']: 147 | try: 148 | __import__(name) 149 | extensions.append(name) 150 | break 151 | except ImportError: 152 | pass 153 | else: 154 | print("NOTE: linkcode extension not found -- no links to source generated") 155 | 156 | def linkcode_resolve(domain, info): 157 | """ 158 | Determine the URL corresponding to Python object 159 | """ 160 | if domain != 'py': 161 | return None 162 | 163 | modname = info['module'] 164 | fullname = info['fullname'] 165 | 166 | submod = sys.modules.get(modname) 167 | if submod is None: 168 | return None 169 | 170 | obj = submod 171 | for part in fullname.split('.'): 172 | try: 173 | obj = getattr(obj, part) 174 | except Exception: 175 | return None 176 | 177 | # strip decorators, which would resolve to the source of the decorator 178 | # possibly an upstream bug in getsourcefile, bpo-1764286 179 | try: 180 | unwrap = inspect.unwrap 181 | except AttributeError: 182 | pass 183 | else: 184 | obj = unwrap(obj) 185 | 186 | try: 187 | fn = inspect.getsourcefile(obj) 188 | except Exception: 189 | fn = None 190 | if not fn: 191 | return None 192 | 193 | try: 194 | source, lineno = inspect.getsourcelines(obj) 195 | except Exception: 196 | lineno = None 197 | 198 | if lineno: 199 | linespec = "#L%d-L%d" % (lineno, lineno + len(source) - 1) 200 | else: 201 | linespec = "" 202 | 203 | fn = relpath(fn, start=dirname(pyvib.__file__)) 204 | 205 | return "https://github.com/pawsen/pyvib/blob/master/pyvib/%s%s" % (fn, linespec) 206 | 207 | # if 'dev' in numpy.__version__: 208 | # return "https://github.com/pawsen/vib/blob/master/vib/%s%s" % ( 209 | # fn, linespec) 210 | # else: 211 | # return "https://github.com/numpy/numpy/blob/v%s/numpy/%s%s" % ( 212 | # numpy.__version__, fn, linespec) 213 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: pyvib 2 | 3 | Contributing 4 | ============ 5 | 6 | I happily accept contributions. 7 | 8 | If you wish to add a new feature or fix a bug: 9 | 10 | #. `Check for open issues `_ or open 11 | a fresh issue to start a discussion around a feature idea or a bug. 12 | #. Fork the `pyvib repository on Github `_ 13 | to start making your changes. 14 | #. Write a test which shows that the bug was fixed or that the feature works 15 | as expected. 16 | #. Send a pull request and bug the maintainer until it gets merged and published. 17 | :) Make sure to add yourself to ``CONTRIBUTORS.txt``. 18 | 19 | 20 | Setting up your development environment 21 | --------------------------------------- 22 | 23 | It is recommended, and even enforced by the make file, that you use a 24 | `virtualenv 25 | `_:: 26 | 27 | $ python3 -m venv venv3 28 | $ source venv3/bin/activate 29 | $ pip install -r dev-requirements.txt 30 | 31 | 32 | Running the tests 33 | ----------------- 34 | 35 | Run the test suite to ensure no additional problem arises. Our ``Makefile`` 36 | handles much of this for you as long as you're running it `inside of a 37 | virtualenv `_:: 38 | 39 | $ make test 40 | [... magically installs dependencies and runs tests on your virtualenv] 41 | Ran 182 tests in 1.633s 42 | 43 | OK (SKIP=6) 44 | -------------------------------------------------------------------------------- /docs/guide/index.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | User's Guide 3 | ============ 4 | 5 | Welcome to the user guide for pyvib 6 | 7 | 8 | 9 | Analyzing 10 | --------- 11 | 12 | 13 | Modeling 14 | --------- 15 | 16 | 17 | 18 | Understanding 19 | ------------- 20 | 21 | 22 | .. toctree:: 23 | :maxdepth: 3 24 | -------------------------------------------------------------------------------- /docs/guide/pnlss.rst: -------------------------------------------------------------------------------- 1 | PNLSS 2 | ===== 3 | 4 | 5 | 6 | 7 | http://homepages.ulb.ac.be/~jschouk/toolboxes/pnlss/pnlss_v1_0.pdf 8 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: pyvib 2 | 3 | Welcome to the pyvib documentation 4 | ================================== 5 | 6 | Pyvib is a python program for analyzing nonlinear vibrations and estimating(and 7 | simulating) models from measured data. 8 | 9 | The highlights are 10 | 11 | - Analyzing (working on data): 12 | **Restoring force surface** (RFS) to visualize the functional form of the 13 | nonlinearties. 14 | 15 | **Wavelet transform** gives a frequency and time resolution plots from where 16 | the type of nonlinearity can be deducted. 17 | 18 | **Quantification of noise and nonlinear distortion** using the best linear 19 | approximation (BLA). 20 | * Modeling (working on data): 21 | White box, using subspace identification and specified polynomials and 22 | splines to model the identified nonlinearities, known as **frequency 23 | nonlinear system identification** (FNSI). 24 | 25 | Black box, using **polynomial nonlinear state-space** (PNLSS). 26 | * Understanding (working on identified FE model): 27 | **Harmonic balance continuation** to reveal bifurcations and jumps 28 | 29 | **Nonlinear normal modes** to reveal internal resonances and energy transfer 30 | between modes [#nnm]_. 31 | 32 | See the `references`_ for description of the methods and the `credits`_. 33 | 34 | Usage 35 | ----- 36 | 37 | The :doc:`user-guide` provides a detailed description of the `pnlss example 38 | `_, to 39 | show how pyvib works. See the `examples directory 40 | `_ for additional examples. 41 | 42 | The :doc:`modules/index` documentation provides API-level documentation. 43 | :doc:`contributing` shows how to contribute to the program or report bugs. 44 | 45 | 46 | .. toctree:: 47 | :maxdepth: 2 48 | :caption: Contents: 49 | 50 | install 51 | user-guide 52 | modules/index 53 | contributing 54 | 55 | 56 | Credits 57 | ------- 58 | 59 | The PNLSS functionality is a translation of a matlab program written by the 60 | staff at `Vrije Universiteit Brussel 61 | `_ (VUB) . The documentation is 62 | written by `Koen Tiels `_ and he have 63 | also written a short primer on `pnlss `_ 64 | 65 | The FNSI method is developed by `Jean-Philippe Noël `_. He 66 | also kindly provided a matlab implementation of the spline method during my 67 | thesis. 68 | 69 | References 70 | ---------- 71 | 72 | PNLSS: Identification of nonlinear systems using polynomial nonlinear state 73 | space models. 74 | `PhD thesis 75 | `_. 76 | `article `_ 77 | 78 | FNSI: Frequency-domain subspace identification for nonlinear mechanical systems. 79 | PhD thesis Not longer available online. I will ask if it can be uploaded. 80 | `article `_ 81 | 82 | RFS and wavelet transform are described in the PhD thesis of J.P. Noël. 83 | 84 | HBC: The harmonic balance method for bifurcation analysis of large-scale 85 | nonlinear mechanical systems. 86 | PhD thesis Not longer available online. 87 | `article `_ 88 | 89 | NNM: Nonlinear normal modes, Part II: Toward a practical computation using 90 | numerical continuation techniques 91 | PhD thesis by Maxime Peeters no longer available. 92 | `article `_ 93 | 94 | You can also read my master thesis which describe the methods and provides 95 | additional references. 96 | 97 | The project is on `GitHub`_ and you are welcome to modify, comment or suggest changes to the code. 98 | 99 | Written by `Paw `_, PhD student at the University of 100 | Liege. 101 | 102 | Indices and tables 103 | ================== 104 | 105 | * :ref:`genindex` 106 | * :ref:`modindex` 107 | * :ref:`search` 108 | 109 | 110 | .. _pawsen@gmail.com: mailto:pawsen@gmail.com 111 | .. _GitHub: https://github.com/pawsen/pyvib 112 | 113 | .. rubric:: Footnotes 114 | .. [#nnm] Note this uses the shooting method. For a usable, practical implementation the harmonic balance method should be used instead. 115 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: sh 2 | .. currentmodule:: pyvib 3 | 4 | Installation 5 | ============ 6 | 7 | This tutorial will walk you through the process of installing pyvib. To follow, 8 | you really only need two basic things: 9 | 10 | * A working `Python3 `_ installation. Python3.6 is 11 | required. 12 | * The python packages *numpy*, *scipy*, *matplotlib* 13 | 14 | Step 0: Install prerequisites 15 | ----------------------------- 16 | **Recommended**: 17 | 18 | The necessary Python packages can be installed via the Anaconda 19 | Python distribution (https://www.anaconda.com/download/). Python 3 is needed. 20 | Anaconda is a collection of often used python packages, the python program and 21 | `conda`, a open source package management system. It runs on Windows, macOS and 22 | Linux. Especially on windows it makes the installation process simpler, as numpy 23 | and scipy depends on external libraries which can be difficult to install, but 24 | are automatically included when `conda` is used to install the python packages. 25 | 26 | If you do not want to install all the packages included in Anaconda (some 3GB of 27 | space), `miniconda `_ can be installed instead 28 | which just includes conda and the python program. The needed packages NumPy, 29 | SciPy and matplotlib can be installed in miniconda via the command:: 30 | 31 | $ conda install numpy scipy matplotlib 32 | 33 | **Manual** 34 | 35 | If you do not want to use `conda`, the packages can be installed on a 36 | debian-like(linux) system:: 37 | 38 | $ sudo apt install python3 python3-pip 39 | $ pip3 install numpy scipy matplotlib 40 | 41 | On windows, check these wheels (binaries) for pip: 42 | `numpy `_ 43 | `scipy `_ 44 | 45 | Step 1: Download and unpack pyvib 46 | ----------------------------------- 47 | 48 | `Download pyvib `_ and unpack it:: 49 | 50 | $ unzip pyvib-master.zip 51 | 52 | If you're downloading from git, do:: 53 | 54 | $ git clone https://github.com/pawsen/pyvib.git 55 | 56 | Step 2: Build pyvib 57 | -------------------- 58 | 59 | Just type:: 60 | 61 | $ cd pyvib-master # if you're not there already 62 | $ python setup.py install 63 | 64 | Step 3: Test pyvib 65 | ------------------- 66 | Just type:: 67 | 68 | $ cd test 69 | $ py.test 70 | 71 | Advanced 72 | -------- 73 | 74 | Get better speed by linking numpy and scipy with optimised blas libraries. This 75 | is taken care of if you uses the anaconda distribution. Anaconda default ships 76 | with `Intel Math Kernel Library` (MKL) blas, which is probably the fastest blas 77 | implementation. It is Open License (not Open Source). If you wish an Open Source 78 | blas, numpy/scipy linked to OpenBlas can be installed with:: 79 | 80 | $ conda install -c conda-forge numpy scipy 81 | 82 | `-c` specifies the channel to install from and `conda-forge` is a community 83 | driven package central. In the folder `~/miniconda3/conda-meta/` you can see 84 | which blas the current numpy is shipped with. 85 | 86 | Another reason the prefer OpenBlas is space considerations. MKL takes around 87 | 800MB whereas OpenBlas is < 10MB. 88 | 89 | If you install numpy/scipy from pip they are linked towards OpenBlas. 90 | -------------------------------------------------------------------------------- /docs/modules/index.rst: -------------------------------------------------------------------------------- 1 | Modules 2 | ======= 3 | 4 | * :ref:`genindex` 5 | * :ref:`modindex` 6 | * :ref:`search` 7 | 8 | List of python packages 9 | 10 | .. toctree:: 11 | :maxdepth: 4 12 | 13 | pyvib 14 | -------------------------------------------------------------------------------- /docs/modules/modules.rst: -------------------------------------------------------------------------------- 1 | pyvib 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | pyvib 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.common.rst: -------------------------------------------------------------------------------- 1 | pyvib.common module 2 | =================== 3 | 4 | .. automodule:: pyvib.common 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.data.rst: -------------------------------------------------------------------------------- 1 | pyvib.data module 2 | ================= 3 | 4 | .. automodule:: pyvib.data 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.filter.rst: -------------------------------------------------------------------------------- 1 | pyvib.filter module 2 | =================== 3 | 4 | .. automodule:: pyvib.filter 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.fnsi.rst: -------------------------------------------------------------------------------- 1 | pyvib.fnsi module 2 | ================= 3 | 4 | .. automodule:: pyvib.fnsi 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.forcing.rst: -------------------------------------------------------------------------------- 1 | pyvib.forcing module 2 | ==================== 3 | 4 | .. automodule:: pyvib.forcing 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.frf.rst: -------------------------------------------------------------------------------- 1 | pyvib.frf module 2 | ================ 3 | 4 | .. automodule:: pyvib.frf 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.frf_test.rst: -------------------------------------------------------------------------------- 1 | pyvib.frf\_test module 2 | ====================== 3 | 4 | .. automodule:: pyvib.frf_test 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.interpolate.rst: -------------------------------------------------------------------------------- 1 | pyvib.interpolate module 2 | ======================== 3 | 4 | .. automodule:: pyvib.interpolate 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.modal.rst: -------------------------------------------------------------------------------- 1 | pyvib.modal module 2 | ================== 3 | 4 | .. automodule:: pyvib.modal 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.morletWT.rst: -------------------------------------------------------------------------------- 1 | pyvib.morletWT module 2 | ===================== 3 | 4 | .. automodule:: pyvib.morletWT 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.newmark.rst: -------------------------------------------------------------------------------- 1 | pyvib.newmark module 2 | ==================== 3 | 4 | .. automodule:: pyvib.newmark 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.nlforce.rst: -------------------------------------------------------------------------------- 1 | pyvib.nlforce module 2 | ==================== 3 | 4 | .. automodule:: pyvib.nlforce 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.nnm.rst: -------------------------------------------------------------------------------- 1 | pyvib.nnm module 2 | ================ 3 | 4 | .. automodule:: pyvib.nnm 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.pnlss.rst: -------------------------------------------------------------------------------- 1 | pyvib.pnlss module 2 | ================== 3 | 4 | .. automodule:: pyvib.pnlss 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.rfs.rst: -------------------------------------------------------------------------------- 1 | pyvib.rfs module 2 | ================ 3 | 4 | .. automodule:: pyvib.rfs 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.rst: -------------------------------------------------------------------------------- 1 | pyvib package 2 | ============= 3 | 4 | Submodules 5 | ---------- 6 | 7 | .. toctree:: 8 | 9 | pyvib.common 10 | pyvib.data 11 | pyvib.filter 12 | pyvib.fnsi 13 | pyvib.forcing 14 | pyvib.frf 15 | pyvib.frf_test 16 | pyvib.interpolate 17 | pyvib.modal 18 | pyvib.morletWT 19 | pyvib.newmark 20 | pyvib.nlforce 21 | pyvib.nnm 22 | pyvib.pnlss 23 | pyvib.rfs 24 | pyvib.signal 25 | pyvib.spline 26 | pyvib.statespace 27 | pyvib.subspace 28 | pyvib.test 29 | pyvib.test2 30 | 31 | Module contents 32 | --------------- 33 | 34 | .. automodule:: pyvib 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | -------------------------------------------------------------------------------- /docs/modules/pyvib.signal.rst: -------------------------------------------------------------------------------- 1 | pyvib.signal module 2 | =================== 3 | 4 | .. automodule:: pyvib.signal 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.spline.rst: -------------------------------------------------------------------------------- 1 | pyvib.spline module 2 | =================== 3 | 4 | .. automodule:: pyvib.spline 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.statespace.rst: -------------------------------------------------------------------------------- 1 | pyvib.statespace module 2 | ======================= 3 | 4 | .. automodule:: pyvib.statespace 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.subspace.rst: -------------------------------------------------------------------------------- 1 | pyvib.subspace module 2 | ===================== 3 | 4 | .. automodule:: pyvib.subspace 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.test.rst: -------------------------------------------------------------------------------- 1 | pyvib.test module 2 | ================= 3 | 4 | .. automodule:: pyvib.test 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/modules/pyvib.test2.rst: -------------------------------------------------------------------------------- 1 | pyvib.test2 module 2 | ================== 3 | 4 | .. automodule:: pyvib.test2 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | -------------------------------------------------------------------------------- /docs/user-guide.rst: -------------------------------------------------------------------------------- 1 | .. currentmodule:: pyvib 2 | 3 | User Guide 4 | ========== 5 | 6 | Below is a detailed description of the `pnlss example 7 | `_, to 8 | show how pyvib works. See the 9 | `examples directory `_ for 10 | additional examples and :doc:`modules/index` for description of individual functions. 11 | 12 | For a description of the pnlss methods, see the `guide from VUB 13 | `_. They also provide a 14 | `matlab program `_. 15 | 16 | 17 | 18 | In python we need to explicit load the functionality we want to use. Thus we 19 | start by loading the necessary packages.:: 20 | 21 | from pyvib.statespace import StateSpace as linss 22 | from pyvib.statespace import Signal 23 | from pyvib.pnlss import PNLSS 24 | from pyvib.common import db 25 | from pyvib.forcing import multisine 26 | import numpy as np 27 | import matplotlib.pyplot as plt 28 | 29 | `numpy` provides matrix support(termed arrays no matter the dimension) and 30 | linear algebra. `matplotlib` is a plotting library mimicking the plotting in 31 | matlab. 32 | 33 | In pyvib the models are stored as object. This makes it easier to compare 34 | different models in the same script. `StateSpace` is the class for linear state 35 | space models and `PNLSS` is the class for pnlss models. 36 | To create a object we call the class. To call the `StateSpace` class we need to 37 | provide a signal object, which is first created:: 38 | 39 | # create signal object 40 | sig = Signal(u,y) 41 | sig.set_lines(lines) 42 | um, ym = sig.average() 43 | npp, F = sig.npp, sig.F 44 | R, P = sig.R, sig.P 45 | 46 | We provide the measured signals(u,y) which will be used for estimation. Then we 47 | call the method `set_lines` of the object and give the excited lines as input. 48 | The signal object calculate some of the signal properties automatically, as the 49 | 'number of points per period' (npp), 'number of excited lines' (F), etc. By 50 | calling `sig.average()`, the signals u,y are averaged over periods P. 51 | 52 | A empty linear state space object is created, and when the method 53 | `linmodel.bla(sig)` is called, the 'best linear approximation', total 54 | distortion, and noise distortion are calculated using the signal stored in `sig`.:: 55 | 56 | linmodel = linss() 57 | linmodel.bla(sig) 58 | models, infodict = linmodel.scan(nvec, maxr) 59 | lin_errvec = linmodel.extract_model(yval, uval) 60 | 61 | `nvec` and `maxr` are specifies the dimension parameters for the subspace 62 | identification. The best model among the identified is extracted by comparing 63 | the error on new, unseen validation data. 64 | Now `linmodel` contains the best subspace model. To see all the variables 65 | contained within `linmodel` run the following in a python interpreter or see 66 | :class:`pyvib.statespace.StateSpace`:: 67 | 68 | dir(linmodel) # show all the variable names 69 | vars(linmodel) # show all variables along with their data 70 | 71 | The pnlss model is initialized from linmodel. We start by setting the structure 72 | of the model (full, ie. mix of all possible monomials in the state and output 73 | equation for degree 2 and 3).:: 74 | 75 | T1 = np.r_[npp, np.r_[0:(R-1)*npp+1:npp]] 76 | model = PNLSS(linmodel.A, linmodel.B, linmodel.C, linmodel.D) 77 | model.signal = linmodel.signal 78 | model.nlterms('x', [2,3], 'full') 79 | model.nlterms('y', [2,3], 'full') 80 | model.transient(T1) 81 | model.optimize(lamb=100, weight=True, nmax=60) 82 | 83 | `T1` is the transient settings, ie. how many time samples is prepended each 84 | realization in the averaged time signal `um`. This is done to ensure steady state 85 | of the output, when the model is simulated using `um`. In this case there are two 86 | realizations and each have 1024 points. This gives:: 87 | 88 | print(T1) 89 | >>> array([1024, 0, 1024]) 90 | 91 | meaning that 1024 samples are prepended and the starting index of the 92 | realizations are 0 and 1014 (remember python is 0-indexed). 93 | 94 | To see the difference between the linear and nonlinear model, the output is 95 | calculated using the same data used for estimation. Remember `um` is the 96 | averaged signal calculated earlier:: 97 | 98 | tlin, ylin, xlin = linmodel.simulate(um, T1=T1) 99 | _, ynlin, _ = model.simulate(um) 100 | 101 | We now have a pnlss model using at most `nmax=60` Levenberg–Marquardt steps. To 102 | prevent overfitting the model at each step is saved and all models are validated 103 | using unseen validation data; the best performing model is kept. Note that the 104 | transient settings is changed as there is only one realization.:: 105 | 106 | nl_errvec = model.extract_model(yval, uval, T1=sig.npp) 107 | 108 | `nl_errvec` contains the error of each model. 109 | 110 | Finally we plot the linear and nonlinear model error. As found from the 111 | estimation data calculated above:: 112 | 113 | plt.figure() 114 | plt.plot(ym) 115 | plt.plot(ym-ylin) 116 | plt.plot(ym-ynlin) 117 | plt.xlabel('Time index') 118 | plt.ylabel('Output (errors)') 119 | plt.legend(('output','linear error','PNLSS error')) 120 | plt.title('Estimation results') 121 | figs['estimation_error'] = (plt.gcf(), plt.gca()) 122 | 123 | `figs` is a dictionary (dict) used for storing the handle and axes for the 124 | figure; the handle is used later for storing the figure to disk. dicts resembles 125 | matlab cells and are a way of storing data using a string as index/key. 126 | 127 | -------------------------------------------------------------------------------- /examples/2dof/fnsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # common python libraries 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from collections import namedtuple 8 | import pickle 9 | 10 | # pyvib.ation libraries 11 | from pyvib.signal import Signal_legacy 12 | from pyvib.fnsi import FNSI 13 | from pyvib.modal import modal_ac, frf_mkc 14 | from pyvib.helper.modal_plotting import (plot_knl, plot_frf, plot_svg) 15 | from pyvib.frf import periodic 16 | from pyvib.nlforce import NL_force, NL_polynomial, NL_spline 17 | 18 | from pyvib.nonlinear_elements import Polynomial 19 | 20 | # Parameters for 2dof model 21 | from parameters import par 22 | 23 | show_periodicity = False 24 | 25 | # conversion between Hz and rad/s 26 | sca = 2*np.pi 27 | 28 | # load data 29 | path = 'data/' 30 | filename = 'pyds_multisinevrms' 31 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs ns') 32 | lin = pickle.load(open(path + filename + '0.01' + '.pkl', 'rb')) 33 | nlin = pickle.load(open(path + filename + '2' + '.pkl', 'rb')) 34 | 35 | # which dof to get H1 estimate from/show periodicity 36 | dof = 0 37 | 38 | # Frequency interval of interest 39 | fmin = 0 40 | fmax = 5/2/np.pi 41 | # Setup the signal/extract periods 42 | slin = Signal_legacy(lin.u, lin.fs, lin.y) 43 | snlin = Signal_legacy(nlin.u, nlin.fs, nlin.y) 44 | 45 | # show periodicity, to select periods from 46 | if show_periodicity: 47 | slin.periodicity(lin.ns, dof, offset=0) 48 | snlin.periodicity(nlin.ns, dof, offset=0) 49 | 50 | per = [7,8] 51 | slin.cut(lin.ns, per) 52 | snlin.cut(lin.ns, per) 53 | 54 | # inl: connection. -1 is ground. enl: exponent. knl: coefficient. Always 1. 55 | inl = np.array([[0,-1], 56 | [1,-1]]) 57 | enl = np.array([3,3]) 58 | knl = np.array([1,1]) 59 | nl_pol = NL_polynomial(inl, enl, knl) 60 | nl = NL_force(nl_pol) 61 | 62 | # zero-based numbering of dof 63 | # idof are selected dofs. 64 | # iu are dofs of force 65 | iu = 0 66 | idof = [0,1] 67 | 68 | # ims: matrix block order. At least n+1 69 | # nmax: max model order for stabilisation diagram 70 | # ncur: model order for estimation 71 | ims = 22 72 | nmax = 20 73 | ncur = 4 74 | nlist = np.arange(2, nmax+3, 2) 75 | 76 | ## nonlinear identification at high level 77 | # Calculate stabilization diagram 78 | fnsi = FNSI() 79 | exponents = [3,3] 80 | w = np.array([[1,0,0,0], [0,1,0,0]]) 81 | nly = [Polynomial(exponent=exponents, w=w)] 82 | fnsi.set_signal(slin) 83 | fnsi.add_nl(nly=nly) 84 | fnsi.estimate(n=2, r=5) 85 | fnsi.optimize(lamb=100, nmax=25) 86 | 87 | print("oh very good") 88 | 89 | fnsi.calc_EY() 90 | fnsi.svd_comp(ims) 91 | sd = fnsi.stabilization(nlist) 92 | # Do estimation 93 | fnsi.id(ncur) 94 | fnsi.nl_coeff(iu, dof) 95 | 96 | ## linear identification at high level 97 | nl = NL_force() 98 | fnsi2 = FNSI(snlin, nl, idof, fmin, fmax) 99 | fnsi2.calc_EY() 100 | fnsi2.svd_comp(ims) 101 | fnsi2.id(ncur) 102 | fnsi2.nl_coeff(iu, dof) 103 | 104 | ## linear identification at low level 105 | fnsi3 = FNSI(slin, nl, idof, fmin, fmax) 106 | fnsi3.calc_EY() 107 | fnsi3.svd_comp(ims) 108 | fnsi3.id(ncur) 109 | fnsi3.nl_coeff(iu, dof) 110 | 111 | def print_modal(fnsi): 112 | # calculate modal parameters 113 | modal = modal_ac(fnsi.A, fnsi.C) 114 | natfreq = modal['wn'] 115 | dampfreq = modal['wd'] 116 | damping = modal['zeta'] 117 | nw = min(len(natfreq), 8) 118 | 119 | print('Undamped ω: {}'.format(natfreq[:nw]*sca)) 120 | print('damped ω: {}'.format(dampfreq[:nw]*sca)) 121 | print('damping: {}'.format(damping[:nw])) 122 | return modal 123 | 124 | print('## nonlinear identified at high level') 125 | modal = print_modal(fnsi) 126 | print('## linear identified at high level') 127 | modal2 = print_modal(fnsi2) 128 | print('## linear identified at low level') 129 | modal3 = print_modal(fnsi3) 130 | 131 | # Stabilization plot 132 | fnsi.plot_stab(sca=sca) 133 | 134 | ## Compare FRFs 135 | # FRF 136 | m = snlin.u_per.shape[0] 137 | p = snlin.y_per.shape[0] 138 | R = 1 139 | u = snlin.u_per.reshape((m,snlin.nsper,R,snlin.nper),order='F').swapaxes(0,1) 140 | y = snlin.y_per.reshape((p,snlin.nsper,R,snlin.nper),order='F').swapaxes(0,1) 141 | 142 | frf_freq, frf_H, covG, covGn = periodic(u,y, fs=snlin.fs, fmin=1e-3, fmax=5/2/np.pi) 143 | 144 | ## MCK 145 | M, C, K = par['M'], par['C'], par['K'] 146 | freq_mck, H_mck = frf_mkc(M, K, C=C, fmin=1e-3, fmax=fmax, fres=0.01) 147 | 148 | fH1, ax = plt.subplots() 149 | plot_frf(frf_freq, frf_H, p=dof, sca=sca, ax=ax, ls='-', c='k', label='From high signal') 150 | plot_frf(freq_mck, H_mck.T, p=dof, sca=sca, ax=ax, label='mck', ls=':', c='b') 151 | fnsi.plot_frf(p=dof, sca=sca, ax=ax, label='nl high', ls='--', c='C1') 152 | fnsi2.plot_frf(p=dof, sca=sca, ax=ax, label='lin high', ls='--', c='C2') 153 | fnsi3.plot_frf(p=dof, sca=sca, ax=ax, label='lin low', ls='-.', c='C3') 154 | ax.legend() 155 | #ax.legend_ = None 156 | ax.set_xlabel('Frequency (rad/s)') 157 | # For linear scale: 'Amplitude (m/N)' 158 | ax.set_ylabel('Amplitude (dB)') 159 | 160 | # The phase from FRF is shown as: 161 | m = slin.u_per.shape[0] 162 | p = slin.y_per.shape[0] 163 | R = 1 164 | u = slin.u_per.reshape((m,slin.nsper,R,slin.nper),order='F').swapaxes(0,1) 165 | y = slin.y_per.reshape((p,slin.nsper,R,slin.nper),order='F').swapaxes(0,1) 166 | frfl_freq, frfl_H, covG, covGn = periodic(u,y, fs=slin.fs, fmin=1e-3, fmax=5/2/np.pi) 167 | plt.figure() 168 | plt.plot(frf_freq*sca, np.angle(frfl_H[:,dof]) / np.pi * 180) 169 | plt.xlabel('Frequency (rad/s)') 170 | plt.ylabel('Phase angle (deg)') 171 | plt.yticks(np.linspace(-180, 180, 360//90)) 172 | 173 | 174 | plt.show() 175 | 176 | # ## nonlinear identified at high level 177 | # Undamped ω: [ 1. 3.31662479] 178 | # damped ω: [ 0.99874922 3.31624788] 179 | # damping: [ 0.05 0.01507557] 180 | # ## linear identified at high level 181 | # Undamped ω: [ 1.45233167 3.49273364] 182 | # damped ω: [ 1.45152709 3.49244239] 183 | # damping: [ 0.03328181 0.01291384] 184 | # ## linear identified at low level 185 | # Undamped ω: [ 1.0000266 3.31663515] 186 | # damped ω: [ 0.99877587 3.31625824] 187 | # damping: [ 0.04999828 0.0150755 ] 188 | -------------------------------------------------------------------------------- /examples/2dof/hb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import pickle 7 | import parameters 8 | 9 | from pyvib.hb.hb import HB 10 | from pyvib.hb.hbcommon import hb_signal 11 | from pyvib.nlforce import NL_force, NL_polynomial 12 | from pyvib.helper.plotting import nfrc 13 | 14 | savedata = True 15 | par = parameters.par 16 | M = par['M'] 17 | C = par['C'] 18 | K = par['K'] 19 | 20 | fdof = 0 21 | vrms = 2 22 | f0 = 0.6/2/np.pi 23 | f1 = 1e-4/2/np.pi 24 | f2 = 5/2/np.pi 25 | 26 | inl = par['inl'] 27 | enl = par['enl'] 28 | knl = par['knl'] 29 | 30 | par_hb ={ 31 | 'NH': 5, 32 | 'npow2': 8, 33 | 'nu': 1, 34 | 'stability': True, 35 | 'rcm_permute': False, 36 | 'tol_NR': 1e-6, 37 | 'max_it_NR': 15, 38 | 'scale_x': 1, 39 | 'scale_t': 1, 40 | 'amp0':1e-4, 41 | 'xstr':'rad/s', 42 | 'sca':1, 43 | 'anim': True, 44 | } 45 | par_cont = { 46 | 'omega_cont_min': f1*2*np.pi, 47 | 'omega_cont_max': f2*2*np.pi, 48 | 'cont_dir': 1, 49 | 'opt_it_NR': 3, 50 | 'step': 0.001, 51 | 'step_min': 0.05, 52 | 'step_max': 0.01, 53 | 'angle_max_pred': 90, 54 | 'it_cont_max': 1e6, 55 | 'adaptive_stepsize': True, 56 | 'detect':{'fold':True,'NS':True,'BP':True}, 57 | 'default_bp':True, 58 | } 59 | 60 | nl_pol = NL_polynomial(inl, enl, knl) 61 | nl = NL_force(nl_pol) 62 | hb = HB(M,C,K,nl, **par_hb) 63 | hb.periodic(f0, vrms, fdof) 64 | hb.continuation(**par_cont) 65 | 66 | if savedata: 67 | relpath = 'data/' 68 | filename = relpath + 'hb.pkl' 69 | pickle.dump(hb, open(filename, "wb")) 70 | print('data saved as {}'.format(filename)) 71 | 72 | ffrf, ax = nfrc(dof=0, hb=hb, interactive=False, xscale=1, xunit='(rad/s)') 73 | plt.show(block=True) 74 | -------------------------------------------------------------------------------- /examples/2dof/hb_plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | from collections import namedtuple 7 | import pickle 8 | 9 | from pyvib.hb.hbcommon import hb_components 10 | from pyvib.helper.plotting import (phase, periodic, stability, harmonic, 11 | nfrc) 12 | 13 | path='data/' 14 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs ns') 15 | hb = pickle.load(open(path + 'hb.pkl', 'rb')) 16 | sweep1 = pickle.load(open(path + 'pyds_sweepvrms2.pkl', 'rb')) 17 | nnm1 = pickle.load(open(path + 'nnm1' + '.pkl', 'rb')) 18 | nnm2 = pickle.load(open(path + 'nnm2' + '.pkl', 'rb')) 19 | 20 | 21 | dof=0 22 | 23 | ## Plot NFRC and sweep 24 | plt.figure(1) 25 | plt.clf() 26 | plt.plot(sweep1.finst*2*np.pi, sweep1.y[dof]) 27 | 28 | x = np.asarray(hb.omega_vec)/hb.scale_t 29 | y = np.asarray(hb.xamp_vec).T[dof] 30 | stab_vec = np.asarray(hb.stab_vec) 31 | idx1 = ~stab_vec 32 | idx2 = stab_vec 33 | plt.plot(np.ma.masked_where(idx1, x), 34 | np.ma.masked_where(idx1, y), '-k', 35 | np.ma.masked_where(idx2, x), 36 | np.ma.masked_where(idx2, y), '--k') 37 | 38 | fig = plt.gcf() 39 | ax = plt.gca() 40 | nfrc(nnm=nnm1, interactive=False, xscale=1, xunit='(rad/s)',fig=fig,ax=ax) 41 | nfrc(nnm=nnm2, interactive=False, xscale=1, xunit='(rad/s)',fig=fig,ax=ax) 42 | 43 | plt.ylabel('Amplitude (m)') 44 | plt.xlabel('Frequency (rad/s)') 45 | 46 | plt.xlim([0,5]) 47 | plt.ylim([-5,5]) 48 | 49 | ## Plot harmonic components 50 | # get harmonic components 51 | NH = hb.NH 52 | n = hb.n 53 | cvec = [] 54 | for z in hb.z_vec: 55 | c, phi, cnorm = hb_components(z, n, NH) 56 | cvec.append(cnorm) 57 | cvec = np.asarray(cvec) 58 | cvec = np.moveaxis(cvec, 0, -1) 59 | x = np.asarray(hb.omega_vec)/hb.scale_t 60 | 61 | plt.figure(2) 62 | plt.clf() 63 | for i in range(cvec.shape[1]): 64 | plt.plot(x,cvec[dof,i], label=str(i)) 65 | 66 | plt.title('Harmonic components') 67 | plt.xlabel('Frequency (rad/s)') 68 | plt.ylabel('Nomalised components') 69 | plt.legend() 70 | 71 | ## plot interactive NFRC 72 | plotlist = [periodic, phase, stability, harmonic] 73 | nfrc(plotlist=plotlist, hb=hb) 74 | 75 | plt.show() 76 | 77 | 78 | -------------------------------------------------------------------------------- /examples/2dof/newmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from scipy.linalg import eigvals 7 | import pickle 8 | import parameters 9 | 10 | from pyvib.nlforce import NL_force, NL_polynomial 11 | from pyvib.forcing import sine, multisine, sinesweep, toMDOF 12 | from pyvib.newmark import newmark_beta_nl as newmark_beta 13 | 14 | plot = True 15 | savedata = True 16 | savefig = False 17 | 18 | dof = 0 19 | 20 | par = parameters.par 21 | 22 | M = par['M'] 23 | C = par['C'] 24 | K = par['K'] 25 | 26 | ftype = 'sweep' 27 | fdof = 0 28 | vrms = 2 29 | f1 = 0.001/2/np.pi 30 | f2 = 5/np.pi 31 | fs = 20*f2 32 | nper = 2 33 | nsper = 10000 34 | vsweep = 0.01 35 | inctype = 'lin' 36 | 37 | inl = par['inl'] 38 | enl = par['enl'] 39 | knl = par['knl'] 40 | nl_pol = NL_polynomial(inl, enl, knl) 41 | nl = NL_force(nl_pol) 42 | 43 | x0, y0 = 0, 0 44 | ndof = M.shape[0] 45 | # get external forcing 46 | if ftype == 'multisine': 47 | u, lines, freqs, t = multisine(f1=f1, f2=f2, N=nsper, P=nper) 48 | 49 | elif ftype == 'sweep': 50 | u, t, finst = sinesweep(vrms, fs, f1, f2, vsweep, nper, inctype) 51 | # we dont use the first value, as it is not repeated when the force is 52 | # generated. This is taken care of in multisine. 53 | nsper = (len(u)-1) // nper 54 | elif ftype == 'sine': 55 | u, t = sine(vrms, f=f1, fs=fs, ns=nsper) 56 | else: 57 | raise ValueError('Wrong type of forcing', ftype) 58 | fext = toMDOF(u, ndof, fdof) 59 | 60 | dt = t[1] - t[0] 61 | x, xd, xdd = newmark_beta(M, C, K, x0, y0, dt, fext, nl, sensitivity=False) 62 | 63 | 64 | plt.figure() 65 | plt.clf() 66 | plt.plot(t, x[:,0], '-k', label=r'$x_1$') 67 | plt.plot(t, x[:,1], '-r', label=r'$x_2$') 68 | plt.xlabel('Time (t)') 69 | plt.ylabel('Displacement (m)') 70 | plt.title('Force type: {}, periods:{:d}'.format(ftype, nper)) 71 | plt.legend() 72 | 73 | plt.show() 74 | -------------------------------------------------------------------------------- /examples/2dof/nnm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import pickle 7 | import parameters 8 | 9 | from pyvib.nnm import NNM 10 | from pyvib.nlforce import NL_force, NL_polynomial 11 | 12 | savedata = True 13 | 14 | par = parameters.par 15 | 16 | M = par['M'] 17 | K = par['K'] 18 | 19 | fdof = par['fdof'] 20 | vrms = par['vrms'] 21 | f1 = par['f1'] 22 | f2 = par['f2'] 23 | 24 | inl = par['inl'] 25 | enl = par['enl'] 26 | knl = par['knl'] 27 | nl_pol = NL_polynomial(inl, enl, knl) 28 | nl = NL_force(nl_pol) 29 | 30 | f2 = 2 31 | par_nnm = { 32 | 'omega_min': f1*2*np.pi, 33 | 'omega_max': f2*2*np.pi, 34 | 'opt_it_NR': 3, 35 | 'max_it_NR': 15, 36 | 'tol_NR': 1e-6, 37 | 'step': 0.01, 38 | 'step_min': 1e-6, 39 | 'step_max': 1e-2, 40 | 'scale': 1e-4, 41 | 'angle_max_beta': 90, 42 | 'adaptive_stepsize': True, 43 | 'mode': 0, 44 | 'unit': 'rad/s', 45 | 'sca': 1 46 | } 47 | 48 | nnm1 = NNM(M, K, nl, **par_nnm) 49 | nnm1.periodic() 50 | nnm1.continuation() 51 | 52 | par_nnm['mode'] = 1 53 | nnm2 = NNM(M, K, nl, **par_nnm) 54 | nnm2.periodic() 55 | nnm2.continuation() 56 | 57 | relpath = 'data/' 58 | if savedata: 59 | pickle.dump(nnm1, open(relpath + 'nnm1' + '.pkl', 'wb')) 60 | pickle.dump(nnm2, open(relpath + 'nnm2' + '.pkl', 'wb')) 61 | print('data saved as {}'.format(relpath + 'nnm')) 62 | -------------------------------------------------------------------------------- /examples/2dof/nnm_plot.py: -------------------------------------------------------------------------------- 1 | #from matplotlib2tikz import save as tikz_save 2 | import pickle 3 | import os 4 | import sys 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | 8 | from scipy.linalg import eigvals 9 | from pyvib.helper.plotting import (phase, periodic, stability, configuration, 10 | nfrc) 11 | 12 | filename = 'data/nnm' 13 | nnm1 = pickle.load(open(filename + '1' + '.pkl', 'rb')) 14 | nnm2 = pickle.load(open(filename + '2' + '.pkl', 'rb')) 15 | 16 | 17 | fig1, ax1 = plt.subplots() 18 | nfrc(nnm=nnm1, interactive=False, xscale=1, xunit='(rad/s)', fig=fig1, ax=ax1) 19 | nfrc(nnm=nnm2, interactive=False, xscale=1, xunit='(rad/s)', fig=fig1, ax=ax1) 20 | 21 | fig2, ax2 = plt.subplots() 22 | nfrc(nnm=nnm1, interactive=False, xscale=1, xunit='(rad/s)', energy_plot=True, 23 | fig=fig2, ax=ax2) 24 | nfrc(nnm=nnm2, interactive=False, xscale=1, xunit='(rad/s)', energy_plot=True, 25 | fig=fig2, ax=ax2) 26 | 27 | 28 | plotlist = [periodic, phase, stability, configuration] 29 | nfrc(nnm=nnm1, interactive=True, xscale=1, xunit='(rad/s)', energy_plot=True, 30 | plotlist=plotlist) 31 | 32 | plt.show() 33 | 34 | # # get full periodic solution 35 | # T = 2*np.pi/nnm.omega_vec[-1] 36 | # x, xd, xdd, PhiT, dt = nnm.numsim(nnm.X0_vec[-1], T) 37 | # lamb = eigvals(PhiT) 38 | # ns = x.shape[1] 39 | # t = np.arange(ns)*dt 40 | -------------------------------------------------------------------------------- /examples/2dof/ode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import matplotlib.pylab as plt 5 | import numpy as np 6 | from scipy.integrate import odeint 7 | from scipy.interpolate import interp1d 8 | import parameters 9 | from pyvib.forcing import multisine 10 | 11 | plot = True 12 | 13 | def system_2dof(w, t, *args): 14 | # x1 x2 # 15 | # +--> +--> # 16 | # | | # 17 | # d1 __ +--------------+ d2 __ +--------------+ d3 __ # 18 | #-----__|----| |-----__|----| |-----__|----# 19 | # k1 | | k3 | | k4 # 20 | #__/\ /\ __| M1 |__/\ /\ __| M2 |__/\ /\ __# 21 | # \/ \/ | | \/ \/ | | \/ \/ # 22 | # k2 ^ | | | | k2 ^ # 23 | #__/\/ /\ __| | | |__/\/ /\ __# 24 | # /\/ \/ +--------------+ +--------------+ /\/ \/ # 25 | x1, x2, y1, y2 = w 26 | m1, m2, c1, c2, k1, k2, k3, mu1, mu2 = args 27 | 28 | global force 29 | # Create f = (x1',x2',y1',y2') 30 | f = [y1, 31 | y2, 32 | (-(k1 + k2) * x1 -c1*x1 + k2 * x2 - mu1 * x1**3 + force(t)) / m1, 33 | (k2 * x1 - (k2 + k3) * x2 -c2*x2 - mu2 * x2**3) / m2] 34 | return f 35 | 36 | par = parameters.par 37 | pars = [par[key] for key in ['m1', 'm2', 'c1', 'c2', 'k1', 'k2', 'k3', 'mu1', 'mu2']] 38 | pars = tuple(pars) 39 | 40 | fs = 100 41 | ns = 100 42 | nrep = 20 43 | f1 = 0.5/2/np.pi 44 | f2 = 1.5/2/np.pi 45 | vrms = 2.0 46 | u, lines, freq, t = multisine(f1=f1,fs=fs, N=ns, P=nrep, rms=vrms) 47 | 48 | force = interp1d(t, u, kind='linear') 49 | t = t[:-1] 50 | 51 | w0 = (0,0,0,0) 52 | abserr = 1.0e-12 53 | relerr = 1.0e-12 54 | wsol = odeint(system_2dof, w0, t, args=pars, atol=abserr, rtol=relerr) 55 | 56 | def recover_acc(t, y, v): 57 | """Recover the acceleration from the RHS: 58 | """ 59 | a = np.empty(len(t)) 60 | for i in range(len(t)): 61 | a[i] = system_2dof((y[i],v[i]),t[i],pars)[1] 62 | print('accelerations recovered') 63 | return a 64 | 65 | 66 | plt.figure(1) 67 | plt.clf() 68 | plt.plot(t, wsol[:,0], '', label=r'$x_1$') 69 | plt.plot(t, wsol[:,1], '--', label=r'$x_2$') 70 | plt.legend(loc='best') 71 | plt.xlabel('Time (t)') 72 | plt.ylabel('Distance (m)') 73 | plt.show() 74 | -------------------------------------------------------------------------------- /examples/2dof/parameters.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | 6 | """ 7 | Parameters for 2DOF duffing system 8 | 9 | # x1 x2 # 10 | # +--> +--> # 11 | # | | # 12 | # d1 __ +--------------+ d2 __ +--------------+ d3 __ # 13 | #-----__|----| |-----__|----| |-----__|----# 14 | # k1 | | k3 | | k4 # 15 | #__/\ /\ __| M1 |__/\ /\ __| M2 |__/\ /\ __# 16 | # \/ \/ | | \/ \/ | | \/ \/ # 17 | # k2 ^ | | | | k2 ^ # 18 | #__/\/ /\ __| | | |__/\/ /\ __# 19 | # /\/ \/ +--------------+ +--------------+ /\/ \/ # 20 | 21 | Mode & Frequency (rad/s) & Damping ratio (%) 22 | 1 & 1.00 & 5.00 23 | 2 & 3.32 & 1.51 24 | """ 25 | 26 | m1 = 1 # kg 27 | m2 = 1 28 | k1 = 1 # N/m 29 | k2 = 5 30 | k3 = 1 31 | c1 = 0.1 # N/ms 32 | c2 = 0.1 33 | 34 | mu1 = 1 # N/m^3 35 | mu2 = 1 # N/m^3 36 | 37 | fdof = 0 38 | vrms = 2 # N 39 | f0 = 1.2/2/np.pi # Hz 40 | f1 = 1e-4/2/np.pi 41 | f2 = 5/2/np.pi 42 | fs = 10 43 | vsweep = 5 44 | nper = 1 45 | nsper = 1000 46 | inctype = 'log' 47 | 48 | M = np.array([[m1,0],[0,m2]]) 49 | C = np.array([[c1,0],[0,c2]]) 50 | K = np.array([[k1+k2, -k2],[-k2, k2+k3]]) 51 | M, C, K = np.atleast_2d(M,C,K) 52 | 53 | inl = np.array([[0,-1], 54 | [1,-1]]) 55 | enl = np.array([3,3,2,2]) 56 | knl = np.array([mu1, mu2]) 57 | 58 | par = { 59 | 'M': M, 60 | 'C': C, 61 | 'K': K, 62 | 'fdof': fdof, 63 | 'vrms': vrms, 64 | 'f0': f0, 65 | 'f1': f1, 66 | 'f2': f2, 67 | 'inl': inl, 68 | 'enl': enl, 69 | 'knl': knl, 70 | 'fs': fs, 71 | 'vsweep': vsweep, 72 | 'nper': nper, 73 | 'nsper': nsper, 74 | 'inctype': inctype, 75 | 'm1': m1, 76 | 'm2': m2, 77 | 'c1': c1, 78 | 'c2': c2, 79 | 'k1': k1, 80 | 'k2': k2, 81 | 'k3': k3, 82 | 'mu1': mu1, 83 | 'mu2': mu2 84 | } 85 | 86 | -------------------------------------------------------------------------------- /examples/2dof/pyds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import PyDSTool as dst 5 | from matplotlib import pyplot as plt 6 | import numpy as np 7 | from collections import namedtuple 8 | import pickle 9 | import time 10 | 11 | from pyvib.forcing import sine, multisine, sinesweep 12 | import parameters 13 | par = parameters.par 14 | 15 | savedata = False 16 | 17 | # The vrms (in N) values are chosen as 18 | # vrms = 0.01, linear 19 | # vrms = 2, nonlinear 20 | 21 | vrms = 0.01 22 | y10, y20, v10, v20 = 0, 0, 0, 0 23 | 24 | ftype = 'multisine' 25 | # ftype = 'sweep' 26 | 27 | # get external forcing 28 | finst = [] 29 | if ftype == 'multisine': 30 | ns = 25000 31 | nrep = 20 32 | f1 = 0/2/np.pi 33 | f2 = 20/2/np.pi 34 | fs = 20*f2 35 | # vrms = 2 36 | u, lines, freq, t_ran = multisine(f1=f1, N=ns, P=nrep, rms=vrms) 37 | 38 | saveacc = False 39 | elif ftype == 'sweep': 40 | saveacc = True 41 | nrep = 1 42 | f1 = 1e-3/2/np.pi 43 | f2 = 5/2/np.pi 44 | fs = 20*f2 45 | vsweep = 0.005 46 | inctype ='lin' 47 | # change sweep-direction by: sinesweep(vrms,fs, f2,f1,-vsweep, nper, inctype) 48 | u, t_ran, finst = sinesweep(vrms,fs, f1,f2,vsweep, nrep, inctype) 49 | # we dont use the first value, as it is not repeated when the force is 50 | # generated. This is taken care of in multisine. 51 | ns = (len(u)-1) // nrep 52 | elif ftype == 'sine': 53 | ns = 10000 54 | u, t_ran = sine(vrms, f1, fs, nsper=ns) 55 | else: 56 | raise ValueError('Wrong type of forcing', ftype) 57 | 58 | print('\n parameters:') 59 | print('ftype \t = %s' % ftype) 60 | print('vrms \t = %f' % vrms) 61 | print('fs \t = %d' % fs) 62 | print('f1 \t = %f' % f1) 63 | print('f2 \t = %f' % f2) 64 | print('nsper \t = %d' % ns) 65 | print('nrep \t = %d' % nrep) 66 | print('ns_tot \t = %d' % len(u)) 67 | 68 | 69 | # Interpolation-table for the force 70 | xData = {'force': u} 71 | my_input = dst.InterpolateTable({'tdata': t_ran, 72 | 'ics': xData, 73 | 'name': 'interp1d', 74 | 'method': 'linear', # next 3 not necessary 75 | 'checklevel': 1, 76 | 'abseps': 1e-6, 77 | }).compute('interp1d') 78 | 79 | DSargs = dst.args(name='duffing') 80 | tdomain = [t_ran[0], t_ran[-1]] 81 | DSargs.tdata = tdomain 82 | DSargs.inputs = my_input.variables['force'] 83 | DSargs.pars = {key: par[key] for key in ['m1', 'm2', 'c1', 'c2', 'k1', 'k2', 84 | 'k3', 'mu1', 'mu2']} 85 | 86 | DSargs.varspecs = {'y1': 'v1', 87 | 'y2': 'v2', 88 | 'v1': \ 89 | '(-(k1 + k2) * y1' \ 90 | '-c1*v1 + k2 * y2' \ 91 | ' - mu1 * y1**3 + force) / m1', 92 | 'v2': \ 93 | '(k2 * y1 - (k2 + k3) * y2' \ 94 | ' -c2*v2 - mu2 * y2**3) / m2', 95 | 'inval': 'force'} 96 | DSargs.vars = ['y1', 'v1', 'y2', 'v2'] 97 | DSargs.ics = {'y1': y10, 'v1': v10, 'y2': y20, 'v2': v20} 98 | DSargs.algparams = {'init_step': 0.01, 'max_step': 0.01, 'max_pts': 2000000} 99 | DSargs.checklevel = 2 100 | 101 | python = False 102 | if python: 103 | DS = dst.Generator.Vode_ODEsystem(DSargs) 104 | else: 105 | DS = dst.Generator.Dopri_ODEsystem(DSargs) 106 | 107 | startTime = time.time() 108 | 109 | # in order not to get too many points for a simulation, the simulation is 110 | # splitted up. If simulation points exceed max_pts, pydstool fails with a 111 | # message: 'No trajectory created' 112 | int_time = (t_ran[-1]-t_ran[0])/nrep 113 | t0 = 0 114 | t1 = int_time 115 | y, v, t, u_pol, a = [], [], [], [], [] 116 | 117 | y = np.empty((2,0)) 118 | v = np.empty((2,0)) 119 | #for i in range(nrep): 120 | for i in range(1): 121 | t0, t1 = t_ran[0], t_ran[-1] 122 | DS.set(tdata=[t0, t1], 123 | ics={'y1':y10, 'v1':v10,'y2':y20, 'v2':v20}) 124 | traj = DS.compute('in-table') 125 | pts = traj.sample(dt=1/fs, precise=True) 126 | # Dont save the last point, as it will be the first point for next round 127 | y = np.hstack((y,[pts['y1'][:-1], pts['y2'][:-1]])) 128 | v = np.hstack((v,[pts['v1'][:-1], pts['v2'][:-1]])) 129 | t.extend(pts['t'][:-1]) 130 | u_pol.extend(pts['inval'][:-1]) 131 | y10 = pts['y1'][-1] 132 | v10 = pts['v1'][-1] 133 | y20 = pts['y2'][-1] 134 | v20 = pts['v2'][-1] 135 | t0 = pts['t'][-1] 136 | t1 = t0 + int_time 137 | 138 | y = np.hstack((y,np.vstack([pts['y1'][-1], pts['y2'][-1]]))) 139 | v = np.hstack((v,np.vstack([pts['v1'][-1], pts['v2'][-1]]))) 140 | t.extend([pts['t'][-1]]) 141 | u_pol.extend([pts['inval'][-1]]) 142 | 143 | print('Integration done in: {}'.format(time.time()-startTime)) 144 | 145 | def recover_acc(t, y, v): 146 | # Recover the acceleration from the RHS: 147 | n, ns = y.shape 148 | a = np.empty(y.shape) 149 | for i in range(ns): 150 | a[:,i] = DS.Rhs(t[i], {'y1':y[0,i], 'v1':v[0,i], 'y2':y[1,i], 'v2':v[1,i]}, DS.pars)[:n] 151 | print('accelerations recovered') 152 | return a 153 | 154 | if saveacc: 155 | a = recover_acc(t, y, v) 156 | 157 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs ns') 158 | if savedata: 159 | data = Nm(y,v,a,u_pol,t,finst,fs,ns) 160 | filename = 'data/' + 'pyds_' + ftype + 'vrms' + str(vrms) 161 | pickle.dump(data, open(filename + '.pkl', 'wb')) 162 | print('data saved as {}'.format(filename)) 163 | 164 | 165 | plt.figure() 166 | plt.plot(t, y[0], '-k', label = r'$x_1$') 167 | plt.plot(t, y[1], '--r', label = r'$x_2$') 168 | plt.xlabel('Time (t)') 169 | plt.ylabel('Displacement (m)') 170 | plt.title('Force type: {}, periods:{:d}'.format(ftype, nrep)) 171 | plt.legend() 172 | plt.show() 173 | -------------------------------------------------------------------------------- /examples/2dof/pyds_stepped.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import PyDSTool as dst 6 | import numpy as np 7 | import time 8 | from parameters import par 9 | np.set_printoptions(precision=5) 10 | 11 | savedata = True 12 | vrms = 2 13 | y10, y20, v10, v20 = 0, 0, 0, 0 14 | 15 | # Set simulation parameters 16 | ntransient = 500 # transient periods of ext excitation 17 | nsteady = 100 # state state periods of ext excitation 18 | nSimTimeSteps = 25 # sampling rate 19 | OMEGA_start = 0.1 # start of excitation freq range 20 | OMEGA_stop = 5 # end of excitation freq range 21 | n_OMEGA = 100 # numbers in excitation freq range 22 | 23 | DSargs = dst.args(name='stepped') 24 | DSargs.pars = {key: par[key] for key in ['m1', 'm2', 'c1', 'c2', 'k1', 'k2', 25 | 'k3', 'mu1', 'mu2'] 26 | } 27 | DSargs.pars.update({ 28 | 'q': vrms, 29 | 'OMEGA': 0}) 30 | 31 | DSargs.varspecs = {'y1': 'v1', 32 | 'y2': 'v2', 33 | 'v1': \ 34 | '(-(k1 + k2) * y1' \ 35 | '-c1*v1 + k2 * y2' \ 36 | '- mu1 * y1**3 - q*cos(OMEGA*t)) / m1', 37 | 'v2': \ 38 | '(k2 * y1 - (k2 + k3) * y2 \ 39 | -c2*v2 - mu2 * y2**3) / m2' 40 | } 41 | 42 | DSargs.vars = ['y1', 'v1', 'y2', 'v2'] 43 | DSargs.ics = {'y1': y10, 'v1': v10, 'y2': y20, 'v2': v20} 44 | DSargs.algparams = {'max_pts': 1000000} 45 | DSargs.checklevel = 2 46 | 47 | python = True 48 | python = False 49 | if python: 50 | ode = dst.Generator.Vode_ODEsystem(DSargs) 51 | else: 52 | ode = dst.Generator.Dopri_ODEsystem(DSargs) 53 | 54 | # increase/decrease ext excitation freq 55 | if len(sys.argv) > 1: 56 | arg = sys.argv[1] 57 | if arg == 'True': 58 | increasing = True 59 | else: 60 | increasing = False 61 | else: 62 | increasing = False 63 | increasing = True 64 | 65 | # Sweep OMEGA 66 | if increasing: 67 | filename = 'data/stepped_inc' 68 | OMEGA_vec = np.linspace(OMEGA_start, OMEGA_stop, n_OMEGA) 69 | else: 70 | filename = 'data/stepped_dec' 71 | OMEGA_vec = np.linspace(OMEGA_stop, OMEGA_start, n_OMEGA) 72 | 73 | t_transient = [] 74 | t_steady = [] 75 | y_transient = [] 76 | y_steady = [] 77 | v_transient = [] 78 | v_steady = [] 79 | 80 | print('looping OMEGA from %f to %f in %d steps' \ 81 | % (OMEGA_vec[0], OMEGA_vec[-1], n_OMEGA)) 82 | 83 | startTime = time.time() 84 | i = 0 85 | for OMEGA in OMEGA_vec: 86 | 87 | print('OMEGA=%f' % OMEGA) 88 | 89 | # adjust time domain and timestep: 90 | t0 = 0.0 # start time for for the simulation 91 | ttransient = ntransient*2.0*np.pi/OMEGA # periods of the excitation force 92 | tstop = ttransient + nsteady*2.0*np.pi/OMEGA # periods of the excitation force 93 | dt = 2*np.pi/OMEGA / nSimTimeSteps # timesteps per period of the excitation force 94 | 95 | # set excitation frequency and update time doamain 96 | ode.set(pars={'OMEGA': OMEGA}) 97 | 98 | # solve for transient motion: 99 | ode.set(tdata=[t0, ttransient], 100 | ics={'y1':y10, 'v1':v10,'y2':y20, 'v2':v20}) 101 | 102 | traj_transient = ode.compute('transient') # integrate ODE 103 | pts = traj_transient.sample(dt=dt, precise=True) # sampling data for plotting 104 | pts_transient = pts 105 | y10 = pts['y1'][-1] 106 | v10 = pts['v1'][-1] 107 | y20 = pts['y2'][-1] 108 | v20 = pts['v2'][-1] 109 | t0 = pts['t'][-1] 110 | 111 | # solve for steady state motion: 112 | ode.set(tdata=[t0, tstop+0.5*dt], 113 | ics={'y1':y10, 'v1':v10,'y2':y20, 'v2':v20}) 114 | traj_steady = ode.compute('steady') 115 | pts = traj_steady.sample(dt=dt, precise=True) 116 | pts_steady = pts 117 | 118 | # update initial conditions 119 | y10 = pts['y1'][-1] 120 | v10 = pts['v1'][-1] 121 | y20 = pts['y2'][-1] 122 | v20 = pts['v2'][-1] 123 | 124 | # Save time data 125 | t_transient.append(pts_transient['t']) 126 | t_steady.append(pts_steady) 127 | y_transient.append([pts_transient['y1'],pts_transient['y2']]) 128 | y_steady.append([pts_steady['y1'],pts_steady['y2']]) 129 | v_transient.append([pts_transient['v1'],pts_transient['v2']]) 130 | v_steady.append([pts_steady['v1'],pts_steady['v2']]) 131 | ymax = np.max(y_steady[-1],axis=1) 132 | ymin = np.min(y_steady[-1],axis=1) 133 | print("max A {} ".format(np.abs(0.5*(ymax-ymin)))) 134 | 135 | i += 1 136 | 137 | totalTime = time.time()-startTime 138 | print('') 139 | print(' %d Sweeps with %d transient and %d steady periods.' \ 140 | %(len(OMEGA_vec), ntransient, nsteady)) 141 | print(' Each period is sampled in %d steps, a total of %d steps' \ 142 | %(nSimTimeSteps, len(OMEGA_vec)*ntransient*nsteady*nSimTimeSteps)) 143 | print(' Total time: %f, time per sweep: %f' % (totalTime,totalTime/len(OMEGA_vec))) 144 | 145 | if savedata: 146 | np.savez( 147 | filename, 148 | timestep=dt, 149 | OMEGA_vec=OMEGA_vec, 150 | #t_transient=t_transient, 151 | #y_transient=y_transient, 152 | #v_transient=v_transient, 153 | t_steady=t_steady, 154 | y_steady=y_steady, 155 | v_steady=v_steady 156 | ) 157 | 158 | -------------------------------------------------------------------------------- /examples/2dof/pyds_stepped_plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import sys 6 | import numpy as np 7 | from matplotlib import pyplot as plt 8 | import pickle 9 | try: 10 | from matplotlib2tikz import save as tikz_save 11 | except: 12 | pass 13 | 14 | savefig = True 15 | 16 | abspath = '' 17 | path = abspath + 'data/' 18 | filename = path + 'stepped' 19 | data_inc = np.load(filename + '_inc.npz') 20 | data_dec = np.load(filename + '_dec.npz') 21 | hb = pickle.load(open(path + 'hb.pkl', 'rb')) 22 | 23 | def tohz(y): 24 | return y/(2*np.pi) 25 | 26 | def steady_amp(data): 27 | # compute steady state amplitudes 28 | omegas = data['OMEGA_vec'] 29 | y_steady = data['y_steady'] 30 | A = [] 31 | for i in range(len(omegas)): 32 | y1 = y_steady[i,0] 33 | y2 = y_steady[i,1] 34 | ymax = np.max(y1) 35 | ymin = np.min(y1) 36 | A.append(np.abs(0.5*(ymax-ymin))) 37 | return A 38 | 39 | A_inc = steady_amp(data_inc) 40 | A_dec = steady_amp(data_dec) 41 | 42 | # plot amplitude against omega. Ie. FRF 43 | fig1 = plt.figure(1) 44 | plt.clf() 45 | plt.plot(data_inc['OMEGA_vec'][::3], A_inc[::3], 'kx', 46 | label=r'$\Delta \Omega>0$') 47 | plt.plot(data_dec['OMEGA_vec'][::3], A_dec[::3], 'ro', mfc='none', 48 | label=r'$\Delta \Omega<0$') 49 | 50 | dof = 0 51 | x = np.asarray(hb.omega_vec)/hb.scale_t 52 | y = np.asarray(hb.xamp_vec).T[dof] 53 | stab_vec = np.asarray(hb.stab_vec) 54 | idx1 = ~stab_vec 55 | idx2 = stab_vec 56 | plt.plot(np.ma.masked_where(idx1, x), 57 | np.ma.masked_where(idx1, y), '-k', 58 | np.ma.masked_where(idx2, x), 59 | np.ma.masked_where(idx2, y), '--k') 60 | 61 | 62 | plt.legend() 63 | plt.xlabel('Frequency (rad/s)') 64 | plt.ylabel('Amplitude (m)') 65 | 66 | fnfrc = plt.gcf() 67 | 68 | 69 | def save(fig, filename): 70 | fig.savefig(filename + '.pdf') 71 | try: 72 | tikz_save(filename + '.tikz', figure=fig, show_info=False, strict=True) 73 | except: 74 | pass 75 | 76 | if savefig: 77 | path = abspath + 'plots/' 78 | save(fnfrc, path + 'nfrc') 79 | 80 | plt.show() 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /examples/2dof/rfs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import pickle 7 | from collections import namedtuple 8 | 9 | from pyvib.signal import Signal2 as Signal 10 | from pyvib.rfs import RFS 11 | 12 | path = 'data/' 13 | filename = 'pyds_sweepvrms' 14 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs ns') 15 | #sweep_l = pickle.load(open(path + filename + '0.01' + '.pkl', 'rb')) 16 | sweep_h = pickle.load(open(path + filename + '2' + '.pkl', 'rb')) 17 | 18 | 19 | signal = Signal(sweep_h.u, sweep_h.fs, sweep_h.ydd) 20 | signal.set_signal(y=sweep_h.y, yd=sweep_h.yd, ydd=sweep_h.ydd) 21 | 22 | rfs = RFS(signal, dof=0) 23 | rfs.plot() 24 | 25 | plt.show() 26 | # extract force slice/subplot for saving 27 | # ax2 = rfs.plot.ax2d 28 | # fig2 = plt.figure() 29 | # ax2.figure=fig2 30 | # fig2.axes.append(ax2) 31 | # fig2.add_axes(ax2) 32 | 33 | # dummy = fig2.add_subplot(111) 34 | # ax2.set_position(dummy.get_position()) 35 | # dummy.remove() 36 | # ax2.set_title('') 37 | -------------------------------------------------------------------------------- /examples/2dof/wt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import pickle 7 | from collections import namedtuple 8 | 9 | from pyvib.morletWT import WT 10 | from pyvib.signal import Signal2 as Signal 11 | 12 | path = 'data/' 13 | filename = 'pyds_sweepvrms' 14 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs ns') 15 | sweep_l = pickle.load(open(path + filename + '0.01' + '.pkl', 'rb')) 16 | sweep_h = pickle.load(open(path + filename + '2' + '.pkl', 'rb')) 17 | 18 | 19 | f1 = 1e-3 20 | f2 = 10/2/np.pi 21 | nf = 50 22 | f00 = 5 23 | dof = 0 24 | sca = 2*np.pi 25 | 26 | lin = Signal(sweep_l.u, sweep_l.fs, sweep_l.ydd) 27 | wtlin = WT(lin) 28 | wtlin.morlet(f1, f2, nf, f00, dof=0) 29 | 30 | nlin = Signal(sweep_h.u, sweep_h.fs, sweep_h.ydd) 31 | wtnlin = WT(nlin) 32 | wtnlin.morlet(f1, f2, nf, f00, dof=0) 33 | 34 | dof = 0 35 | plt.figure() 36 | plt.plot(sweep_h.finst*sca,sweep_h.y[dof]) 37 | plt.title('Sweep') 38 | plt.xlabel('Frequency') 39 | plt.ylabel('Amplitude (m)') 40 | 41 | 42 | fwlin, ax = wtlin.plot(fss=sweep_l.finst,sca=sca) 43 | # ax.set_xlim([0,20]) 44 | # ax.set_ylim([0,20]) 45 | 46 | fwnlin, ax = wtnlin.plot(fss=sweep_h.finst,sca=sca) 47 | # ax.set_xlim([0,20]) 48 | # ax.set_ylim([0,20]) 49 | 50 | plt.show() 51 | -------------------------------------------------------------------------------- /examples/README.txt: -------------------------------------------------------------------------------- 1 | Missing data 2 | =============== 3 | The examples includes files for generating data needed for the identification 4 | process(ie. FNSI, RFS, FRF, WT), but the data itself is not included. Data can 5 | either be generated by running the relevant files or downloaded by running:: 6 | 7 | >>> import pyvib.data.sample 8 | 9 | Alternatively there is a direct link[1]_ which then have to be extracted to the 10 | relevant directories. 11 | 12 | 13 | Structure 14 | =========== 15 | In general the structure is: 16 | 17 | Data generation 18 | ------------------ 19 | Depending on the DOFs, data can either be generated by ode.py, pyds.py or nm.py. 20 | ode.py and pyds.py solves a system of 1.order ODEs, ie. the state-space 21 | formulation, whereas nm uses a newmark solver on the FEM formulation. 22 | 23 | ode.py uses the built-in ode-solver in numpy(or scipy) where pyds needs an 24 | external package downloaded from [2]. pyds is much faster as the ode-system is 25 | compiled into c-code. ode.py is only provided as an example, as I use pyds or 26 | newmark. 27 | 28 | Identification 29 | ----------------- 30 | wt.py 31 | rfs.py 32 | fnsi.py 33 | 34 | Design 35 | ------- 36 | nnm.py 37 | hb.py 38 | 39 | 40 | [1] 41 | http://www.student.dtu.dk/~s082705/vib/data.zip 42 | [2] 43 | https://github.com/robclewley/pydstool 44 | -------------------------------------------------------------------------------- /examples/contact/fnsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | 7 | import pickle 8 | from collections import namedtuple 9 | 10 | from pyvib.signal import Signal 11 | from pyvib.fnsi import FNSI 12 | from pyvib.common import db, frf_mkc 13 | from pyvib.helper.fnsi_plots import (plot_modes, plot_knl, plot_linfrf, 14 | plot_stab, plot_svg) 15 | from pyvib.frf import FRF 16 | from pyvib.nlforce import NL_force, NL_polynomial, NL_spline 17 | from pyvib.interpolate import piecewise_linear 18 | from pyvib.modal import modal_ac 19 | 20 | 21 | 22 | 23 | sca = 2*np.pi 24 | 25 | 26 | ftype = 'multisine' 27 | filename = 'data/' + 'pyds_' + ftype + 'vrms0.2' 28 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs ns') 29 | msine = pickle.load(open(filename + '.pkl', 'rb')) 30 | 31 | # which dof to get H1 estimate from/show periodicity for 32 | dof = 0 33 | 34 | fmin = 0 35 | fmax = 10/2/np.pi 36 | nlin = Signal(msine.u, msine.fs, y=msine.y) 37 | fper, ax = nlin.periodicity(msine.ns, dof, offset=0) 38 | 39 | 40 | per = [7,9] 41 | nlin.cut(msine.ns, per) 42 | 43 | isnoise = False 44 | 45 | inl = np.array([[0,-1]]) 46 | nl_spline = NL_spline(inl, nspl=15) 47 | nl = NL_force() 48 | nl.add(nl_spline) 49 | iu = 0 50 | idof = [0] 51 | nldof = [] 52 | 53 | ims = 60 54 | nmax = 40 55 | ncur = 6 56 | nlist = np.arange(2, nmax+3, 2) 57 | dof = 0 58 | 59 | # nonlinear identification 60 | fnsi = FNSI(nlin, nl, idof, fmin, fmax) 61 | fnsi.calc_EY() 62 | fnsi.svd_comp(ims) 63 | sd = fnsi.stabilisation_diagram(nlist) 64 | fnsi.id(ncur) 65 | fnsi.nl_coeff(iu, dof) 66 | fsdlin, ax = plot_stab(fnsi, nlist, sca) 67 | 68 | # Linear identification at high level 69 | nl = NL_force() 70 | fnsi2 = FNSI(nlin, nl, idof, fmin, fmax) 71 | fnsi2.calc_EY() 72 | fnsi2.svd_comp(ims) 73 | fnsi2.id(ncur) 74 | fnsi2.nl_coeff(iu, dof) 75 | 76 | # Linear identification at low level 77 | filename = 'data/' + 'pyds_' + ftype + 'vrms0.005' 78 | msine_lin = pickle.load(open(filename + '.pkl', 'rb')) 79 | lin = Signal(msine_lin.u, msine_lin.fs, y=msine_lin.y) 80 | per = [7] 81 | lin.cut(msine_lin.ns, per) 82 | nl = NL_force() 83 | fnsi_lin = FNSI(lin, nl, idof, fmin, fmax) 84 | fnsi_lin.calc_EY() 85 | fnsi_lin.svd_comp(ims) 86 | fnsi_lin.id(ncur) 87 | fnsi_lin.nl_coeff(iu, dof) 88 | 89 | 90 | # identified spline knots 91 | if len(fnsi.nonlin.nls) > 0: 92 | knl_c = np.mean(fnsi.nonlin.nls[0].knl,axis=1) 93 | knl = np.real(knl_c) 94 | kn = fnsi.nonlin.nls[0].kn 95 | from scipy.interpolate import CubicSpline 96 | cs = CubicSpline(kn, knl) 97 | x = np.linspace(min(kn),max(kn), 1000) 98 | 99 | plt.figure(2) 100 | plt.clf() 101 | plt.plot(x,cs(x),'-k') 102 | plt.plot(kn, knl, 'xk') 103 | b = 0.1745 104 | plt.axvline(-b,c='k', ls='--') 105 | plt.axvline(b,c='k', ls='--') 106 | 107 | b = 0.1745 108 | alpha = 0 109 | beta = 3 110 | slope = np.array([beta, alpha, beta]) 111 | xk = np.array([-b, b]) 112 | yk = np.array([0, 0]) #np.array([-b, b]) 113 | y2 = piecewise_linear(xk,yk,slope,delta=None,xv=x) 114 | plt.plot(x,y2,'--') 115 | 116 | plt.xlabel('Displacement') 117 | plt.ylabel('Nonlinear restoring force') 118 | figrf = plt.gcf() 119 | 120 | def print_modal(fnsi): 121 | # calculate modal parameters 122 | modal = modal_ac(fnsi.A, fnsi.C) 123 | natfreq = modal['wn'] 124 | dampfreq = modal['wd'] 125 | damping = modal['zeta'] 126 | nw = min(len(natfreq), 8) 127 | 128 | print('Undamped ω: {}'.format(natfreq[:nw]*sca)) 129 | print('damped ω: {}'.format(dampfreq[:nw]*sca)) 130 | print('damping: {}'.format(damping[:nw])) 131 | return modal 132 | 133 | # FRF 134 | frf = FRF(nlin, fmin=1e-3, fmax=5/2/np.pi) 135 | frf_freq, frf_H = frf.periodic() 136 | 137 | print('## linear identified at low level') 138 | print_modal(fnsi_lin) 139 | print('## linear identified at high level') 140 | print_modal(fnsi2) 141 | print('## nonlinear identified at high level') 142 | print_modal(fnsi) 143 | 144 | fH1, ax = plt.subplots() 145 | ax.plot(frf_freq*sca, db(np.abs(frf_H[dof])), '-.k', label='From signal') 146 | plot_linfrf(fnsi_lin, dof, sca=sca, fig=fH1, ax=ax, label='lin low level') 147 | plot_linfrf(fnsi, dof, sca=sca, fig=fH1, ax=ax, label='nl high level') 148 | plot_linfrf(fnsi2, dof, sca=sca, fig=fH1, ax=ax, ls='--', label='lin high level') 149 | # ax.set_title(''); ax.legend_ = None 150 | ax.legend() 151 | 152 | plt.show() 153 | -------------------------------------------------------------------------------- /examples/contact/hb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | import pickle 8 | import parameters 9 | 10 | from pyvib.hb.hb import HB 11 | from pyvib.hb.hbcommon import hb_signal 12 | from pyvib.nlforce import NL_force, NL_piecewise_linear 13 | from pyvib.helper.plotting import (phase, periodic, stability, harmonic, 14 | nfrc) 15 | 16 | 17 | savedata = True 18 | 19 | M = np.array([[1]]) 20 | C = np.array([[0.025*2]]) 21 | K = np.array([[1]]) 22 | 23 | fdof = 0 24 | vrms = 0.03 25 | f0 = 1e-3 26 | f1 = 1e-4 27 | f2 = 1.8 28 | b = 0.1745 29 | alpha = 0 30 | beta = 3 31 | 32 | inl = np.array([[0, -1]]) 33 | delta = [5e-4, 5e-4] 34 | slope = np.array([beta, alpha, beta]) 35 | x = np.array([-b, b]) 36 | y = np.array([0, 0]) 37 | 38 | nl_piece = NL_piecewise_linear(x, y, slope, inl, delta) 39 | nl = NL_force() 40 | nl.add(nl_piece) 41 | 42 | par_hb ={ 43 | 'NH': 12, 44 | 'npow2': 8, 45 | 'nu': 1, 46 | 'stability': True, 47 | 'rcm_permute': False, 48 | 'tol_NR': 1e-6, 49 | 'max_it_NR': 25, 50 | 'scale_x': 1, 51 | 'scale_t': 1, 52 | 'amp0':1e-4, 53 | 'xstr':'rad/s', 54 | 'xscale':1 55 | } 56 | par_cont = { 57 | 'omega_cont_min': f1, 58 | 'omega_cont_max': f2, 59 | 'cont_dir': 1, 60 | 'opt_it_NR': 3, 61 | 'step': 0.001, 62 | 'step_min': 0.00001, 63 | 'step_max': 0.01, 64 | 'angle_max_pred': 90, 65 | 'it_cont_max': 1e6, 66 | 'adaptive_stepsize': True 67 | } 68 | 69 | # run full continuation 70 | hb = HB(M,C,K,nl, **par_hb) 71 | omega, z, stab, lamb = hb.periodic(f0, vrms, fdof) 72 | hb.continuation(**par_cont) 73 | 74 | 75 | if savedata: 76 | with open('data/hb' + '.pkl', 'wb') as f: 77 | pickle.dump(hb, f) 78 | print('data saved as {}'.format(filename)) 79 | 80 | 81 | ffrf, ax = nfrc(dof=0, hb=hb, interactive=False, xscale=1, 82 | xunit='(rad/s)') 83 | 84 | 85 | plt.show() 86 | 87 | """ 88 | One-sided: 89 | 'NH': 12, 90 | 'npow2': 8, 91 | 'step_max': 0.01, 92 | vrms = 0.01 93 | b = 0.08 94 | beta = 1 95 | slope = np.array([0, -beta]) 96 | x = np.array([b]) 97 | y = np.array([0]) 98 | 99 | """ 100 | -------------------------------------------------------------------------------- /examples/contact/hb_plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | from collections import namedtuple 7 | import pickle 8 | 9 | from pyvib.hb.hbcommon import hb_components 10 | 11 | savefig = True 12 | # savefig = False 13 | 14 | path='data/' 15 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs') 16 | hb = pickle.load(open(path + 'hb.pkl', 'rb')) 17 | sweep1 = pickle.load(open(path + 'pyds_sweepvrms0.03.pkl', 'rb')) 18 | 19 | dof=0 20 | 21 | plt.figure(1) 22 | plt.clf() 23 | plt.plot(sweep1.finst*2*np.pi, sweep1.y) 24 | #plt.plot(sweep2.finst, sweep2.y[dof]) 25 | x = np.asarray(hb.omega_vec)/hb.scale_t 26 | y = np.asarray(hb.xamp_vec).T[dof] 27 | stab_vec = np.asarray(hb.stab_vec) 28 | idx1 = ~stab_vec 29 | idx2 = stab_vec 30 | plt.plot(np.ma.masked_where(idx1, x), 31 | np.ma.masked_where(idx1, y), '-k', 32 | np.ma.masked_where(idx2, x), 33 | np.ma.masked_where(idx2, y), '--k') 34 | 35 | # plt.plot(x,y,'-k') 36 | plt.ylabel('Amplitude (m)') 37 | plt.xlabel('Frequency (rad/s)') 38 | fnfrc = plt.gcf() 39 | 40 | # get harmonic components 41 | NH = hb.NH 42 | n = hb.n 43 | cvec = [] 44 | for z in hb.z_vec: 45 | c, phi, cnorm = hb_components(z, n, NH) 46 | cvec.append(cnorm) 47 | cvec = np.asarray(cvec) 48 | cvec = np.moveaxis(cvec, 0, -1) 49 | x = np.asarray(hb.omega_vec)/hb.scale_t 50 | 51 | plt.figure(2) 52 | plt.clf() 53 | for i in range(cvec.shape[1]): 54 | plt.plot(x,cvec[dof,i]) 55 | 56 | plt.xlabel('Frequency (Hz)') 57 | plt.ylabel('Nomalised components') 58 | fhar = plt.gcf() 59 | 60 | plt.show() 61 | 62 | -------------------------------------------------------------------------------- /examples/contact/pyds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import PyDSTool as dst 5 | from matplotlib import pyplot as plt 6 | import numpy as np 7 | import time 8 | from collections import namedtuple 9 | import pickle 10 | from pyvib.import forcing 11 | 12 | savedata = True 13 | saveacc = True 14 | 15 | """ 16 | The discontinuous stiffness at impact distance b, is given by 17 | alpha: the slope in-between impacts, 18 | beta: the slope after impact. 19 | g: multiplication of the discontinuous force, ie. with 20 | alpha = 0, beta = 1 and g = 3, then the slope after impacts is 3. 21 | """ 22 | 23 | y0, v0 = 0, 0 24 | alpha = 0 25 | beta = 1 26 | b = 0.1745 27 | alpha = 0 28 | beta = 1 29 | g = 3 30 | vsweep = 0.01 31 | 32 | ftype = 'sweep' 33 | ftype = 'multisine' 34 | 35 | targetlang = 'python' 36 | targetlang = 'c' 37 | 38 | # ns = 30000 39 | # fs =40*f2 40 | if ftype == 'multisine': 41 | vrms = 0.2 42 | nrep = 10 43 | ns = 15000 44 | f1 = 1e-3/2/np.pi 45 | f2 = 10/2/np.pi 46 | fs = 20*f2 47 | u, t_ran = forcing.multisine(vrms,fs, f1,f2,ns=ns, nrep=nrep) 48 | saveacc = False 49 | finst = 0 50 | elif ftype == 'sweep': 51 | nrep = 1 52 | vrms = 0.03 53 | f1 = 1e-3/2/np.pi 54 | f2 = 2/2/np.pi 55 | fs = 40*f2 56 | inctype='log' 57 | inctype ='lin' 58 | u, t_ran, finst = forcing.sinesweep(vrms,fs, f1,f2,vsweep, nrep, inctype) 59 | # we dont use the first value, as it is not repeated when the force is 60 | # generated. This is taken care of in multisine. 61 | ns = (len(u)-1) // nrep 62 | elif ftype == 'sine': 63 | u, t_ran = forcing.sine(vrms, f1, fs, nsper=ns) 64 | else: 65 | raise ValueError('Wrong type of forcing', ftype) 66 | 67 | print(ftype) 68 | print(ns) 69 | 70 | abseps = 1e-9 71 | xData = {'force': u} 72 | my_input = dst.InterpolateTable({'tdata': t_ran, 73 | 'ics': xData, 74 | 'name': 'interp1d', 75 | 'method': 'linear', # next 3 not necessary 76 | 'checklevel': 1, 77 | 'abseps': 1e-12, 78 | }).compute('interp1d') 79 | 80 | DSargs = dst.args(name='duffing_sweep') 81 | tdomain = [t_ran[0], t_ran[-1]] 82 | DSargs.tdata = tdomain 83 | DSargs.inputs = my_input.variables['force'] 84 | DSargs.pars = {'m':1, 'c':0.045*2, 'k':1.0, 'alpha':alpha, 'beta':beta, 85 | 'b':b, 'g':g} 86 | 87 | DSargs.varspecs = {'y': 'v', 88 | 'v': \ 89 | '(-k * y' \ 90 | '-c*v ' \ 91 | '-f(y) + force) / m', 92 | 'inval': 'force'} 93 | DSargs.vars = ['y', 'v'] 94 | 95 | # Piecewise linear function: 96 | DSargs.fnspecs = { 97 | 'f': (['y'], '(y+(beta-alpha)*(abs(y-b) - abs(y+b))/2)*g') 98 | } 99 | 100 | DSargs.ics = {'y': y0, 'v': v0} 101 | DSargs.algparams = {'max_step': 0.01,'rtol':1e-14, 'max_pts': 3000000,'refine': 102 | 0}#, 'jac_recompute':1e-6} 103 | DSargs.checklevel = 2 104 | 105 | if targetlang == 'python': 106 | DS = dst.Generator.Vode_ODEsystem(DSargs) 107 | else: # use radau for stiff problems 108 | #DS = dst.Generator.Dopri_ODEsystem(DSargs) 109 | DS = dst.Generator.Radau_ODEsystem(DSargs) 110 | 111 | startTime = time.time() 112 | 113 | DS.set(#tdata=[t0, t1], 114 | tdata=[t_ran[0], t_ran[-1]], 115 | ics={'y':y0, 'v':v0}) 116 | traj = DS.compute('in-table') 117 | pts = traj.sample(dt=1/fs, precise=True) 118 | 119 | print('Integration done in: {}'.format(time.time()-startTime)) 120 | 121 | a = 0 122 | y = pts['y'] 123 | v = pts['v'] 124 | t = pts['t'] 125 | u_pol = pts['inval'] 126 | 127 | def recover_acc(t, y, v): 128 | """Recover the acceleration from the RHS: 129 | """ 130 | ns = len(y) 131 | a = np.empty(y.shape) 132 | for i in range(ns): 133 | a[i] = DS.Rhs(t[i], {'y':y[i], 'v':v[i]}, DS.pars)[0] 134 | print('accelerations recovered') 135 | return a 136 | if saveacc: 137 | a = recover_acc(t, y, v) 138 | 139 | 140 | relpath = 'data/' + 'pyds_' + ftype + 'vrms' + str(vrms) 141 | Nm = namedtuple('Nm', 'y yd ydd u_pol t finst fs ns') 142 | sweep1 = Nm(y,v,a,u,t,finst,fs,ns) 143 | if savedata: 144 | pickle.dump(sweep1, open(relpath + '.pkl', 'wb')) 145 | print('data saved as {}'.format(relpath)) 146 | 147 | 148 | plt.figure(1) 149 | plt.clf() 150 | plt.plot(t, y, '-k') #, label = 'disp') 151 | plt.xlabel('Time (t)') 152 | plt.ylabel('Displacement (m)') 153 | 154 | plt.show() 155 | # plt.legend() 156 | 157 | 158 | 159 | 160 | # Does not really work. I do not know how to update fnspecs on impact 161 | # impact1_args = {'eventtol': abseps/10, 162 | # 'eventdelay': abseps*10, 163 | # 'eventinterval': abseps*10, 164 | # 'active': True, 165 | # 'term': False, 166 | # 'precise': True, 167 | # 'name': 'impact1'} 168 | # impact2_args = {'eventtol': abseps/10, 169 | # 'eventdelay': abseps*10, 170 | # 'eventinterval': abseps*10, 171 | # 'active': True, 172 | # 'term': False, 173 | # 'precise': True, 174 | # 'name': 'impact2'} 175 | 176 | # # Upper: y-0.1',1 177 | # # Low y+0.1',-1, 178 | # impact_low_ev = dst.Events.makeZeroCrossEvent('y+0.1',-1, 179 | # impact1_args, 180 | # ['y'], 181 | # ['b', 'beta'], 182 | # #fnspecs={'f': (['y'], '0')}, 183 | # #fnspecs={'f': (['y'], '5*beta*y')}, 184 | # fnspecs={'f':(['y'], '0')}, 185 | # targetlang=targetlang) 186 | # impact_middle_ev = dst.Events.makeZeroCrossEvent('y+0.1',1, 187 | # impact2_args, 188 | # ['y'], 189 | # ['b', 'beta'], 190 | # fnspecs={'f': (['y'], '0')}, 191 | # targetlang=targetlang) 192 | 193 | #DSargs.events = [impact_low_ev, impact_middle_ev] 194 | -------------------------------------------------------------------------------- /examples/contact/rfs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import pickle 5 | from collections import namedtuple 6 | 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | 10 | from pyvib.rfs import RFS 11 | from pyvib.signal import Signal2 as Signal 12 | 13 | path = 'data/' 14 | filename = 'pyds_sweepvrms0.03' 15 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs') 16 | sweep1 = pickle.load(open(path + filename + '.pkl', 'rb')) 17 | #sweep2 = pickle.load(open(path + 'sweep2.pkl', 'rb')) 18 | 19 | y=sweep1.y 20 | ydd = sweep1.ydd 21 | yd = sweep1.yd 22 | 23 | try: 24 | raise TypeError('Uncomment this line to do numerical differentiation') 25 | fs = sweep1.fs 26 | # select only part of the signal for better differentiation using 27 | # regularization 28 | low_idx, high_idx = int(1000*fs), int(1600*fs) 29 | t = np.arange(len(y))/fs 30 | y = y[low_idx:high_idx] 31 | ydd = sweep1.ydd[low_idx:high_idx] 32 | t = t[low_idx:high_idx] 33 | 34 | # this library breaks because of too high memory usage 35 | # import sys 36 | # # https://pythonhosted.org/scikits.datasmooth/regularsmooth.html 37 | # sys.path.append('/home/paw/src/scikit-datasmooth') 38 | # from scikits import datasmooth as ds 39 | # yd = ds.calc_derivative(t,y,d=1) 40 | 41 | # noise-free data. Just do it simple 42 | yd = np.gradient(y, 1/fs) 43 | ydd = np.gradient(yd, 1/fs) 44 | 45 | # differentiate using total variation regularization 46 | # https://github.com/pawsen/tvregdiff 47 | # import sys 48 | # sys.path.append('/home/paw/src/tvregdiff') 49 | # from tvregdiff import TVRegDiff 50 | # yd = TVRegDiff(y, itern=200, alph=0.1, dx=1/fs, ep=1e-2, 51 | # scale='large', plotflag=0) 52 | # ydd = TVRegDiff(yd, itern=200, alph=1e-1, dx=1/fs, ep=1e-2, 53 | # scale='large', plotflag=0) 54 | except Exception as err: 55 | import traceback 56 | traceback.print_exc() 57 | print(err) 58 | 59 | signal = Signal(sweep1.u, sweep1.fs, sweep1.ydd) 60 | signal.set_signal(y=y, yd=yd, ydd=ydd) 61 | 62 | rfs = RFS(signal,dof=0) 63 | rfs.plot() 64 | 65 | ## Extract subplot 66 | frfs = rfs.plot.fig 67 | b = 0.1745 68 | ax2 = rfs.plot.ax2d 69 | ax2.axvline(-b,c='k', ls='--') 70 | ax2.axvline(b,c='k', ls='--') 71 | fig2 = plt.figure() 72 | ax2.figure=fig2 73 | fig2.axes.append(ax2) 74 | fig2.add_axes(ax2) 75 | 76 | dummy = fig2.add_subplot(111) 77 | ax2.set_position(dummy.get_position()) 78 | dummy.remove() 79 | ax2.set_title('') 80 | 81 | plt.show() 82 | -------------------------------------------------------------------------------- /examples/contact/test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from scipy import io 4 | 5 | # from pyvib.interpolate import piecewise_linear 6 | 7 | # delta = None 8 | # delta = [5e-5,5e-5] 9 | # slope = np.array([1e8, 0, 5e9]) 10 | # x = np.array([-1e-4, 5e-3]) 11 | # y = np.array([0,0]) 12 | # xx = np.linspace(-1e-2,1e-2,1000) 13 | # yy = piecewise_linear(x,y,slope,delta=delta,xv=xx) 14 | # yy2 = piecewise_linear(x,y,slope,delta=None,xv=xx) 15 | 16 | # plt.ion() 17 | 18 | # plt.figure(1) 19 | # plt.clf() 20 | # plt.plot(xx,yy) 21 | # plt.plot(xx,yy2) 22 | # plt.plot(x,y,'s') 23 | # plt.show() 24 | 25 | plt.ion() 26 | 27 | beta = 1 28 | alpha = 0 29 | b = 0.1745 30 | def f(y): 31 | f = y+(beta-alpha)*(np.abs(y-b) - np.abs(y+b)) 32 | return f 33 | 34 | x = np.linspace(-0.3, 0.3, 100) 35 | y = f(x) 36 | plt.figure(2) 37 | plt.clf() 38 | plt.plot(x,y) 39 | # plt.show() 40 | -------------------------------------------------------------------------------- /examples/contact/wt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import pickle 7 | from collections import namedtuple 8 | 9 | from pyvib.morletWT import WT 10 | from pyvib.signal import Signal2 as Signal 11 | 12 | 13 | path = 'data/' 14 | filename = 'pyds_sweepvrms0.03' 15 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs') 16 | sweep1 = pickle.load(open(path + filename + '.pkl', 'rb')) 17 | #sweep2 = pickle.load(open(path + 'sweep2.pkl', 'rb')) 18 | 19 | 20 | nlin = Signal(sweep1.u, sweep1.fs, sweep1.ydd) 21 | nlin.set_signal(y=sweep1.y, yd=sweep1.yd, ydd=sweep1.ydd) 22 | 23 | f1 = 1e-3 24 | f2 = 10/2/np.pi 25 | nf = 100 26 | f00 = 7 27 | dof = 0 28 | sca = 2*np.pi 29 | 30 | wtnlin = WT(nlin) 31 | wtnlin.morlet(f1, f2, nf, f00, dof=0) 32 | fwnlin, ax = wtnlin.plot(sweep1.finst, sca) 33 | 34 | plt.show() 35 | -------------------------------------------------------------------------------- /examples/nlbeam/fnsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from scipy import io 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | from collections import namedtuple 8 | 9 | from pyvib.signal import Signal 10 | from pyvib.fnsi import FNSI 11 | from pyvib.nlforce import NL_force, NL_polynomial 12 | from pyvib.modal import modal_ac, frf_mkc 13 | from pyvib.helper.modal_plotting import (plot_modes, plot_knl, plot_frf) 14 | from pyvib.frf import periodic 15 | 16 | sca = 1 17 | def print_modal(fnsi): 18 | # calculate modal parameters 19 | modal = modal_ac(fnsi.A, fnsi.C) 20 | natfreq = modal['wn'] 21 | dampfreq = modal['wd'] 22 | damping = modal['zeta'] 23 | nw = min(len(natfreq), 8) 24 | print('Undamped ω: {}'.format(natfreq[:nw])*sca) 25 | print('damped ω: {}'.format(dampfreq[:nw])*sca) 26 | print('damping: {}'.format(damping[:nw])) 27 | return modal 28 | 29 | def load(nonlin): 30 | path = 'data/' 31 | if nonlin: 32 | mat_u = io.loadmat(path + 'u_15.mat') 33 | mat_y = io.loadmat(path + 'y_15.mat') 34 | else: 35 | mat_u = io.loadmat(path + 'u_01.mat') 36 | mat_y = io.loadmat(path + 'y_01.mat') 37 | 38 | fs = mat_u['fs'].item() # 3000 39 | fmin = mat_u['fmin'].item() # 5 40 | fmax = mat_u['fmax'].item() # 500 41 | iu = mat_u['iu'].item() # 2 location of force 42 | nper = mat_u['P'].item() 43 | nsper = mat_u['N'].item() 44 | u = mat_u['u'].squeeze() 45 | y = mat_y['y'] 46 | 47 | # zero-based numbering of dof 48 | # iu are dofs of force 49 | sig = namedtuple('sig', 'u y fs fmin fmax iu nper nsper') 50 | return sig(u,y,fs,fmin,fmax,iu-1,nper,nsper) 51 | 52 | lin = load(nonlin=False) 53 | slin = Signal(u=lin.u, fs=lin.fs, y=lin.y) 54 | per = [4,5,6,7,8,9] 55 | slin.cut(lin.nsper, per) 56 | 57 | # idof are selected dofs, ie. all dofs here 58 | idof = np.arange(7) 59 | # dof where nonlinearity is 60 | nldof = 6 61 | # method to estimate BD 62 | bd_method = 'explicit' 63 | #bd_method = 'nr' 64 | 65 | # ims: matrix block order. At least n+1 66 | # nmax: max model order for stabilisation diagram 67 | # ncur: model order for erstimation 68 | ims = 40 69 | nmax = 20 70 | ncur = 6 71 | nlist = np.arange(2,nmax+3,2) 72 | 73 | nl = NL_force() 74 | fnsi = FNSI(slin, nl, idof, lin.fmin, lin.fmax) 75 | fnsi.calc_EY() 76 | fnsi.svd_comp(ims) 77 | fnsi.stabilization(nlist) 78 | # Do identification 79 | fnsi.id(ncur, bd_method) 80 | fnsi.calc_modal() 81 | fnsi.nl_coeff(lin.iu, nldof) 82 | 83 | # Load nonlinear signal 84 | nlin = load(nonlin=True) 85 | snlin = Signal(u=nlin.u, fs=nlin.fs, y=nlin.y) 86 | snlin.cut(nlin.nsper, per) 87 | 88 | # Linear identification on nonlinear signal 89 | fnsi_nl1 = FNSI(snlin, nl, idof, nlin.fmin, nlin.fmax) 90 | fnsi_nl1.calc_EY() 91 | fnsi_nl1.svd_comp(ims) 92 | fnsi_nl1.stabilization(nlist) 93 | fnsi_nl1.id(ncur, bd_method) 94 | fnsi_nl1.calc_modal() 95 | fnsi_nl1.nl_coeff(nlin.iu, nldof) 96 | 97 | 98 | enl = np.array([3,2]) 99 | knl = np.array([1,1]) 100 | inl = np.array([[6,-1], [6,-1]]) 101 | nl_pol = NL_polynomial(inl, enl, knl) 102 | nl = NL_force(nl_pol) 103 | 104 | fnsi_nl2 = FNSI(snlin, nl, idof, nlin.fmin, nlin.fmax) 105 | fnsi_nl2.calc_EY() 106 | fnsi_nl2.svd_comp(ims) 107 | fnsi_nl2.stabilization(nlist) 108 | fnsi_nl2.id(ncur, bd_method) 109 | fnsi_nl2.calc_modal() 110 | fnsi_nl2.nl_coeff(nlin.iu, nldof) 111 | 112 | # print modal characteristics 113 | print('## linear identified at low level') 114 | print_modal(fnsi) 115 | print('## linear identified at high level') 116 | print_modal(fnsi_nl1) 117 | print('## nonlinear identified at high level') 118 | print_modal(fnsi_nl2) 119 | 120 | # FRF 121 | m = snlin.u_per.shape[0] 122 | p = snlin.y_per.shape[0] 123 | R = 1 124 | u = snlin.u_per.reshape((m,snlin.nsper,R,snlin.nper),order='F').swapaxes(0,1) 125 | y = snlin.y_per.reshape((p,snlin.nsper,R,snlin.nper),order='F').swapaxes(0,1) 126 | frf_freq, frf_H, covG, covGn = periodic(u,y, fs=snlin.fs, fmin=nlin.fmin, fmax=nlin.fmax) 127 | 128 | 129 | # Do some plotting 130 | dof = 6 131 | 132 | # periodicity 133 | # slin.periodicity(lin.nsper, dof) 134 | # fper, ax = snlin.periodicity(nlin.nsper, dof) 135 | # #ax.set_title(''); ax.legend_ = None 136 | # ax.yaxis.label.set_size(20) 137 | # ax.xaxis.label.set_size(20) 138 | # ax.tick_params(labelsize=20) 139 | 140 | # FRF 141 | fH1, ax = plt.subplots() 142 | nfd = len(frf_freq) 143 | plot_frf(frf_freq, frf_H, p=dof,sca=sca, ax=ax, ls='-.', 144 | c='k', label='From signal') 145 | fnsi.plot_frf(fig=fH1, ax=ax, label='lin') 146 | fnsi_nl1.plot_frf(fig=fH1, ax=ax, label='nl_1') 147 | fnsi_nl2.plot_frf(fig=fH1, ax=ax, ls='--', label='nl2') 148 | #ax.set_title(''); ax.legend_ = None 149 | ax.legend() 150 | 151 | # Modes 152 | fmodes, ax = plt.subplots() 153 | plot_modes(idof, fnsi.modal, sca, fig=fmodes) 154 | plot_modes(idof, fnsi_nl1.modal, sca, fig=fmodes) 155 | plot_modes(idof, fnsi_nl2.modal, sca, fig=fmodes) 156 | #ax.set_title(''); ax.legend_ = None 157 | 158 | # stab diagram 159 | # fstab, ax1 = plt.subplots() 160 | # ax2 = ax1.twinx() 161 | 162 | fsdlin, ax = fnsi.plot_stab(sca) 163 | ax.set_title('Linear at low level') #; ax.legend_ = None 164 | fsdnlin1, ax = fnsi_nl1.plot_stab(sca) 165 | ax.set_title('Linear at high level') #; ax.legend_ = None 166 | fsdnlin2, ax = fnsi_nl2.plot_stab(sca) 167 | ax.set_title('Nonlinear at high level') #; ax.legend_ = None 168 | 169 | # knl 170 | fknl, axknl = plot_knl(fnsi_nl2, sca) 171 | for ax in axknl: 172 | ax[0].legend().remove() 173 | ax[0].set_title('') 174 | axknl[0][0].set_ylim(np.array([0.99, 1.01])*7.996e9) 175 | axknl[1][0].set_ylim(-np.array([0.99, 1.01])*1.049e7) 176 | 177 | plt.show() 178 | -------------------------------------------------------------------------------- /examples/nlbeam/hb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from scipy import io 7 | import pickle 8 | from collections import namedtuple 9 | 10 | from pyvib.hb.hb import HB 11 | from pyvib.hb.hbcommon import hb_signal 12 | from pyvib.nlforce import NL_force, NL_polynomial 13 | from pyvib.helper.plotting import (phase, periodic, stability, harmonic, nfrc) 14 | 15 | savedata = True 16 | 17 | mat = io.loadmat('data/NLBeam.mat') 18 | M = mat['M'] 19 | C = mat['C'] 20 | K = mat['K'] 21 | 22 | # Force parameters 23 | # location of harmonic force 24 | fdofs = 7 25 | f_amp = 3 26 | # Excitation frequency. lowest sine freq in Hz 27 | f0 = 25 28 | par_hb ={ 29 | 'NH': 5, 30 | 'npow2': 9, 31 | 'nu': 1, 32 | 'stability': True, 33 | 'rcm_permute': False, 34 | 'tol_NR': 1e-6, 35 | 'max_it_NR': 15, 36 | 'scale_x': 5e-6, # == 5e-12 37 | 'scale_t': 3000, 38 | 'amp0': 1e-4 39 | } 40 | 41 | par_cont = { 42 | 'omega_cont_min': 25*2*np.pi, 43 | 'omega_cont_max': 40*2*np.pi, 44 | 'cont_dir': 1, 45 | 'opt_it_NR': 3, 46 | 'step': 0.1, 47 | 'step_min': 0.1, 48 | 'step_max': 20, 49 | 'angle_max_pred': 90, 50 | 'it_cont_max': 1e4, 51 | 'adaptive_stepsize': True 52 | } 53 | 54 | 55 | inl = np.array([[27,-1], [27,-1]]) 56 | enl = np.array([3,2]) 57 | knl = np.array([8e9,-1.05e7]) 58 | nl_pol = NL_polynomial(inl, enl, knl) 59 | nl = NL_force() 60 | nl.add(nl_pol) 61 | 62 | 63 | hb = HB(M, C, K, nl, **par_hb) 64 | omega, z, stab, B = hb.periodic(f0, f_amp, fdofs) 65 | tp, omegap, zp, cnorm, c, cd, cdd = hb.get_components() 66 | hb.continuation(**par_cont) 67 | 68 | if savedata: 69 | filename = abspath + 'data/hb.pkl' 70 | pickle.dump(hb, open(filename, "wb")) 71 | -------------------------------------------------------------------------------- /examples/nlbeam/hb_plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | from collections import namedtuple 7 | import pickle 8 | from pyvib.hb.hbcommon import hb_components 9 | 10 | path = 'data/' 11 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs') 12 | 13 | hb = pickle.load(open(path + 'hb.pkl', 'rb')) 14 | sweep1 = pickle.load(open(path + 'sweep1.pkl', 'rb')) 15 | sweep2 = pickle.load(open(path + 'sweep2.pkl', 'rb')) 16 | 17 | dof=27 18 | 19 | plt.figure(1) 20 | plt.clf() 21 | plt.plot(sweep1.finst, sweep1.y[dof]) 22 | plt.plot(sweep2.finst, sweep2.y[dof]) 23 | x = np.asarray(hb.omega_vec)/hb.scale_t/2/np.pi 24 | y = np.asarray(hb.xamp_vec).T[dof] 25 | stab_vec = np.asarray(hb.stab_vec) 26 | idx1 = ~stab_vec 27 | idx2 = stab_vec 28 | plt.plot(np.ma.masked_where(idx1, x), 29 | np.ma.masked_where(idx1, y), '-k', 30 | np.ma.masked_where(idx2, x), 31 | np.ma.masked_where(idx2, y), '--k') 32 | 33 | # plt.plot(x,y,'-k') 34 | plt.ylabel('Amplitude (m)') 35 | plt.xlabel('Frequency (Hz)') 36 | fnfrc = plt.gcf() 37 | 38 | # get harmonic components 39 | NH = hb.NH 40 | n = hb.n 41 | cvec = [] 42 | for z in hb.z_vec: 43 | c, phi, cnorm = hb_components(z, n, NH) 44 | cvec.append(cnorm) 45 | cvec = np.asarray(cvec) 46 | cvec = np.moveaxis(cvec, 0, -1) 47 | x = np.asarray(hb.omega_vec)/hb.scale_t/2/np.pi 48 | 49 | plt.figure(2) 50 | plt.clf() 51 | plt.plot(x,cvec[dof,0],'-') 52 | plt.plot(x,cvec[dof,1],'-k') 53 | plt.plot(x,cvec[dof,2],':') 54 | plt.plot(x,cvec[dof,3],'-.') 55 | plt.plot(x,cvec[dof,4],'-') 56 | plt.plot(x,cvec[dof,5],'--') 57 | plt.xlabel('Frequency (Hz)') 58 | plt.ylabel('Nomalised components') 59 | fhar = plt.gcf() 60 | 61 | plt.show() 62 | -------------------------------------------------------------------------------- /examples/nlbeam/newmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | from scipy import io 7 | 8 | 9 | from pyvib.newmark import newmark_beta_nl as newmark_beta 10 | from pyvib.forcing import sine, multisine, sinesweep, toMDOF 11 | from pyvib.nlforce import NL_force, NL_polynomial 12 | from collections import namedtuple 13 | import pickle 14 | 15 | 16 | savedata = True 17 | 18 | mat = io.loadmat('data/NLBeam.mat') 19 | M = mat['M'] 20 | C = mat['C'] 21 | K = mat['K'] 22 | 23 | inl = np.array([[27,-1], [27,-1]]) 24 | enl = np.array([3,2]) 25 | knl = np.array([8e9,-1.05e7]) 26 | nl_pol = NL_polynomial(inl, enl, knl) 27 | nl = NL_force(nl_pol) 28 | 29 | # forcing parameters 30 | ftype = 'sine' 31 | #ftype = 'multisine' 32 | ftype = 'sweep' 33 | dt = 0.0002 34 | fs = 1/dt 35 | 36 | vrms = 3 37 | f1 = 25 38 | f2 = 40 39 | vsweep = 10 40 | fdof = 7 41 | inctype = 'lin' 42 | nper = 1 43 | ndof = M.shape[0] 44 | 45 | x0 = 0 46 | xd0 = 0 47 | 48 | # get external forcing 49 | if ftype == 'multisine': 50 | nsper = 1 51 | u, t = multisine(vrms,fs, f1,f2,nsper, nper) 52 | elif ftype == 'sweep': 53 | u, t, finst = sinesweep(vrms,fs, f1,f2,vsweep, nper, inctype) 54 | # we dont use the first value, as it is not repeated when the force is 55 | # generated. This is taken care of in multisine. 56 | nsper = (len(u)-1) // nper 57 | elif ftype == 'sine': 58 | u, t = sine(vrms, f1, fs, nsper) 59 | else: 60 | raise ValueError('Wrong type of forcing', ftype) 61 | fext = toMDOF(u, ndof, fdof) 62 | print('ns_tot \t = %d' % len(u)) 63 | 64 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs') 65 | 66 | x, xd, xdd = newmark_beta(M, C, K, x0, xd0, dt, fext, nl, sensitivity=False) 67 | sweep1 = Nm(x,xd,xdd,u,t,finst,fs) 68 | 69 | u, t, finst = sinesweep(vrms,fs, f2,f1,-vsweep, nper, inctype) 70 | fext = toMDOF(u, ndof, fdof) 71 | x, xd, xdd = newmark_beta(M, C, K, x0, xd0, dt, fext, nl, sensitivity=False) 72 | sweep2 = Nm(x,xd,xdd,u,t,finst,fs) 73 | 74 | path = 'data/' 75 | if savedata: 76 | pickle.dump(sweep1, open(path + 'sweep1.pkl', 'wb')) 77 | pickle.dump(sweep2, open(path + 'sweep2.pkl', 'wb')) 78 | print('data saved as {}'.format(path)) 79 | 80 | 81 | plt.figure() 82 | plt.clf() 83 | plt.plot(t,x[0],'-k') 84 | plt.xlabel('Time (t)') 85 | plt.ylabel('Displacement (m)') 86 | plt.title('Force type: {}, periods:{:d}'.format(ftype, nper)) 87 | plt.show() 88 | -------------------------------------------------------------------------------- /examples/nlbeam/nnm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | from scipy import io 7 | import pickle 8 | 9 | from pyvib.nnm import NNM 10 | from pyvib.nlforce import NL_force, NL_polynomial 11 | 12 | savedata = True 13 | 14 | mat = io.loadmat('data/NLBeam.mat') 15 | M = mat['M'] 16 | K = mat['K'] 17 | inl = np.array([[27,-1], [27,-1]]) 18 | enl = np.array([3,2]) 19 | knl = np.array([8e9,-1.05e7]) 20 | nl_pol = NL_polynomial(inl, enl, knl) 21 | nl = NL_force(nl_pol) 22 | 23 | par_nnm = { 24 | 'omega_min': 1*2*np.pi, 25 | 'omega_max': 170*2*np.pi, 26 | 'opt_it_NR': 3, 27 | 'max_it_NR': 25, 28 | 'tol_NR': 1e-6, 29 | 'step': 0.001, 30 | 'step_min': 1e-6, 31 | 'step_max': 0.1, 32 | 'scale': 1e-4, 33 | 'angle_max_beta': 90, 34 | 'adaptive_stepsize': True, 35 | 'mode': 0, 36 | 'anim':False 37 | } 38 | 39 | nnm1 = NNM(M, K, nl, **par_nnm) 40 | nnm1.periodic() 41 | nnm1.continuation() 42 | 43 | par_nnm['mode'] = 1 44 | nnm2 = NNM(M, K, nl, **par_nnm) 45 | nnm2.periodic() 46 | nnm2.continuation() 47 | 48 | filename = 'data/nnm' 49 | if savedata: 50 | pickle.dump(nnm1, open(filename + '1' + '.pkl', 'wb')) 51 | pickle.dump(nnm2, open(filename + '2' + '.pkl', 'wb')) 52 | 53 | if nnm.anim: 54 | plt.show() 55 | -------------------------------------------------------------------------------- /examples/nlbeam/nnm_plot.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import pickle 5 | import numpy as np 6 | import matplotlib.pyplot as plt 7 | 8 | from scipy.linalg import eigvals 9 | from pyvib.helper.plotting import (phase, periodic, stability, configuration, 10 | nfrc) 11 | 12 | plot = False 13 | 14 | path = 'data/' 15 | nnm = pickle.load(open(path + 'nnm1' + '.pkl', 'rb')) 16 | 17 | def plots(t, x, xd, xxd, lamb, T, dof, inl, ptype='displ', idx='', savefig=False): 18 | fper, ax = periodic(t, x, dof=0, ls='-', c='k') 19 | fper, ax = periodic(t, x, dof=1, fig=fper, ax=ax, ls='--', c='k') 20 | fphase, ax = phase(x, xd, dof, c='k') 21 | fconf, ax = configuration(x, c='k') 22 | 23 | 24 | dof = 0 25 | # get full periodic solution for last entry 26 | T = 2*np.pi/nnm.omega_vec[-1] 27 | x, xd, xdd, PhiT, dt = nnm.numsim(nnm.X0_vec[-1], T) 28 | lamb = eigvals(PhiT) 29 | ns = x.shape[1] 30 | t = np.arange(ns)*dt 31 | if plot: 32 | plots(t, x, xd, xdd, lamb, T, dof, inl, ptype='displ', idx='', savefig=savefig) 33 | 34 | 35 | ffrf, ax = nfrc(nnm=nnm, interactive=False, xscale=1/2/np.pi, xunit='(Hz)') 36 | ffep, ax = nfrc(nnm=nnm, interactive=False, xscale=1/2/np.pi, 37 | xunit='(Hz)', energy_plot=True) 38 | -------------------------------------------------------------------------------- /examples/nlbeam/rfs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | import pickle 7 | from collections import namedtuple 8 | 9 | from pyvib.signal import Signal_legacy as Signal 10 | from pyvib.rfs import RFS 11 | 12 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs') 13 | sweep1 = pickle.load(open('data/sweep1.pkl', 'rb')) 14 | # sweep2 = pickle.load(open('data/sweep2.pkl', 'rb')) 15 | 16 | signal = Signal(sweep1.u, sweep1.fs, sweep1.ydd) 17 | signal.set_signal(y=sweep1.y, yd=sweep1.yd, ydd=sweep1.ydd) 18 | 19 | rfs = RFS(signal,dof=27) 20 | rfs.plot() 21 | 22 | plt.show() 23 | -------------------------------------------------------------------------------- /examples/nlbeam/wt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import matplotlib.pyplot as plt 5 | import numpy as np 6 | import pickle 7 | 8 | from collections import namedtuple 9 | 10 | from pyvib.morletWT import WT 11 | from pyvib.signal import Signal 12 | 13 | Nm = namedtuple('Nm', 'y yd ydd u t finst fs') 14 | sweep1 = pickle.load(open('data/sweep1.pkl', 'rb')) 15 | #sweep2 = pickle.load(open(path + 'sweep2.pkl', 'rb')) 16 | 17 | # dof where nonlinearity is 18 | nldof = 27 19 | 20 | # wt parameters 21 | nf = 100 22 | f00 = 10 23 | 24 | # Load nonlinear signal 25 | snlin = Signal(sweep1.u, sweep1.fs, sweep1.ydd) 26 | wtnlin = WT(snlin) 27 | wtnlin.morlet(f1=5, f2=300, nf=nf, f00=f00, dof=nldof) 28 | fwnlin, ax = wtnlin.plot(fss=sweep1.finst) 29 | #ax.set_xlim([0,20]) 30 | #ax.set_ylim([0,20]) 31 | 32 | plt.show() 33 | -------------------------------------------------------------------------------- /examples/nnm/nnm_periodic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import matplotlib.pylab as plt 6 | from scipy.integrate import odeint 7 | 8 | 9 | T = 2.0215 10 | ns = 100 11 | x0= 4.9009 12 | v0 = 0 13 | M = 1 14 | k = 1 15 | k3 = 0.5 16 | 17 | points = [[4.9009, 0], [-1.0313, -12.9188], [-2.9259, 11.8894]] 18 | p = [1, 5, 3] 19 | lw = 1 20 | 21 | def system_1dof(w, t): 22 | """SDOF Duffing oscillator for odeint 23 | 24 | w : state vector 25 | t : current time step 26 | f : Return system matrix 27 | """ 28 | x1, y1 = w 29 | # Create f = (x1',y1') 30 | f = [y1, 31 | (-k*x1 - k3*x1**3)/M] 32 | return f 33 | 34 | 35 | x0_vec = np.array([1, 0.9, 1.1]) * x0 36 | sol_vec = [] 37 | t = np.arange(ns+1)/ns*T 38 | 39 | for x0_ in x0_vec: 40 | w0 = x0_, v0 41 | sol = odeint(system_1dof, w0, t) 42 | sol_vec.append(sol) 43 | 44 | print(sol_vec[0][ns//p[0],0], sol_vec[0][ns//p[0],1], 45 | sol_vec[0][ns//p[1]*4,0], sol_vec[0][ns//p[1]*4,1], 46 | sol_vec[0][ns//p[2],0], sol_vec[0][ns//p[2],1] 47 | ) 48 | 49 | 50 | plt.ion() 51 | plt.figure(1) 52 | plt.clf() 53 | plt.xlabel('Time (s)') 54 | plt.ylabel(r'$x$') 55 | plt.grid(True) 56 | plt.plot(t, sol_vec[0][:,0], 'k', linewidth=lw, label=r'$x_1$') 57 | plt.plot(t, sol_vec[1][:,0], 'k--', linewidth=lw, label=r'$x_2$') 58 | plt.plot(t, sol_vec[2][:,0], 'k-.', linewidth=lw, label=r'$x_2$') 59 | plt.plot(t[0],sol_vec[0][0,0],'dk', mfc='none') 60 | plt.plot(t[ns//p[0]],sol_vec[0][ns//p[0],0],'dk', mfc='none') 61 | plt.plot(t[ns//p[1]*4],sol_vec[0][ns//p[1]*4,0],'sk', mfc='none') 62 | plt.plot(t[ns//p[2]],sol_vec[0][ns//p[2],0],'ok', mfc='none') 63 | # plt.legend() 64 | plt.tight_layout() 65 | fig1 = plt.gcf() 66 | 67 | plt.figure(2) 68 | plt.clf() 69 | plt.plot(sol_vec[0][:,0], sol_vec[0][:,1], 'k', linewidth=lw, label=r'$x_1$') 70 | plt.plot(sol_vec[1][:,0], sol_vec[1][:,1], 'k--', linewidth=lw, label=r'$x_2$') 71 | plt.plot(sol_vec[2][:,0], sol_vec[2][:,1], 'k-.', linewidth=lw, label=r'$x_2$') 72 | plt.plot(sol_vec[0][ns//p[0],0], sol_vec[0][ns//p[0],1],'dk', mfc='none') 73 | plt.plot(sol_vec[0][ns//p[1]*4,0], sol_vec[0][ns//p[1]*4,1],'sk', mfc='none') 74 | plt.plot(sol_vec[0][ns//p[2],0], sol_vec[0][ns//p[2],1],'ok', mfc='none') 75 | plt.xlabel(r'$x$') 76 | plt.ylabel(r'$\dot x$') 77 | # plt.axis('equal') 78 | plt.tight_layout() 79 | fig2 = plt.gcf() 80 | 81 | 82 | plt.show() 83 | -------------------------------------------------------------------------------- /examples/pnlss/hammerstein_x0u0.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from copy import deepcopy 5 | 6 | import matplotlib.pyplot as plt 7 | import numpy as np 8 | from scipy import signal 9 | 10 | from pyvib.pnlss import PNLSS 11 | from pyvib.signal import Signal 12 | 13 | print("""Estimating Hammerstein system with and without estimated non-zero initial condition.""") 14 | 15 | np.random.seed(10) 16 | N = int(2e3) # Number of samples 17 | NTrans = 100 # Number of samples after zero initial conditions 18 | u = np.random.randn(NTrans + N) 19 | 20 | 21 | def f_NL(x): 22 | return x + 0.2 * x**2 + 0.1 * x**3 23 | 24 | 25 | b, a = signal.cheby1(2, 5, 2 * 0.3, "low", analog=False) 26 | x = f_NL(u) # Intermediate signal 27 | y = signal.lfilter(b, a, x) # output 28 | # remove first NTrans samples to obtain non-zero initial conditions 29 | mask = np.ones(len(u), dtype=bool) 30 | mask[np.r_[:NTrans]] = False 31 | u = u[mask] 32 | x = x[mask] 33 | y = y[mask] 34 | scale = np.linalg.lstsq(u[:, None], x)[0].item() 35 | # Initial linear model = scale factor times underlying dynamics 36 | sys = signal.tf2ss(scale * b, a) 37 | T1 = 0 # No periodic transient handling 38 | # XXX: transient handling doesnt work, throws an error if T2 != 0 39 | T2 = 0 # No transient samples to discard 40 | sig = Signal(u[:, None, None, None], y[:, None, None, None]) 41 | sig.average() 42 | model = PNLSS(sys, sig) 43 | 44 | # Hammerstein system only has nonlinear terms in the input. 45 | # Quadratic and cubic terms in state- and output equation 46 | model.nlterms("x", [2, 3], "inputsonly") 47 | model.nlterms("y", [2, 3], "inputsonly") 48 | model.transient(T1=T1, T2=T2) 49 | 50 | print("Optimizing without estimated initial conditions") 51 | model.optimize(weight=False, nmax=50) 52 | yopt = model.simulate(u)[1].squeeze() 53 | 54 | # Estimate initial conditions. Start from nonlinear model 55 | model_x0u0 = deepcopy(model) 56 | print("Optimizing with estimated initial conditions") 57 | model_x0u0.optimize(weight=False, nmax=50) 58 | yopt_x0u0 = model_x0u0.simulate(u)[1].squeeze() 59 | 60 | 61 | def rmse(y, yhat): 62 | return np.sqrt(np.mean((y - yhat) ** 2)) 63 | 64 | 65 | print(f"RMS error without init cond estimated {rmse(y, yopt)}") 66 | print(f"RMS error with init cond estimated {rmse(y, yopt_x0u0)}") 67 | 68 | err_noinit = y - yopt 69 | err_init = y - yopt_x0u0 70 | t = np.arange(N) 71 | 72 | plt.figure() 73 | plt.plot(t, err_noinit - err_init) 74 | plt.xlabel("Time") 75 | plt.ylabel("Error") 76 | plt.legend( 77 | ["Difference in error between PNLSS with and without estimated inittial conditions"] 78 | ) 79 | 80 | plt.figure() 81 | plt.title("Output and errors") 82 | plt.plot(t, y) 83 | plt.plot(t, err_noinit, ".", lw=3) 84 | plt.plot(t, err_init, "--k", lw=3) 85 | plt.xlabel("Time") 86 | plt.ylabel("Output") 87 | plt.legend(["Output", "Error PNLSS", "Error PNLSS (initial conditions estimated)"]) 88 | 89 | plt.show() 90 | -------------------------------------------------------------------------------- /examples/pnlss/multisine.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy import signal 3 | import matplotlib.pyplot as plt 4 | from pyvib.forcing import multisine 5 | from pyvib.common import db 6 | 7 | 8 | def plot(sig): 9 | plt.figure() 10 | for i, u in enumerate(sig): 11 | U = np.fft.fft(u) 12 | plt.subplot(2,2,1+i) 13 | plt.plot(db(U),'+') 14 | plt.xlabel('Frequency line') 15 | plt.ylabel('Amplitude (dB)') 16 | plt.title(f'Phase realization {i+1}') 17 | plt.subplot(2,2,3+i) 18 | plt.plot(np.angle(U),'+') 19 | plt.xlabel('Frequency line') 20 | plt.ylabel('Phase (rad)') 21 | plt.title(f'Phase realization {i+1}') 22 | 23 | 24 | # Generate two realizations of a full multisine with 1000 samples and 25 | # excitation up to one third of the Nyquist frequency 26 | N = 1000 # One thousand samples 27 | kind = 'full' # Full multisine 28 | f2 = round(N//6) # Excitation up to one sixth of the sample frequency 29 | R = 2 # Two phase realizations 30 | u, lines, freq = multisine(f2=f2,N=N,lines=kind,R=R) 31 | # Check spectra 32 | plot(u) 33 | # The two realizations have the same amplitude spectrum, but different phase 34 | # realizations (uniformly distributed between [-π,π)) 35 | 36 | # Generate a random odd multisine where the excited odd lines are split in 37 | # groups of three consecutive lines and where one line is randomly chosen in 38 | # each group to be a detection line (i.e. without excitation) 39 | N = 1000 40 | kind = 'oddrandom' 41 | f2 = round(N//6) 42 | R = 1 43 | # One out of three consecutive odd lines is randomly selected to be a detection line 44 | ngroup = 3 45 | u1,lines, freq = multisine(f2=f2,N=N,lines=kind,R=R,ngroup=ngroup) 46 | # Generate another phase realization of this multisine with the same excited 47 | # lines and detection lines 48 | u2,*_ = multisine(N=N,lines=lines,R=1) 49 | plot((u1[0], u2[0])) 50 | 51 | # Change the coloring and rms level of a default multisine 52 | u3 = multisine()[0][0] 53 | b, a = signal.cheby1(2,10,2*0.1) # Filter coefficients 54 | U3 = np.fft.fft(u3) 55 | worN = 2*np.pi*np.arange(len(u3))/len(u3) 56 | w, h = signal.freqz(b,a,worN) 57 | U_colored = h * U3 # Filtered spectrum 58 | u_colored = np.real(np.fft.ifft(U_colored)) # Colored multisine signal 59 | u_scaled = 2*u_colored/np.std(u_colored) # Scaled signal to rms value 2 60 | # (u_colored is zero-mean) 61 | plot((u3,u_scaled)) 62 | 63 | plt.show() 64 | -------------------------------------------------------------------------------- /examples/pnlss/nlbeam_pnlss.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from pyvib.statespace import StateSpace as linss 5 | from pyvib.statespace import Signal 6 | from pyvib.pnlss import PNLSS 7 | from pyvib.common import db 8 | from scipy.linalg import norm 9 | import numpy as np 10 | import matplotlib.pyplot as plt 11 | import scipy.io as sio 12 | 13 | """PNLSS model of nonlinear beam 14 | 15 | The NLBeam is synthetic data from a FEM simulation. It is made to resemble the 16 | COST beam, ie. a clamped beam with a thin connection at the tip. The tip 17 | connection can be seen as a nonlinear spring. """ 18 | 19 | # save figures to disk 20 | savefig = True 21 | 22 | data = sio.loadmat('data/NLbeam_u_15.mat') 23 | lines = data['flines'].squeeze() # TODO 24 | fs = data['fs'].item() 25 | npp = data['N'].item() 26 | P = data['P'].item() 27 | iu = data['iu'].item() 28 | fmax = data['fmax'].item() 29 | fmin = data['fmin'].item() 30 | u = data['u'] 31 | 32 | datay = sio.loadmat('data/NLbeam_y_15.mat') 33 | y = datay['y'].T 34 | 35 | NT, m = u.shape 36 | NT, p = y.shape 37 | npp = NT//P 38 | R = 1 39 | # (npp,m,R,P) 40 | u = u.reshape((npp,P,m,R)).transpose(0,2,3,1) 41 | y = y.reshape((npp,P,p,R), order='F').transpose(0,2,3,1) 42 | 43 | y = y[:,3:28:4] 44 | p = 7 45 | 46 | # partitioning the data 47 | # validation data = last period of the last realization. 48 | uval = u[:,:,-1,-1] 49 | yval = y[:,:,-1,-1] 50 | 51 | # estimation data 52 | # discard first Ptr periods to get steady state data 53 | Ptr = 3 54 | uest = u[:,:,:R,Ptr:-1] 55 | yest = y[:,:,:R,Ptr:-1] 56 | 57 | # model orders and Subspace dimensioning parameter 58 | n = 6 59 | maxr = 7 60 | 61 | sig = Signal(uest,yest,fs=1) 62 | sig.lines(lines) 63 | # um: (npp*R, m) # TODO is this the actual format of the output? 64 | um, ym = sig.average() 65 | 66 | linmodel = linss() 67 | # estimate bla, total distortion, and noise distortion 68 | linmodel.bla(sig) 69 | models, infodict = linmodel.scan(n, maxr, weight=False) 70 | 71 | # estimate PNLSS 72 | # transient: Add three periods before the start of each realization. Note that 73 | # this for the signal averaged over periods 74 | T1 = np.r_[Ptr*npp, np.r_[0:(R-1)*npp+1:npp]] 75 | 76 | model = PNLSS(linmodel.A, linmodel.B, linmodel.C, linmodel.D) 77 | model.signal = sig 78 | model.nlterms('x', [2,3], 'statesonly') 79 | model.nlterms('y', [2,3], 'empty') 80 | model.transient(T1) 81 | model.optimize(weight=False, nmax=20) 82 | 83 | # compute linear and nonlinear model output on training data 84 | tlin, ylin, xlin = linmodel.simulate(um, T1=T1) 85 | _, ynlin, _ = model.simulate(um) 86 | 87 | # get best model on validation data. Change Transient settings, as there is 88 | # only one realization 89 | nl_errvec = model.extract_model(yval, uval, T1=Ptr*npp) 90 | 91 | # compute model output on test data(unseen data) 92 | _, ylval, _ = linmodel.simulate(uval, T1=Ptr*npp) 93 | _, ynlval, _ = model.simulate(uval, T1=Ptr*npp) 94 | 95 | # compute model output on test data(unseen data) 96 | _, yltest, _ = linmodel.simulate(utest, T1=0) 97 | _, ynltest, _ = model.simulate(utest, T1=0) 98 | yltest = np.delete(yltest,np.s_[:Ntr])[:,None] 99 | ynltest = np.delete(ynltest,np.s_[:Ntr])[:,None] 100 | 101 | ## Plots ## 102 | # store figure handle for saving the figures later 103 | figs = {} 104 | 105 | plt.ion() 106 | # linear and nonlinear model error 107 | resamp = 1 108 | plt.figure() 109 | plt.plot(ym[::resamp]) 110 | plt.plot(ym[::resamp]-ylin[::resamp]) 111 | plt.plot(ym[::resamp]-ynlin[::resamp]) 112 | plt.xlabel('Time index') 113 | plt.ylabel('Output (errors)') 114 | plt.legend(('output','linear error','PNLSS error')) 115 | plt.title('Estimation results') 116 | figs['estimation_error'] = (plt.gcf(), plt.gca()) 117 | 118 | # optimization path for PNLSS 119 | plt.figure() 120 | plt.plot(db(nl_errvec)) 121 | imin = np.argmin(nl_errvec) 122 | plt.scatter(imin, db(nl_errvec[imin])) 123 | plt.xlabel('Successful iteration number') 124 | plt.ylabel('Validation error [dB]') 125 | plt.title('Selection of the best model on a separate data set') 126 | figs['pnlss_path'] = (plt.gcf(), plt.gca()) 127 | 128 | # result on validation data 129 | plt.figure() 130 | N = len(yval) 131 | resamp = 1 132 | freq = np.arange(N)/N*fs 133 | # plot only for the DOF with the nonlinear connection 134 | plottime = np.hstack((yval, yval-ylval, yval-ynlval))[:,6] 135 | plotfreq = np.fft.fft(plottime, axis=0) / sqrt(N) 136 | nfd = plotfreq.shape[0] 137 | plt.plot(freq[1:nfd//2:resamp], db(plotfreq[1:nfd//2:resamp]), '.') 138 | plt.xlabel('Frequency') 139 | plt.ylabel('Output (errors) (dB)') 140 | plt.legend(('Output','Linear error','PNLSS error')) 141 | plt.title('Validation results') 142 | figs['val_data'] = (plt.gcf(), plt.gca()) 143 | 144 | # subspace plots 145 | figs['subspace_optim'] = linmodel.plot_info() 146 | figs['subspace_models'] = linmodel.plot_models() 147 | 148 | if savefig: 149 | for k, fig in figs.items(): 150 | fig = fig if isinstance(fig, list) else [fig] 151 | for i, f in enumerate(fig): 152 | f[0].tight_layout() 153 | f[0].savefig(f"SNbenchmark_{k}{i}.pdf") 154 | -------------------------------------------------------------------------------- /examples/pnlss/silverbox_benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import pickle 5 | from copy import deepcopy 6 | 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | import scipy.io as sio 10 | from scipy.linalg import norm 11 | 12 | from pyvib.common import db 13 | from pyvib.frf import covariance 14 | from pyvib.pnlss import PNLSS 15 | from pyvib.signal import Signal 16 | from pyvib.subspace import Subspace 17 | 18 | """PNLSS model of the silverbox. 19 | 20 | The Silverbox system can be seen as an electroninc implementation of the 21 | Duffing oscilator. It is build as a 2nd order linear time-invariant system with 22 | a 3rd degree polynomial static nonlinearity around it in feedback. This type of 23 | dynamics are, for instance, often encountered in mechanical systems. 24 | nonlinearity. The input-output data is synthetic. 25 | 26 | See http://www.nonlinearbenchmark.org/#Silverbox 27 | """ 28 | 29 | # save figures to disk 30 | savefig = True 31 | savedata = True 32 | 33 | data = sio.loadmat('data/SNLS80mV.mat') 34 | # partitioning the data 35 | u = data['V1'].T 36 | y = data['V2'].T 37 | u -= u.mean() 38 | y -= y.mean() 39 | 40 | R = 9 41 | P = 1 42 | fs = 1e7/2**14 43 | m = 1 # number of inputs 44 | p = 1 # number of outputs 45 | 46 | npp = 8192 47 | Nini = 86 # number of initial samples before the test starts. 48 | Ntest = int(40e3) # number of validation samples. 49 | Nz = 100 # number of zero samples separating the blocks visually. 50 | Ntr = 400 # number of transient samples. 51 | 52 | # odd multisine till 200 Hz, without DC. 53 | lines = np.arange(1,2683,2) 54 | 55 | # partitioning the data 56 | # Test data: only 86 zero samples in the initial arrow-like input. 57 | utest = u[Nini + np.r_[:Ntest+Ntr]] 58 | ytest = y[Nini+Ntr + np.r_[:Ntest]] 59 | 60 | # Estimation data. 61 | u = np.delete(u, np.s_[:Nini+Ntr+Ntest]) 62 | y = np.delete(y, np.s_[:Nini+Ntr+Ntest]) 63 | 64 | uest = np.empty((npp,R,P)) 65 | yest = np.empty((npp,R,P)) 66 | for r in range(R): 67 | u = np.delete(u, np.s_[:Nz+Ntr]) 68 | y = np.delete(y, np.s_[:Nz+Ntr]) 69 | 70 | uest[:,r] = u[:npp,None] 71 | yest[:,r] = y[:npp,None] 72 | 73 | u = np.delete(u, np.s_[:npp]) 74 | y = np.delete(y, np.s_[:npp]) 75 | 76 | #uest = np.repeat(uest[:,None],p,axis=1) 77 | uest = uest.reshape(npp,m,R,P) 78 | yest = yest.reshape(npp,p,R,P) 79 | Pest = yest.shape[-1] 80 | 81 | # Validation data. 82 | u = np.delete(u, np.s_[:Nz+Ntr]) 83 | y = np.delete(y, np.s_[:Nz+Ntr]) 84 | 85 | uval = u[:npp, None] 86 | yval = y[:npp, None] 87 | 88 | # model orders and Subspace dimensioning parameter 89 | n = 2 90 | maxr = 20 91 | 92 | sig = Signal(uest,yest,fs=fs) 93 | sig.lines = lines 94 | # estimate bla, total distortion, and noise distortion 95 | sig.bla() 96 | # average signal over periods. Used for training of PNLSS model 97 | # Even if there's only 1 period, pnlss expect this to be run first. It also 98 | # reshapes the signal, so um: (npp*m*R) 99 | um, ym = sig.average() 100 | 101 | linmodel = Subspace(sig) 102 | # estimate bla, total distortion, and noise distortion 103 | linmodel.estimate(n,maxr) 104 | linmodel2 = deepcopy(linmodel) 105 | linmodel2.optimize(weight=True) 106 | 107 | # estimate PNLSS 108 | # transient: Add two periods before the start of each realization. Note that 109 | # this for the signal averaged over periods 110 | T1 = np.r_[2*npp, np.r_[0:(R-1)*npp+1:npp]] 111 | 112 | pnlss1 = PNLSS(linmodel2) 113 | pnlss1.nlterms('x', [2,3], 'full') 114 | # pnlss1.nlterms('y', [2,3], 'empty') 115 | pnlss1.transient(T1) 116 | 117 | covY = np.ones((round(npp//2),1,1)) 118 | pnlss2= deepcopy(pnlss1) 119 | pnlss1.optimize(weight=False, nmax=50) 120 | pnlss2.optimize(weight=covY, nmax=50) 121 | models = [linmodel, linmodel2, pnlss1, pnlss2] 122 | descrip = ('Subspace','Subspace opt','pnlss','pnlss weight') 123 | nmodels = len(models) 124 | 125 | # simulation error 126 | est = np.empty((len(models),len(um))) 127 | val = np.empty((len(models),len(uval))) 128 | test = np.empty((len(models),len(utest))) 129 | 130 | # add two transient period 131 | Ptr2 = 2 132 | opt_path = [[] for i in range(nmodels)] 133 | for i, model in enumerate(models): 134 | est[i] = model.simulate(um, T1=Ptr2*npp)[1].T 135 | try: 136 | nl_errvec = model.extract_model(yval, uval, T1=Ptr2*npp) 137 | opt_path[i].append(nl_errvec) 138 | except: 139 | pass 140 | val[i] = model.simulate(uval, T1=Ptr2*npp)[1].T 141 | test[i] = model.simulate(utest, T1=0)[1].T 142 | 143 | # delete transient data from test 144 | test = np.delete(test, np.s_[:Ntr], axis=1) 145 | 146 | rms = lambda y: np.sqrt(np.mean(y**2, axis=0)) 147 | est_err = np.hstack((ym, (ym.T - est).T)) 148 | val_err = np.hstack((yval, (yval.T - val).T)) 149 | test_err = np.hstack((ytest, (ytest.T - test).T)) 150 | noise = np.abs(np.sqrt(Pest*covY.squeeze())) 151 | 152 | print(descrip) 153 | print(f'rms error est:\n {rms(est_err[:,1:])}\ndb: {db(rms(est_err[:,1:]))}') 154 | print(f'rms error val:\n {rms(val_err[:,1:])}\ndb: {db(rms(val_err[:,1:]))}') 155 | print(f'rms error test:\n {rms(test_err[:,1:])}\ndb: {db(rms(test_err[:,1:]))}') 156 | 157 | if savedata: 158 | data = {'models':models, 'opt_path':opt_path, 'est_err':est_err, 159 | 'val_err':val_err, 'test_err':test_err, 'descrip':descrip} 160 | pickle.dump(data, open('sn_benchmark_pnlss.pkl', 'bw')) 161 | 162 | 163 | 164 | ## Plots ## 165 | # store figure handle for saving the figures later 166 | figs = {} 167 | 168 | #plt.ion() 169 | # result on estimation data 170 | resamp = 20 171 | plt.figure() 172 | plt.plot(est_err[::resamp]) 173 | plt.xlabel('Time index') 174 | plt.ylabel('Output (errors)') 175 | plt.legend(('Output',) + descrip) 176 | plt.title('Estimation results') 177 | figs['estimation_error'] = (plt.gcf(), plt.gca()) 178 | 179 | # result on validation data 180 | resamp = 2 181 | plt.figure() 182 | plottime = val_err 183 | N = plottime.shape[0] 184 | freq = np.arange(N)/N*fs 185 | plotfreq = np.fft.fft(plottime, axis=0)/np.sqrt(N) 186 | nfd = plotfreq.shape[0] 187 | plt.plot(freq[1:nfd//2:resamp], db(plotfreq[1:nfd//2:resamp]), '.') 188 | #plt.ylim([-110,10]) 189 | plt.xlim((0, 300)) 190 | plt.xlabel('Frequency') 191 | plt.ylabel('Output (errors) (dB)') 192 | plt.legend(('Output',) + descrip + ('Noise',)) 193 | plt.title('Validation results') 194 | figs['val_data'] = (plt.gcf(), plt.gca()) 195 | 196 | # result on test data 197 | resamp = 2 198 | plt.figure() 199 | plottime = test_err 200 | N = plottime.shape[0] 201 | freq = np.arange(N)/N*fs 202 | plotfreq = np.fft.fft(plottime, axis=0)/np.sqrt(N) 203 | nfd = plotfreq.shape[0] 204 | plt.plot(freq[1:nfd//2:resamp], db(plotfreq[1:nfd//2:resamp]), '.') 205 | #plt.ylim([-110,10]) 206 | plt.xlim((0, 300)) 207 | plt.xlabel('Frequency') 208 | plt.ylabel('Output (errors) (dB)') 209 | plt.legend(('Output',) + descrip + ('Noise',)) 210 | plt.title('Test results') 211 | figs['test_data'] = (plt.gcf(), plt.gca()) 212 | 213 | # optimization path for PNLSS 214 | plt.figure() 215 | for err in [opt_path[2], opt_path[3]]: 216 | plt.plot(db(err)) 217 | imin = np.argmin(err) 218 | plt.scatter(imin, db(err[imin])) 219 | plt.xlabel('Successful iteration number') 220 | plt.ylabel('Validation error [dB]') 221 | plt.legend(descrip[2:]) 222 | plt.title('Selection of the best model on a separate data set') 223 | figs['fnsi_path'] = (plt.gcf(), plt.gca()) 224 | 225 | # subspace plots 226 | figs['subspace_optim'] = linmodel.plot_info() 227 | figs['subspace_models'] = linmodel.plot_models() 228 | 229 | if savefig: 230 | for k, fig in figs.items(): 231 | fig = fig if isinstance(fig, list) else [fig] 232 | for i, f in enumerate(fig): 233 | f[0].tight_layout() 234 | f[0].savefig(f"fig/SNbenchmark_{k}{i}.pdf") 235 | -------------------------------------------------------------------------------- /examples/pnlss/silverbox_jp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import pickle 5 | from copy import deepcopy 6 | 7 | import matplotlib.pyplot as plt 8 | import numpy as np 9 | import scipy.io as sio 10 | from scipy.linalg import norm 11 | 12 | from pyvib.common import db 13 | from pyvib.frf import covariance 14 | from pyvib.pnlss import PNLSS 15 | from pyvib.signal import Signal 16 | from pyvib.subspace import Subspace 17 | 18 | """PNLSS model of the silverbox system. 19 | 20 | The Silverbox system can be seen as an electroninc implementation of the 21 | Duffing oscilator. It is build as a 2nd order linear time-invariant system with 22 | a 3rd degree polynomial static nonlinearity around it in feedback. This type of 23 | dynamics are, for instance, often encountered in mechanical systems. 24 | nonlinearity. The input-output data is synthetic. 25 | See http://www.nonlinearbenchmark.org/#Silverbox 26 | 27 | This code correspond to the article 28 | Grey-box state-space identification of nonlinear mechanical vibrations 29 | JP. Noël & J. Schoukens 30 | http://dx.doi.org/10.1080/00207179.2017.1308557 31 | 32 | Values from paper: 33 | Estimated nonliner coefficients at different sampling rates 34 | | fs (Hz) | c1 | c2 | 35 | |---------+--------+------| 36 | | 2441 | -0.256 | 3.98 | 37 | | 12205 | -0.267 | 3.96 | 38 | 39 | Identified at low level (5V) 40 | | Nat freq (Hz) | Damping ratio (%) | 41 | |---------------+-------------------| 42 | | 68.58 | 4.68 | 43 | """ 44 | 45 | # save figures to disk 46 | savefig = True 47 | savedata = True 48 | 49 | def load(var, amp, fnsi=True): 50 | fnsi = 'FNSI_' if fnsi else '' 51 | path = 'data/' 52 | fname = f"{path}SNJP_{var}m_full_{fnsi}{amp}.mat" 53 | data = sio.loadmat(fname) 54 | if var == 'u': 55 | um, fs, flines, P = [data[k] for k in ['um', 'fs', 'flines', 'P']] 56 | return um, fs.item(), flines.squeeze(), P.item() 57 | else: 58 | return data['ym'] 59 | 60 | 61 | # estimation data. 62 | # 1 realization, 30 periods of 8192 samples. 5 discarded as transient (Ptr) 63 | amp = 100 64 | u, fs, lines, P = load('u',amp) 65 | lines = lines - 1 66 | y = load('y',amp) 67 | 68 | NT, R = u.shape 69 | NT, R = y.shape 70 | npp = NT//P 71 | Ptr = 5 72 | m = 1 73 | p = 1 74 | 75 | # partitioning the data 76 | u = u.reshape(npp,P,R,order='F').swapaxes(1,2)[:,None,:,Ptr:] 77 | y = y.reshape(npp,P,R,order='F').swapaxes(1,2)[:,None,:,Ptr:] 78 | uest = u 79 | yest = y 80 | Pest = yest.shape[-1] 81 | # noise estimate over estimation periods 82 | covY = covariance(yest) 83 | 84 | # Validation data. 50 different realizations of 3 periods. Use the last 85 | # realization and last period 86 | uval_raw, _, _, Pval = load('u', 100, fnsi=False) 87 | yval_raw = load('y', 100, fnsi=False) 88 | uval_raw = uval_raw.reshape(npp,Pval,50,order='F').swapaxes(1,2)[:,None] 89 | yval_raw = yval_raw.reshape(npp,Pval,50,order='F').swapaxes(1,2)[:,None] 90 | uval = uval_raw[:,:,-1,-1] 91 | yval = yval_raw[:,:,-1,-1] 92 | utest = uval_raw[:,:,1,-1] 93 | ytest = yval_raw[:,:,1,-1] 94 | Rval = uval_raw.shape[2] 95 | 96 | sig = Signal(uest,yest, fs=fs) 97 | sig.lines = lines 98 | # estimate bla, total distortion, and noise distortion 99 | sig.bla() 100 | um, ym = sig.average() 101 | 102 | # model orders and Subspace dimension parameter 103 | n = 2 104 | maxr = 20 105 | 106 | # subspace model 107 | linmodel = Subspace(sig) 108 | # models, infodict = linmodel.scan(n, maxr, weight=False) 109 | # ensure we use same dimension as for the fnsi model 110 | linmodel.estimate(n,maxr) 111 | linmodel2 = deepcopy(linmodel) 112 | linmodel2.optimize(weight=False) 113 | 114 | pnlss1 = PNLSS(linmodel) 115 | pnlss1.nlterms('x', [2,3], 'statesonly') 116 | pnlss1.transient(T1=npp) 117 | pnlss2= deepcopy(pnlss1) 118 | 119 | pnlss1.optimize(weight=False, nmax=50) 120 | pnlss2.optimize(weight=True, nmax=50) 121 | 122 | models = [linmodel, linmodel2, pnlss1, pnlss2] 123 | descrip = ('Subspace','Subspace opt','pnlss', 'pnlss weight') 124 | 125 | # find validation error for all models 126 | Ptr2 = 1 127 | nmodels = len(models) 128 | x0 = [[] for i in range(nmodels)] 129 | opt_path = [[] for i in range(nmodels)] 130 | est = np.empty((nmodels,len(um))) 131 | val_err = np.zeros((nmodels,Rval,len(uval))) 132 | for i, model in enumerate(models): 133 | est[i] = model.simulate(um, T1=npp)[1].T 134 | for j in range(Rval): 135 | uval = uval_raw[:,:,j,-1] 136 | yval = yval_raw[:,:,j,-1] 137 | try: 138 | # select best model on fresh data (val) 139 | nl_errvec = model.extract_model(yval, uval, T1=npp) 140 | opt_path[i].append(nl_errvec) 141 | except: 142 | pass 143 | x0[i].append(model.flatten()) 144 | val = model.simulate(uval, T1=Ptr2*npp)[1].T 145 | val_err[i,j] = (yval.T - val) 146 | 147 | rms = lambda y: np.sqrt(np.mean(y**2, axis=0)) 148 | rms2 = lambda y: np.sqrt(np.mean(y**2, axis=2)) 149 | val_rms = rms2(val_err) 150 | est_err = np.hstack((ym, (ym.T - est).T)) 151 | noise = np.abs(np.sqrt(Pest*covY.squeeze())) 152 | print(descrip) 153 | print(f'rms error noise. db: {db(rms(noise))} ') 154 | print(f'rms error est:\n {rms(est_err[:,1:])}\ndb: {db(rms(est_err[:,1:]))}') 155 | print(f'rms error val:\n{val_rms.T}\ndb:\n{db(val_rms.T)}') 156 | idx = np.argmin(val_rms,axis=1) 157 | print(f'Minimum db rms {db(val_rms.min(axis=1))}') 158 | print(f'index {idx}') 159 | 160 | if savedata: 161 | data = {'models':models, 'opt_path':opt_path, 'est_err':est_err, 162 | 'val_err':val_err, 'descrip':descrip, 'x0':x0} 163 | pickle.dump(data, open('sn_jp_pnlss.pkl', 'bw')) 164 | -------------------------------------------------------------------------------- /examples/pnlss/silverbox_jp_fnsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | import pickle 6 | from copy import deepcopy 7 | 8 | import matplotlib.pyplot as plt 9 | import numpy as np 10 | import scipy.io as sio 11 | from scipy.linalg import norm 12 | 13 | from pyvib.common import db 14 | from pyvib.fnsi import FNSI 15 | from pyvib.frf import covariance, periodic 16 | from pyvib.helper.modal_plotting import plot_frf, plot_knl, plot_svg 17 | from pyvib.modal import frf_mkc, modal_ac 18 | from pyvib.signal import Signal 19 | from pyvib.subspace import Subspace 20 | 21 | """FNSI model of the silverbox system. 22 | 23 | The Silverbox system can be seen as an electroninc implementation of the 24 | Duffing oscilator. It is build as a 2nd order linear time-invariant system with 25 | a 3rd degree polynomial static nonlinearity around it in feedback. This type of 26 | dynamics are, for instance, often encountered in mechanical systems. 27 | nonlinearity. The input-output data is synthetic. 28 | See http://www.nonlinearbenchmark.org/#Silverbox 29 | 30 | This code correspond to the article 31 | Grey-box state-space identification of nonlinear mechanical vibrations 32 | JP. Noël & J. Schoukens 33 | http://dx.doi.org/10.1080/00207179.2017.1308557 34 | 35 | Values from paper: 36 | Estimated nonliner coefficients at different sampling rates 37 | | fs (Hz) | c1 | c2 | 38 | |---------+--------+------| 39 | | 2441 | -0.256 | 3.98 | 40 | | 12205 | -0.267 | 3.96 | 41 | 42 | Identified at low level (5V) 43 | | Nat freq (Hz) | Damping ratio (%) | 44 | |---------------+-------------------| 45 | | 68.58 | 4.68 | 46 | 47 | RMS of validation data 48 | | Unit | Output | Lin err | fnsi init | fnsi opt | 49 | |------+----------+----------+-----------+----------| 50 | | V | 0.16 | 0.09 | 0.002 | 0.001 | 51 | | db | -15.9176 | -20.9151 | -53.9794 | -60. | 52 | 53 | """ 54 | 55 | savedata = True 56 | 57 | def load(var, amp, fnsi=True): 58 | fnsi = 'FNSI_' if fnsi else '' 59 | path = 'data/' 60 | fname = f"{path}SNJP_{var}m_full_{fnsi}{amp}.mat" 61 | data = sio.loadmat(fname) 62 | if var == 'u': 63 | um, fs, flines, P = [data[k] for k in ['um', 'fs', 'flines', 'P']] 64 | return um, fs.item(), flines.squeeze(), P.item() 65 | else: 66 | return data['ym'] 67 | 68 | # estimation data. 69 | # 1 realization, 30 periods of 8192 samples. 5 discarded as transient (Ptr) 70 | amp = 100 71 | u, fs, lines, P = load('u',amp) 72 | lines = lines - 1 73 | y = load('y',amp) 74 | 75 | NT, R = u.shape 76 | NT, R = y.shape 77 | npp = NT//P 78 | Ptr = 5 79 | m = 1 80 | p = 1 81 | 82 | # partitioning the data 83 | u = u.reshape(npp,P,R,order='F').swapaxes(1,2)[:,None,:,Ptr:] 84 | y = y.reshape(npp,P,R,order='F').swapaxes(1,2)[:,None,:,Ptr:] 85 | # FNSI can only use one realization 86 | uest = u[:,:,0,:] 87 | yest = y[:,:,0,:] 88 | Pest = yest.shape[-1] 89 | # noise estimate over Pest periods 90 | covY = covariance(yest[:,:,None]) 91 | 92 | # Validation data. 50 different realizations of 3 periods. Use the last 93 | # realization and last period 94 | uval_raw, _, _, Pval = load('u', 100, fnsi=False) 95 | yval_raw = load('y', 100, fnsi=False) 96 | uval_raw = uval_raw.reshape(npp,Pval,50,order='F').swapaxes(1,2)[:,None] 97 | yval_raw = yval_raw.reshape(npp,Pval,50,order='F').swapaxes(1,2)[:,None] 98 | uval = uval_raw[:,:,-1,-1] 99 | yval = yval_raw[:,:,-1,-1] 100 | utest = uval_raw[:,:,1,-1] 101 | ytest = yval_raw[:,:,1,-1] 102 | Rval = uval_raw.shape[2] 103 | 104 | sig = Signal(uest,yest, fs=fs) 105 | sig.lines = lines 106 | um, ym = sig.average() 107 | # sig.periodicity() 108 | 109 | # for subspace model (from BLA) 110 | sig2 = Signal(uest[:,:,None],yest[:,:,None], fs=fs) 111 | sig2.lines = lines 112 | sig2.bla() 113 | 114 | # model orders and Subspace dimensioning parameter 115 | n = 2 116 | maxr = 20 117 | dof = 0 118 | iu = 0 119 | xpowers = np.array([[2],[3]]) 120 | 121 | # subspace model 122 | lin1 = Subspace(sig2) 123 | # models, infodict = linmodel.scan(n, maxr, weight=False) 124 | # ensure we use same dimension as for the fnsi model 125 | lin1.estimate(n,maxr) 126 | lin2 = deepcopy(lin1) 127 | lin2.optimize(weight=False) 128 | 129 | # Linear model 130 | fnsi1 = FNSI(sig) 131 | fnsi1.estimate(n,maxr) 132 | fnsi1.nl_coeff(iu) 133 | 134 | # initial nonlinear model 135 | fnsi2 = FNSI(sig) 136 | fnsi2.nlterms('state',xpowers) 137 | fnsi2.estimate(n,maxr) 138 | fnsi2.nl_coeff(iu) 139 | fnsi2.transient(T1=npp) 140 | 141 | covY = np.ones((round(npp//2),1,1)) 142 | # optimized models 143 | fnsi3 = deepcopy(fnsi2) 144 | fnsi4 = deepcopy(fnsi2) # freq. weighted model 145 | fnsi5 = deepcopy(fnsi2) # freq. weighted model 146 | weights = (False, True, covY) 147 | for w, model in zip(weights,[fnsi3, fnsi4, fnsi5]): 148 | model.optimize(weight=w, nmax=50, xtol=1e-20, ftol=1e-20, gtol=1e-20) 149 | model.nl_coeff(iu) 150 | 151 | models = [lin1, lin2, fnsi1, fnsi2, fnsi3, fnsi4, fnsi5] 152 | descrip = ('subspace', 'subspace opt', 'fnsi linear','fnsi init', 153 | 'fnsi opt', 'fnsi weight', 'fnsi unit') 154 | 155 | # find validation error for all models 156 | # add one transient period 157 | Ptr2 = 2 158 | nmodels = len(models) 159 | opt_path = [[] for i in range(nmodels)] 160 | knl = [[] for i in range(nmodels)] 161 | x0 = [[] for i in range(nmodels)] 162 | est = np.empty((nmodels,len(um))) 163 | val_err = np.zeros((nmodels,Rval,len(uval))) 164 | for i, model in enumerate(models): 165 | # get est error on best model 166 | est[i] = model.simulate(um, T1=npp)[1].T 167 | for j in range(Rval): 168 | uval = uval_raw[:,:,j,-1] 169 | yval = yval_raw[:,:,j,-1] 170 | try: 171 | # select best model on fresh data (val) 172 | nl_errvec = model.extract_model(yval, uval, T1=npp) 173 | opt_path[i].append(nl_errvec) 174 | knl[i].append(model.nl_coeff(iu)[1]) 175 | except: 176 | pass 177 | x0[i].append(model.flatten()) 178 | val = model.simulate(uval, T1=Ptr2*npp)[1].T 179 | val_err[i,j] = (yval.T - val) 180 | 181 | rms = lambda y: np.sqrt(np.mean(y**2, axis=0)) 182 | rms2 = lambda y: np.sqrt(np.mean(y**2, axis=2)) 183 | val_rms = rms2(val_err) 184 | est_err = np.hstack((ym, (ym.T - est).T)) 185 | noise = np.abs(np.sqrt(Pest*covY.squeeze())) 186 | print(descrip) 187 | print(f'rms error noise. db: {db(rms(noise))} ') 188 | print(f'rms error est:\n {rms(est_err[:,1:])}\ndb: {db(rms(est_err[:,1:]))}') 189 | print(f'rms error val:\n{val_rms.T}\ndb:\n{db(val_rms.T)}') 190 | idx = np.argmin(val_rms,axis=1) 191 | print(descrip) 192 | print(f'Minimum db rms {db(val_rms.min(axis=1))}') 193 | print(f'index {idx}') 194 | 195 | if savedata: 196 | data = {'models':models, 'opt_path':opt_path, 'est_err':est_err, 197 | 'val_err':val_err, 'descrip':descrip, 'knl': knl, 'x0':x0} 198 | pickle.dump(data, open('sn_jp_fnsi.pkl', 'bw')) 199 | -------------------------------------------------------------------------------- /examples/pnlss/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from pyvib.subspace import Subspace 5 | from pyvib.signal import Signal 6 | from scipy.linalg import norm 7 | import numpy as np 8 | import matplotlib.pyplot as plt 9 | import scipy.io as sio 10 | 11 | 12 | # excitation signal 13 | RMSu = 0.05 # Root mean square value for the input signal 14 | npp = 1024 # Number of samples 15 | R = 4 # Number of phase realizations (one for validation and one for 16 | # testing) 17 | P = 3 # Number of periods 18 | kind = 'Odd' # 'Full','Odd','SpecialOdd', or 'RandomOdd': kind of multisine 19 | f1 = 0 # first excited line 20 | f2 = round(0.9*npp/2) # Last excited line 21 | fs = npp 22 | m = 1 # number of inputs 23 | p = 1 # number of outputs 24 | 25 | data2 = sio.loadmat('data/data2.mat') 26 | u_output = data2['u_output'] 27 | y_output = data2['y_output'] 28 | u = u_output.transpose((2,1,0)) 29 | y = y_output.transpose((2,1,0)) 30 | 31 | u = u.reshape((R,P,npp)).transpose((2,0,1)) # (npp,R,P) 32 | u = np.repeat(u[:,None],m,axis=1) # (npp,m,R,P) 33 | y = y.reshape((R,P,npp)).transpose((2,0,1)) 34 | y = np.repeat(y[:,None],m,axis=1) 35 | 36 | # Add colored noise to the output. randn generate white noise 37 | np.random.seed(10) 38 | noise = 1e-3*np.std(y[:,-1,-1]) * np.random.randn(*y.shape) 39 | # Do some filtering to get colored noise 40 | noise[1:-2] += noise[2:-1] 41 | y += noise 42 | 43 | # visualize periodicity 44 | # TODO 45 | 46 | ## START of Identification ## 47 | # partitioning the data 48 | # test for performance testing and val for model selection 49 | utest = u[:,:,-1,-1] 50 | ytest = y[:,:,-1,-1] 51 | uval = u[:,:,-2,-1] 52 | yval = y[:,:,-2,-1] 53 | # all other realizations are used for estimation 54 | u = u[...,:-2,:] 55 | y = y[...,:-2,:] 56 | 57 | # model orders and Subspace dimensioning parameter 58 | nvec = [2,3] 59 | maxr = 5 60 | 61 | # create signal object 62 | sig = Signal(u,y) 63 | sig.lines(lines) 64 | # average signal over periods. Used for training of PNLSS model 65 | um, ym = sig.average() 66 | npp, F = sig.npp, sig.F 67 | R, P = sig.R, sig.P 68 | 69 | linmodel = linss() 70 | # estimate bla, total distortion, and noise distortion 71 | linmodel.bla(sig) 72 | # get best model on validation data 73 | models, infodict = linmodel.scan(nvec, maxr) 74 | l_errvec = linmodel.extract_model(yval, uval) 75 | 76 | # estimate PNLSS 77 | # transient: Add one period before the start of each realization. Note that 78 | # this for the signal averaged over periods 79 | T1 = np.r_[npp, np.r_[0:(R-1)*npp+1:npp]] 80 | 81 | model = PNLSS(linmodel.A, linmodel.B, linmodel.C, linmodel.D) 82 | model.signal = linmodel.signal 83 | model.nlterms('x', [2,3], 'full') 84 | model.nlterms('y', [2,3], 'full') 85 | model.transient(T1) 86 | model.optimize(lamb=100, weight=True, nmax=60) 87 | 88 | # compute linear and nonlinear model output on training data 89 | tlin, ylin, xlin = linmodel.simulate(um, T1=T1) 90 | _, ynlin, _ = model.simulate(um) 91 | 92 | # get best model on validation data. Change Transient settings, as there is 93 | # only one realization 94 | nl_errvec = model.extract_model(yval, uval, T1=sig.npp) 95 | 96 | # compute model output on test data(unseen data) 97 | _, yltest, _ = linmodel.simulate(utest, T1=sig.npp) 98 | _, ynltest, _ = model.simulate(utest, T1=sig.npp) 99 | 100 | ## Plots ## 101 | # store figure handle for saving the figures later 102 | figs = {} 103 | 104 | # linear and nonlinear model error 105 | plt.figure() 106 | plt.plot(ym) 107 | plt.plot(ym-ylin) 108 | plt.plot(ym-ynlin) 109 | plt.xlabel('Time index') 110 | plt.ylabel('Output (errors)') 111 | plt.legend(('output','linear error','PNLSS error')) 112 | plt.title('Estimation results') 113 | figs['estimation_error'] = (plt.gcf(), plt.gca()) 114 | 115 | # optimization path for PNLSS 116 | plt.figure() 117 | plt.plot(db(nl_errvec)) 118 | imin = np.argmin(nl_errvec) 119 | plt.scatter(imin, db(nl_errvec[imin])) 120 | plt.xlabel('Successful iteration number') 121 | plt.ylabel('Validation error [dB]') 122 | plt.title('Selection of the best model on a separate data set') 123 | figs['pnlss_path'] = (plt.gcf(), plt.gca()) 124 | 125 | # result on test data 126 | plt.figure() 127 | # Normalized frequency vector 128 | freq = np.arange(sig.npp)/sig.npp*sig.fs 129 | plottime = np.hstack((ytest, ytest-yltest, ytest-ynltest)) 130 | plotfreq = np.fft.fft(plottime, axis=0) 131 | nfd = plotfreq.shape[0] 132 | plt.plot(freq[:nfd//2], db(plotfreq[:nfd//2]), '.') 133 | plt.xlabel('Frequency (normalized)') 134 | plt.ylabel('Output (errors) (dB)') 135 | plt.legend(('Output','Linear error','PNLSS error')) 136 | plt.title('Test results') 137 | figs['test_data'] = (plt.gcf(), plt.gca()) 138 | 139 | # subspace plots 140 | figs['subspace_optim'] = linmodel.plot_info() 141 | figs['subspace_models'] = linmodel.plot_models() 142 | 143 | if savefig: 144 | for k, fig in figs.items(): 145 | fig = fig if isinstance(fig, list) else [fig] 146 | for i, f in enumerate(fig): 147 | f[0].tight_layout() 148 | f[0].savefig(f"tutorial_{k}{i}.pdf") 149 | 150 | plt.show() 151 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1659877975, 6 | "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1666198336, 21 | "narHash": "sha256-VTrWD8Bb48h2pi57P1++LuvZIgum3gSLiRzZ/8q3rg0=", 22 | "owner": "nixos", 23 | "repo": "nixpkgs", 24 | "rev": "db25c4da285c5989b39e4ce13dea651a88b7a9d4", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "nixos", 29 | "ref": "nixos-unstable", 30 | "repo": "nixpkgs", 31 | "type": "github" 32 | } 33 | }, 34 | "root": { 35 | "inputs": { 36 | "flake-utils": "flake-utils", 37 | "nixpkgs": "nixpkgs" 38 | } 39 | } 40 | }, 41 | "root": "root", 42 | "version": 7 43 | } 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Nix flake for developing pyvib"; 3 | 4 | # To update all inputs: 5 | # $ nix flake update --recreate-lock-file 6 | 7 | inputs = { 8 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 9 | flake-utils.url = "github:numtide/flake-utils"; 10 | }; 11 | 12 | outputs = { self, nixpkgs, flake-utils }: 13 | flake-utils.lib.eachDefaultSystem (system: 14 | let 15 | pkgs = import nixpkgs { 16 | inherit system; 17 | config.allowUnfree = true; 18 | # Defines dependencies missing from nixpkgs here 19 | # overlays = [ 20 | # (import ./python-overlay.nix) 21 | # ]; 22 | }; 23 | python = pkgs.python39; 24 | pythonPackages = python.pkgs; 25 | 26 | pythonEnv = python.withPackages (ps: with ps; [ 27 | numpy 28 | scipy 29 | matplotlib 30 | # Dev dependencies 31 | pip 32 | black # format 33 | debugpy # for dap/remote debugging 34 | ipython 35 | ipdb # ipython debugger 36 | # it seems pdbpp is not packaged for nixos yet 37 | # pdbpp # replacement for pdb. Try to write `sticky` 38 | isort # sort imports 39 | pyflakes # linter 40 | # pytest 41 | ]); 42 | buildInputs = with pkgs; [ 43 | pythonEnv 44 | # LSP 45 | nodePackages.pyright 46 | # only for PyDSTool (which is only needed to run the 2dof example) 47 | swig 48 | gfortran 49 | ]; 50 | defaultPackage = pythonPackages.buildPythonPackage { 51 | name = "pyvib"; 52 | format = "setuptools"; 53 | src = ./.; 54 | inherit buildInputs; 55 | doCheck = false; 56 | }; 57 | in rec { 58 | inherit defaultPackage; 59 | devShell = pkgs.mkShell { 60 | nativeBuildInputs = [ 61 | defaultPackage 62 | ] ++ buildInputs; 63 | # buildInputs = [ 64 | # pkgs.nodePackages.pyright 65 | # ]; 66 | 67 | # allow pip to install into a temp directory 68 | # ${pwd} is the folder where `nix develop` is run from. Using git will always return the base/root dir 69 | # XXX: remember to change python version below 70 | # XXX: pwd=$() is bash syntax. Thus the pip alias does not work for fish shells 71 | # `nix print-dev-env` to see the dev env 72 | # run the shellHook manually with 73 | # eval $shellHook 74 | shellHook = '' 75 | pwd=$(git rev-parse --show-toplevel) 76 | alias pip="PIP_PREFIX='$pwd/_build/pip_packages' TMPDIR='$HOME' command pip" 77 | export PYTHONBREAKPOINT="ipdb.set_trace" 78 | export PYTHONPATH="$pwd:$pwd/_build/pip_packages/lib/python3.9/site-packages:$PYTHONPATH" 79 | export PATH="$pwd/_build/pip_packages/bin:$PATH" 80 | ''; 81 | }; 82 | }); 83 | } 84 | -------------------------------------------------------------------------------- /push_documentation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from subprocess import run, CalledProcessError 4 | from tempfile import TemporaryDirectory 5 | from os.path import abspath, dirname, isdir, join 6 | from os import listdir, remove 7 | from shutil import copytree, rmtree 8 | import sys 9 | 10 | 11 | if __name__ == '__main__': 12 | if len(sys.argv) > 1: 13 | remote = sys.argv[1] 14 | else: 15 | remote = input('Name of remote to push documentation to: ') 16 | 17 | src = abspath(dirname(__file__)) 18 | 19 | # Ensure that local branch gh-pages exists 20 | try: 21 | run(['git', 'rev-parse', 'gh-pages'], cwd=src, check=True) 22 | except CalledProcessError: 23 | run(['git', 'branch', 'gh-pages', '{}/gh-pages'.format(remote)], check=True) 24 | 25 | # Ensure that local branch gh-pages is updated 26 | run(['git', 'fetch', remote, 'gh-pages:gh-pages'], check=True) 27 | 28 | with TemporaryDirectory() as tgt: 29 | 30 | # Clone to a temporary directory and check out the gh-pages branch 31 | run(['git', 'clone', src, tgt], check=True) 32 | run(['git', 'checkout', 'gh-pages'], cwd=tgt, check=True) 33 | 34 | # Copy files from doc/_build/html over 35 | build = join(src, 'doc', '_build', 'html') 36 | for c in listdir(build): 37 | run(['cp', '-R', join(build, c), tgt]) 38 | 39 | # Add them all, show git status and commit 40 | run(['git', 'add', '-A'], cwd=tgt, check=True) 41 | run(['git', 'status'], cwd=tgt, check=True) 42 | run(['git', 'commit', '--allow-empty', '-m', 'Update documentation'], cwd=tgt, check=True) 43 | 44 | # Push to local repository from the temp repository 45 | run(['git', 'push', 'origin', 'gh-pages'], cwd=tgt, check=True) 46 | 47 | # Push to the remote 48 | run(['git', 'push', remote, 'gh-pages'], cwd=src, check=True) 49 | -------------------------------------------------------------------------------- /pyvib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | 6 | class UnsupportedPythonError(Exception): 7 | pass 8 | 9 | 10 | __minimum_python_version__ = "3.6" 11 | if sys.version_info < tuple((int(val) for val in __minimum_python_version__.split('.'))): 12 | raise UnsupportedPythonError(f"pyvib does not support Python < {__minimum_python_version__}") 13 | 14 | 15 | __version__ = "0.2.dev1" 16 | 17 | # this indicates whether or not we are in the package's setup.py 18 | # see https://github.com/astropy/astropy/blob/master/astropy/__init__.py#L63 19 | try: 20 | _PYVIB_SETUP_ 21 | except NameError: 22 | import builtins 23 | builtins._PYVIB_SETUP_ = False 24 | 25 | if not _PYVIB_SETUP_: 26 | pass 27 | #from pyvib.utils.config import load_config, print_config 28 | #from pyvib.utils.sysinfo import system_info 29 | # Load user configuration 30 | #config = load_config() 31 | 32 | #__all__ = ['config', 'system_info'] 33 | -------------------------------------------------------------------------------- /pyvib/hb/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pawsen/pyvib/3fd96436ef9f27c2743b11aa55f2fd1b713fbe10/pyvib/hb/__init__.py -------------------------------------------------------------------------------- /pyvib/hb/hbcommon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | from scipy.fftpack import fft, ifft 6 | 7 | 8 | def hb_components(z, n, NH): 9 | """Get HB coefficient c's 10 | 11 | Parameters 12 | ---------- 13 | n : int 14 | Number of DOFs, ie M.shape[0] 15 | """ 16 | z = np.hstack([np.zeros(n), z]) 17 | # reorder so first column is zeros, then one column for each dof 18 | z = np.reshape(z, (n, 2*(NH+1)), order='F') 19 | 20 | # first column in z is zero, thus this will a 'division by zero' warning. 21 | # Instead we just set the first column in phi to pi/2 (arctan(inf) = pi/2) 22 | # phi = np.arctan(z[:,1::2] / z[:,::2]) 23 | phi = np.empty((n, NH+1)) 24 | phi[:, 1:] = np.arctan(z[:, 3::2] / z[:, 2::2]) 25 | phi[:, 0] = np.pi/2 26 | 27 | c = z[:, ::2] / np.cos(phi) 28 | c[:, 0] = z[:, 1] 29 | 30 | # normalize components for each dof 31 | cnorm = np.abs(c)/np.sum(np.abs(c), axis=1)[:, None] 32 | 33 | return c, phi, cnorm 34 | 35 | 36 | def hb_signal(omega, t, c, phi): 37 | """Get real signal from HB coefficients(components)""" 38 | n = c.shape[0] 39 | NH = c.shape[1]-1 40 | nt = len(t) 41 | tt = np.arange(1, NH+1)[:, None] * omega * t 42 | 43 | x = np.zeros((n, nt)) 44 | for i in range(n): 45 | 46 | tmp = tt + np.outer(phi[i, 1:], np.ones(nt)) 47 | tmp = c[i, 0]*np.ones(nt) + c[i, 1:] @ np.sin(tmp) 48 | x[i] = tmp # np.sum(tmp, axis=0) 49 | 50 | return x 51 | 52 | 53 | def fft_coeff(x, NH): 54 | """ Extract FFT-coefficients from X=fft(x) 55 | """ 56 | # n: dofs 57 | n, nt = x.shape 58 | # Format of X after transpose: (nt, n) 59 | X = fft(x).T / nt 60 | 61 | re_fft_im_fft = np.hstack([-2*np.imag(X[1:NH+1]), 62 | 2 * np.real(X[1:NH+1])]) 63 | 64 | # X[0] only contains real numbers (it is the dc/0-frequency-part), but we 65 | # still need to extract the real part. Otherwise z is casted to complex128 66 | z = np.hstack([np.real(X[0]), re_fft_im_fft.ravel()]) 67 | 68 | return z 69 | 70 | 71 | def ifft_coeff(z, n, nt, NH): 72 | """ extract iFFT-coefficients from x=ifft(X) 73 | """ 74 | 75 | X = np.zeros((n, nt), dtype='complex') 76 | X[:, 0] = nt*z[:n] 77 | 78 | for i in range(NH): 79 | X[:, i+1] = 2 * (nt/2 * z[(n*(2*i+1)+n): (n*(2*i+1)+2*n)] - 80 | nt/2*1j * z[(n*(2*i+1)): (n*(2*i+1)+n)]) 81 | 82 | x = np.real(ifft(X)) 83 | 84 | return x 85 | -------------------------------------------------------------------------------- /pyvib/hb/stability.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | from scipy.linalg import block_diag, eig, eigvals, inv 6 | from scipy.sparse.csgraph import reverse_cuthill_mckee 7 | 8 | 9 | class Hills(object): 10 | """Estimate Floquet multipliers from Hills method. 11 | 12 | If one of the Floquet exponent have a positive real part, the solution is 13 | unstable. 14 | Only the 2n eigenvalues with lowest imaginary modulus approximate the 15 | Floquet multipliers λ. The rest are spurious. (n is the number of DOFs) 16 | 17 | The Hill matrix is simply a by-product of a harmonic-balance continuation 18 | method in the frequency domain. Thus there is no need to switch from one 19 | domain to another for computing stability. 20 | (The only term that needs to be evaluated when z varies is h_z) 21 | The monodromy matrix might be faster computationally-wise, but this is a 22 | by-product of the shooting continuation method in the time-domain approach. 23 | Hills method is effective for large systems.[1]_ 24 | 25 | Assemble Δs of the linear eigenvalue problem, eq. 44. B2 is assembled now. 26 | B1 requires h_z and is thus assembled after the steady state solution is 27 | found. 28 | 29 | Notes 30 | ----- 31 | [1]_: Peletan, Loïc, et al. "A comparison of stability computational 32 | methods for periodic solution of nonlinear problems with application to 33 | rotordynamics." Nonlinear dynamics 72.3 (2013): 671-682. 34 | """ 35 | 36 | def __init__(self, hb): 37 | 38 | scale_t = hb.scale_t 39 | scale_x = hb.scale_x 40 | NH = hb.NH 41 | M0 = hb.M * scale_t**2 / scale_x 42 | C0 = hb.C * scale_t / scale_x 43 | K0 = hb.K / scale_x 44 | 45 | Delta2 = M0 46 | M_inv = inv(M0) 47 | Delta2_inv = M_inv 48 | 49 | for i in range(1, NH+1): 50 | Delta2 = block_diag(Delta2, M0, M0) 51 | Delta2_inv = block_diag(Delta2_inv, M_inv, M_inv) 52 | 53 | # eq. 45 54 | b2 = np.vstack(( 55 | np.hstack((Delta2, np.zeros(Delta2.shape))), 56 | np.hstack((np.zeros(Delta2.shape), np.eye(Delta2.shape[0]))))) 57 | 58 | b2_inv = - np.vstack(( 59 | np.hstack((Delta2_inv, np.zeros(Delta2_inv.shape))), 60 | np.hstack((np.zeros(Delta2_inv.shape), np.eye(Delta2.shape[0]))))) 61 | 62 | self.Delta2 = Delta2 63 | self.b2_inv = b2_inv 64 | self.hb = hb 65 | # return Delta2, b2_inv 66 | 67 | def stability(self, omega, J_z, it=None): 68 | """Calculate B, Hills matrix. 69 | 70 | The 2n eigenvalues of B with lowest imaginary part, is the estimated 71 | Floquet exponents. They are collected in B_tilde. 72 | 73 | Returns 74 | ------- 75 | B: ndarray (2Nh+1)2n x (2Nh+1)2n 76 | Hills coefficients 77 | """ 78 | scale_x = self.hb.scale_x 79 | scale_t = self.hb.scale_t 80 | M0 = self.hb.M * scale_t**2 / scale_x 81 | C0 = self.hb.C * scale_t / scale_x 82 | K0 = self.hb.K / scale_x 83 | 84 | n = self.hb.n 85 | rcm_permute = self.hb.rcm_permute 86 | NH = self.hb.NH 87 | nu = self.hb.nu 88 | 89 | Delta2 = self.Delta2 90 | b2_inv = self.b2_inv 91 | 92 | omega2 = omega/nu 93 | # eq. 38 94 | Delta1 = C0 95 | for i in range(1, NH+1): 96 | blk = np.vstack(( 97 | np.hstack((C0, - 2*i * omega2/scale_t * M0)), 98 | np.hstack((2*i * omega2/scale_t * M0, C0)))) 99 | Delta1 = block_diag(Delta1, blk) 100 | 101 | # eq. 45 102 | A0 = J_z/scale_x 103 | A1 = Delta1 104 | A2 = Delta2 105 | b1 = np.vstack(( 106 | np.hstack((A1, A0)), 107 | np.hstack((-np.eye(A0.shape[0]), np.zeros(A0.shape))))) 108 | 109 | # eq. 46 110 | mat_B = b2_inv @ b1 111 | if rcm_permute: 112 | # permute B to get smaller bandwidth which gives faster linalg comp. 113 | p = reverse_cuthill_mckee(mat_B) 114 | B = mat_B[p] 115 | else: 116 | B = mat_B 117 | 118 | return B 119 | 120 | def reduc(self, B): 121 | """Extract Floquet exponents λ from Hills matrix. 122 | 123 | Find the 2n first eigenvalues with lowest imaginary part 124 | """ 125 | 126 | n = self.hb.n 127 | w = eigvals(B) 128 | idx = np.argsort(np.abs(np.imag(w)))[:2*n] 129 | B_tilde = w[idx] 130 | 131 | if np.max(np.real(B_tilde)) <= 0: 132 | stab = True 133 | else: 134 | stab = False 135 | return B_tilde, stab 136 | 137 | def vec(self, B): 138 | """Find the 2n first eigenvalues with lowest imaginary part and right 139 | eigenvectors 140 | """ 141 | 142 | n = self.hb.n 143 | # w: eigenvalues. vr: right eigenvector 144 | # Remember: column v[:,i] is the eigenvector corresponding to the eigenvalue w[i]. 145 | w, vr = eig(B) 146 | 147 | idx = np.argsort(np.abs(np.imag(w)))[:2*n] 148 | B_tilde = w[idx] 149 | 150 | return B_tilde, vr, idx 151 | -------------------------------------------------------------------------------- /pyvib/helper/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /pyvib/interpolate.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | def spline(x, a, xv): 5 | """ 6 | 7 | Parameters 8 | ---------- 9 | x: ndarray 10 | Spline knot (x)-coordinate 11 | a: ndarray [(len(x),4)] 12 | Coefficients for each cubic spline 13 | xv: float 14 | Current point 15 | 16 | Returns 17 | ------- 18 | yv: float 19 | Ordinate(y) for point xv 20 | yvp: float 21 | Derivative of y for point xv 22 | """ 23 | 24 | # Point is smaller than the first spline knot 25 | if xv < x[0]: 26 | x0 = x[0] 27 | a0 = a[0, 1] 28 | a1 = a[0, 2] 29 | a2 = a[0, 3] 30 | a3 = a[0, 4] 31 | y0 = a0 + a1 * x0 + a2 * x0**2 + a3 * x0**3 32 | s = a1 + 2 * a2 * x0 + 3 * a3 * x0**2 33 | p = y0 - s * x0 34 | yv = s * xv + p 35 | yvp = s 36 | return yv, yvp 37 | 38 | # Point is larger than the last spline knot 39 | if xv > x[-1]: 40 | x0 = x[-1] 41 | a0 = a[-1, 0] 42 | a1 = a[-1, 1] 43 | a2 = a[-1, 2] 44 | a3 = a[-1, 3] 45 | y0 = a0 + a1 * x0 + a2 * x0**2 + a3 * x0**3 46 | s = a1 + 2 * a2 * x0 + 3 * a3 * x0**2 47 | p = y0 - s * x0 48 | yv = s * xv + p 49 | yvp = s 50 | return yv, yvp 51 | 52 | # Find the segment the point is in 53 | iseg, = np.where(x <= xv) 54 | iseg = iseg[-1] 55 | aa = a[iseg] 56 | a0 = aa[0] 57 | a1 = aa[1] 58 | a2 = aa[2] 59 | a3 = aa[3] 60 | 61 | yv = a0 + a1 * xv + a2 * xv**2 + a3 * xv**3 62 | yvp = a1 + 2 * a2 * xv + 3 * a3 * xv**2 63 | 64 | return yv, yvp 65 | 66 | 67 | def piecewise_linear(x, y, s, delta=None, xv=[]): 68 | """Interpolate piecewise segments. 69 | 70 | Parameters 71 | ---------- 72 | x: ndarray 73 | x-coordinate for knots 74 | y: ndarray 75 | y-coordinate for knots 76 | s: ndarray. [len(x)+1] 77 | Slopes for line segments. Len: 'number of knots' + 1 78 | delta: ndarray 79 | Regularization interval, ie. enforce continuity of the derivative 80 | xv: ndarray 81 | x-coordinates for points to be interpolated 82 | 83 | Return 84 | ------ 85 | yv: ndarray 86 | Interpolated values 87 | """ 88 | if len(x) != len(y) or len(x) >= len(s): 89 | raise ValueError('Wrong length. Length of slope should be len(x)+1') 90 | 91 | n = len(x) 92 | nv = xv.shape 93 | 94 | # Find out which segments, the xv points are located in. 95 | indv = np.outer(x[:, None], np.ones(nv)) - \ 96 | np.outer(np.ones((n,)), xv) 97 | indv = np.floor((n - sum(np.sign(indv), 0)) / 2) 98 | indv = indv.reshape(nv) 99 | 100 | yv = np.zeros(nv) 101 | for i in range(1, n+1): 102 | ind = np.where(indv == i) 103 | yv[ind] = s[i] * xv[ind] + y[i-1] - s[i] * x[i-1] 104 | 105 | ind = np.where(indv == 0) 106 | yv[ind] = s[0] * xv[ind] + y[0] - s[0] * x[0] 107 | 108 | if delta is None: 109 | return yv 110 | 111 | if len(x) > len(delta): 112 | raise ValueError('Wrong length of delta. Should be len(x)', len(delta)) 113 | 114 | for i in range(n): 115 | dd = delta[i] 116 | indv = np.where(abs(xv - x[i]) <= dd) 117 | 118 | xa = x[i] - dd 119 | sa = s[i] 120 | sb = s[i+1] 121 | ya = y[i] - sa * dd 122 | yb = y[i] + sb * dd 123 | 124 | t = (xv[indv] - xa) / 2/dd 125 | h00 = 2*t**3 - 3*t**2 + 1 126 | h10 = t**3 - 2*t**2 + t 127 | h01 = - 2*t**3 + 3*t**2 128 | h11 = t**3 - t**2 129 | yv[indv] = h00*ya + 2*h10*dd*sa + h01*yb + 2*h11*dd*sb 130 | return yv 131 | 132 | 133 | def piecewise_linear_der(x, y, s, delta=None, xv=[]): 134 | n = len(x) 135 | nv = xv.shape 136 | 137 | indv = np.outer(x[:, None], np.ones(nv)) - \ 138 | np.outer(np.ones((n,)), xv) 139 | indv = np.floor((n - sum(np.sign(indv), 0)) / 2) 140 | indv = indv.reshape(nv) 141 | 142 | yvd = np.zeros(nv) 143 | for i in range(0, n+1): 144 | ind = np.where(indv == i) 145 | yvd[ind] = s[i] 146 | 147 | if delta is None: 148 | return yvd 149 | 150 | for i in range(n): 151 | dd = delta[i] 152 | indv = np.where(abs(xv - x[i]) <= dd) 153 | 154 | xa = x[i] - dd 155 | sa = s[i] 156 | sb = s[i+1] 157 | ya = y[i] - sa * dd 158 | yb = y[i] + sb * dd 159 | 160 | t = (xv[indv] - xa) / 2/dd 161 | dh00 = 6*t**2 - 6*t 162 | dh10 = 3*t**2 - 4*t + 1 163 | dh01 = -6*t**2 + 6*t 164 | dh11 = 3*t**2 - 2*t 165 | yvd[indv] = (dh00*ya + 2*dh10*dd*sa + dh01*yb + 2*dh11*dd*sb) \ 166 | / 2/dd 167 | 168 | return yvd 169 | -------------------------------------------------------------------------------- /pyvib/lti_conversion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | from numpy.linalg import solve 6 | from scipy.linalg import eigvals, logm, norm 7 | 8 | 9 | def is_stable(A, domain='z'): 10 | """Determines if a linear state-space model is stable from eigenvalues of `A` 11 | 12 | Parameters 13 | ---------- 14 | A : ndarray(n,n) 15 | state matrix 16 | domain : str, optional {'z', 's'} 17 | 'z' for discrete-time, 's' for continuous-time state-space models 18 | 19 | returns 20 | ------- 21 | bool 22 | """ 23 | 24 | if domain == 'z': # discrete-time 25 | # Unstable if at least one pole outside unit circle 26 | if any(abs(eigvals(A)) > 1): 27 | return False 28 | elif domain == 's': # continuous-time 29 | # Unstable if at least one pole in right-half plane 30 | if any(np.real(eigvals(A)) > 0): 31 | return False 32 | else: 33 | raise ValueError(f"{domain} wrong. Use 's' or 'z'") 34 | return True 35 | 36 | 37 | def ss2phys(A, B, C, D=None): 38 | """Calculate state space matrices in physical domain using a similarity 39 | transform T 40 | 41 | See eq. (20.10) in 42 | Etienne Gourc, JP Noel, et.al 43 | "Obtaining Nonlinear Frequency Responses from Broadband Testing" 44 | https://orbi.uliege.be/bitstream/2268/190671/1/294_gou.pdf 45 | """ 46 | 47 | # Similarity transform 48 | T = np.vstack((C, C @ A)) 49 | C = solve(T.T, C.T).T # (C = C*T^-1) 50 | A = solve(T.T, (T @ A).T).T # (A = T*A*T^-1) 51 | B = T @ B 52 | return A, B, C, D, T 53 | 54 | 55 | def ss2frf(A, B, C, D, freq): 56 | """Compute frequency response function from state-space parameters 57 | (discrete-time) 58 | 59 | Computes the frequency response function (FRF) or matrix (FRM) Ĝ at the 60 | normalized frequencies `freq` from the state-space matrices `A`, `B`, `C`, 61 | and `D`. :math:`G(f) = C*inv(exp(2j*pi*f)*I - A)*B + D` 62 | 63 | Parameters 64 | ---------- 65 | A : ndarray(n,n) state matrix 66 | B : ndarray(n,m) input matrix 67 | C : ndarray(p,n) output matrix 68 | D : ndarray(p,m) feed-through matrix 69 | freq : ndarray(F) 70 | vector of normalized frequencies at which the FRM is computed 71 | (0 < freq < 0.5) 72 | 73 | Returns 74 | ------- 75 | Gss : ndarray(F,p,m) 76 | frequency response matrix 77 | 78 | """ 79 | # Z-transform variable 80 | z = np.exp(2j*np.pi*freq) 81 | In = np.eye(*A.shape) 82 | # Use broadcasting. Much faster than for loop. 83 | Gss = C @ solve((z*In[..., None] - A[..., None] 84 | ).transpose((2, 0, 1)), B[None]) + D 85 | return Gss 86 | 87 | 88 | def discrete2cont(ad, bd, cd, dd, dt, method='zoh', alpha=None): 89 | """Convert linear system from discrete to continuous time-domain. 90 | 91 | This is the inverse of :func:`scipy.signal.cont2discrete`. This will not 92 | work in general, for instance with the ZOH method when the system has 93 | discrete poles at ``0``. 94 | 95 | Parameters 96 | ---------- 97 | A,B,C,D : :data:`linear_system_like` 98 | Linear system representation. 99 | dt : ``float`` 100 | Time-step used to *undiscretize* ``sys``. 101 | method : ``string``, optional 102 | Method of discretization. Defaults to zero-order hold discretization 103 | (``'zoh'``), which assumes that the input signal is held constant over 104 | each discrete time-step. 105 | alpha : ``float`` or ``None``, optional 106 | Weighting parameter for use with ``method='gbt'``. 107 | Returns 108 | ------- 109 | :class:`.LinearSystem` 110 | Continuous linear system (``analog=True``). 111 | See Also 112 | -------- 113 | :func:`scipy.signal.cont2discrete` 114 | Examples 115 | -------- 116 | Converting a linear system 117 | >>> from nengolib.signal import discrete2cont, cont2discrete 118 | >>> from nengolib import DoubleExp 119 | >>> sys = DoubleExp(0.005, 0.2) 120 | >>> assert dsys == discrete2cont(cont2discrete(sys, dt=0.1), dt=0.1) 121 | 122 | """ 123 | 124 | sys = (ad, bd, cd, dd) 125 | if dt <= 0: 126 | raise ValueError("dt (%s) must be positive" % (dt,)) 127 | 128 | n = ad.shape[0] 129 | m = n + bd.shape[1] 130 | 131 | if method == 'gbt': 132 | if alpha is None or alpha < 0 or alpha > 1: 133 | raise ValueError("alpha (%s) must be in range [0, 1]" % (alpha,)) 134 | 135 | In = np.eye(n) 136 | ar = solve(alpha*dt*ad.T + (1-alpha)*dt*In, ad.T - In).T 137 | M = In - alpha*dt*ar 138 | 139 | br = np.dot(M, bd) / dt 140 | cr = np.dot(cd, M) 141 | dr = dd - alpha*np.dot(cr, bd) 142 | 143 | elif method in ('bilinear', 'tustin'): 144 | return discrete2cont(*sys, dt, method='gbt', alpha=0.5) 145 | 146 | elif method in ('euler', 'forward_diff'): 147 | return discrete2cont(*sys, dt, method='gbt', alpha=0.0) 148 | 149 | elif method == 'backward_diff': 150 | return discrete2cont(*sys, dt, method='gbt', alpha=1.0) 151 | 152 | elif method == 'zoh': 153 | # see https://en.wikipedia.org/wiki/Discretization#Discretization_of_linear_state_space_models 154 | M = np.zeros((m, m)) 155 | M[:n, :n] = ad 156 | M[:n, n:] = bd 157 | M[n:, n:] = np.eye(bd.shape[1]) 158 | E = logm(M) / dt 159 | 160 | ar = E[:n, :n] 161 | br = E[:n, n:] 162 | cr = cd 163 | dr = dd 164 | else: 165 | raise ValueError("invalid method: '%s'" % (method,)) 166 | 167 | return ar, br, cr, dr 168 | -------------------------------------------------------------------------------- /pyvib/morletWT.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import matplotlib as mpl 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | from scipy.fftpack import fft, ifft 8 | 9 | from .common import db, next_pow2 10 | 11 | 12 | class WT(): 13 | def __init__(self, signal): 14 | self.signal = signal 15 | 16 | def morlet(self, f1, f2, nf=50, f00=10, dof=0, pad=0): 17 | self.f1 = f1 18 | self.f2 = f2 19 | self.nf = nf 20 | self.f00 = f00 21 | self.dof = dof 22 | self.pad = pad 23 | 24 | fs = self.signal.fs 25 | x = self.signal.y[dof] 26 | finst, wtinst, time, freq, y = morletWT(x, fs, f1, f2, nf, f00, pad) 27 | self.finst = finst 28 | self.wtinst = wtinst 29 | self.time = time 30 | self.freq = freq 31 | self.y = y 32 | 33 | def plot(self, fss=None, sca=1, **kwargs): 34 | 35 | fig, ax = waveletPlot(self.finst, self.wtinst, self.time, self.freq, 36 | self.y, fss, sca, **kwargs) 37 | self.fig = fig 38 | self.ax = ax 39 | return fig, ax 40 | 41 | 42 | def morletWT(x, fs, f1, f2, nf, f00, pad=0): 43 | """ 44 | 45 | Parameters 46 | ---------- 47 | x: ndarray 48 | Displacements (or velocities or accelerations) for a single DOF 49 | fs: float 50 | Sampling frequency 51 | nf: int 52 | Frequency steps 53 | f00: float in range [2-20] 54 | Morlet coefficient 55 | pad: int 56 | Padding 57 | 58 | Returns 59 | ------- 60 | finst: ndarray, len(x) 61 | 62 | wtinst: ndarray, len(x) 63 | 64 | time: ndarray, len(x) 65 | Time for wt, ie. x-axis 66 | freq: ndarray, len:nf 67 | Instantaneous frequency, ie. y-axis 68 | y: ndarray [nf, len(x)] 69 | FFT Amplitudes. Stored as [Freq, time]. Ie most likely to be used as y.T 70 | """ 71 | x = np.squeeze(x) 72 | 73 | dt = 1/fs 74 | df = (f2 - f1) / nf 75 | freq = np.linspace(f1, f2, nf) 76 | 77 | a = f00 / (f1 + np.outer(np.arange(nf), df)) 78 | na = len(a) - 1 79 | 80 | k = 2**pad 81 | NX = len(x) 82 | NX2 = next_pow2(NX) 83 | N = 2**NX2 84 | N = k*N 85 | 86 | time = np.arange(N)*dt 87 | f = np.linspace(0, fs/2, N//2) 88 | omega = f*2*np.pi 89 | 90 | filt = np.sqrt(2*a @ np.ones((1, N//2))) * \ 91 | np.exp(-0.5*(a @ omega[None, :] - 2*np.pi*f00)**2) 92 | filt[np.isnan(filt)] = 0 93 | 94 | X = fft(x, N, axis=0) 95 | X = np.conj(filt) * (np.ones((na+1, 1)) @ X[None, :N//2]) 96 | y = np.zeros((na+1, N), dtype=complex) 97 | for j in range(na+1): 98 | y[j] = ifft(X[j], N) 99 | 100 | y = y.T 101 | mod = np.abs(y) 102 | 103 | imax = np.argmax(mod, axis=1) 104 | wtinst = np.max(mod, axis=1) 105 | finst = f00 / a[imax].squeeze() 106 | finst = finst[:NX] 107 | wtinst = wtinst[:NX] 108 | y = y[:NX] 109 | time = time[:NX] 110 | 111 | return finst, wtinst, time, freq, y 112 | 113 | 114 | def waveletPlot(finst, wtinst, time, freq, y, fss=None, sca=1, **kwargs): 115 | 116 | if sca == 1: 117 | unit = ' (Hz)' 118 | else: 119 | unit = ' (rad/s)' 120 | 121 | if fss is None: 122 | vx = time 123 | xstr = 'Time (s)' 124 | else: 125 | vx = fss*sca 126 | xstr = 'Sweep frequency' + unit 127 | 128 | # Some textture settings. Used to reduce the textture size, but not needed 129 | # for now. 130 | nmax = len(freq) if len(freq) > len(time) else len(time) 131 | n1 = len(freq) // nmax 132 | n1 = 1 if n1 < 1 else n1 133 | n2 = len(vx) // nmax 134 | n2 = 1 if n2 < 1 else n2 135 | 136 | freq = freq[::n1]*sca 137 | vx = vx[::n2] 138 | finst = finst[::n2] 139 | y = y[::n2, ::n1] 140 | 141 | T, F = np.meshgrid(vx, freq) 142 | va = db(y) 143 | va[va < - 200] = -200 144 | 145 | # fig = plt.figure(1) 146 | # plt.clf() 147 | # ax = fig.add_subplot(111) 148 | fig, ax = plt.subplots() 149 | 150 | extends = ["neither", "both", "min", "max"] 151 | cmap = plt.cm.get_cmap("jet") 152 | cmap.set_under("white") 153 | cmap.set_over("yellow") 154 | 155 | cs = ax.contourf(T, F, va.T, 10, cmap=cmap, extend=extends[0]) 156 | ax.grid(which='minor', alpha=0.2) 157 | ax.grid(which='major', alpha=0.5) 158 | 159 | ax2 = fig.add_axes([0.9, 0.1, 0.03, 0.8]) 160 | # obtain the colormap limits 161 | vmin, vmax = cs.get_clim() 162 | # Define a normalised scale 163 | cNorm = mpl.colors.Normalize(vmin=vmin, vmax=vmax) 164 | # Plot the colormap in the created axes 165 | cbar = mpl.colorbar.ColorbarBase(ax2, norm=cNorm, cmap=cmap) 166 | fig.subplots_adjust(left=0.05, right=0.85) 167 | 168 | cbar.ax.set_ylabel('Amplitude (dB)') 169 | ax.set_xlabel(xstr) 170 | ax.set_ylabel('Instantaneous frequency' + unit) 171 | 172 | return fig, ax 173 | -------------------------------------------------------------------------------- /pyvib/nonlinear_elements_newmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from copy import deepcopy 5 | 6 | import numpy as np 7 | 8 | 9 | """Nonlinear functions to simulate: using :class:`.newmark.Newmark`. 10 | 11 | New nonlinear elements should derive :class:`Nonlinear_Element` and implement 12 | :: 13 | def fnl(q,u) # nonlinear force 14 | def dfdq(q,u) # derivative wrt position 15 | def dfdu(q,u) # derivative wrt velocity 16 | """ 17 | 18 | 19 | class Nonlinear_Element: 20 | """Bare class 21 | """ 22 | 23 | def __init__(self, **kwargs): 24 | # only initialize things if they are not already set 25 | super().__init__(**kwargs) 26 | 27 | 28 | class NLS(object): 29 | """ Assemble nonlinear attachments for usage with :class:`.newmark.Newmark` 30 | 31 | Note that each attachment is copied to ensure it truly belong to the NLS 32 | it was initiated with 33 | 34 | For identification, see :class:`.nlss.NLSS` 35 | """ 36 | 37 | def __init__(self, nls=None): 38 | self.nls = [] 39 | self.n_nl = 0 40 | if nls is not None: 41 | self.add(nls) 42 | 43 | def add(self, nls): 44 | # prevent wrapping NLS, ie NLS(NLS(nls)), etc 45 | if isinstance(nls, NLS): 46 | nls = nls.nls 47 | if not isinstance(nls, list): 48 | nls = [nls] 49 | for nl in nls: 50 | self.nls.append(deepcopy(nl)) 51 | 52 | def fnl(self, q, u): 53 | """Nonlinear force 54 | 55 | q: ndarray(ndof, ns), displacement 56 | u: ndarray(ndof, ns), velocity 57 | 58 | Returns 59 | ------- 60 | fnl: ndarray(ndof, ns) 61 | If ns = 1, returns 1d array 62 | """ 63 | # return zero array in case of no nonlinearities 64 | if len(self.nls) == 0: 65 | return np.zeros(len(q)) 66 | ndof = len(q) 67 | fnl = np.zeros(ndof) 68 | for nl in self.nls: 69 | fnl += nl.fnl(q, u) 70 | return fnl 71 | 72 | def dfnl(self, q, u): 73 | """Derivative of nonlinear force wrt. `q` and `u` 74 | 75 | q: ndarray(ndof, ns), displacement 76 | u: ndarray(ndof, ns), velocity 77 | 78 | Returns 79 | ------- 80 | dfnl_dq: ndarray (ndof,ndof,ns) 81 | If ns=1, returns 2d array 82 | dfnl_du: ndarray (ndof,ndof,ns) 83 | If ns=1, returns 2d array 84 | """ 85 | # return zero array in case of no nonlinearities 86 | if len(self.nls) == 0: 87 | return np.zeros((len(q), len(q))), np.zeros((len(q), len(q))) 88 | 89 | # q = np.atleast_2d(q) 90 | # ndof, ns = q.shape 91 | ndof = len(q) 92 | dfnl_dq = np.zeros((ndof, ndof)) 93 | dfnl_du = np.zeros((ndof, ndof)) 94 | for nl in self.nls: 95 | dfnl_dq += nl.dfnl_dq(q, u) 96 | dfnl_du += nl.dfnl_du(q, u) 97 | 98 | return dfnl_dq, dfnl_du 99 | 100 | 101 | class Tanhdryfriction(Nonlinear_Element): 102 | """Regularized friction model. 103 | 104 | `f = kt*tanh(ẏ/eps)` 105 | 106 | sign(ẏ) approximated by tanh. eps control the slope. kt is the 107 | friction limit force. 108 | """ 109 | 110 | def __init__(self, eps, w, kt=1, **kwargs): 111 | self.eps = eps 112 | self.w = np.atleast_1d(w) 113 | self.kt = kt 114 | super().__init__(**kwargs) 115 | 116 | def fnl(self, q, u): 117 | # displacement of dofs attached to nl 118 | unl = np.inner(self.w, u) 119 | f = np.outer(self.w, self.kt*np.tanh(unl / self.eps)) 120 | return f.ravel() 121 | 122 | def dfnl_dq(self, q, u): 123 | return np.zeros((len(q), len(q))) 124 | 125 | def dfnl_du(self, q, u): 126 | unl = np.inner(self.w, u) 127 | dfnl_du = np.outer(self.w, 128 | self.kt*(1 - np.tanh(unl / self.eps)**2) / self.eps 129 | * self.w) 130 | return dfnl_du 131 | 132 | 133 | class Polynomial(Nonlinear_Element): 134 | """Polynomial output nonlinearity 135 | 136 | Example 137 | ------- 138 | fnl = (y₁-y₂)ẏ₁, where y = [y₁, y₂, ẏ₁, ẏ₂] 139 | exponents = [1,1] 140 | w = np.array([[1,-1,0,0], [0,1,0,0]] 141 | """ 142 | 143 | def __init__(self, exp, w, k=1, **kwargs): 144 | """ 145 | exponents: ndarray (n_ny) 146 | w: ndarray (n_ny, p) 147 | """ 148 | self.w = np.atleast_1d(w) 149 | self.exp = np.atleast_1d(exp) 150 | self.k = k 151 | 152 | def fnl(self, q, u): 153 | qnl = np.inner(self.w, q) 154 | f = np.outer(self.w, self.k * qnl**self.exp) 155 | return f.ravel() 156 | 157 | def dfnl_du(self, q, u): 158 | """Displacement nl, thus zero""" 159 | return np.zeros((len(q), len(q))) 160 | 161 | def dfnl_dq(self, q, u): 162 | """ 163 | dfdy (1, p, ns) 164 | """ 165 | qnl = np.inner(self.w, q) 166 | dfnl_dq = np.outer(self.w, self.exp*qnl**(self.exp-1) * self.w) 167 | 168 | return dfnl_dq 169 | -------------------------------------------------------------------------------- /pyvib/spline.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # import matplotlib.pyplot as plt 5 | import numpy as np 6 | from scipy.linalg import norm, solve 7 | 8 | 9 | def spline(d, nspline): 10 | """The inputs are: 11 | 12 | * d: the displacement (or velocity) time series. 13 | * n: the number of spline segments (or, the number of knots - 1). 14 | 15 | The outputs are: 16 | 17 | * xs: the sought basis functions. 18 | * kn: the knot locations. 19 | * dx: this output is basically useless for your use. 20 | """ 21 | 22 | ns = len(d) 23 | nk = nspline+1 24 | 25 | maxd = 1.01*max(d) 26 | mind = 1.01*min(d) 27 | kn = np.linspace(mind, maxd, nspline+1) 28 | 29 | # index of kn before zero 30 | j0 = np.where(kn <= 0)[0][-1] 31 | Q = np.zeros((nk, nk)) 32 | 33 | for j in range(nk): 34 | if j == j0: 35 | t0 = -kn[j]/(kn[j+1]-kn[j]) 36 | 37 | Q[0, j] = (t0**3-2*t0**2+t0) * (kn[j+1]-kn[j]) 38 | Q[0, j+1] = (t0**3-t0**2) * (kn[j+1]-kn[j]) 39 | 40 | Q[nk-1, j] = (3*t0**2-4*t0+1) * (kn[j+1]-kn[j]) 41 | Q[nk-1, j+1] = (3*t0**2-2*t0) * (kn[j+1]-kn[j]) 42 | 43 | if j != 0: 44 | Q[j, j-1] = 1/(kn[j]-kn[j-1]) 45 | Q[j, j] = 2 * (1/(kn[j]-kn[j-1]) + 1/(kn[j+1]-kn[j])) 46 | Q[j, j+1] = 1/(kn[j+1]-kn[j]) 47 | 48 | elif j != 0 and j != j0 and j != nk-1: 49 | Q[j, j-1] = 1/(kn[j]-kn[j-1]) 50 | Q[j, j] = 2*(1/(kn[j]-kn[j-1]) + 1/(kn[j+1]-kn[j])) 51 | Q[j, j+1] = 1/(kn[j+1]-kn[j]) 52 | 53 | S = np.zeros((nk, nk)) 54 | for j in range(nk): 55 | if j == j0: 56 | t0 = -kn[j]/(kn[j+1]-kn[j]) 57 | 58 | S[0, j] = -(2*t0**3-3*t0**2+1) 59 | S[0, j+1] = -(-2*t0**3+3*t0**2) 60 | 61 | S[nk-1, j] = 6*(t0-t0**2) 62 | S[nk-1, j+1] = -6*(t0-t0**2) 63 | 64 | if j != 0: 65 | S[j, j-1] = -3/(kn[j]-kn[j-1])**2 66 | S[j, j] = 3 * (1/(kn[j]-kn[j-1])**2 - 1/(kn[j+1]-kn[j])**2) 67 | S[j, j+1] = 3/(kn[j+1]-kn[j])**2 68 | 69 | elif j != 0 and j != j0 and j != nk-1: 70 | S[j, j-1] = -3/(kn[j]-kn[j-1])**2 71 | S[j, j] = 3*(1/(kn[j]-kn[j-1])**2 - 1/(kn[j+1]-kn[j])**2) 72 | S[j, j+1] = 3/(kn[j+1]-kn[j])**2 73 | 74 | dx = solve(Q, S) 75 | x = np.zeros((nspline, nk, ns)) 76 | 77 | for j in range(nspline): 78 | 79 | u = np.zeros(ns) 80 | t = np.zeros(ns) 81 | t1 = np.zeros(ns) 82 | 83 | if j == 0: 84 | mask = (kn[j] <= d) & (d <= kn[j+1]) 85 | else: 86 | mask = (kn[j] < d) & (d <= kn[j+1]) 87 | 88 | u[mask] = d[mask] 89 | t[mask] = (u[mask]-kn[j]) / (kn[j+1]-kn[j]) 90 | t1[mask] = 1 91 | 92 | A = 2*t**3 - 3*t**2 + t1 93 | B = -2*t**3 + 3*t**2 94 | C = (t**3-2*t**2+t) * (kn[j+1]-kn[j]) 95 | D = (t**3-t**2) * (kn[j+1]-kn[j]) 96 | 97 | x[j, j, :] += A 98 | x[j, j+1, :] += B 99 | 100 | for k in range(nk): 101 | x[j, k, :] += C*dx[j, k] + D*dx[j+1, k] 102 | 103 | xs = np.squeeze(np.sum(x, 0)) 104 | return xs, kn, dx 105 | 106 | 107 | # fs = 20 108 | # x = np.arange(1000)/fs 109 | # y = np.sin(x) 110 | # xs, kn, dx = spline(y,5) 111 | -------------------------------------------------------------------------------- /pyvib/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /pyvib/utils/config.py: -------------------------------------------------------------------------------- 1 | """pyvibs configuration file functionality""" 2 | import os 3 | import tempfile 4 | import configparser 5 | 6 | import pyvib 7 | 8 | __all__ = ['load_config', 'print_config'] 9 | 10 | # dir where config file is stored 11 | CONFIGDIR ='utils' 12 | 13 | def load_config(): 14 | """ 15 | Read the pyvibrc configuration file. If one does not exists in the user's 16 | home directory then read in the defaults from module 17 | """ 18 | config = configparser.ConfigParser() 19 | 20 | # Get locations of Pyvib configuration files to be loaded 21 | config_files = _find_config_files() 22 | 23 | # Read in configuration files 24 | config.read(config_files) 25 | 26 | # Specify the working directory as a default so that the user's home 27 | # directory can be located in an OS-independent manner 28 | if not config.has_option('general', 'working_dir'): 29 | config.set('general', 'working_dir', os.path.join(_get_home(), "pyvib")) 30 | 31 | # Specify the database url as a default so that the user's home 32 | # directory can be located in an OS-independent manner 33 | if not config.has_option('database', 'url'): 34 | config.set('database', 'url', "sqlite:///" + os.path.join( 35 | _get_home(), "pyvib/pyvibdb.sqlite")) 36 | 37 | # Use absolute filepaths and adjust OS-dependent paths as needed 38 | filepaths = [ 39 | ('downloads', 'download_dir'), 40 | ('downloads', 'sample_dir') 41 | ] 42 | _fix_filepaths(config, filepaths) 43 | 44 | return config 45 | 46 | 47 | def get_and_create_download_dir(): 48 | ''' 49 | Get the config of download directory and create one if not present. 50 | ''' 51 | if not os.path.isdir(pyvib.config.get('downloads', 'download_dir')): 52 | os.makedirs(pyvib.config.get('downloads', 'download_dir')) 53 | 54 | return pyvib.config.get('downloads', 'download_dir') 55 | 56 | 57 | def get_and_create_sample_dir(): 58 | ''' 59 | Get the config of download directory and create one if not present. 60 | ''' 61 | if not os.path.isdir(pyvib.config.get('downloads', 'sample_dir')): 62 | os.makedirs(pyvib.config.get('downloads', 'sample_dir')) 63 | 64 | return pyvib.config.get('downloads', 'sample_dir') 65 | 66 | 67 | def print_config(): 68 | """Print current configuration options""" 69 | print("FILES USED:") 70 | for file_ in _find_config_files(): 71 | print(" " + file_) 72 | 73 | print("\nCONFIGURATION:") 74 | for section in pyvib.config.sections(): 75 | print(" [{0}]".format(section)) 76 | for option in pyvib.config.options(section): 77 | print(" {} = {}".format(option, pyvib.config.get(section, option))) 78 | print("") 79 | 80 | 81 | def _is_writable_dir(p): 82 | """Checks to see if a directory is writable""" 83 | return os.path.isdir(p) and os.access(p, os.W_OK) 84 | 85 | 86 | def _get_home(): 87 | """Find user's home directory if possible. 88 | Otherwise raise error. 89 | """ 90 | path = os.path.expanduser("~") 91 | 92 | if not os.path.isdir(path): 93 | for evar in ('HOME', 'USERPROFILE', 'TMP'): 94 | try: 95 | path = os.environ[evar] 96 | if os.path.isdir(path): 97 | break 98 | except KeyError: 99 | pass 100 | if path: 101 | return path 102 | else: 103 | raise RuntimeError('please define environment variable $HOME') 104 | 105 | 106 | def _find_config_files(): 107 | """Finds locations of Pyvib configuration files""" 108 | config_files = [] 109 | config_filename = 'pyvibrc' 110 | 111 | # find default configuration file 112 | module_dir = os.path.dirname(pyvib.__file__) 113 | config_files.append(os.path.join(module_dir, CONFIGDIR, 'pyvibrc')) 114 | 115 | # if a user configuration file exists, add that to list of files to read 116 | # so that any values set there will override ones specified in the default 117 | # config file 118 | config_path = _get_user_configdir() 119 | 120 | if os.path.exists(os.path.join(config_path, config_filename)): 121 | config_files.append(os.path.join(config_path, config_filename)) 122 | 123 | return config_files 124 | 125 | 126 | def _get_user_configdir(): 127 | """ 128 | Return the string representing the configuration dir. 129 | The default is "HOME/.pyvib". You can override this with the 130 | PYVIB_CONFIGDIR environment variable 131 | """ 132 | configdir = os.environ.get('PYVIB_CONFIGDIR') 133 | 134 | if configdir is not None: 135 | if not _is_writable_dir(configdir): 136 | raise RuntimeError('Could not write to PYVIB_CONFIGDIR="{0}"' 137 | .format(configdir)) 138 | 139 | return configdir 140 | 141 | h = _get_home() 142 | p = os.path.join(_get_home(), '.pyvib') 143 | 144 | if os.path.exists(p): 145 | if not _is_writable_dir(p): 146 | raise RuntimeError("'{0}' is not a writable dir; you must set {1}/." 147 | "pyvib to be a writable dir. You can also set " 148 | "environment variable PYVIB_CONFIGDIR to any " 149 | "writable directory where you want matplotlib " 150 | "data stored ".format(h, h)) 151 | else: 152 | if not _is_writable_dir(h): 153 | raise RuntimeError("Failed to create {0}/.pyvib; consider setting " 154 | "PYVIB_CONFIGDIR to a writable directory for " 155 | "pyvib configuration data".format(h)) 156 | 157 | os.mkdir(p) 158 | 159 | return p 160 | 161 | 162 | def _fix_filepaths(config, filepaths): 163 | """Converts relative filepaths to absolute filepaths""" 164 | # Parse working_dir 165 | working_dir = _expand_filepath(config.get("general", "working_dir")) 166 | config.set('general', 'working_dir', working_dir) 167 | 168 | for f in filepaths: 169 | val = config.get(*f) 170 | 171 | filepath = _expand_filepath(val, working_dir) 172 | 173 | # Replace config value with full filepath 174 | params = f + (filepath,) 175 | config.set(*params) 176 | 177 | 178 | def _expand_filepath(filepath, working_dir=""): 179 | """Checks a filepath and expands it if necessary""" 180 | # Expand home directory 181 | if filepath[0] == "~": 182 | return os.path.abspath(os.path.expanduser(filepath)) 183 | # Check for /tmp 184 | elif filepath == "/tmp": 185 | return tempfile.gettempdir() 186 | # Relative filepaths 187 | elif not filepath.startswith("/"): 188 | return os.path.abspath(os.path.join(working_dir, filepath)) 189 | # Absolute filepath 190 | else: 191 | return os.path.abspath(filepath) 192 | -------------------------------------------------------------------------------- /pyvib/utils/decorators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Licensed under a 3-clause BSD style license - see LICENSE.rst 3 | """Sundry function and class decorators.""" 4 | 5 | import functools 6 | 7 | # https://github.com/astropy/astropy/blob/master/astropy/utils/decorators.py 8 | 9 | __all__ = ['classproperty'] 10 | 11 | _NotFound = object() 12 | 13 | 14 | # TODO: This can still be made to work for setters by implementing an 15 | # accompanying metaclass that supports it; we just don't need that right this 16 | # second 17 | class classproperty(property): 18 | """ 19 | Similar to `property`, but allows class-level properties. That is, 20 | a property whose getter is like a `classmethod`. 21 | The wrapped method may explicitly use the `classmethod` decorator (which 22 | must become before this decorator), or the `classmethod` may be omitted 23 | (it is implicit through use of this decorator). 24 | .. note:: 25 | classproperty only works for *read-only* properties. It does not 26 | currently allow writeable/deletable properties, due to subtleties of how 27 | Python descriptors work. In order to implement such properties on a class 28 | a metaclass for that class must be implemented. 29 | Parameters 30 | ---------- 31 | fget : callable 32 | The function that computes the value of this property (in particular, 33 | the function when this is used as a decorator) a la `property`. 34 | doc : str, optional 35 | The docstring for the property--by default inherited from the getter 36 | function. 37 | lazy : bool, optional 38 | If True, caches the value returned by the first call to the getter 39 | function, so that it is only called once (used for lazy evaluation 40 | of an attribute). This is analogous to `lazyproperty`. The ``lazy`` 41 | argument can also be used when `classproperty` is used as a decorator 42 | (see the third example below). When used in the decorator syntax this 43 | *must* be passed in as a keyword argument. 44 | Examples 45 | -------- 46 | :: 47 | >>> class Foo: 48 | ... _bar_internal = 1 49 | ... @classproperty 50 | ... def bar(cls): 51 | ... return cls._bar_internal + 1 52 | ... 53 | >>> Foo.bar 54 | 2 55 | >>> foo_instance = Foo() 56 | >>> foo_instance.bar 57 | 2 58 | >>> foo_instance._bar_internal = 2 59 | >>> foo_instance.bar # Ignores instance attributes 60 | 2 61 | As previously noted, a `classproperty` is limited to implementing 62 | read-only attributes:: 63 | >>> class Foo: 64 | ... _bar_internal = 1 65 | ... @classproperty 66 | ... def bar(cls): 67 | ... return cls._bar_internal 68 | ... @bar.setter 69 | ... def bar(cls, value): 70 | ... cls._bar_internal = value 71 | ... 72 | Traceback (most recent call last): 73 | ... 74 | NotImplementedError: classproperty can only be read-only; use a 75 | metaclass to implement modifiable class-level properties 76 | When the ``lazy`` option is used, the getter is only called once:: 77 | >>> class Foo: 78 | ... @classproperty(lazy=True) 79 | ... def bar(cls): 80 | ... print("Performing complicated calculation") 81 | ... return 1 82 | ... 83 | >>> Foo.bar 84 | Performing complicated calculation 85 | 1 86 | >>> Foo.bar 87 | 1 88 | If a subclass inherits a lazy `classproperty` the property is still 89 | re-evaluated for the subclass:: 90 | >>> class FooSub(Foo): 91 | ... pass 92 | ... 93 | >>> FooSub.bar 94 | Performing complicated calculation 95 | 1 96 | >>> FooSub.bar 97 | 1 98 | """ 99 | 100 | def __new__(cls, fget=None, doc=None, lazy=False): 101 | if fget is None: 102 | # Being used as a decorator--return a wrapper that implements 103 | # decorator syntax 104 | def wrapper(func): 105 | return cls(func, lazy=lazy) 106 | 107 | return wrapper 108 | 109 | return super().__new__(cls) 110 | 111 | def __init__(self, fget, doc=None, lazy=False): 112 | self._lazy = lazy 113 | if lazy: 114 | self._cache = {} 115 | fget = self._wrap_fget(fget) 116 | 117 | super().__init__(fget=fget, doc=doc) 118 | 119 | # There is a buglet in Python where self.__doc__ doesn't 120 | # get set properly on instances of property subclasses if 121 | # the doc argument was used rather than taking the docstring 122 | # from fget 123 | # Related Python issue: https://bugs.python.org/issue24766 124 | if doc is not None: 125 | self.__doc__ = doc 126 | 127 | def __get__(self, obj, objtype): 128 | if self._lazy and objtype in self._cache: 129 | return self._cache[objtype] 130 | 131 | # The base property.__get__ will just return self here; 132 | # instead we pass objtype through to the original wrapped 133 | # function (which takes the class as its sole argument) 134 | val = self.fget.__wrapped__(objtype) 135 | 136 | if self._lazy: 137 | self._cache[objtype] = val 138 | 139 | return val 140 | 141 | def getter(self, fget): 142 | return super().getter(self._wrap_fget(fget)) 143 | 144 | def setter(self, fset): 145 | raise NotImplementedError( 146 | "classproperty can only be read-only; use a metaclass to " 147 | "implement modifiable class-level properties") 148 | 149 | def deleter(self, fdel): 150 | raise NotImplementedError( 151 | "classproperty can only be read-only; use a metaclass to " 152 | "implement modifiable class-level properties") 153 | 154 | @staticmethod 155 | def _wrap_fget(orig_fget): 156 | if isinstance(orig_fget, classmethod): 157 | orig_fget = orig_fget.__func__ 158 | 159 | # Using stock functools.wraps instead of the fancier version 160 | # found later in this module, which is overkill for this purpose 161 | 162 | @functools.wraps(orig_fget) 163 | def fget(obj): 164 | return orig_fget(obj.__class__) 165 | 166 | return fget 167 | -------------------------------------------------------------------------------- /pyvib/utils/misc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Licensed under a 3-clause BSD style license - see LICENSE.rst 3 | """ 4 | A "grab bag" of relatively small general-purpose utilities that don't have 5 | a clear module/package to live in. 6 | """ 7 | 8 | 9 | def isiterable(obj): 10 | """Returns `True` if the given object is iterable.""" 11 | 12 | try: 13 | iter(obj) 14 | return True 15 | except TypeError: 16 | return False 17 | 18 | 19 | def indent(s, shift=1, width=4): 20 | """Indent a block of text. The indentation is applied to each line.""" 21 | 22 | indented = '\n'.join(' ' * (width * shift) + l if l else '' 23 | for l in s.splitlines()) 24 | if s[-1] == '\n': 25 | indented += '\n' 26 | 27 | return indented 28 | 29 | 30 | class NumpyRNGContext: 31 | """ 32 | A context manager (for use with the ``with`` statement) that will seed the 33 | numpy random number generator (RNG) to a specific value, and then restore 34 | the RNG state back to whatever it was before. 35 | This is primarily intended for use in the astropy testing suit, but it 36 | may be useful in ensuring reproducibility of Monte Carlo simulations in a 37 | science context. 38 | Parameters 39 | ---------- 40 | seed : int 41 | The value to use to seed the numpy RNG 42 | Examples 43 | -------- 44 | A typical use case might be:: 45 | with NumpyRNGContext(): 46 | from numpy import random 47 | randarr = random.randn(100) 48 | ... run your test using `randarr` ... 49 | #Any code using numpy.random at this indent level will act just as it 50 | #would have if it had been before the with statement - e.g. whatever 51 | #the default seed is. 52 | """ 53 | 54 | def __init__(self, seed): 55 | self.seed = seed 56 | 57 | def __enter__(self): 58 | from numpy import random 59 | 60 | self.startstate = random.get_state() 61 | random.seed(self.seed) 62 | 63 | def __exit__(self, exc_type, exc_value, traceback): 64 | from numpy import random 65 | 66 | random.set_state(self.startstate) 67 | -------------------------------------------------------------------------------- /pyvib/utils/pyvibrc: -------------------------------------------------------------------------------- 1 | 2 | ; SunPy Configuration 3 | ; 4 | ; This is a sample sunpy configuration file - you can find a copy 5 | ; of it on your system in site-packages/sunpy/data/sunpyrc. If you edit it 6 | ; there, please note that it will be overridden in your next install. 7 | ; If you want to keep a permanent local copy that will not be 8 | ; over-written, place it in HOME/.sunpy/sunpyrc (unix/linux 9 | ; like systems) and C:\Documents and Settings\yourname\.sunpy 10 | ; (win32 systems). 11 | ; 12 | ; Note that any relative filepaths specified in the SunPy configuration file 13 | ; will be relative to SunPy's working directory. 14 | ; 15 | 16 | ;;;;;;;;;;;;;;;;;;; 17 | ; General Options ; 18 | ;;;;;;;;;;;;;;;;;;; 19 | [general] 20 | 21 | ; The SunPy working directory is the parent directory where all generated 22 | ; and download files will be stored. 23 | ; Default Value: /sunpy 24 | ; working_dir = /home/$USER/sunpy 25 | 26 | ; Time Format to be used for displaying time in output (e.g. graphs) 27 | ; The default time format is based on ISO8601 (replacing the T with space) 28 | ; note that the extra '%'s are escape characters 29 | time_format = %%Y-%%m-%%d %%H:%%M:%%S 30 | 31 | ;;;;;;;;;;;;; 32 | ; Downloads ; 33 | ;;;;;;;;;;;;; 34 | [downloads] 35 | 36 | ; Location to save download data to. Path should be specified relative to the 37 | ; SunPy working directory. 38 | ; Default value: data/ 39 | ;download_dir = /tmp 40 | download_dir = data 41 | 42 | ; Location where the sample data will be downloaded. Path should be specified 43 | ; relative to the SunPy working directory. 44 | sample_dir = data/sample_data 45 | 46 | ;;;;;;;;;;;; 47 | ; Database ; 48 | ;;;;;;;;;;;; 49 | [database] 50 | 51 | ; Location to specify sunpy database parameters. 52 | ; The default url of the database location can specified here. The url of 53 | ; the database comprises the database driver, its location and name, as well 54 | ; as optional authentication parameters 55 | ; Default value: sqlite://///sunpy/sunpydb.sqlite 56 | ; url = sqlite:////home/$USER/sunpy/sunpydb.sqlite 57 | -------------------------------------------------------------------------------- /pyvib/utils/sample.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """pyvib sample data files""" 3 | import socket 4 | import os.path 5 | import warnings 6 | from shutil import move 7 | from tempfile import TemporaryDirectory 8 | from subprocess import check_call 9 | from .config import get_and_create_sample_dir 10 | 11 | 12 | __all__ = ['download_sample_data', 'get_sample_file'] 13 | 14 | # https://api.github.com/repos/pawsen/pyvib_data/contents/pyvib/data/nlbeam 15 | # https://stackoverflow.com/a/18194523/1121523 16 | #_github_downloader = 'https://minhaskamal.github.io/DownGit/#/home?url=' 17 | _base_urls = ( 18 | 'https://api.github.com/repos/pawsen/pyvib_data/contents/pyvib/data/', 19 | #'https://github.com/pawsen/pyvib_data/tree/master/pyvib/data/', 20 | ) 21 | 22 | 23 | # files or folders to download 24 | sample_files = { 25 | "NLBEAM": "nlbeam", 26 | "2DOF": "2dof", 27 | "BOUCWEN": "boucwen", 28 | "SILVERBOX": "silverbox", 29 | } 30 | 31 | def download_sample_data(show_progress=True): 32 | """ 33 | Download all sample data at once. This will overwrite any existing files. 34 | Parameters 35 | ---------- 36 | show_progress: `bool` 37 | Show a progress bar during download 38 | Returns 39 | ------- 40 | None 41 | """ 42 | for filename in sample_files.values(): 43 | get_sample_file(filename, url_list=_base_urls, overwrite=True) 44 | 45 | 46 | def get_sample_file(filename, url_list=_base_urls, overwrite=False): 47 | """ 48 | Downloads a sample file. Will download a sample data file and move it to 49 | the sample data directory. Also, uncompresses zip files if necessary. 50 | Returns the local file if exists. 51 | Parameters 52 | ---------- 53 | filename: `str` 54 | Name of the file 55 | url_list: `str` or `list` 56 | urls where to look for the file 57 | show_progress: `bool` 58 | Show a progress bar during download 59 | overwrite: `bool` 60 | If True download and overwrite an existing file. 61 | timeout: `float` 62 | The timeout in seconds. If `None` the default timeout is used from 63 | `astropy.utils.data.Conf.remote_timeout`. 64 | Returns 65 | ------- 66 | result: `str` 67 | The local path of the file. None if it failed. 68 | """ 69 | 70 | # Creating the directory for sample files to be downloaded 71 | sampledata_dir = get_and_create_sample_dir() 72 | src = os.path.join(sampledata_dir, filename) 73 | if not overwrite and os.path.isfile(src): 74 | return src 75 | else: 76 | # check each provided url to find the file 77 | for base_url in url_list: 78 | try: 79 | url = base_url + filename 80 | with TemporaryDirectory() as d: 81 | rc = check_call(['github-download.sh', url], cwd=d) 82 | # move files to the data directory 83 | move(d, src) 84 | return src 85 | except (socket.error, socket.timeout) as e: 86 | warnings.warn("Download failed with error {}. \n" 87 | "Retrying with different mirror.".format(e)) 88 | # if reach here then file has not been downloaded. 89 | warnings.warn("File {} not found.".format(filename)) 90 | return None 91 | -------------------------------------------------------------------------------- /pyvib/utils/sysinfo.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import datetime 3 | 4 | __all__ = ['get_sys_dict', 'system_info'] 5 | 6 | 7 | def get_sys_dict(): 8 | """ 9 | Test which packages are installed on system. 10 | Returns 11 | ------- 12 | sys_prop : `dict` 13 | A dictionary containing the programs and versions installed on this 14 | machine 15 | """ 16 | 17 | try: 18 | from pyvib.version import version as pyvib_version 19 | from pyvib.version import githash as pyvib_git_description 20 | except ImportError: 21 | pyvib_version = 'Missing version.py; re-run setup.py' 22 | pyvib_git_description = 'N/A' 23 | 24 | # Dependencies 25 | try: 26 | from numpy import __version__ as numpy_version 27 | except ImportError: 28 | numpy_version = "NOT INSTALLED" 29 | 30 | try: 31 | from scipy import __version__ as scipy_version 32 | except ImportError: 33 | scipy_version = "NOT INSTALLED" 34 | 35 | try: 36 | from matplotlib import __version__ as matplotlib_version 37 | except ImportError: 38 | matplotlib_version = "NOT INSTALLED" 39 | 40 | try: 41 | from pandas import __version__ as pandas_version 42 | except ImportError: 43 | pandas_version = "NOT INSTALLED" 44 | 45 | try: 46 | from bs4 import __version__ as bs4_version 47 | except ImportError: 48 | bs4_version = "NOT INSTALLED" 49 | 50 | try: 51 | from PyQt4.QtCore import PYQT_VERSION_STR as pyqt_version 52 | except ImportError: 53 | pyqt_version = "NOT INSTALLED" 54 | 55 | try: 56 | from zeep import __version__ as zeep_version 57 | except ImportError: 58 | zeep_version = "NOT INSTALLED" 59 | 60 | try: 61 | from sqlalchemy import __version__ as sqlalchemy_version 62 | except ImportError: 63 | sqlalchemy_version = "NOT INSTALLED" 64 | 65 | sys_prop = {'Time': datetime.datetime.utcnow().strftime("%A, %d. %B %Y %I:%M%p UT"), 66 | 'System': platform.system(), 'Processor': platform.processor(), 67 | 'Pyvib': pyvib_version, 'Pyvib_git': pyvib_git_description, 68 | 'Arch': platform.architecture()[0], "Python": platform.python_version(), 69 | 'NumPy': numpy_version, 70 | 'SciPy': scipy_version, 'matplotlib': matplotlib_version, 71 | 'Pandas': pandas_version, 72 | 'beautifulsoup': bs4_version, 'PyQt': pyqt_version, 73 | 'Zeep': zeep_version, 'Sqlalchemy': sqlalchemy_version 74 | } 75 | return sys_prop 76 | 77 | 78 | def system_info(): 79 | """ 80 | Takes dictionary from sys_info() and prints the contents in an attractive fashion. 81 | """ 82 | sys_prop = get_sys_dict() 83 | 84 | # title 85 | print("==========================================================") 86 | print(" Pyvib Installation Information\n") 87 | print("==========================================================\n") 88 | 89 | # general properties 90 | print("###########") 91 | print(" General") 92 | print("###########") 93 | # OS and architecture information 94 | 95 | for sys_info in ['Time', 'System', 'Processor', 'Arch', 'Pyvib', 'Pyvib_git']: 96 | print('{0} : {1}'.format(sys_info, sys_prop[sys_info])) 97 | 98 | if sys_prop['System'] == "Linux": 99 | distro = " ".join(platform.linux_distribution()) 100 | print("OS: {0} (Linux {1} {2})".format(distro, platform.release(), sys_prop['Processor'])) 101 | elif sys_prop['System'] == "Darwin": 102 | print("OS: Mac OS X {0} ({1})".format(platform.mac_ver()[0], sys_prop['Processor'])) 103 | elif sys_prop['System'] == "Windows": 104 | print("OS: Windows {0} {1} ({2})".format(platform.release(), platform.version(), 105 | sys_prop['Processor'])) 106 | else: 107 | print("Unknown OS ({0})".format(sys_prop['Processor'])) 108 | 109 | print("\n") 110 | # required libraries 111 | print("###########") 112 | print(" Required Libraries ") 113 | print("###########") 114 | 115 | for sys_info in ['Python', 'NumPy', 'SciPy', 'matplotlib']: 116 | print('{0}: {1}'.format(sys_info, sys_prop[sys_info])) 117 | 118 | print("\n") 119 | 120 | # recommended 121 | print("###########") 122 | print(" Recommended Libraries ") 123 | print("###########") 124 | 125 | for sys_info in ['beautifulsoup', 'PyQt', 'Zeep', 'Sqlalchemy', 'Pandas']: 126 | print('{0}: {1}'.format(sys_info, sys_prop[sys_info])) 127 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | matplotlib 4 | Sphinx 5 | sphinx_rtd_theme 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | [metadata] 3 | name = pyvib 4 | package_name = pyvib 5 | description = Pyvib: Python for system identification 6 | long_descripton = file: README.org 7 | author = Paw 8 | author_email = pawsen@gmail.com 9 | license = BSD 2-Clause 10 | url = https://pyvib.readthedocs.io 11 | edit_on_github = True 12 | github_project = pawsen/pyvib 13 | version = 1.0.dev 14 | 15 | [options] 16 | python_requires = >=3.7 17 | packages = find: 18 | include_package_data = True 19 | install_requires = 20 | numpy>=1.11 21 | scipy 22 | matplotlib>=1.3 23 | 24 | [options.extras_require] 25 | tests = 26 | pytest 27 | pytest-cov 28 | docs = 29 | sphinx 30 | 31 | [pycodestyle] 32 | max_line_length = 80 33 | 34 | [flake8] 35 | #https://pycodestyle.readthedocs.io/en/latest/intro.html#error-codes 36 | ignore = E231,E226,E302,E41 37 | max-line-length = 80 38 | 39 | [bumpversion] 40 | current_version = 0.2 41 | commit = True 42 | tag = True 43 | 44 | [yapf] 45 | column_limit = 80 46 | dedent_closing_brackets = False 47 | 48 | [tool:pytest] 49 | testpaths = "pyvib" "docs" 50 | # Skip pyvib/data to prevent importing the sample data (there are no tests in that dir anyway) 51 | norecursedirs = ".tox" "build" "docs[\/]_build" "docs[\/]generated" "*.egg-info" "astropy_helpers" "examples" "pyvib[\/]data" 52 | python_files = *_test.py 53 | 54 | [run] 55 | omit = *_test.py 56 | 57 | [bumpversion:file:setup.py] 58 | 59 | [bumpversion:file:pyvib/__init__.py] 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import setup, find_packages 4 | from pyvib import __version__ 5 | 6 | def readme(): 7 | with open('README.org') as f: 8 | return f.read() 9 | 10 | 11 | setup(name='pyvib', 12 | version=__version__, 13 | # use find_packages to include subfolders with a __init__.py 14 | # https://stackoverflow.com/a/43254082 15 | packages=find_packages(), 16 | description='Nonlinear modeling for python', 17 | long_description=readme(), 18 | classifiers=[], 19 | keywords=['nonlinear','identification','bifurcation','simulation'], 20 | url='https://github.com/pawsen/pyvib', 21 | author='Paw Møller', 22 | author_email='pawsen@gmail.com', 23 | license='BSD', 24 | zip_safe=False, 25 | install_requires=[ 26 | 'numpy', 27 | 'scipy', 28 | 'matplotlib'], 29 | extras_require={ 30 | 'tests': [ 31 | #'nose', 32 | #'pycodestyle >= 2.1.0' 33 | ], 34 | 'docs': [ 35 | 'sphinx >= 1.4', 36 | 'sphinx_rtd_theme']} 37 | ) 38 | -------------------------------------------------------------------------------- /test/forcing_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | from matplotlib import pyplot as plt 5 | import numpy as np 6 | import os 7 | import sys 8 | sys.path.insert(1, os.path.join(sys.path[0], '..')) 9 | import forcing 10 | import numpy.testing as npt 11 | from common import db 12 | 13 | 14 | """ TODO: When nrep != 1, the frequency content is not the same as when nrep = 15 | 1. Why? """ 16 | vrms = 1 17 | fs = 50 18 | f1 = 5 19 | f2 = 20 20 | ns = 100 21 | nrep = 1 22 | 23 | u, t = forcing.multisine(vrms,fs, f1,f2,ns, nrep) 24 | 25 | npt.assert_allclose(np.linalg.norm(t), 11.6335721083) 26 | npt.assert_allclose(np.linalg.norm(u), 10.0498756211) 27 | 28 | 29 | U = np.fft.fft(u) 30 | idx = len(U)//2 31 | 32 | U_plot = db(np.abs(U[0:idx])) 33 | freq = np.fft.fftfreq(len(U), d=1/fs) 34 | freq = freq[0:idx] 35 | 36 | fig1 = plt.figure() 37 | plt.clf() 38 | plt.plot(freq, U_plot, '*k' ) 39 | plt.axvline(f1,color='k', linestyle='--') 40 | plt.axvline(f2,color='k', linestyle='--') 41 | plt.title('Frequency content for multisine signal') 42 | plt.xlabel( 'Frequency (Hz)') 43 | plt.ylabel('FFT basis periodic random (dB)') 44 | plt.grid() 45 | 46 | 47 | amp = 1 48 | fs = 100 49 | f1 = 1 50 | f2 = 5 51 | vsweep = 50 52 | inctypes = ['lin','log'] 53 | t0 = 0 54 | 55 | norm_t = [60.8105911828, 26.8334474118] 56 | norm_u = [15.5130744548, 11.8222391343] 57 | for i, inctype in enumerate(inctypes): 58 | u, t, finst = forcing.sinesweep(amp, fs, f1, f2, vsweep, nrep=1, inctype=inctype, t0=t0) 59 | 60 | npt.assert_allclose(np.linalg.norm(t), norm_t[i]) 61 | npt.assert_allclose(np.linalg.norm(u), norm_u[i]) 62 | 63 | 64 | fig2 = plt.figure() 65 | plt.clf() 66 | plt.plot(t, u, '-k' ) 67 | plt.ylim([-1.5*amp, 1.5*amp]) 68 | plt.title('Time history for sinesweep signal, type: {}'.format(inctype)) 69 | plt.xlabel('Time (s)') 70 | plt.ylabel('Sine sweep excitation (N)') 71 | plt.grid() 72 | 73 | fig3 = plt.figure() 74 | plt.clf() 75 | plt.plot(finst, u,'-k' ) 76 | plt.ylim([ -1.5*amp, 1.5*amp]) 77 | plt.title('Frequency history for sinesweep signal, type: {}'.format(inctype)) 78 | plt.xlabel('Instantaneous frequency (Hz)') 79 | plt.ylabel( 'Sine sweep excitation (N)') 80 | plt.grid() 81 | 82 | fig4 = plt.figure() 83 | plt.clf() 84 | plt.plot(t, finst, '-k' ) 85 | plt.title('Frequency history for sinesweep signal, type: {}'.format(inctype)) 86 | plt.xlabel('Time (s)') 87 | plt.ylabel('Instantaneous frequency (Hz)') 88 | plt.grid() 89 | 90 | U = np.fft.fft(u) 91 | idx = len(U)//2 92 | U_plot = db(np.abs(U[0:idx])) 93 | freq = np.fft.fftfreq(len(U), d=1/fs) 94 | freq = freq[0:idx] 95 | 96 | fig5 = plt.figure() 97 | plt.clf() 98 | plt.plot(freq, U_plot, '*k' ) 99 | plt.axvline(f1,color='k', linestyle='--') 100 | plt.axvline(f2,color='k', linestyle='--') 101 | plt.title('Frequency content for sinesweep signal, type: {}'.format(inctype)) 102 | plt.xlabel( 'Frequency (Hz)') 103 | plt.ylabel('FFT basis periodic random (dB)') 104 | plt.grid() 105 | 106 | fig1.savefig('freq_multisine.png') 107 | fig5.savefig('freq_sinesweep.png') 108 | 109 | plt.show() 110 | -------------------------------------------------------------------------------- /test/multisine_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import numpy as np 5 | import matplotlib.pyplot as plt 6 | from numpy.fft import fft 7 | from vib.forcing import multisine 8 | from vib.common import db 9 | 10 | """Simple example of quantifying odd/even nonlinearities using multisine. 11 | 12 | The nonparametric analysis/quantifying only works for one period, P=1. 13 | The nonlinearities become visible at the unexcited frequencies by using 14 | ftype={odd, oddrandom}. 15 | """ 16 | 17 | fs = 4096 # sampling frequency, Hz 18 | N = 4096 # number of points per period 19 | P = 1 # number of periods 20 | M = 1 # number of realizations 21 | f1 = 1 # lowest excited frequency 22 | f2 = 100 # highest excited frequency 23 | uStd = 1 # standard deviation of the generated signal 24 | 25 | u, t, lines, freq = multisine(f1, f2, fs, N, P, M, ftype='oddrandom', std=uStd) 26 | 27 | # apply input to a static nonlinear system 28 | y = u + 0.001*u**2 - 0.1*u**3 29 | # perform nonlinear analysis 30 | Y = fft(y) 31 | # separate even and odd lines. 1 is dc, ie. 1+2n is even 32 | lines_even = np.arange(0,N,2) 33 | lines_odd_det = np.arange(1,N,2) 34 | lines_odd_det = np.setdiff1d(lines_odd_det, lines) 35 | 36 | plt.ion() 37 | # signal plots 38 | plt.figure(1) 39 | plt.clf() 40 | plt.plot(t, u[0]) 41 | plt.xlabel('time (s)') 42 | plt.ylabel('magnitude') 43 | plt.title('Multisine: {} realizations of {} periods of {} ' 44 | 'samples per period'.format(M,P,N)) 45 | 46 | plt.figure(2) 47 | plt.clf() 48 | plt.subplot(2,1,1) 49 | plt.plot(freq,db(fft(u[0,:N])),'-*') 50 | plt.xlabel('frequency (Hz)') 51 | plt.ylabel('magnitude (dB)') 52 | plt.title('FFT of one period of the multisine realizations') 53 | 54 | plt.subplot(2,1,2) 55 | plt.plot(freq,np.angle(fft(u[0,:N])),'-*') 56 | plt.xlabel('frequency (Hz)') 57 | plt.ylabel('phase (rad)') 58 | plt.title('FFT of one period of the multisine realizations') 59 | plt.tight_layout() 60 | 61 | # nl-test plot 62 | plt.figure(3) 63 | plt.clf() 64 | plt.plot(u[0],y[0],'.') 65 | plt.xlabel('input') 66 | plt.ylabel('output') 67 | plt.title('Static Nonlinear Function') 68 | 69 | plt.figure(4) 70 | plt.clf() 71 | plt.plot(freq[lines],db(Y[0,lines]),'*') 72 | plt.plot(freq[lines_odd_det],db(Y[0,lines_odd_det]),'^') 73 | plt.plot(freq[lines_even],db(Y[0,lines_even]),'o') 74 | plt.xlim([0,fs/2]) 75 | plt.xlabel('frequency (Hz)') 76 | plt.ylabel('magnitude (dB)') 77 | plt.title('FFT output') 78 | plt.legend(('excited lines','odd detection lines','even detection lines')) 79 | 80 | 81 | plt.show() 82 | -------------------------------------------------------------------------------- /test/plot_helper.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | def axhlines(ys, **plot_kwargs): 5 | """ 6 | Draw horizontal lines across plot 7 | :param ys: A scalar, list, or 1D array of vertical offsets 8 | :param plot_kwargs: Keyword arguments to be passed to plot 9 | :return: The plot object corresponding to the lines. 10 | """ 11 | ys = np.array((ys, ) if np.isscalar(ys) else ys, copy=False) 12 | lims = plt.gca().get_xlim() 13 | y_points = np.repeat(ys[:, None], repeats=3, axis=1).flatten() 14 | x_points = np.repeat(np.array(lims + (np.nan, ))[None, :], repeats=len(ys), axis=0).flatten() 15 | plot = plt.plot(x_points, y_points, scalex = False, **plot_kwargs) 16 | return plot 17 | 18 | 19 | def axvlines(xs, **plot_kwargs): 20 | """ 21 | Draw vertical lines on plot 22 | :param xs: A scalar, list, or 1D array of horizontal offsets 23 | :param plot_kwargs: Keyword arguments to be passed to plot 24 | :return: The plot object corresponding to the lines. 25 | """ 26 | xs = np.array((xs, ) if np.isscalar(xs) else xs, copy=False) 27 | lims = plt.gca().get_ylim() 28 | x_points = np.repeat(xs[:, None], repeats=3, axis=1).flatten() 29 | y_points = np.repeat(np.array(lims + (np.nan, ))[None, :], repeats=len(xs), axis=0).flatten() 30 | plot = plt.plot(x_points, y_points, scaley = False, **plot_kwargs) 31 | return plot 32 | --------------------------------------------------------------------------------