<!--
SPDX-FileCopyrightText: Copyright © 2024-2026 Damian Anders <damian.anders@tu-dresden.de>

SPDX-License-Identifier: AGPL-3.0-or-later
-->

pyOutRun
--------

![License](https://img.shields.io/badge/license-AGPL--3.0-blue)
![Python](https://img.shields.io/badge/python-3.9%2B-blue)

**pyOutRun** is a Python package for performing runout analyses on rotating shafts and workpieces. It provides tools for fitting eccentric motion models, separating spindle eccentricity from true surface geometry using multi-probe methods, and simulating synthetic test signals for validation purposes.

The authors would like to thank VolkswagenStiftung for funding the project “Open Science Hardware Instrumente für wissenschaftliche Experimente” as part of the program “Pioniervorhaben - Impulse für das Wissenschaftssystem”.

---

# Overview
## Disclaimer

**THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.**

**pyOutRun is a research and engineering tool developed at TU Dresden. It is NOT certified for use in safety-critical applications. The authors and contributors accept NO LIABILITY for any damages, losses, or injuries arising from the use or misuse of this software. Results produced by this package must be independently verified by qualified personnel before being used in any decision-making process, manufacturing workflow, or quality assurance procedure.**

**Users are responsible for validating that the algorithms, models, and outputs are appropriate for their specific application. The cosine approximation model (`'cos'`) and the exact eccentric model (`'exc'`) each carry inherent assumptions and limitations — using them outside their valid operating range may produce incorrect or misleading results without warning.**

**This software is licensed under the [GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)](https://www.gnu.org/licenses/agpl-3.0.html). By using this software, you agree to comply with all terms of this license, including the requirement to make source code available when providing network access to the software.**

---

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Module Overview](#module-overview)
  - [fit\_eccentricity](#fit_eccentricity)
  - [runout\_multiprobe](#runout_multiprobe)
  - [generate\_geometry](#generate_geometry)
  - [rotation\_angle](#rotation_angle)
  - [reference\_angle](#reference_angle)
  - [rfft\_corrected](#rfft_corrected)
  - [harmonic\_suppression](#harmonic_suppression)
  - [resample](#resample)
  - [evaluate\_noisy\_fit](#evaluate_noisy_fit)
- [API Reference](#api-reference)
- [Dependencies](#dependencies)
- [License](#license)
- [Author](#author)

---

## Features

- **Eccentricity fitting** via differential evolution with both exact geometric and cosine approximation models
- **Multi-probe runout separation** (Jansen method) to isolate spindle error motion from workpiece form deviations
- **Synthetic signal generation** for eccentrically rotating circles and random surface profiles
- **Rotation angle analysis** to identify regions of constant rotational speed from encoder signals
- **Reference angle detection** from signal flanks/impulses with automatic threshold optimization
- **Corrected FFT utilities** with proper normalization for real-valued signals and Tukey windowing
- **Harmonic suppression analysis** via condition number evaluation of sensor position matrices
- **Signal resampling** to align harmonics of a base frequency with FFT frequency bins
- **Noisy fit evaluation** tools for robustness and sensitivity analysis at controlled SNR levels
- **Numba-accelerated** fitness functions and linear algebra kernels for high performance

---

## Installation

```bash
pip install pyOutRun
```

---

## Quick Start

An **Examples Jupyter Notebook** (`Examples.ipynb`) is included in the repository for an interactive quick start. It walks through the complete workflow — from generating synthetic data with eccentricity, form deviations, and noise, to fitting parameters and separating runout components.

Below is a minimal example:

```python
import numpy as np
import pyOutRun as por

# Setup
fSampling = 10_000.0  # Hz
fRot = 5              # Hz
tMax = 10             # s
radius = 1.0
runout = 0.01

t = np.linspace(0, tMax, num=int(fSampling * tMax), endpoint=False)
phi = 2 * np.pi * fRot * t
posSensors = [0.0, 1.0, np.pi, 3.8]

# Generate synthetic eccentric signals
signals, *_ = por.eccentric_signals(phi, radius, [runout, 0.0], posSensors)

# Fit eccentricity model
res = por.fit_eccentricity(t, signals, fit_function='cos')
print(f"Fitted rotation frequency: {res.x[0]:.3f} Hz")

# Separate eccentricity from form deviation
eccentricity, radius_phi, phi_out = por.runout_multiprobe(
    signals, np.asarray(posSensors), fSampling, fRot
)
```

For the full walkthrough including visualization, see the **Examples** notebook.

---

## Module Overview

### `fit_eccentricity`

Core module for fitting geometric models to shaft runout and eccentricity data using differential evolution optimization. Supports exact eccentric geometry (`'exc'`) and simplified cosine (`'cos'`) models, with automatic boundary estimation from spectral analysis.

### `runout_multiprobe`

Implements the multi-probe separation method to decompose measured sensor signals into spindle eccentricity (radial error motion) and true workpiece radius variations (form deviations) in the frequency domain.

### `generate_geometry`

Provides simulation tools for generating synthetic position sensor signals from an eccentrically rotating circle, as well as random surface profiles with controlled spectral content.

### `rotation_angle`

Utilities for identifying regions of constant rotational speed from angle encoder signals and extracting the corresponding frequency via linear fitting.

### `reference_angle`

Functions for detecting reference impulses (flanks) in periodic signals and computing their angular positions, with automatic threshold optimization and optional lowpass filtering.

### `rfft_corrected`

FFT utilities that apply Tukey windowing and correct normalization for real-valued input signals, along with the corresponding inverse transform.

### `harmonic_suppression`

Evaluates the condition numbers of the frequency-dependent sensor position matrix, which characterizes how well harmonics can be separated for a given sensor arrangement.

### `resample`

Resamples time-domain signals so that harmonics of a specified base frequency align exactly with FFT frequency bins, using cubic spline interpolation and zero-padding.

### `evaluate_noisy_fit`

Testing and validation utilities that apply controlled noise at a target SNR to clean signals and evaluate fitting performance, useful for sensitivity and robustness analysis.

---

## API Reference

### `pyOutRun.fit_eccentricity`

#### `fit_eccentricity(time_values, signal, radius=1.0, fit_function='exc', bounds=None, angle_tol=5, position_zero=True, tukey_alpha=0.2, f_lowpass=None)`

Fit a mathematical model to eccentric motion data using differential evolution. Estimates rotational frequency, eccentricity magnitude, and sensor-specific parameters (angular positions and offsets) by minimizing the squared error between measured signals and a geometric model. Supports both an exact eccentric geometry model and a simplified cosine approximation. If `bounds` is `None`, plausible boundaries are automatically estimated from the signal spectra. Returns a `scipy.optimize.OptimizeResult` whose `x` attribute contains `[fRot, eccentricity, angle_s1, offset_s1, ..., angle_sN, offset_sN]`.

---

### `pyOutRun.runout_multiprobe`

#### `runout_multiprobe(sensor_sig, sensor_pos, f_sampling, f_rotation, n_min=2, n_max=100, tukey_alpha=0.02, coordinate_system='co')`

Calculate eccentricity, radius variations, and the angle vector from multiprobe sensor signals. Resamples time-domain signals into the angular domain, computes Fourier spectra, applies the multi-probe mathematical separation on a defined range of harmonic frequencies, and transforms the separated spectra back into the angular domain. Returns a tuple of `(eccentricity, radius_phi, phi)` where `eccentricity` contains x/y components of radial error motion, `radius_phi` contains true radius variations, and `phi` is the corresponding angle vector.

---

### `pyOutRun.generate_geometry`

#### `eccentric_signals(phi, radius, exzentrizitaet, posSensor, modus='xy')`

Simulate position sensor signals for an eccentrically rotating circle. Computes the displacement signals that would be observed by sensors at specified angular positions around a shaft with a given eccentricity. The eccentricity can be specified in Cartesian (`'xy'`) or polar (`'ra'`) coordinates. Returns a tuple of `(delta_s, gamma, l_1)` — the simulated signals, the angles between the x-axis and the sensor-circle intersection, and the parallel component of the centre point shift. All angles are in radians.

#### `surface_profile(phi, f_crit, seed=555, filter_order=10)`

Generate a simulated surface profile with random form deviations. Creates a synthetic roundness error signal by applying a Butterworth low-pass filter to normally distributed random values. Ensures the generated profile is smooth and continuous at the 2π→0 transition. Returns a normalized (std=1) array of radial deviations. Raises an exception if no matching continuous segment is found within the search limit — in that case, change the `seed` parameter.

---

### `pyOutRun.rotation_angle`

#### `angle_values_to_rotation(time_values, angle_values, percentage=75)`

Calculate the velocity (slope) and offset of an angle signal over time. Determines the rotational speed by computing the gradient of the angle sequence, selecting values above a specified percentile, and performing a linear least-squares fit. Returns a tuple `(speed, offset)`. Raises `ValueError` if no valid angle gradients are found above the specified percentile.

#### `find_constant_angle_slope(time_values, angle_values, fit_slope, fit_intercept, percentage=25)`

Find the slice indices of the longest continuous segment matching a linear fit. Calculates the absolute deviation between a linear fit and measured values, determines a maximum deviation threshold using the specified percentile, and identifies the longest contiguous block of points below this threshold. Returns a 1D array `[start, end]` where `end` is incremented by 1 for direct use in array slicing. Raises `ValueError` if no constant gradient region is found.

---

### `pyOutRun.reference_angle`

#### `angle_position_flanks(phi, signal, threshold_percentile=None, lowpass_harmonics=500)`

Calculate the angular positions of signal flanks (positive impulses) and the offset. Processes a 2D signal array and its corresponding angle array to find the centers of positive impulses. Optionally applies a Butterworth lowpass filter and can automatically determine an optimal threshold percentile for flank detection. Returns `(phi_center, offset)` where `phi_center` contains relative angular positions shifted so the first channel is at 0, and `offset` is the absolute angular position of the first channel's impulse. Raises `ValueError` for mismatched input dimensions or invalid percentile values.

---

### `pyOutRun.rfft_corrected`

#### `rfft_tukey_corrected(x, fs, axis=0, tukey=0.05)`

Apply a Tukey window to the input signal, perform a normalized real-valued FFT (RFFT), and correct the spectrum for a real-valued input signal. Also computes the corresponding frequency values. Returns a tuple `(spectrum, freq)`. 

#### `irfft_corrected(spektrum, axis=0)`

Compute the inverse real-to-complex FFT after reversing the amplitude normalization applied by `rfft_tukey_corrected`. Returns the real-valued time-domain signal.

---

### `pyOutRun.harmonic_suppression`

#### `sensor_angle_condition_numbers(freq, positions)`

Determine the condition numbers of the frequency-dependent sensor position matrix from the sensor angular positions and the frequency lines. This is useful for evaluating how well a given sensor arrangement can separate harmonics at each frequency. Returns an array of condition numbers corresponding to each frequency line. Accelerated with Numba.

---

### `pyOutRun.resample`

#### `resample_to_harmonics(x_vector, y_array, f_sampling, f_base, tukey_alpha)`

Resample and zero-pad a signal so that harmonics of a base frequency align exactly with FFT frequency bins. Uses cubic spline interpolation for resampling and applies a Tukey window before padding. The sampling rate is only ever reduced, never increased. Returns `(y_array_resampled, x_array_resampled, f_s_resampled, steps)` where `steps` is the number of frequency bins between consecutive harmonics. Raises `ValueError` for invalid input dimensions, parameter ranges, or frequency constraints.

---

### `pyOutRun.evaluate_noisy_fit`

#### `evaluate_noisy_fit(time_values, signal, noise, SNR, radius, fit_function, bounds, f_lowpass)`

Evaluate fitting performance by applying controlled noise to a clean signal. Scales a noise vector to reach a target Signal-to-Noise Ratio (SNR), adds it to the reference signal, and performs a runout fit. Used to assess how accurately parameters can be recovered from noisy measurements. Returns a `scipy.optimize.OptimizeResult`.

#### `getNoiseScaling(signal, noise, SNR)`

Determine the scaling factor by which a noise vector must be multiplied to achieve a desired SNR relative to a given signal. Returns `0` for infinite SNR (no noise).

---

### `pyOutRun._protocols`

#### `FitFunction` (Protocol)

A typing protocol defining the interface for custom fitness functions that can be passed to `fit_eccentricity`. A conforming callable must accept `(params, xVektor, yArray, *args)` and return a `np.float64` scalar representing the cost.

---

## Dependencies

- **Python** ≥ 3.10
- [NumPy](https://numpy.org/)
- [SciPy](https://scipy.org/)
- [Numba](https://numba.pydata.org/)

For running the Examples notebook:
- [Matplotlib](https://matplotlib.org/)
- [Jupyter](https://jupyter.org/)

---

## License

This project is licensed under the **GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)**.

See the `LICENSES` folder for the full license texts.

---

## Copyright

© 2024–2026 Damian Anders
damian.anders@tu-dresden.de