Wave propagation analysis of rotors with longitudinal periodicity, in Python.
Rotating machines are mainly composed by a rotating shaft with working elements (impellers) mounted on it. These working elements add localized inertia to the system, and when this inertia is evenly distributed along the shaft, a dynamic effect occurs: wave band gap zones form in the frequency spectrum of the system. These regions are characterized by the lack of resonances in a bandwidth much larger than the average distance between two other resonances of the system. These dynamic characteristics are interesting for rotating systems since the operate at specific frequency ranges and have known operating excitations, e.g., unbalance, misalignment, and gearmesh frequencies. Thus, by placing the operating excitations of the machine in a band gap zone, we guarantee a low vibration transmission with a good safety margin.
rotorwave computes where those band gaps are, how they move with the rotating speed, and separates the forward and backward whirl wave modes
It is the open-source implementation of the methodology presented in:
P. B. Lamas, R. Nicoletti, Wave analysis of rotors with longitudinal periodicity, Journal of Sound and Vibration 571 (2024) 118095. doi:10.1016/j.jsv.2023.118095
and reproduces every figure of that paper from the scripts in examples/.
No installation needed to try it. Open run_me.py in Spyder (or any IDE
that comes with Anaconda) and press F5; or open rotorwave_demo.ipynb in
Jupyter. Both put src/ on the Python path themselves, as do the scripts in
examples/.
To install the package so you can import rotorwave from your own scripts:
pip install -e ".[plot,dev]" # from a clone of this repositoryRequires Python ≥ 3.10, NumPy and SciPy; matplotlib is needed only for the figures.
Windows/Anaconda users: step-by-step instructions in Portuguese, including the
usual error messages and what they mean, are in INSTALACAO.md;
double-clicking instalar_windows.bat runs the installation and checks it.
Six lines are enough to get the band gap of the rotor of the paper:
from rotorwave import OmegaKSolver, reference_rotor
rotor = reference_rotor(n_disks=11) # the paper's case study
dispersion = OmegaKSolver(rotor.unit_cell()).solve(spin_rpm=6000)
print(dispersion.summary("forward"))
# omega(k) dispersion at 6000 rpm — 8 branches, 41 wavenumbers, cell length 136.36 mm
# forward band gap: 2136.1 - 3679.8 Hz (width 1543.7 Hz, 53.1 %)The unit cell. A rotor with equally spaced disks repeats one piece of
itself — a length of shaft plus one disk — over and over. That piece is the
unit cell. Bloch–Floquet theory says that everything about wave propagation
in the infinite repetition of a cell can be obtained from that single cell, by
imposing q_right = λ q_left with λ = e^{−ikΔ}, where Δ is the cell length.
So the whole analysis costs one small eigenvalue problem, not a model of the
entire rotor.
Two ways to ask the question. They are complementary and the package solves both:
| Prescribe | Obtain | Answers | |
|---|---|---|---|
| ω(k) | a real wavenumber | real frequencies | where the band gaps are |
| k(ω) | a real frequency | complex wavenumbers | how strongly each frequency is attenuated |
The ω(k) problem gives clean band edges (they are branch extrema, so they do not depend on any grid). The k(ω) problem gives the attenuation per cell, which ω(k) cannot say, and it also flags frequencies that are strongly attenuated without being strictly evanescent.
Forward and backward whirl. A rotor is not an ordinary periodic structure:
the gyroscopic effect splits every wave into a forward-whirling and a
backward-whirling one, and it does so differently at every speed. rotorwave
labels each wave by its own eigenvector (the whirl index, a Rayleigh quotient
of the rotation generator), so the labels remain correct at any speed and
across branch crossings. For an isotropic rotor it goes further and
block-diagonalises the problem into a forward half and a backward half, which
makes the separation exact rather than a post-processing step.
The check that matters. The dispersion describes an infinite periodic
medium. The ReceptanceSolver computes the frequency response of the finite
rotor, which is what an experiment measures. A predicted stop band is only
believable if the measured response collapses inside it — which is exactly what
Figure 4 of the paper shows, and what examples/dispersion_omega_k.py
reproduces.
More detail, with the equations, is in docs/theory.md.
| Problem | Class | Given | Returns |
|---|---|---|---|
| ω(k) | OmegaKSolver |
real wavenumber | propagating frequencies, mode shapes, whirl |
| k(ω) | KOmegaSolver |
real frequency | complex wavenumbers (propagation and attenuation), whirl |
| receptance | ReceptanceSolver |
frequency grid | FRFs and directional FRFs of the finite rotor |
| band gaps | .band_gaps(), band_gap_map |
either dispersion | stop bands per precession direction, and their drift with speed |
The finite element model underneath is a Nelson–McVaugh rotating
Euler–Bernoulli shaft with rigid disks, four degrees of freedom per node
(x, y, β, γ), assembled as M q̈ + (C − ΩG) q̇ + K q = f.
Each script has a parameter block at the top — geometry, material, mesh, speed —
so you can point it at a different rotor without reading the rest of the file.
Figures are written to examples/figures/.
Reproducing the paper
python examples/dispersion_omega_k.py # Fig. 4: dispersion + receptance, 0 and 6000 rpm
python examples/dispersion_k_omega.py # complex wavenumbers + receptance
python examples/directional_dispersion_dFRF.py # two-sided diagram + directional FRFs
python examples/wave_campbell.py # wave-Campbell diagram
python examples/disk_count_sweep.py # Fig. 3: modal and wave maps vs number of disksGoing beyond it
python examples/band_gap_map.py # band gap edges vs rotating speedYour own rotor
python examples/custom_rotor.pyStart from this one: the top of the file is the geometry, the material, the mesh density and the speed; the bottom shows how to build a non-uniform cell element by element, for a rotor that is not a shaft with identical disks.
Reproduced by the test suite, for the 1500 mm × 100 mm steel shaft carrying 11 disks of 380 mm × 22 mm:
| Quantity | rotorwave |
Paper |
|---|---|---|
| First band gap at rest | 2037 – 3579 Hz | "between 2000 and 3500 Hz" |
| Forward gap at 6000 rpm | 2136 – 3680 Hz | shifts up |
| Backward gap at 6000 rpm | 1943 – 3482 Hz | shifts down |
| Gap width, 0 → 6000 rpm | 1542 → 1544 Hz (fw), 1539 Hz (bw) | width preserved |
| Peak attenuation across the rotor | ≈ 166 dB | — |
The gap moves with speed but keeps its width — the quantitative version of
a statement the paper makes qualitatively, now measured by band_gap_map.
The suite also checks the model against things that are known independently: the analytical natural frequencies of a simply supported beam, the exact integration of the element matrices, the isotropy of the assembled rotor, mesh convergence, the agreement between the ω(k) and k(ω) formulations, and the agreement between the whirl-decoupled solver and the general one.
pytest # 34 tests, about a minuteThe physics is the same; the numerics and the interfaces are not.
Correctness
- Whirl classification from the eigenvectors. Forward and backward branches
were identified by sorting frequencies and picking every other one, which
breaks at zero spin speed, near crossings and whenever the assumed ordering
does not hold. Here the whirl direction is the Rayleigh quotient of the
rotation generator
J(rotorwave.whirl), so it is a property of the mode, valid at any speed. - Exact forward/backward decoupling.
Jcommutes with the matrices of an isotropic rotor, so the k(ω) problem is block-diagonalised into a forward and a backward half. Every wave is labelled by construction, and near-degenerate pairs — which a general eigensolver returns hopelessly mixed — come out clean. The commutation is checked, not assumed, with a fallback for the general case; the two paths are tested against each other. - Branch tracking. Sorting by frequency swaps branches at every veering. Branches are followed with a mass-weighted MAC plus frequency extrapolation, matched with the Hungarian algorithm; the k(ω) waves are ordered by continuity of their Bloch multipliers, which is what removes the spurious vertical strokes from the diagrams.
- Accurate Bloch multipliers. The quadratic eigenvalue problem is solved
from the exactly expanded characteristic polynomial instead of a companion
linearisation. Measured on the reference rotor between 10 Hz and 1 kHz — the
stiff, ill-conditioned end — the propagating waves satisfy
||λ|−1| ≤ 6e-9against2e-3for the linearisation: the difference between a clean dispersion diagram and one peppered with spurious 10 Hz stop bands. - Guarded fast paths. The Hermitian eigensolver is used only when the
modes it returns are verified to satisfy the original quadratic problem; at
k = 0the reduced stiffness is numerically singular (condition number around 1e17), so a factorisation test alone would pass or fail by rounding luck. The rigid-body modes then come out as exactly zero at every mesh density, instead of a spurious few thousandths of a hertz. - A documented dFRF sign convention, verified in the test suite against the forward branches of the wave analysis rather than assumed.
Speed
- The ω(k) problem is posed as a Hermitian eigenvalue problem
(
K + ωΩH − ω²MwithH = −iG), which gives real eigenvalues by construction and lets only the requested low modes be computed — roughly an order of magnitude faster than the state-space form, with a state-space fallback whereKis singular (k = 0). - The receptance uses a banded factorisation:
O(n b²)per frequency instead ofO(n³), so a 1300-point sweep on a 1764-dof rotor takes well under a second.
Usability
- Object model —
Material,ShaftElement,RigidDisk,RotorFEModel,UnitCell,PeriodicRotor— instead of matrices passed positionally between scripts. Hollow shafts, arbitrary disk positions, elastic or rigid supports are parameters now. - One
PeriodicRotorbuilds both the unit cell and the full rotor, so the wave analysis and the receptance can never drift apart. - Results are objects with methods (
band_gaps,summary,attenuation_db_per_cell,to_dict) rather than loose arrays. - Units are explicit in every signature (
spin_rpm,frequencies_hz); SI internally.
run_me.py # runnable demo, no installation needed
rotorwave_demo.ipynb # the same, as a notebook
INSTALACAO.md # setup guide (Windows/Anaconda, in Portuguese)
docs/theory.md # the equations behind the code
src/rotorwave/
materials.py elements.py model.py # finite element model
waves.py dispersion.py whirl.py # wave analysis and results
frf.py analysis.py plotting.py # receptance, higher level studies, figures
examples/ # the figures of the paper, and two new ones
tests/ # pytest suite
- The Bloch–Floquet condition describes an infinite periodic medium built
from one cell, so the dispersion results carry no information about the boundaries.
Finite-rotor effects (boundary conditions, support anisotropy, localized modes)
have to be studied with the
ReceptanceSolveron the full model, which the examples do side by side with the dispersion. - The shaft element is Euler–Bernoulli. Shear deformation matters for short,
thick shafts and for the high branches of a fine cell; check with
mesh_convergencebefore trusting a branch near the top of the computed range. - Damping is still not part of the wave analysis: the band gaps computed here are the
undamped ones.
ReceptanceSolveraccepts a damping matrix for the response.
If this code contributes to your work, please cite the original paper (above) and this software:
@software{rotorwave,
author = {Lamas, P. B. and Nicoletti, R.},
title = {rotorwave: wave propagation analysis of rotors with longitudinal periodicity},
year = {2026},
url = {https://github.com/patrickblamas/rotorwave}
}CITATION.cff in the repository root keeps this up to date; GitHub turns it
into a ready-made citation under "Cite this repository".
MIT — see LICENSE. You may use, modify and redistribute the code, including commercially, provided the copyright notice is kept.
Bug reports, questions and pull requests are welcome through the issue tracker. If you find a case where the results disagree with an independent calculation, please open an issue with the geometry — that is the most useful contribution of all.