Try it — compute all 353 features on a real signal
Pick a preset (MAFAULDA-shape synthetic with textbook diagnostic
signatures) or upload your own triaxial .csv / .npy. Values appear
inline next to every feature card below. Computed by the production
v5.lib.features_v5.extract_features on kaltech-ml.
Contents
Time-domain statistics
11 feature definitionsStatistical moments and shape factors computed directly on the acceleration waveform. Kurtosis and crest factor are the textbook early-warning indicators of impulsive faults; RMS and peak govern ISO 10816 severity zoning.
Randall 2011 §2.4 covers the classical 11-feature set. Kurtosis convention is Pearson (non-excess) — Gaussian = 3.0 — which all KALTECH thresholds (4.0, 4.5, 8.0) are calibrated to.
Shape numbers: what averaging can and cannot see
A bearing pit announces itself as a 1 ms tick, once per pass — a sliver of energy in a sea of normal vibration. RMS divides that sliver over thousands of samples and reports almost nothing. The shape statistics don't average; they weigh the tails: kurtosis raises each sample to the 4th power, so one spike counts like ten thousand ordinary samples, and crest factor compares the single worst sample to the average.
Healthy machine vibration is near-Gaussian — kurtosis ≈ 3.0 (Pearson convention), crest ≈ 4. The engine's tier gates read kurtosis against the 4.0 / 4.5 / 8.0 thresholds and crest factor above ~6 as the first, cheapest evidence that something is striking rather than rubbing.
rms Root Mean Square per-axis (×3) ———
- Units
- g (acceleration)
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:960–965 :: extract_features _signal_for_env, _med_filter, _med_info = med(signal)
signal_env = _signal_for_env
_med_aborted = _med_info["aborted"]
_med_converged = float(_med_info["converged"])
_med_kurt_before = float(_med_info["kurtosis_before"])
_med_kurt_after = float(_med_info["kurtosis_after"])
peak Peak amplitude per-axis (×3) ———
- Units
- g
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:960–965 :: extract_features _signal_for_env, _med_filter, _med_info = med(signal)
signal_env = _signal_for_env
_med_aborted = _med_info["aborted"]
_med_converged = float(_med_info["converged"])
_med_kurt_before = float(_med_info["kurtosis_before"])
_med_kurt_after = float(_med_info["kurtosis_after"])
mean Mean (DC offset) per-axis (×3) ———
- Units
- g
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:967–973 :: extract_features else:
signal_env = signal
# F7 Wave-2: explicit "disabled" sentinel so the (aborted, converged)
# pair is unambiguous. Pre-fix: aborted="" + converged=0.0 in the
# disabled branch collided with the THEORETICAL "MED ran cleanly but
# converged=0" combination — downstream consumers checking
# `if features["med_aborted"]:` to detect "MED skipped" would
std_dev Standard deviation per-axis (×3) ———
- Units
- g
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:967–973 :: extract_features else:
signal_env = signal
# F7 Wave-2: explicit "disabled" sentinel so the (aborted, converged)
# pair is unambiguous. Pre-fix: aborted="" + converged=0.0 in the
# disabled branch collided with the THEORETICAL "MED ran cleanly but
# converged=0" combination — downstream consumers checking
# `if features["med_aborted"]:` to detect "MED skipped" would
variance Variance per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:967–973 :: extract_features else:
signal_env = signal
# F7 Wave-2: explicit "disabled" sentinel so the (aborted, converged)
# pair is unambiguous. Pre-fix: aborted="" + converged=0.0 in the
# disabled branch collided with the THEORETICAL "MED ran cleanly but
# converged=0" combination — downstream consumers checking
# `if features["med_aborted"]:` to detect "MED skipped" would
kurtosis Kurtosis (Pearson) per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:967–973 :: extract_features else:
signal_env = signal
# F7 Wave-2: explicit "disabled" sentinel so the (aborted, converged)
# pair is unambiguous. Pre-fix: aborted="" + converged=0.0 in the
# disabled branch collided with the THEORETICAL "MED ran cleanly but
# converged=0" combination — downstream consumers checking
# `if features["med_aborted"]:` to detect "MED skipped" would
skewness Skewness per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:967–973 :: extract_features else:
signal_env = signal
# F7 Wave-2: explicit "disabled" sentinel so the (aborted, converged)
# pair is unambiguous. Pre-fix: aborted="" + converged=0.0 in the
# disabled branch collided with the THEORETICAL "MED ran cleanly but
# converged=0" combination — downstream consumers checking
# `if features["med_aborted"]:` to detect "MED skipped" would
crest_factor Crest factor per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:960–965 :: extract_features _signal_for_env, _med_filter, _med_info = med(signal)
signal_env = _signal_for_env
_med_aborted = _med_info["aborted"]
_med_converged = float(_med_info["converged"])
_med_kurt_before = float(_med_info["kurtosis_before"])
_med_kurt_after = float(_med_info["kurtosis_after"])
shape_factor Shape factor per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:975–978 :: extract_features # produced a usable result.
_med_aborted = "disabled"
_med_converged = 0.0
_med_kurt_before = 0.0
impulse_factor Impulse factor per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:975–978 :: extract_features # produced a usable result.
_med_aborted = "disabled"
_med_converged = 0.0
_med_kurt_before = 0.0
clearance_factor Clearance factor per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Randall 2011, §2.4, p. 31–38 — Vibration-based Condition Monitoring of Machinery; time-domain stats
v5/lib/features_v5.py:978–983 :: extract_features _med_kurt_before = 0.0
_med_kurt_after = 0.0
_med_n_iter = 0.0
# -----------------------------------------------------------------------
# TIME-DOMAIN STATISTICAL (11 features)
Frequency-domain statistics
10 feature definitionsBulk descriptors of the FFT magnitude spectrum — used as priors by the ML head and as gating features in the verdict engine. None of these alone diagnoses a bearing fault; they characterise the energy distribution and are aggregated with defect-frequency energies (Group 4–8) for diagnosis.
Randall 2011 §3.6. Computed from |rfft(x)|·(2/N) with DC and Nyquist bins scaled by 1/N — matches NumPy's amplitude convention used in features.py compute_fft.
Spectrum shape: the distribution, before the diagnosis
Before asking "is there a tone at BPFO", it pays to ask "what does the energy distribution look like at all?" These ten descriptors summarise the whole FFT as a shape: where its centre of mass sits (centroid), how spread it is (bandwidth), where 85% of the energy has accumulated (rolloff), and whether it is tonal or noise-like (flatness, entropy), plus four coarse band energies.
None of them names a fault — by design. They are the priors the ML head consumes and the gating context for the verdict engine: developing damage drags the centroid toward the high bands and whitens the floor long before any single line is unambiguous.
dominant_freq_hz Dominant frequency per-axis (×3) ———
- Units
- Hz
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
- Visualisation
- spectrum
v5/lib/features_v5.py:990–994 :: extract_features std_dev = float(np.std(signal))
var = std_dev ** 2
# Non-excess kurtosis: mu_4/sigma^4 (Gaussian = 3.0, NOT 0.0)
# All downstream thresholds (4.0, 8.0) are calibrated to this definition.
kurtosis = float(np.mean((signal - mean) ** 4) / (var ** 2)) if var > 0 else 0.0
spectral_centroid Spectral centroid per-axis (×3) ———
- Units
- Hz
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
- Visualisation
- spectrum
v5/lib/features_v5.py:998–1001 :: extract_features mean_abs = float(np.mean(abs_signal))
shape_factor = rms / mean_abs if mean_abs > 0 else 0.0
impulse_factor = peak / mean_abs if mean_abs > 0 else 0.0
spectral_bandwidth Spectral bandwidth per-axis (×3) ———
- Units
- Hz
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
- Visualisation
- spectrum
v5/lib/features_v5.py:998–1001 :: extract_features mean_abs = float(np.mean(abs_signal))
shape_factor = rms / mean_abs if mean_abs > 0 else 0.0
impulse_factor = peak / mean_abs if mean_abs > 0 else 0.0
spectral_rolloff Spectral rolloff (85%) per-axis (×3) ———
- Units
- Hz
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
- Visualisation
- spectrum
v5/lib/features_v5.py:1003–1006 :: extract_features clearance_factor = peak / (mean_sqrt_abs ** 2) if mean_sqrt_abs > 0 else 0.0
# -----------------------------------------------------------------------
# FREQUENCY-DOMAIN (7 features)
spectral_flatness Spectral flatness (Wiener entropy) per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1008–1018 :: extract_features freqs, magnitudes = compute_fft(signal, fs)
# Exclude DC bin (index 0) for spectral statistics
freqs_ndc = freqs[1:]
mags_ndc = magnitudes[1:]
dominant_freq_hz = float(freqs_ndc[np.argmax(mags_ndc)]) if len(mags_ndc) > 0 else 0.0
# Spectral power (for weighted statistics)
power = mags_ndc ** 2
total_power = float(np.sum(power))
spectral_entropy Spectral entropy per-axis (×3) ———
- Units
- nats
- Textbook
- Brandt 2011, §3 + §6 — Noise and Vibration Analysis — DSP fundamentals
v5/lib/features_v5.py:1190–1199 :: extract_features # g·s → m/s for second integration
vel_raw *= 9.81
displacement_m = cumulative_trapezoid(vel_raw, dx=dt, initial=0.0)
displacement_m -= np.linspace(displacement_m[0], displacement_m[-1], len(displacement_m))
if nyquist > 100:
b_d, a_d = butter(2, [max(2.0 / nyquist, 0.001), min(100.0 / nyquist, 0.99)], btype="band")
disp_filtered = filtfilt(b_d, a_d, displacement_m)
# ISO 20816-3: peak-to-peak displacement in µm
pp_displacement_um = float(np.max(disp_filtered) - np.min(disp_filtered)) * 1e6
else:
band_energy_low Band energy — low per-axis (×3) ———
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1020–1037 :: extract_features if total_power > 0:
weights = power / total_power
spectral_centroid = float(np.sum(freqs_ndc * weights))
spectral_bandwidth = float(np.sqrt(np.sum(((freqs_ndc - spectral_centroid) ** 2) * weights)))
# Rolloff: frequency below which 85% of spectral energy resides
cumulative_energy = np.cumsum(power) / total_power
rolloff_idx = np.searchsorted(cumulative_energy, 0.85)
spectral_rolloff = float(freqs_ndc[min(rolloff_idx, len(freqs_ndc) - 1)])
# Flatness: geometric mean / arithmetic mean of power spectrum
# High flatness → noise-like; low flatness → tonal/harmonic
log_power = np.log(power + 1e-20)
geo_mean = float(np.exp(np.mean(log_power)))
arith_mean = float(np.mean(power))
spectral_flatness = geo_mean / arith_mean if arith_mean > 0 else 0.0
else:
spectral_centroid = 0.0
band_energy_mid Band energy — mid per-axis (×3) ———
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1020–1037 :: extract_features if total_power > 0:
weights = power / total_power
spectral_centroid = float(np.sum(freqs_ndc * weights))
spectral_bandwidth = float(np.sqrt(np.sum(((freqs_ndc - spectral_centroid) ** 2) * weights)))
# Rolloff: frequency below which 85% of spectral energy resides
cumulative_energy = np.cumsum(power) / total_power
rolloff_idx = np.searchsorted(cumulative_energy, 0.85)
spectral_rolloff = float(freqs_ndc[min(rolloff_idx, len(freqs_ndc) - 1)])
# Flatness: geometric mean / arithmetic mean of power spectrum
# High flatness → noise-like; low flatness → tonal/harmonic
log_power = np.log(power + 1e-20)
geo_mean = float(np.exp(np.mean(log_power)))
arith_mean = float(np.mean(power))
spectral_flatness = geo_mean / arith_mean if arith_mean > 0 else 0.0
else:
spectral_centroid = 0.0
band_energy_high Band energy — high per-axis (×3) ———
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1020–1037 :: extract_features if total_power > 0:
weights = power / total_power
spectral_centroid = float(np.sum(freqs_ndc * weights))
spectral_bandwidth = float(np.sqrt(np.sum(((freqs_ndc - spectral_centroid) ** 2) * weights)))
# Rolloff: frequency below which 85% of spectral energy resides
cumulative_energy = np.cumsum(power) / total_power
rolloff_idx = np.searchsorted(cumulative_energy, 0.85)
spectral_rolloff = float(freqs_ndc[min(rolloff_idx, len(freqs_ndc) - 1)])
# Flatness: geometric mean / arithmetic mean of power spectrum
# High flatness → noise-like; low flatness → tonal/harmonic
log_power = np.log(power + 1e-20)
geo_mean = float(np.exp(np.mean(log_power)))
arith_mean = float(np.mean(power))
spectral_flatness = geo_mean / arith_mean if arith_mean > 0 else 0.0
else:
spectral_centroid = 0.0
band_energy_ultrasonic Band energy — ultrasonic per-axis (×3) ———
- Units
- dimensionless (0–1)
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1180–1188 :: extract_features #
# Effective band by fs is intentional — the firmware/common parity
# contract requires this exact clamp to match the C implementation
# (see firmware/common/dsp/kaltech_filter_coeffs.h
# KAL_FILT_DISP_PP_COEFFS). Changing here would break design specification
# bit-exact parity.
#
# Use unfiltered velocity for integration to avoid double-filtering.
vel_raw = cumulative_trapezoid(signal, dx=dt, initial=0.0)
ISO 10816 / 20816 broadband severity
4 feature definitionsThe four canonical industrial severity scalars. RMS velocity is the headline ISO 10816 zone metric; peak-to-peak displacement is the slow-speed equivalent used in API 670; RMS acceleration completes the integrate-once vs integrate-twice triple for the 10–1000 Hz band.
ISO 10816-3 / ISO 20816-1 Class IV (large rotating machinery on flexible foundation) zone thresholds: GOOD ≤ 2.3 mm/s, SATISFACTORY ≤ 4.5, UNSATISFACTORY ≤ 7.1, UNACCEPTABLE > 7.1.
ISO velocity: the plant-floor severity language
RMS velocity in the 10–1000 Hz band is how the entire industry agrees on "how bad is the shaking" — velocity, because a mm/s means roughly the same fatigue stress at 20 Hz as at 800 Hz. The engine zones it against the ISO 10816 Class IV thresholds baked into this group: 2.3 / 4.5 / 7.1 mm/s.
It is the headline number for structural faults — imbalance, misalignment, looseness pump smooth, sustained energy that RMS integrates faithfully. But it is an average: a 1 ms bearing tick divided over a full window contributes almost nothing, which is why a bearing can be visibly failing in the envelope groups below while this gauge still reads green.
rms_velocity_mm_s RMS velocity per-axis (×3) ———
- Units
- mm/s
- Textbook
- ISO 10816-1/3 — Mechanical vibration — broadband velocity severity zones
v5/lib/features_v5.py:793–821 :: compute_rms_velocity hop_length: Hop size between STFT frames.
Returns:
float32 array of shape (n_mels, time_frames) normalised to [0, 1].
"""
_f, _t, Zxx = stft(segment, fs=fs, nperseg=n_fft, noverlap=n_fft - hop_length)
power = np.abs(Zxx) ** 2
mel_basis = _mel_filterbank(fs, n_fft, n_mels)
mel_spec = mel_basis @ power[: n_fft // 2 + 1, :]
log_mel = np.log(mel_spec + 1e-9)
log_mel = (log_mel - log_mel.min()) / (log_mel.max() - log_mel.min() + 1e-9)
return log_mel.astype(np.float32)
# ---------------------------------------------------------------------------
# RMS Velocity (ISO 10816)
# ---------------------------------------------------------------------------
def compute_rms_velocity(signal: np.ndarray, fs: int = CWRU_FS) -> float:
"""
Integrate acceleration signal to velocity and compute RMS in mm/s.
ISO 10816-1 specifies velocity RMS in the 10-1000 Hz band.
Integration via cumulative trapezoidal rule, followed by 10-1000 Hz
bandpass filter, then RMS computation.
peak_velocity_mm_s Peak velocity per-axis (×3) ———
- Units
- mm/s
- Textbook
- ISO 10816-1/3 — Mechanical vibration — broadband velocity severity zones
v5/lib/features_v5.py:793–821 :: compute_rms_velocity hop_length: Hop size between STFT frames.
Returns:
float32 array of shape (n_mels, time_frames) normalised to [0, 1].
"""
_f, _t, Zxx = stft(segment, fs=fs, nperseg=n_fft, noverlap=n_fft - hop_length)
power = np.abs(Zxx) ** 2
mel_basis = _mel_filterbank(fs, n_fft, n_mels)
mel_spec = mel_basis @ power[: n_fft // 2 + 1, :]
log_mel = np.log(mel_spec + 1e-9)
log_mel = (log_mel - log_mel.min()) / (log_mel.max() - log_mel.min() + 1e-9)
return log_mel.astype(np.float32)
# ---------------------------------------------------------------------------
# RMS Velocity (ISO 10816)
# ---------------------------------------------------------------------------
def compute_rms_velocity(signal: np.ndarray, fs: int = CWRU_FS) -> float:
"""
Integrate acceleration signal to velocity and compute RMS in mm/s.
ISO 10816-1 specifies velocity RMS in the 10-1000 Hz band.
Integration via cumulative trapezoidal rule, followed by 10-1000 Hz
bandpass filter, then RMS computation.
rms_acceleration_g RMS acceleration (10–1000 Hz) per-axis (×3) ———
- Units
- g
- Textbook
- ISO 10816-1/3 — Mechanical vibration — broadband velocity severity zones
v5/lib/features_v5.py:1499–1623 :: extract_randall_features # Dominant order (excluding DC)
non_dc = orders_trim > 0.5
if np.any(non_dc):
dominant_idx = np.argmax(mags_trim[non_dc])
order_energies["dominant_order"] = float(orders_trim[non_dc][dominant_idx])
else:
order_energies["dominant_order"] = 0.0
# 1x/2x ratio (imbalance indicator — high 1x relative to 2x)
e1x = order_energies.get("order_1x_energy", 0.0)
e2x = order_energies.get("order_2x_energy", 0.0)
order_energies["order_1x_2x_ratio"] = min(e1x / (e2x + 1e-8), 1000.0)
# Sub-synchronous energy (orders < 1.0) — looseness indicator
mask_sub = (orders_trim > 0.1) & (orders_trim < 0.95)
order_energies["order_subsync_energy"] = float(np.sum(mags_trim[mask_sub] ** 2))
# Include the defect order values for reference
order_energies.update(defect_orders)
return order_energies
def extract_randall_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, Any]:
"""
Extract advanced diagnostic features using Randall's techniques.
Complements extract_features() with:
- Cepstrum-based periodicity detection
- Spectral kurtosis band selection
- Optimal-band envelope analysis
"""
features: dict[str, Any] = {}
# --- Cepstrum analysis ---
quefrency, cepstrum = compute_cepstrum(signal, fs)
# Peak cepstrum value (excluding near-zero quefrency)
min_quef = 0.002 # 2ms minimum (500 Hz max)
max_quef = 0.1 # 100ms maximum (10 Hz min)
mask = (quefrency >= min_quef) & (quefrency <= max_quef)
if np.any(mask):
peak_idx = np.argmax(cepstrum[mask])
features["cepstrum_peak_quefrency"] = float(quefrency[mask][peak_idx])
features["cepstrum_peak_magnitude"] = float(cepstrum[mask][peak_idx])
features["cepstrum_peak_freq"] = (
1.0 / features["cepstrum_peak_quefrency"]
if features["cepstrum_peak_quefrency"] > 0
else 0.0
)
else:
features["cepstrum_peak_quefrency"] = 0.0
features["cepstrum_peak_magnitude"] = 0.0
features["cepstrum_peak_freq"] = 0.0
# Check if cepstrum peak matches any defect frequency.
# A bearing impact train has cepstrum peaks at the IMPACT PERIOD (1/BPFO)
# AND its integer multiples (2/BPFO, 3/BPFO, ...). Original implementation
# only matched the fundamental cep_freq vs defect freq within ±5%, which
# missed signals where the algorithm picked a higher cepstral harmonic.
# Fix: check if peak_quefrency × defect_freq is within ±10% of any integer
# K ∈ [1, 5]. K=1 reduces to the original test (slightly relaxed), K≥2
# captures higher-order cepstral harmonics. Mirrors firmware fix in
# kaltech_cepstrum_extended_f32.
# cepstrum_defect_match: categorical {none, bpfo, bpfi, bsf, ftf}.
# Pre-seed to "none" so the key always exists in the returned dict —
# downstream callers (services/features_api/main.py contract check,
# NPZ training pipeline) require stable schema regardless of whether
# rpm is provided. Wave-2 fix C-1.
features["cepstrum_defect_match"] = "none"
if rpm is not None and rpm > 0:
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
peak_q = features["cepstrum_peak_quefrency"]
if peak_q > 0:
best_err = 1e30
# … 45 more lines truncated …
pp_displacement_um Peak-to-peak displacement per-axis (×3) ———
- Units
- µm
- Textbook
- ISO 10816-1/3 — Mechanical vibration — broadband velocity severity zones
v5/lib/features_v5.py:1499–1623 :: extract_randall_features # Dominant order (excluding DC)
non_dc = orders_trim > 0.5
if np.any(non_dc):
dominant_idx = np.argmax(mags_trim[non_dc])
order_energies["dominant_order"] = float(orders_trim[non_dc][dominant_idx])
else:
order_energies["dominant_order"] = 0.0
# 1x/2x ratio (imbalance indicator — high 1x relative to 2x)
e1x = order_energies.get("order_1x_energy", 0.0)
e2x = order_energies.get("order_2x_energy", 0.0)
order_energies["order_1x_2x_ratio"] = min(e1x / (e2x + 1e-8), 1000.0)
# Sub-synchronous energy (orders < 1.0) — looseness indicator
mask_sub = (orders_trim > 0.1) & (orders_trim < 0.95)
order_energies["order_subsync_energy"] = float(np.sum(mags_trim[mask_sub] ** 2))
# Include the defect order values for reference
order_energies.update(defect_orders)
return order_energies
def extract_randall_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, Any]:
"""
Extract advanced diagnostic features using Randall's techniques.
Complements extract_features() with:
- Cepstrum-based periodicity detection
- Spectral kurtosis band selection
- Optimal-band envelope analysis
"""
features: dict[str, Any] = {}
# --- Cepstrum analysis ---
quefrency, cepstrum = compute_cepstrum(signal, fs)
# Peak cepstrum value (excluding near-zero quefrency)
min_quef = 0.002 # 2ms minimum (500 Hz max)
max_quef = 0.1 # 100ms maximum (10 Hz min)
mask = (quefrency >= min_quef) & (quefrency <= max_quef)
if np.any(mask):
peak_idx = np.argmax(cepstrum[mask])
features["cepstrum_peak_quefrency"] = float(quefrency[mask][peak_idx])
features["cepstrum_peak_magnitude"] = float(cepstrum[mask][peak_idx])
features["cepstrum_peak_freq"] = (
1.0 / features["cepstrum_peak_quefrency"]
if features["cepstrum_peak_quefrency"] > 0
else 0.0
)
else:
features["cepstrum_peak_quefrency"] = 0.0
features["cepstrum_peak_magnitude"] = 0.0
features["cepstrum_peak_freq"] = 0.0
# Check if cepstrum peak matches any defect frequency.
# A bearing impact train has cepstrum peaks at the IMPACT PERIOD (1/BPFO)
# AND its integer multiples (2/BPFO, 3/BPFO, ...). Original implementation
# only matched the fundamental cep_freq vs defect freq within ±5%, which
# missed signals where the algorithm picked a higher cepstral harmonic.
# Fix: check if peak_quefrency × defect_freq is within ±10% of any integer
# K ∈ [1, 5]. K=1 reduces to the original test (slightly relaxed), K≥2
# captures higher-order cepstral harmonics. Mirrors firmware fix in
# kaltech_cepstrum_extended_f32.
# cepstrum_defect_match: categorical {none, bpfo, bpfi, bsf, ftf}.
# Pre-seed to "none" so the key always exists in the returned dict —
# downstream callers (services/features_api/main.py contract check,
# NPZ training pipeline) require stable schema regardless of whether
# rpm is provided. Wave-2 fix C-1.
features["cepstrum_defect_match"] = "none"
if rpm is not None and rpm > 0:
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
peak_q = features["cepstrum_peak_quefrency"]
if peak_q > 0:
best_err = 1e30
# … 45 more lines truncated …
FFT defect energies (1× / 2× / 3×)
12 feature definitionsDirect FFT energy at the four bearing defect frequencies and their 2nd and 3rd harmonics. Computed by summing |X[k]|² for bins within ±5 Hz of each target. The fundamentals are sometimes masked by shaft-rate spillover; the harmonics often persist and are the diagnostic signal of choice in noisy installations.
Randall §5.4 — bearing defect frequencies derived from geometry (pitch diameter, ball diameter, contact angle, number of rolling elements) per the Harris formulae.
Defect frequencies: geometry gives every part its own clock
Bearing geometry — ball count, pitch and ball diameter, contact angle — fixes the rate at which a rolling element passes any fixed point. So a pit on the outer ring is struck at exactly BPFO, the inner ring at BPFI, a ball defect at BSF, a cage fault at FTF. These features simply integrate spectrum energy in a ±5 Hz window at each rate and its 2× and 3× harmonics: is there energy exactly where this part's clock says it should be?
The sidebands carry a second story. An outer-ring pit sits still in the load zone — every impact equal, a clean tone. An inner-ring pit rides the shaft, loud once per revolution as it sweeps through the load zone: that amplitude modulation splits the BPFI tone into sidebands spaced at exactly 1× shaft. A ball defect gets modulated at the cage rate instead. The sideband spacing confirms the diagnosis.
energy_at_bpfo FFT energy at BPFO per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_bpfi FFT energy at BPFI per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_bsf FFT energy at BSF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_ftf FFT energy at FTF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_2x_bpfo FFT energy at 2× BPFO per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_2x_bpfi FFT energy at 2× BPFI per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_2x_bsf FFT energy at 2× BSF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_2x_ftf FFT energy at 2× FTF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_3x_bpfo FFT energy at 3× BPFO per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_3x_bpfi FFT energy at 3× BPFI per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_3x_bsf FFT energy at 3× BSF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
energy_at_3x_ftf FFT energy at 3× FTF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
Envelope-spectrum defect energies (1× / 2× / 3×)
12 feature definitionsEnvelope demodulation in the broadband bearing-resonance band (default 2–5 kHz at fs=25.6 kHz) followed by FFT, then energy summation around each defect frequency × harmonic combination. The textbook bearing-fault detector — Randall §5.5.
Randall 2011 §5.5 eq. 5.27: x → bandpass (Butterworth-4 SOS, filtfilt) → |Hilbert(·)| → DC-remove → |FFT|/N. Squared variant (Group 7/8) replaces |Hilbert(·)| with |Hilbert(·)|² per Fig 5.39.
Envelope analysis: demodulating the rhythm of the ticks
Bearing impacts are tiny ticks that excite the machine's high-frequency resonance — the structure rings like a struck bell at every impact. The engine demodulates a 2–5 kHz band by default: bandpass throws away shaft harmonics and keeps only the ringing; the Hilbert envelope strips away the resonance carrier and leaves just the rhythm of the ticks; the FFT of that envelope shows energy at whichever geometric rate is ticking — BPFO for a pit on the outer ring, BPFI with shaft-rate sidebands for the inner ring, BSF with cage modulation for a ball.
This is the textbook bearing-fault detector and the flagship of the feature set: it names the failing part and, trended over days, shows it worsening. The twelve features here are the envelope-spectrum energies at {1×, 2×, 3×} of each defect rate — the direct evidence the verdict engine cites for a bearing call.
envelope_energy_bpfo Envelope energy at BPFO per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_bpfi Envelope energy at BPFI per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_bsf Envelope energy at BSF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_ftf Envelope energy at FTF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_2x_bpfo Envelope energy at 2× BPFO per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_2x_bpfi Envelope energy at 2× BPFI per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_2x_bsf Envelope energy at 2× BSF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_2x_ftf Envelope energy at 2× FTF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_3x_bpfo Envelope energy at 3× BPFO per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_3x_bpfi Envelope energy at 3× BPFI per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_3x_bsf Envelope energy at 3× BSF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
envelope_energy_3x_ftf Envelope energy at 3× FTF per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, eq. 5.27 — Hilbert envelope demodulation
- Visualisation
- envelope
v5/lib/features_v5.py:336–382 :: envelope_spectrum
def envelope_spectrum(
signal: np.ndarray,
fs: int = CWRU_FS,
band_low: float = 2000.0,
band_high: float = 5000.0,
squared: bool = False,
) -> tuple[np.ndarray, np.ndarray]:
"""
Hilbert-transform envelope analysis.
Steps:
1. Bandpass filter around housing resonance frequency.
2. Hilbert transform → analytic signal → take magnitude (envelope).
3. Optionally square the envelope (Randall §5.5, Fig 5.39).
4. Remove DC component.
5. FFT of envelope → reveals fault modulation frequencies.
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
band_low: Lower edge of bandpass filter (Hz).
band_high: Upper edge of bandpass filter (Hz).
squared: If True, FFT operates on envelope² instead of envelope.
Squared envelope prevents aliasing of the magnitude operation
(Randall p.201) and concentrates energy at modulation lines.
V5 uses squared=True for the new slr_sq_* features.
Returns:
(freqs, fft_env) — frequency axis and envelope spectrum magnitudes.
"""
nyq = fs / 2.0
b, a = butter(4, [band_low / nyq, band_high / nyq], btype="band")
filtered = filtfilt(b, a, signal)
analytic = hilbert(filtered)
envelope = np.abs(analytic)
if squared:
# Square then DC-remove → preserves Randall's recommended pipeline:
# |FFT(envelope² - mean(envelope²))| / N
envelope = envelope * envelope
envelope -= np.mean(envelope)
N = len(envelope)
freqs = np.fft.rfftfreq(N, d=1.0 / fs)
fft_env = np.abs(np.fft.rfft(envelope)) / N
Optimal-band envelope defect energies (1× / 2× / 3×)
12 feature definitionsKurtogram-selected demodulation band replaces the fixed broadband band. The optimal band is the (centre frequency, bandwidth) pair with maximum spectral kurtosis across a 4-level scan (window sizes 64/128/256/512). Catches bearing damage when the resonance has shifted or when the broadband band is contaminated by structural modes.
Antoni 2007, Fast Kurtogram (MSSP 21(1)). Filter bank catalogue in firmware/common/dsp/kaltech_filter_bank.h pre-computes SOS coefficients for each (fs, band) pair to keep MCU work bounded.
Same detector, smarter band
The broadband envelope group assumes the bearing's impacts ring the structure inside one fixed band. Real machines disagree: the excited resonance shifts with mounting, load and temperature, and structural modes can pollute the default band. This group re-runs the exact same envelope pipeline, but inside the band the kurtogram chose — the (centre, bandwidth) pair with maximum spectral kurtosis from a 4-level scan (windows 64/128/256/512).
In the animation, note the grey dashed rectangle (the fixed band) versus the glowing one (the selected band): when the resonance moves, the fixed band demodulates the wrong part of the spectrum while the selected band follows the impulsive energy.
optimal_env_bpfo Optimal-band envelope energy at BPFO per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_bpfi Optimal-band envelope energy at BPFI per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_bsf Optimal-band envelope energy at BSF per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_ftf Optimal-band envelope energy at FTF per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_2x_bpfo Optimal-band envelope energy at 2× BPFO per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_2x_bpfi Optimal-band envelope energy at 2× BPFI per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_2x_bsf Optimal-band envelope energy at 2× BSF per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_2x_ftf Optimal-band envelope energy at 2× FTF per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_3x_bpfo Optimal-band envelope energy at 3× BPFO per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_3x_bpfi Optimal-band envelope energy at 3× BPFI per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_3x_bsf Optimal-band envelope energy at 3× BSF per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_env_3x_ftf Optimal-band envelope energy at 3× FTF per-axis (×3) ———
- Units
- g²
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
DRS + squared envelope, broadband (new)
4 feature definitionsTwelve recently-added features: DRS residual → broadband bandpass → squared Hilbert envelope → FFT energy at {1×, 2×, 3×} × {BPFO, BPFI, BSF, FTF}. The patent's bearing-fault diagnostic of choice when the rig has any gear-mesh or shaft-harmonic structure to strip — that is, every real industrial machine.
Sawalhi & Randall 2011 (MSSP 25) for DRS; Randall §5.5 Fig 5.39 for squared envelope. Combined transform was previously available in research code (PyBearingFault, Endaq) but not as on-chip features.
DRS: subtract the predictable, keep the bearing
Gears and shafts are deterministic: mesh tones and shaft harmonics repeat identically every revolution, and on an industrial machine they usually dwarf the bearing signature. Rolling elements, though, slip — a bearing's impact train wanders by a percent or two, making it slightly random. DRS (discrete/random separation) exploits exactly that: it predicts the predictable part of the signal from its own past and subtracts it. Whatever survives is the random part — which is where the bearing lives.
The residual then gets the squared-envelope treatment, and these features read the defect-rate energies out of it. It's the bearing detector of choice on any machine with gear-mesh or strong shaft-harmonic structure — that is, most real installations.
slr_sq_env_bpfo DRS squared-envelope energy at BPFO Newper-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, Fig 5.39 — Squared envelope (variant) prevents aliasing of |·| operator
- Visualisation
- drs
v5/lib/features_v5.py:489–499 :: drs_squared_envelope_features
def drs_squared_envelope_features(*args, **kwargs):
"""DEPRECATED alias for ``slr_squared_envelope_features``.
The original name "DRS" misleadingly implied Sawalhi-Randall Discrete/Random
Separation (2008). What this function actually applies is spectral line
removal (median-of-neighbour-bins comb filter). The keys were renamed in
``drs_sq_*`` to ``slr_sq_*`` for honesty. Use the new name in new
code. This alias kept temporarily to avoid breaking external callers; will
be removed in V6.
slr_sq_env_bpfi DRS squared-envelope energy at BPFI Newper-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, Fig 5.39 — Squared envelope (variant) prevents aliasing of |·| operator
- Visualisation
- drs
v5/lib/features_v5.py:489–499 :: drs_squared_envelope_features
def drs_squared_envelope_features(*args, **kwargs):
"""DEPRECATED alias for ``slr_squared_envelope_features``.
The original name "DRS" misleadingly implied Sawalhi-Randall Discrete/Random
Separation (2008). What this function actually applies is spectral line
removal (median-of-neighbour-bins comb filter). The keys were renamed in
``drs_sq_*`` to ``slr_sq_*`` for honesty. Use the new name in new
code. This alias kept temporarily to avoid breaking external callers; will
be removed in V6.
slr_sq_env_bsf DRS squared-envelope energy at BSF Newper-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, Fig 5.39 — Squared envelope (variant) prevents aliasing of |·| operator
- Visualisation
- drs
v5/lib/features_v5.py:489–499 :: drs_squared_envelope_features
def drs_squared_envelope_features(*args, **kwargs):
"""DEPRECATED alias for ``slr_squared_envelope_features``.
The original name "DRS" misleadingly implied Sawalhi-Randall Discrete/Random
Separation (2008). What this function actually applies is spectral line
removal (median-of-neighbour-bins comb filter). The keys were renamed in
``drs_sq_*`` to ``slr_sq_*`` for honesty. Use the new name in new
code. This alias kept temporarily to avoid breaking external callers; will
be removed in V6.
slr_sq_env_ftf DRS squared-envelope energy at FTF Newper-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §5.5, p. 200–215, Fig 5.39 — Squared envelope (variant) prevents aliasing of |·| operator
- Visualisation
- drs
v5/lib/features_v5.py:489–499 :: drs_squared_envelope_features
def drs_squared_envelope_features(*args, **kwargs):
"""DEPRECATED alias for ``slr_squared_envelope_features``.
The original name "DRS" misleadingly implied Sawalhi-Randall Discrete/Random
Separation (2008). What this function actually applies is spectral line
removal (median-of-neighbour-bins comb filter). The keys were renamed in
``drs_sq_*`` to ``slr_sq_*`` for honesty. Use the new name in new
code. This alias kept temporarily to avoid breaking external callers; will
be removed in V6.
Shaft harmonics
3 feature definitionsEnergy at 1×, 2×, and 3× the shaft rotation rate — the three canonical rotor-dynamics-fault scalars. The relative magnitudes of the three harmonics encode the classical Eshleman diagnostic chart: 1× dominant → imbalance; 2× comparable to or greater than 1× → misalignment; 3× exceeding both → looseness. Computed by integrating |FFT|² in a ±3 Hz band around each integer multiple of the shaft frequency.
Brandt §6.4 + Eshleman 1999. The three canonical imbalance/misalignment/looseness indicators in rotating-machinery vibration, valid for any motor, pump, fan, or compressor with a known shaft RPM.
1× / 2× / 3×: the rotor-dynamics fingerprint
The lowest three multiples of shaft speed encode the classical rotating-machinery diagnosis chart. A rotating heavy spot pulls outward once per revolution — 1× dominant → imbalance. A misaligned coupling flexes twice per revolution as its jaws trade load — 2× rivalling 1× → misalignment. Loose fits let parts rattle and clip the waveform, spraying energy into 3× and a raised harmonic floor → looseness.
The engine integrates |FFT|² in a ±3 Hz band around each multiple and reads the pattern, not the absolute level — the ratios survive changes in mounting and sensor sensitivity that would fool a raw amplitude check.
harmonic_1x 1× shaft harmonic per-axis (×3) ———
- Units
- g²
- Textbook
- Brandt 2011, §6.4 — Harmonic energy at integer multiples of shaft rotation rate
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
harmonic_2x 2× shaft harmonic per-axis (×3) ———
- Units
- g²
- Textbook
- Brandt 2011, §6.4 — Harmonic energy at integer multiples of shaft rotation rate
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
harmonic_3x 3× shaft harmonic per-axis (×3) ———
- Units
- g²
- Textbook
- Brandt 2011, §6.4 — Harmonic energy at integer multiples of shaft rotation rate
- Visualisation
- spectrum
v5/lib/features_v5.py:828–836 :: _energy_near """
dt = 1.0 / fs
velocity = cumulative_trapezoid(signal, dx=dt, initial=0.0)
# Remove linear drift from integration
velocity -= np.linspace(velocity[0], velocity[-1], len(velocity))
# Input is in g, integration gives g·s. Convert: g·s × 9.81 m/s²/g × 1000 mm/m
velocity_mm_s = velocity * 9.81 * 1000.0 # g·s → mm/s (ISO 10816)
# ISO 10816-1: bandpass 10-1000 Hz
nyquist = fs / 2.0
Cepstrum analysis
4 feature definitionsThe real cepstrum reveals periodicity in the *log* spectrum — which manifests as harmonic series of any fundamental frequency, i.e. bearing-defect modulation trains. Four features per axis: peak quefrency, peak magnitude, derived frequency, and a categorical defect-match indicator.
Randall §5.7 + §3.7. Cepstrum = IFFT(log(|FFT(x)|²)). Peaks at 1/f_defect indicate periodic impacts.
Cepstrum: a whole harmonic family in one number
A periodic defect never makes just one spectral line — it makes a comb: 1×, 2×, 3×… of its rate, often with sidebands around every tooth. Counting comb teeth by hand is analyst work. The cepstrum — the spectrum of the log-spectrum — does it in one transform: any evenly-spaced family in the spectrum, no matter how many teeth carry it, collapses into a single peak at the quefrency 1/f.
The peak's position names the strongest repeat rate in the machine; its height says how coherent the family is. The engine compares the derived rate against the bearing defect frequencies to produce the categorical match feature — one robust number that gets stronger as harmonics multiply, exactly when individual lines get harder to read.
cepstrum_peak_quefrency Cepstrum peak quefrency per-axis (×3) ———
- Units
- s (seconds)
- Textbook
- Randall 2011, §3.7 + §5.7, p. 95–101 / 230–238 — Cepstrum — periodicity in the log spectrum
v5/lib/features_v5.py:552–578 :: compute_cepstrum if len(signal) < DEFAULT_SUB_WINDOW + DEFAULT_HOP:
kwargs = {"sub_window": SHORT_SUB_WINDOW, "hop": DEFAULT_HOP}
result = tachless_cot(signal, float(fs),
approximate_rpm=float(approximate_rpm), **kwargs)
except ValueError as e:
# Log the failure so NPZ-build runs leave a trail. Returning zeros is
# appropriate (downstream reads rpm_confidence=0 → unreliable), but we
# want to know if a fixture problem is producing many failed windows.
logger.warning(
"tachless_cot raised ValueError on signal len=%d, fs=%s, "
"approximate_rpm=%s: %s — returning zero RPM features",
len(signal), fs, approximate_rpm, e,
)
return out
out["rpm_estimate_hz"] = float(result.omega_bar_hz)
out["rpm_drift_pct"] = float(result.sigma_pp * 100.0)
out["rpm_is_steady"] = 1.0 if result.is_steady else 0.0
out["rpm_confidence"] = float(result.confidence)
return out
def compute_cepstrum(
signal: np.ndarray,
fs: int = CWRU_FS,
) -> tuple[np.ndarray, np.ndarray]:
"""
Compute the real cepstrum of a signal (Randall Ch. 8).
cepstrum_peak_magnitude Cepstrum peak magnitude per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Randall 2011, §3.7 + §5.7, p. 95–101 / 230–238 — Cepstrum — periodicity in the log spectrum
v5/lib/features_v5.py:552–578 :: compute_cepstrum if len(signal) < DEFAULT_SUB_WINDOW + DEFAULT_HOP:
kwargs = {"sub_window": SHORT_SUB_WINDOW, "hop": DEFAULT_HOP}
result = tachless_cot(signal, float(fs),
approximate_rpm=float(approximate_rpm), **kwargs)
except ValueError as e:
# Log the failure so NPZ-build runs leave a trail. Returning zeros is
# appropriate (downstream reads rpm_confidence=0 → unreliable), but we
# want to know if a fixture problem is producing many failed windows.
logger.warning(
"tachless_cot raised ValueError on signal len=%d, fs=%s, "
"approximate_rpm=%s: %s — returning zero RPM features",
len(signal), fs, approximate_rpm, e,
)
return out
out["rpm_estimate_hz"] = float(result.omega_bar_hz)
out["rpm_drift_pct"] = float(result.sigma_pp * 100.0)
out["rpm_is_steady"] = 1.0 if result.is_steady else 0.0
out["rpm_confidence"] = float(result.confidence)
return out
def compute_cepstrum(
signal: np.ndarray,
fs: int = CWRU_FS,
) -> tuple[np.ndarray, np.ndarray]:
"""
Compute the real cepstrum of a signal (Randall Ch. 8).
cepstrum_peak_freq Cepstrum-derived frequency per-axis (×3) ———
- Units
- Hz
- Textbook
- Randall 2011, §3.7 + §5.7, p. 95–101 / 230–238 — Cepstrum — periodicity in the log spectrum
v5/lib/features_v5.py:552–578 :: compute_cepstrum if len(signal) < DEFAULT_SUB_WINDOW + DEFAULT_HOP:
kwargs = {"sub_window": SHORT_SUB_WINDOW, "hop": DEFAULT_HOP}
result = tachless_cot(signal, float(fs),
approximate_rpm=float(approximate_rpm), **kwargs)
except ValueError as e:
# Log the failure so NPZ-build runs leave a trail. Returning zeros is
# appropriate (downstream reads rpm_confidence=0 → unreliable), but we
# want to know if a fixture problem is producing many failed windows.
logger.warning(
"tachless_cot raised ValueError on signal len=%d, fs=%s, "
"approximate_rpm=%s: %s — returning zero RPM features",
len(signal), fs, approximate_rpm, e,
)
return out
out["rpm_estimate_hz"] = float(result.omega_bar_hz)
out["rpm_drift_pct"] = float(result.sigma_pp * 100.0)
out["rpm_is_steady"] = 1.0 if result.is_steady else 0.0
out["rpm_confidence"] = float(result.confidence)
return out
def compute_cepstrum(
signal: np.ndarray,
fs: int = CWRU_FS,
) -> tuple[np.ndarray, np.ndarray]:
"""
Compute the real cepstrum of a signal (Randall Ch. 8).
cepstrum_defect_match Cepstrum defect match (categorical) per-axis (×3) ———
- Units
- categorical {0, 1, 2, 3, 4}
- Textbook
- Randall 2011, §3.7 + §5.7, p. 95–101 / 230–238 — Cepstrum — periodicity in the log spectrum
v5/lib/features_v5.py:552–578 :: compute_cepstrum if len(signal) < DEFAULT_SUB_WINDOW + DEFAULT_HOP:
kwargs = {"sub_window": SHORT_SUB_WINDOW, "hop": DEFAULT_HOP}
result = tachless_cot(signal, float(fs),
approximate_rpm=float(approximate_rpm), **kwargs)
except ValueError as e:
# Log the failure so NPZ-build runs leave a trail. Returning zeros is
# appropriate (downstream reads rpm_confidence=0 → unreliable), but we
# want to know if a fixture problem is producing many failed windows.
logger.warning(
"tachless_cot raised ValueError on signal len=%d, fs=%s, "
"approximate_rpm=%s: %s — returning zero RPM features",
len(signal), fs, approximate_rpm, e,
)
return out
out["rpm_estimate_hz"] = float(result.omega_bar_hz)
out["rpm_drift_pct"] = float(result.sigma_pp * 100.0)
out["rpm_is_steady"] = 1.0 if result.is_steady else 0.0
out["rpm_confidence"] = float(result.confidence)
return out
def compute_cepstrum(
signal: np.ndarray,
fs: int = CWRU_FS,
) -> tuple[np.ndarray, np.ndarray]:
"""
Compute the real cepstrum of a signal (Randall Ch. 8).
Spectral kurtosis + Kurtogram
5 feature definitionsAntoni's spectral kurtosis at a single STFT window (default 256, overlap 75%) and the full 4-level Fast Kurtogram. Outputs the (centre, bandwidth) pair with maximum SK, which both Group 6 and Group 8 use as the demodulation band.
Antoni & Randall 2006 (MSSP 20), Antoni 2007 (MSSP 21). Mirrors scipy.signal.stft with boundary='zeros', padded=True, window='hann' periodic.
Spectral kurtosis: WHERE in frequency to listen
Envelope analysis needs a band to demodulate, and the loudest band is usually the wrong answer — steady tones are loud but carry no ticks. Spectral kurtosis asks a sharper question of every band: is the energy here impulsive? A band that rings at every bearing impact flickers in time and scores high SK; a gear tone that hums steadily scores near zero no matter how loud it is.
The Fast Kurtogram scans window sizes 64/128/256/512 to trade frequency against time resolution, and outputs the (centre, bandwidth) pair with maximum SK. That pair becomes the demodulation band for the optimal-band envelope group — the engine's answer to "every machine rings in a different place".
sk_max Max spectral kurtosis per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
v5/lib/features_v5.py:581–623 :: compute_spectral_kurtosis Peaks in the cepstrum (called "rahmonics") indicate:
- Bearing defects: peak at 1/defect_freq (e.g., 1/BPFO)
- Gear mesh: peak at 1/mesh_freq
- Echo/reflection: peak at delay time
Returns:
(quefrency, cepstrum) — quefrency axis in seconds, cepstrum magnitudes
"""
N = len(signal)
windowed = signal * np.hanning(N)
fft_mag = np.abs(np.fft.rfft(windowed))
fft_mag = np.maximum(fft_mag, 1e-20) # avoid log(0)
log_spectrum = np.log(fft_mag)
cepstrum = np.fft.irfft(log_spectrum)
quefrency = np.arange(len(cepstrum)) / fs
return quefrency, np.abs(cepstrum)
def compute_spectral_kurtosis(
signal: np.ndarray,
fs: int = CWRU_FS,
window_size: int = 256,
overlap: int = 192,
) -> tuple[np.ndarray, np.ndarray]:
"""
Compute Spectral Kurtosis via STFT (Randall Ch. 9, Antoni 2006).
SK(f) = <|X(f)|^4> / <|X(f)|^2>^2 - 2
High SK at a frequency → impulsive/transient content (bearing impacts).
Low SK → stationary content (shaft rotation, noise).
Use SK to identify the optimal frequency band for envelope demodulation.
Convention / baseline note
--------------------------
Returns Antoni (2006) MSSP 20(2):282-307 eq. 6 baseline:
SK = E[|X|^4] / E[|X|^2]^2 - 2.
The -2 term is the complex-circular-Gaussian baseline. We feed the complex
STFT output (scipy.signal.stft) and take |Zxx|² as power. For a white-
sk_mean Mean spectral kurtosis per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
v5/lib/features_v5.py:581–623 :: compute_spectral_kurtosis Peaks in the cepstrum (called "rahmonics") indicate:
- Bearing defects: peak at 1/defect_freq (e.g., 1/BPFO)
- Gear mesh: peak at 1/mesh_freq
- Echo/reflection: peak at delay time
Returns:
(quefrency, cepstrum) — quefrency axis in seconds, cepstrum magnitudes
"""
N = len(signal)
windowed = signal * np.hanning(N)
fft_mag = np.abs(np.fft.rfft(windowed))
fft_mag = np.maximum(fft_mag, 1e-20) # avoid log(0)
log_spectrum = np.log(fft_mag)
cepstrum = np.fft.irfft(log_spectrum)
quefrency = np.arange(len(cepstrum)) / fs
return quefrency, np.abs(cepstrum)
def compute_spectral_kurtosis(
signal: np.ndarray,
fs: int = CWRU_FS,
window_size: int = 256,
overlap: int = 192,
) -> tuple[np.ndarray, np.ndarray]:
"""
Compute Spectral Kurtosis via STFT (Randall Ch. 9, Antoni 2006).
SK(f) = <|X(f)|^4> / <|X(f)|^2>^2 - 2
High SK at a frequency → impulsive/transient content (bearing impacts).
Low SK → stationary content (shaft rotation, noise).
Use SK to identify the optimal frequency band for envelope demodulation.
Convention / baseline note
--------------------------
Returns Antoni (2006) MSSP 20(2):282-307 eq. 6 baseline:
SK = E[|X|^4] / E[|X|^2]^2 - 2.
The -2 term is the complex-circular-Gaussian baseline. We feed the complex
STFT output (scipy.signal.stft) and take |Zxx|² as power. For a white-
optimal_band_center Optimal band — centre frequency per-axis (×3) ———
- Units
- Hz
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
- Visualisation
- kurtogram
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_band_bw Optimal band — bandwidth per-axis (×3) ———
- Units
- Hz
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
optimal_band_sk Optimal band — SK per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Antoni 2007, MSSP 21(1), p. 108–124 — Fast Kurtogram for optimal demodulation band selection
v5/lib/features_v5.py:736–749 :: find_optimal_demod_band # band-integrated kurtogram band width at this STFT level.
bin_spacing = float(fs) / float(win)
eff_bw = _SK_EFFECTIVE_BW_BINS * bin_spacing
if peak_sk > best_sk:
best_sk = peak_sk
best_center = peak_freq
best_bw = eff_bw
band_low = max(best_center - best_bw / 2, 50.0)
band_high = min(best_center + best_bw / 2, fs / 2 - 50.0)
return {
"center_freq": best_center,
Order spectrum analysis
23 feature definitionsTwenty-three features computed on the order spectrum (FFT magnitude rescaled by f_shaft so the abscissa becomes 'orders of shaft'). Includes shaft orders 1×–10×, defect orders at the BPFO/BPFI/BSF/FTF order-equivalents, sidebands around BPFO and BPFI, and scalar summaries (dominant order, sub-synchronous energy, 1×/2× ratio).
Randall §3.6.5. Order tracking removes RPM-drift artefacts; essential for variable-speed machines and tachless tracks (Group 13).
Orders: divide out the speed, keep the physics
Every diagnostic rate in a machine scales with shaft speed — so on a variable-speed drive, spectral peaks wander and smear, and a ±3% speed drift can spread a defect tone across a dozen FFT bins until it disappears into the floor. Order analysis rescales the frequency axis by the shaft rate: the abscissa becomes "multiples of one revolution". On that axis the physics stands still — 1× is always at order 1, and each bearing defect sits at its fixed geometric order, whatever the speed is doing.
The 23 features here read shaft orders 1×–10×, the four defect orders, their sidebands, and summary scalars (dominant order, sub-synchronous energy, 1×/2× ratio) off that speed-normalised spectrum.
order_1x_energy Order spectrum energy — order 1 per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_2x_energy Order spectrum energy — order 2 per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_3x_energy Order spectrum energy — order 3 per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_4x_energy Order spectrum energy — order 4 per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_5x_energy Order spectrum energy — order 5 per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_6x_energy Order spectrum energy — order 6 per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_7x_energy Order spectrum energy — order 7 per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_8x_energy Order spectrum energy — order 8 per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_9x_energy Order spectrum energy — order 9 per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_10x_energy Order spectrum energy — order 10 per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_energy_bpfo_order Order energy at BPFO order per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_energy_bpfi_order Order energy at BPFI order per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_energy_bsf_order Order energy at BSF order per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_energy_ftf_order Order energy at FTF order per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_sideband_bpfo Sideband energy around BPFO per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_sideband_bpfi Sideband energy around BPFI per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
dominant_order Dominant order per-axis (×3) ———
- Units
- orders
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_1x_2x_ratio 1×/2× order ratio per-axis (×3) ———
- Units
- dimensionless
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
order_subsync_energy Sub-synchronous order energy per-axis (×3) ———
- Units
- g²
- Textbook
- Randall 2011, §3.6.5, p. 85–90 — Order tracking and order spectrum
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
bpfo_order BPFO order multiplier per-axis (×3) ———
- Units
- orders
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
bpfi_order BPFI order multiplier per-axis (×3) ———
- Units
- orders
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
bsf_order BSF order multiplier per-axis (×3) ———
- Units
- orders
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
ftf_order FTF order multiplier per-axis (×3) ———
- Units
- orders
- Textbook
- Randall 2011, §5.4, p. 187–202 — Bearing fault diagnosis — defect frequencies
v5/lib/features_v5.py:1394–1496 :: extract_order_features if humidity_pct is None:
logger.warning("humidity_pct not provided; defaulting to 50.0%%")
temp_feats = extract_temperature_features(
surface_temp_c=surface_temp_c,
ambient_temp_c=ambient_temp_c if ambient_temp_c is not None else 25.0,
humidity_pct=humidity_pct if humidity_pct is not None else 50.0,
temp_history=temp_history,
)
features.update(temp_feats)
# MED observability fields (T2 follow-up). NOT in SCALAR_FEATURE_KEYS_V5
# contract — observability only. Downstream consumers (verdict engine,
# dashboards, training scripts) can inspect med_aborted/med_converged
# to know whether MED contributed to the envelope features this window.
features["med_aborted"] = _med_aborted
features["med_converged"] = _med_converged
features["med_kurt_before"] = _med_kurt_before
features["med_kurt_after"] = _med_kurt_after
features["med_n_iter"] = _med_n_iter
return features
def extract_order_features(
signal: np.ndarray,
fs: int = CWRU_FS,
rpm: float = 1800.0,
bearing_type: str = "6205",
max_order: float = 20.0,
) -> dict[str, Any]:
"""
Extract order-based features for VFD-driven motor analysis.
Unlike Hz-based features, order features are RPM-invariant —
the same fault produces the same order signature regardless of VFD speed setpoint.
Features returned:
order_1x_energy .. order_10x_energy — energy at integer shaft orders
order_energy_bpfo_order, _bpfi_order, _bsf_order, _ftf_order — defect order energies
order_sideband_bpfo, order_sideband_bpfi — modulation sideband energies
dominant_order — highest-magnitude order (excluding DC)
order_1x_2x_ratio — imbalance indicator
order_subsync_energy — sub-synchronous energy (looseness indicator)
bpfo_order, bpfi_order, bsf_order, ftf_order — defect order values for reference
Args:
signal: 1-D acceleration time series.
fs: Sampling frequency in Hz.
rpm: Shaft speed in revolutions per minute.
bearing_type: Bearing identifier key in BEARING_DB (default "6205").
max_order: Maximum order to include in analysis (default 20.0).
Returns:
Dict with order-domain features complementing extract_features().
Empty dict if rpm is non-positive.
"""
shaft_freq = rpm / 60.0
if shaft_freq <= 0:
return {}
orders, magnitudes = compute_order_spectrum(signal, fs, rpm)
# Trim to max_order for focused analysis
mask = orders <= max_order
orders_trim = orders[mask]
mags_trim = magnitudes[mask]
if len(orders_trim) == 0:
return {}
# Energy at integer orders (1x through 10x)
order_energies: dict[str, Any] = {}
for n in range(1, 11):
bw = 0.15 # ±0.15 orders bandwidth
mask_n = np.abs(orders_trim - float(n)) <= bw
order_energies[f"order_{n}x_energy"] = float(np.sum(mags_trim[mask_n] ** 2))
# Bearing defect orders (defect_freq / shaft_freq)
defect_freqs = bearing_defect_freqs(rpm, bearing_type)
# … 23 more lines truncated …
Cross-axis features
30 feature definitionsThirty features that require all three accelerometer axes (X = radial, Y = axial, Z = tangential). The first 12 are classical pairwise statistics (correlations, kurtosis ratios, RMS magnitude, energy anisotropy). The remaining 17 capture inter-axis modulation: magnitude-squared coherence at shaft + defect frequencies for all three axis pairs (15 features), the orbit ellipticity at the 1× shaft component, and the direction-of-maximum-vibration angle in the radial-tangential plane.
Wowk 1991 (orbit analysis), Randall §3.8 (coherence). Coherence uses Welch's method (nperseg=256, hann, 50% overlap, detrend='constant') — matches scipy.signal.coherence with all defaults explicit.
Three axes: the shape of the motion, not just its size
A single accelerometer axis sees a one-dimensional shadow of a three-dimensional motion. With X (radial), Y (axial) and Z (tangential) captured together, the engine can reconstruct the orbit — the path the housing actually traces. Pure imbalance drives a near-circular 1× orbit; misalignment constrains the motion into a flattened ellipse with a 2× loop; looseness draws an orbit that never repeats.
Beyond the orbit, magnitude-squared coherence between axis pairs at the shaft and defect frequencies asks whether two axes are being moved by one mechanism or several — and the direction-of-maximum-vibration angle points toward where the force enters. Thirty features that turn three projections into one 3-D picture.
cross_corr_xy Cross-correlation (X,Y) —
- Units
- dimensionless (−1..1)
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
cross_axis_full_valid Cross-axis validity sentinel New —
- Units
- boolean (0/1)
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
cross_corr_xz Cross-correlation (X,Z) —
- Units
- dimensionless (−1..1)
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
cross_corr_yz Cross-correlation (Y,Z) —
- Units
- dimensionless (−1..1)
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
kurtosis_ratio_xy Kurtosis ratio (X/Y) —
- Units
- dimensionless
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
kurtosis_ratio_xz Kurtosis ratio (X/Z) —
- Units
- dimensionless
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
kurtosis_ratio_yz Kurtosis ratio (Y/Z) —
- Units
- dimensionless
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
rms_vector_magnitude RMS vector magnitude —
- Units
- g
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
rms_ratio_xy RMS ratio (X/Y) —
- Units
- dimensionless
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
rms_ratio_xz RMS ratio (X/Z) —
- Units
- dimensionless
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
crest_max_axis Max crest factor across axes —
- Units
- dimensionless
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
energy_anisotropy Energy anisotropy —
- Units
- dimensionless (0–1)
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_mean Mean coherence across all pairs —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_shaft_xy Coherence at SHAFT (XY) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_shaft_xz Coherence at SHAFT (XZ) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_shaft_yz Coherence at SHAFT (YZ) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_bpfo_xy Coherence at BPFO (XY) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_bpfo_xz Coherence at BPFO (XZ) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_bpfo_yz Coherence at BPFO (YZ) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_bpfi_xy Coherence at BPFI (XY) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_bpfi_xz Coherence at BPFI (XZ) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_bpfi_yz Coherence at BPFI (YZ) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_bsf_xy Coherence at BSF (XY) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_bsf_xz Coherence at BSF (XZ) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_bsf_yz Coherence at BSF (YZ) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_ftf_xy Coherence at FTF (XY) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_ftf_xz Coherence at FTF (XZ) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
coherence_at_ftf_yz Coherence at FTF (YZ) —
- Units
- dimensionless (0–1)
- Textbook
- Randall 2011, §3.6, p. 78–95 — Frequency-domain analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
orbit_ellipticity Orbit ellipticity —
- Units
- dimensionless (0–1)
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
direction_of_max_vibration Direction of max vibration —
- Units
- radians (0..π/2 for non-negative RMS)
- Textbook
- Wowk 1991, §4 + §8 — Machinery Vibration: Measurement and Analysis — cross-axis orbit analysis
v5/lib/features_v5.py:1653–1868 :: compute_cross_axis_features# Canonical cross-axis feature keys (29 total)
CROSS_AXIS_KEYS: list[str] = [
# Existing 12
"cross_corr_xy", "cross_corr_xz", "cross_corr_yz",
"kurtosis_ratio_xy", "kurtosis_ratio_xz", "kurtosis_ratio_yz",
"rms_vector_magnitude", "rms_ratio_xy", "rms_ratio_xz",
"crest_max_axis", "energy_anisotropy", "coherence_mean",
# New 17 — Design specification(b), 10, 15
"coherence_at_shaft_xy", "coherence_at_shaft_xz", "coherence_at_shaft_yz",
"coherence_at_bpfo_xy", "coherence_at_bpfo_xz", "coherence_at_bpfo_yz",
"coherence_at_bpfi_xy", "coherence_at_bpfi_xz", "coherence_at_bpfi_yz",
"coherence_at_bsf_xy", "coherence_at_bsf_xz", "coherence_at_bsf_yz",
"coherence_at_ftf_xy", "coherence_at_ftf_xz", "coherence_at_ftf_yz",
"orbit_ellipticity",
"direction_of_max_vibration",
# sentinel — 1.0 when triaxial AND RPM known (so coherence-at-defect-freq
# and orbit features were actually computed), 0.0 otherwise. Lets the model
# treat single-axis-tiled or no-RPM windows differently from real isotropic
# signals. Per internal coding rule.
"cross_axis_full_valid",
]
def compute_cross_axis_features(
seg_x: np.ndarray,
seg_y: np.ndarray,
seg_z: np.ndarray,
fs: int = CWRU_FS,
rpm: float | None = None,
bearing_type: str = "6205",
) -> dict[str, float]:
"""Compute 29 cross-axis features from triaxial vibration segments.
x = radial, y = axial, z = tangential.
For single-axis datasets, pass zeros for y and z — features default to 0.
Features:
Existing 12: correlations, kurtosis ratios, RMS magnitude/ratios,
crest max, energy anisotropy, broadband coherence mean.
New 17 (patent-required):
- Coherence at shaft frequency (3 axis pairs) — Claim 15
- Coherence at 4 defect frequencies × 3 pairs = 12 — Claim 15
- Orbit ellipticity (minor/major from 2 radial axes at 1x) — Claim 4(b), 15
- Direction of maximum vibration (angle in measurement plane) — Claim 4(b), 15
"""
from scipy.stats import kurtosis as sp_kurtosis
result = {k: 0.0 for k in CROSS_AXIS_KEYS}
has_y = np.any(seg_y != 0)
has_z = np.any(seg_z != 0)
if not has_y and not has_z:
return result
eps = 1e-12
# ── RMS per axis ──
rms_x = float(np.sqrt(np.mean(seg_x**2))) + eps
rms_y = float(np.sqrt(np.mean(seg_y**2))) + eps
rms_z = float(np.sqrt(np.mean(seg_z**2))) + eps
# ── Cross-correlations (Pearson) ──
def _corr(a: np.ndarray, b: np.ndarray) -> float:
if np.std(a) < 1e-10 or np.std(b) < 1e-10:
return 0.0
return float(np.corrcoef(a, b)[0, 1])
result["cross_corr_xy"] = _corr(seg_x, seg_y)
result["cross_corr_xz"] = _corr(seg_x, seg_z)
result["cross_corr_yz"] = _corr(seg_y, seg_z)
# ── Kurtosis ratios (Pearson convention, Gaussian=3.0 — matches extract_features) ──
kurt_x = float(sp_kurtosis(seg_x, fisher=False)) if len(seg_x) > 4 else 3.0
kurt_y = float(sp_kurtosis(seg_y, fisher=False)) if has_y and len(seg_y) > 4 else 3.0
kurt_z = float(sp_kurtosis(seg_z, fisher=False)) if has_z and len(seg_z) > 4 else 3.0
result["kurtosis_ratio_xy"] = kurt_x / (abs(kurt_y) + 1e-8)
result["kurtosis_ratio_xz"] = kurt_x / (abs(kurt_z) + 1e-8)
result["kurtosis_ratio_yz"] = kurt_y / (abs(kurt_z) + 1e-8)
# ── RMS vector magnitude and ratios ──
# … 136 more lines truncated …
Ultrasonic features
17 feature definitionsSeventeen features computed on the high-frequency microphone channel (IMP23ABSU on Pro/Rail tiers, 192 kHz PDM decimated to ~100 kHz PCM). Bands: bearing 20–60 kHz, leak 40–100 kHz, electrical 80–100 kHz. **Not computable from MAFAULDA — that dataset has no ultrasonic channel; values reported as N/A.**
ISO 18436-8 (acoustic emission practice). KALTECH band definitions match SDT/UE Systems convention for industrial AE.
Ultrasound: hearing trouble before it shakes
Metal-on-metal friction, electrical discharge and turbulent leaks all emit energy in the tens of kilohertz — far above where vibration features look, and typically before the fault grows heavy enough to shake the structure. The microphone channel is banded by mechanism: bearing friction 20–60 kHz, leaks 40–100 kHz, electrical activity 80–100 kHz.
Two tricks make it usable. Peak-hold: a 2 ms crackle burst drowns in a one-second RMS, so the engine also keeps the maximum 100 ms RMS — it catches the burst and holds it. Steadiness: a leak is a continuous hiss, bearing crackle is intermittent — the steadiness flag plus the baseline-delta in dB separates "flow" from "impact" and "new" from "normal for this machine".
us_rms_overall Overall ultrasonic RMS N/A on MAFAULDA —
- Units
- dBµV
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_rms_bearing Ultrasonic RMS — bearing band N/A on MAFAULDA —
- Units
- dBµV
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_rms_leak Ultrasonic RMS — leak band N/A on MAFAULDA —
- Units
- dBµV
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_rms_electrical Ultrasonic RMS — electrical band N/A on MAFAULDA —
- Units
- dBµV
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_peak Ultrasonic peak N/A on MAFAULDA —
- Units
- dBµV
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_crest_factor Ultrasonic crest factor N/A on MAFAULDA —
- Units
- dimensionless
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_baseline_delta_dB Baseline delta (dB) N/A on MAFAULDA —
- Units
- dB
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_spectral_flatness Ultrasonic spectral flatness N/A on MAFAULDA —
- Units
- dimensionless (0–1)
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_dominant_freq Ultrasonic dominant frequency N/A on MAFAULDA —
- Units
- Hz
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_is_steady Ultrasonic steady flag N/A on MAFAULDA —
- Units
- boolean (0/1)
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_max_rms Max windowed RMS N/A on MAFAULDA —
- Units
- dBµV
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_rms_bearing_band RMS — bearing band (linear) N/A on MAFAULDA —
- Units
- g (mic equivalent)
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_rms_leak_band RMS — leak band (linear) N/A on MAFAULDA —
- Units
- g (mic equivalent)
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_rms_electrical_band RMS — electrical band (linear) N/A on MAFAULDA —
- Units
- g (mic equivalent)
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_kurtosis Ultrasonic kurtosis N/A on MAFAULDA —
- Units
- dimensionless
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_steadiness Ultrasonic steadiness score N/A on MAFAULDA —
- Units
- dimensionless (0–1)
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
us_max_rms_100ms Max 100 ms RMS N/A on MAFAULDA —
- Units
- dBµV
- Textbook
- ISO 18436-2/8 — Vibration analyst categories; ultrasonic AE practice
v5/lib/features_v5.py:1907–2118 :: extract_ultrasonic_features if high_hz >= nyq or low_hz >= nyq or low_hz >= high_hz:
_logging.getLogger("kaltech.features").warning(
"_bandpass_rms: band [%.0f, %.0f] Hz not representable at fs=%d Hz "
"(Nyquist=%.0f Hz). Returning 0.0.",
low_hz, high_hz, fs, nyq,
)
return 0.0
low_n = max(low_hz / nyq, 0.001)
high_n = min(high_hz / nyq, 0.999)
if high_n <= low_n:
return 0.0
b, a = butter(4, [low_n, high_n], btype="band")
filtered = filtfilt(b, a, signal)
return float(np.sqrt(np.mean(filtered ** 2)))
def _rms_to_dbuv(rms: float, ref: float = 1e-6) -> float:
"""Convert RMS amplitude to dBuV (decibels relative to 1 microvolt)."""
if not np.isfinite(rms) or rms <= 0:
return -999.0
return float(20.0 * np.log10(rms / ref))
def extract_ultrasonic_features(
us_signal: np.ndarray,
fs_us: int = 192000,
baseline_rms_dbuv: float | None = None,
) -> dict[str, Any]:
"""
Extract 11 ultrasonic condition monitoring features from IMP23ABSU signal.
Standards: ISO 29821:2018, NASA bearing research, SDT 4CI methodology.
Sensor: IMP23ABSU (100 Hz – 80 kHz airborne MEMS mic, ~$1.50).
Frequency bands:
20–80 kHz overall ultrasonic
25–40 kHz bearing friction / lubrication quality
38–42 kHz compressed air leak detection (ISO 50001)
30–50 kHz electrical discharge / arcing / corona
Alarm thresholds (relative to per-machine baseline):
+8 dB lubrication needed (pre-failure)
+12 dB beginning of failure mode
+16 dB bearing damage confirmed
+35 dB catastrophic failure imminent
Args:
us_signal: Raw signal from IMP23ABSU.
fs_us: Sampling rate (192 kHz for full 80 kHz BW).
baseline_rms_dbuv: Machine's baseline RMS in dBuV (25–40 kHz band).
None if no baseline established yet.
Returns:
Dict with 11 features (us_* prefix).
"""
import logging as _logging
_us_log = _logging.getLogger("kaltech.features.ultrasonic")
nyq = fs_us / 2.0
# --- Input validation ---
_MIN_FILTER_SAMPLES = 27 # filtfilt min for 4th-order Butterworth
if len(us_signal) == 0:
raise ValueError("extract_ultrasonic_features: empty signal")
if len(us_signal) < _MIN_FILTER_SAMPLES:
raise ValueError(
f"extract_ultrasonic_features: signal length {len(us_signal)} < "
f"{_MIN_FILTER_SAMPLES} (minimum for bandpass filtering)"
)
if nyq <= 40000:
raise ValueError(
f"extract_ultrasonic_features: fs_us={fs_us} Hz (Nyquist={nyq:.0f} Hz) "
f"is too low for ultrasonic band (need Nyquist > 40 kHz). "
f"Wrong sample rate passed?"
)
if np.all(np.isnan(us_signal)):
raise ValueError(
"extract_ultrasonic_features: signal is all NaN — sensor failure"
)
if np.all(us_signal == 0.0):
# … 132 more lines truncated …
Temperature features
6 feature definitionsSix features from the surface temperature, ambient temperature, humidity, and their time histories. ISO 14224 / API 670 / UIC 518 converge on the same thresholds: ΔT > 50 °C warns, ΔT > 90 °C alarms; rate-of-rise > 2 °C/min escalates one tier. **N/A for MAFAULDA — accelerometer dataset, no temperature channel.**
ISO 14224 (O&G reliability), API 670 (machinery protection), UIC 518 (railway hot-box), SKF/CSI/B&K CM guidance all converge on these thresholds.
Temperature: the late witness that closes the case
Friction ends as heat — always. Temperature is therefore the most trustworthy and the latest of all condition signals: by the time a bearing housing is hot, the vibration groups above have usually been arguing about it for weeks. The engine uses ΔT = surface − ambient (so a hot afternoon doesn't page anyone) against the converged industrial thresholds: 50 °C warn, 90 °C alarm, and escalates a tier when the rate-of-rise exceeds 2 °C/min — a lubrication collapse heats fast long before it crosses the absolute line.
Its diagnostic power is corroboration: vibration says "outer race", rising ΔT says "and it is now consuming energy" — together they justify urgency.
surface_temp_c Surface temperature N/A on MAFAULDA —
- Units
- °C
- Textbook
- ISO 14224 + API 670 + UIC 518 — Bearing thermal protection — multi-standard threshold convergence
v5/lib/features_v5.py:2125–2201 :: extract_temperature_features "us_rms_leak": us_rms_leak,
"us_rms_electrical": us_rms_electrical,
"us_peak": us_peak,
"us_crest_factor": us_crest_factor,
"us_baseline_delta_dB": us_baseline_delta_dB,
"us_spectral_flatness": us_spectral_flatness,
"us_dominant_freq": us_dominant_freq,
"us_is_steady": us_is_steady,
"us_max_rms": us_max_rms,
# Additional features for 67-feature vector
"us_rms_bearing_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 25000, 35000)),
"us_rms_leak_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 36000, 42000)),
"us_rms_electrical_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 43000, 60000)),
"us_kurtosis": us_kurtosis,
"us_steadiness": us_steadiness,
"us_max_rms_100ms": _rms_to_dbuv(us_max_rms_raw),
}
# ---------------------------------------------------------------------------
# Temperature Feature Extraction (STTS22H / SHT45 / NTC)
# ---------------------------------------------------------------------------
def extract_temperature_features(
surface_temp_c: float,
ambient_temp_c: float,
humidity_pct: float,
temp_history: list[float] | None = None,
rms_history: list[float] | None = None,
) -> dict[str, Any]:
"""
Extract 6 temperature condition monitoring features.
Sensors: STTS22H (surface), SHT45 (ambient + humidity), NTC (hot axle).
Features:
surface_temp_c — direct surface reading (degC)
ambient_temp_c — ambient reading (degC)
delta_t — surface - ambient (>40C = hot axle alert)
humidity_pct — relative humidity (>80% = condensation risk)
temp_rate_of_rise — degC/min from recent temp_history (0.0 if no history)
temp_vibration_correlation — Pearson correlation between temp and RMS histories
(Claim 10: concurrent positive = friction-driven degradation)
Args:
surface_temp_c: Surface temperature from STTS22H/NTC (degC).
ambient_temp_c: Ambient temperature from SHT45 (degC).
humidity_pct: Relative humidity from SHT45 (0-100%).
temp_history: List of recent surface temps at 1-minute intervals,
most recent last. If None or too short, rate_of_rise = 0.
rms_history: List of recent vibration RMS values (same interval as temp_history).
Used to compute temperature-vibration correlation (Claim 10).
Returns:
Dict with 6 temperature features.
"""
if not math.isfinite(surface_temp_c):
logger.warning("surface_temp_c=%s is not finite — possible sensor failure", surface_temp_c)
surface_temp_c = 0.0
if not math.isfinite(ambient_temp_c):
logger.warning("ambient_temp_c=%s is not finite — possible sensor failure", ambient_temp_c)
ambient_temp_c = 25.0
delta_t = surface_temp_c - ambient_temp_c
# Rate of rise: linear regression slope over temp_history (degC/min)
temp_rate_of_rise = 0.0
if temp_history and len(temp_history) >= 2:
n = len(temp_history)
x = np.arange(n, dtype=np.float64)
y = np.array(temp_history, dtype=np.float64)
x_mean = np.mean(x)
y_mean = np.mean(y)
cov_xy = np.sum((x - x_mean) * (y - y_mean))
var_x = np.sum((x - x_mean) ** 2)
if var_x > 0:
temp_rate_of_rise = float(cov_xy / var_x)
ambient_temp_c Ambient temperature N/A on MAFAULDA —
- Units
- °C
- Textbook
- ISO 14224 + API 670 + UIC 518 — Bearing thermal protection — multi-standard threshold convergence
v5/lib/features_v5.py:2125–2201 :: extract_temperature_features "us_rms_leak": us_rms_leak,
"us_rms_electrical": us_rms_electrical,
"us_peak": us_peak,
"us_crest_factor": us_crest_factor,
"us_baseline_delta_dB": us_baseline_delta_dB,
"us_spectral_flatness": us_spectral_flatness,
"us_dominant_freq": us_dominant_freq,
"us_is_steady": us_is_steady,
"us_max_rms": us_max_rms,
# Additional features for 67-feature vector
"us_rms_bearing_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 25000, 35000)),
"us_rms_leak_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 36000, 42000)),
"us_rms_electrical_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 43000, 60000)),
"us_kurtosis": us_kurtosis,
"us_steadiness": us_steadiness,
"us_max_rms_100ms": _rms_to_dbuv(us_max_rms_raw),
}
# ---------------------------------------------------------------------------
# Temperature Feature Extraction (STTS22H / SHT45 / NTC)
# ---------------------------------------------------------------------------
def extract_temperature_features(
surface_temp_c: float,
ambient_temp_c: float,
humidity_pct: float,
temp_history: list[float] | None = None,
rms_history: list[float] | None = None,
) -> dict[str, Any]:
"""
Extract 6 temperature condition monitoring features.
Sensors: STTS22H (surface), SHT45 (ambient + humidity), NTC (hot axle).
Features:
surface_temp_c — direct surface reading (degC)
ambient_temp_c — ambient reading (degC)
delta_t — surface - ambient (>40C = hot axle alert)
humidity_pct — relative humidity (>80% = condensation risk)
temp_rate_of_rise — degC/min from recent temp_history (0.0 if no history)
temp_vibration_correlation — Pearson correlation between temp and RMS histories
(Claim 10: concurrent positive = friction-driven degradation)
Args:
surface_temp_c: Surface temperature from STTS22H/NTC (degC).
ambient_temp_c: Ambient temperature from SHT45 (degC).
humidity_pct: Relative humidity from SHT45 (0-100%).
temp_history: List of recent surface temps at 1-minute intervals,
most recent last. If None or too short, rate_of_rise = 0.
rms_history: List of recent vibration RMS values (same interval as temp_history).
Used to compute temperature-vibration correlation (Claim 10).
Returns:
Dict with 6 temperature features.
"""
if not math.isfinite(surface_temp_c):
logger.warning("surface_temp_c=%s is not finite — possible sensor failure", surface_temp_c)
surface_temp_c = 0.0
if not math.isfinite(ambient_temp_c):
logger.warning("ambient_temp_c=%s is not finite — possible sensor failure", ambient_temp_c)
ambient_temp_c = 25.0
delta_t = surface_temp_c - ambient_temp_c
# Rate of rise: linear regression slope over temp_history (degC/min)
temp_rate_of_rise = 0.0
if temp_history and len(temp_history) >= 2:
n = len(temp_history)
x = np.arange(n, dtype=np.float64)
y = np.array(temp_history, dtype=np.float64)
x_mean = np.mean(x)
y_mean = np.mean(y)
cov_xy = np.sum((x - x_mean) * (y - y_mean))
var_x = np.sum((x - x_mean) ** 2)
if var_x > 0:
temp_rate_of_rise = float(cov_xy / var_x)
delta_t Temperature excess N/A on MAFAULDA —
- Units
- °C
- Textbook
- ISO 14224 + API 670 + UIC 518 — Bearing thermal protection — multi-standard threshold convergence
v5/lib/features_v5.py:2125–2201 :: extract_temperature_features "us_rms_leak": us_rms_leak,
"us_rms_electrical": us_rms_electrical,
"us_peak": us_peak,
"us_crest_factor": us_crest_factor,
"us_baseline_delta_dB": us_baseline_delta_dB,
"us_spectral_flatness": us_spectral_flatness,
"us_dominant_freq": us_dominant_freq,
"us_is_steady": us_is_steady,
"us_max_rms": us_max_rms,
# Additional features for 67-feature vector
"us_rms_bearing_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 25000, 35000)),
"us_rms_leak_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 36000, 42000)),
"us_rms_electrical_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 43000, 60000)),
"us_kurtosis": us_kurtosis,
"us_steadiness": us_steadiness,
"us_max_rms_100ms": _rms_to_dbuv(us_max_rms_raw),
}
# ---------------------------------------------------------------------------
# Temperature Feature Extraction (STTS22H / SHT45 / NTC)
# ---------------------------------------------------------------------------
def extract_temperature_features(
surface_temp_c: float,
ambient_temp_c: float,
humidity_pct: float,
temp_history: list[float] | None = None,
rms_history: list[float] | None = None,
) -> dict[str, Any]:
"""
Extract 6 temperature condition monitoring features.
Sensors: STTS22H (surface), SHT45 (ambient + humidity), NTC (hot axle).
Features:
surface_temp_c — direct surface reading (degC)
ambient_temp_c — ambient reading (degC)
delta_t — surface - ambient (>40C = hot axle alert)
humidity_pct — relative humidity (>80% = condensation risk)
temp_rate_of_rise — degC/min from recent temp_history (0.0 if no history)
temp_vibration_correlation — Pearson correlation between temp and RMS histories
(Claim 10: concurrent positive = friction-driven degradation)
Args:
surface_temp_c: Surface temperature from STTS22H/NTC (degC).
ambient_temp_c: Ambient temperature from SHT45 (degC).
humidity_pct: Relative humidity from SHT45 (0-100%).
temp_history: List of recent surface temps at 1-minute intervals,
most recent last. If None or too short, rate_of_rise = 0.
rms_history: List of recent vibration RMS values (same interval as temp_history).
Used to compute temperature-vibration correlation (Claim 10).
Returns:
Dict with 6 temperature features.
"""
if not math.isfinite(surface_temp_c):
logger.warning("surface_temp_c=%s is not finite — possible sensor failure", surface_temp_c)
surface_temp_c = 0.0
if not math.isfinite(ambient_temp_c):
logger.warning("ambient_temp_c=%s is not finite — possible sensor failure", ambient_temp_c)
ambient_temp_c = 25.0
delta_t = surface_temp_c - ambient_temp_c
# Rate of rise: linear regression slope over temp_history (degC/min)
temp_rate_of_rise = 0.0
if temp_history and len(temp_history) >= 2:
n = len(temp_history)
x = np.arange(n, dtype=np.float64)
y = np.array(temp_history, dtype=np.float64)
x_mean = np.mean(x)
y_mean = np.mean(y)
cov_xy = np.sum((x - x_mean) * (y - y_mean))
var_x = np.sum((x - x_mean) ** 2)
if var_x > 0:
temp_rate_of_rise = float(cov_xy / var_x)
humidity_pct Relative humidity N/A on MAFAULDA —
- Units
- percent
- Textbook
- ISO 14224 + API 670 + UIC 518 — Bearing thermal protection — multi-standard threshold convergence
v5/lib/features_v5.py:2125–2201 :: extract_temperature_features "us_rms_leak": us_rms_leak,
"us_rms_electrical": us_rms_electrical,
"us_peak": us_peak,
"us_crest_factor": us_crest_factor,
"us_baseline_delta_dB": us_baseline_delta_dB,
"us_spectral_flatness": us_spectral_flatness,
"us_dominant_freq": us_dominant_freq,
"us_is_steady": us_is_steady,
"us_max_rms": us_max_rms,
# Additional features for 67-feature vector
"us_rms_bearing_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 25000, 35000)),
"us_rms_leak_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 36000, 42000)),
"us_rms_electrical_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 43000, 60000)),
"us_kurtosis": us_kurtosis,
"us_steadiness": us_steadiness,
"us_max_rms_100ms": _rms_to_dbuv(us_max_rms_raw),
}
# ---------------------------------------------------------------------------
# Temperature Feature Extraction (STTS22H / SHT45 / NTC)
# ---------------------------------------------------------------------------
def extract_temperature_features(
surface_temp_c: float,
ambient_temp_c: float,
humidity_pct: float,
temp_history: list[float] | None = None,
rms_history: list[float] | None = None,
) -> dict[str, Any]:
"""
Extract 6 temperature condition monitoring features.
Sensors: STTS22H (surface), SHT45 (ambient + humidity), NTC (hot axle).
Features:
surface_temp_c — direct surface reading (degC)
ambient_temp_c — ambient reading (degC)
delta_t — surface - ambient (>40C = hot axle alert)
humidity_pct — relative humidity (>80% = condensation risk)
temp_rate_of_rise — degC/min from recent temp_history (0.0 if no history)
temp_vibration_correlation — Pearson correlation between temp and RMS histories
(Claim 10: concurrent positive = friction-driven degradation)
Args:
surface_temp_c: Surface temperature from STTS22H/NTC (degC).
ambient_temp_c: Ambient temperature from SHT45 (degC).
humidity_pct: Relative humidity from SHT45 (0-100%).
temp_history: List of recent surface temps at 1-minute intervals,
most recent last. If None or too short, rate_of_rise = 0.
rms_history: List of recent vibration RMS values (same interval as temp_history).
Used to compute temperature-vibration correlation (Claim 10).
Returns:
Dict with 6 temperature features.
"""
if not math.isfinite(surface_temp_c):
logger.warning("surface_temp_c=%s is not finite — possible sensor failure", surface_temp_c)
surface_temp_c = 0.0
if not math.isfinite(ambient_temp_c):
logger.warning("ambient_temp_c=%s is not finite — possible sensor failure", ambient_temp_c)
ambient_temp_c = 25.0
delta_t = surface_temp_c - ambient_temp_c
# Rate of rise: linear regression slope over temp_history (degC/min)
temp_rate_of_rise = 0.0
if temp_history and len(temp_history) >= 2:
n = len(temp_history)
x = np.arange(n, dtype=np.float64)
y = np.array(temp_history, dtype=np.float64)
x_mean = np.mean(x)
y_mean = np.mean(y)
cov_xy = np.sum((x - x_mean) * (y - y_mean))
var_x = np.sum((x - x_mean) ** 2)
if var_x > 0:
temp_rate_of_rise = float(cov_xy / var_x)
temp_rate_of_rise Temperature rate of rise N/A on MAFAULDA —
- Units
- °C/min
- Textbook
- ISO 14224 + API 670 + UIC 518 — Bearing thermal protection — multi-standard threshold convergence
v5/lib/features_v5.py:2125–2201 :: extract_temperature_features "us_rms_leak": us_rms_leak,
"us_rms_electrical": us_rms_electrical,
"us_peak": us_peak,
"us_crest_factor": us_crest_factor,
"us_baseline_delta_dB": us_baseline_delta_dB,
"us_spectral_flatness": us_spectral_flatness,
"us_dominant_freq": us_dominant_freq,
"us_is_steady": us_is_steady,
"us_max_rms": us_max_rms,
# Additional features for 67-feature vector
"us_rms_bearing_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 25000, 35000)),
"us_rms_leak_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 36000, 42000)),
"us_rms_electrical_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 43000, 60000)),
"us_kurtosis": us_kurtosis,
"us_steadiness": us_steadiness,
"us_max_rms_100ms": _rms_to_dbuv(us_max_rms_raw),
}
# ---------------------------------------------------------------------------
# Temperature Feature Extraction (STTS22H / SHT45 / NTC)
# ---------------------------------------------------------------------------
def extract_temperature_features(
surface_temp_c: float,
ambient_temp_c: float,
humidity_pct: float,
temp_history: list[float] | None = None,
rms_history: list[float] | None = None,
) -> dict[str, Any]:
"""
Extract 6 temperature condition monitoring features.
Sensors: STTS22H (surface), SHT45 (ambient + humidity), NTC (hot axle).
Features:
surface_temp_c — direct surface reading (degC)
ambient_temp_c — ambient reading (degC)
delta_t — surface - ambient (>40C = hot axle alert)
humidity_pct — relative humidity (>80% = condensation risk)
temp_rate_of_rise — degC/min from recent temp_history (0.0 if no history)
temp_vibration_correlation — Pearson correlation between temp and RMS histories
(Claim 10: concurrent positive = friction-driven degradation)
Args:
surface_temp_c: Surface temperature from STTS22H/NTC (degC).
ambient_temp_c: Ambient temperature from SHT45 (degC).
humidity_pct: Relative humidity from SHT45 (0-100%).
temp_history: List of recent surface temps at 1-minute intervals,
most recent last. If None or too short, rate_of_rise = 0.
rms_history: List of recent vibration RMS values (same interval as temp_history).
Used to compute temperature-vibration correlation (Claim 10).
Returns:
Dict with 6 temperature features.
"""
if not math.isfinite(surface_temp_c):
logger.warning("surface_temp_c=%s is not finite — possible sensor failure", surface_temp_c)
surface_temp_c = 0.0
if not math.isfinite(ambient_temp_c):
logger.warning("ambient_temp_c=%s is not finite — possible sensor failure", ambient_temp_c)
ambient_temp_c = 25.0
delta_t = surface_temp_c - ambient_temp_c
# Rate of rise: linear regression slope over temp_history (degC/min)
temp_rate_of_rise = 0.0
if temp_history and len(temp_history) >= 2:
n = len(temp_history)
x = np.arange(n, dtype=np.float64)
y = np.array(temp_history, dtype=np.float64)
x_mean = np.mean(x)
y_mean = np.mean(y)
cov_xy = np.sum((x - x_mean) * (y - y_mean))
var_x = np.sum((x - x_mean) ** 2)
if var_x > 0:
temp_rate_of_rise = float(cov_xy / var_x)
temp_vibration_correlation Temperature–vibration correlation N/A on MAFAULDA —
- Units
- dimensionless (−1..1)
- Textbook
- ISO 14224 + API 670 + UIC 518 — Bearing thermal protection — multi-standard threshold convergence
v5/lib/features_v5.py:2125–2201 :: extract_temperature_features "us_rms_leak": us_rms_leak,
"us_rms_electrical": us_rms_electrical,
"us_peak": us_peak,
"us_crest_factor": us_crest_factor,
"us_baseline_delta_dB": us_baseline_delta_dB,
"us_spectral_flatness": us_spectral_flatness,
"us_dominant_freq": us_dominant_freq,
"us_is_steady": us_is_steady,
"us_max_rms": us_max_rms,
# Additional features for 67-feature vector
"us_rms_bearing_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 25000, 35000)),
"us_rms_leak_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 36000, 42000)),
"us_rms_electrical_band": _rms_to_dbuv(_bandpass_rms(us_signal, fs_us, 43000, 60000)),
"us_kurtosis": us_kurtosis,
"us_steadiness": us_steadiness,
"us_max_rms_100ms": _rms_to_dbuv(us_max_rms_raw),
}
# ---------------------------------------------------------------------------
# Temperature Feature Extraction (STTS22H / SHT45 / NTC)
# ---------------------------------------------------------------------------
def extract_temperature_features(
surface_temp_c: float,
ambient_temp_c: float,
humidity_pct: float,
temp_history: list[float] | None = None,
rms_history: list[float] | None = None,
) -> dict[str, Any]:
"""
Extract 6 temperature condition monitoring features.
Sensors: STTS22H (surface), SHT45 (ambient + humidity), NTC (hot axle).
Features:
surface_temp_c — direct surface reading (degC)
ambient_temp_c — ambient reading (degC)
delta_t — surface - ambient (>40C = hot axle alert)
humidity_pct — relative humidity (>80% = condensation risk)
temp_rate_of_rise — degC/min from recent temp_history (0.0 if no history)
temp_vibration_correlation — Pearson correlation between temp and RMS histories
(Claim 10: concurrent positive = friction-driven degradation)
Args:
surface_temp_c: Surface temperature from STTS22H/NTC (degC).
ambient_temp_c: Ambient temperature from SHT45 (degC).
humidity_pct: Relative humidity from SHT45 (0-100%).
temp_history: List of recent surface temps at 1-minute intervals,
most recent last. If None or too short, rate_of_rise = 0.
rms_history: List of recent vibration RMS values (same interval as temp_history).
Used to compute temperature-vibration correlation (Claim 10).
Returns:
Dict with 6 temperature features.
"""
if not math.isfinite(surface_temp_c):
logger.warning("surface_temp_c=%s is not finite — possible sensor failure", surface_temp_c)
surface_temp_c = 0.0
if not math.isfinite(ambient_temp_c):
logger.warning("ambient_temp_c=%s is not finite — possible sensor failure", ambient_temp_c)
ambient_temp_c = 25.0
delta_t = surface_temp_c - ambient_temp_c
# Rate of rise: linear regression slope over temp_history (degC/min)
temp_rate_of_rise = 0.0
if temp_history and len(temp_history) >= 2:
n = len(temp_history)
x = np.arange(n, dtype=np.float64)
y = np.array(temp_history, dtype=np.float64)
x_mean = np.mean(x)
y_mean = np.mean(y)
cov_xy = np.sum((x - x_mean) * (y - y_mean))
var_x = np.sum((x - x_mean) ** 2)
if var_x > 0:
temp_rate_of_rise = float(cov_xy / var_x)