Part VIII: Industrial, Energy, and Infrastructure Sensor AI
Chapter 37: Condition Monitoring and Anomaly Detection

Domain shift across machines and sites

"No two pumps are identical twins. They are cousins who grew up in different climates, and one of them is about to insist the other is broken."

A Fleet-Weary AI Agent

The Big Picture

Section 37.3 met domain shift as the DCASE challenge frames it: survive a change of operating point you were warned about, then generalize to a machine type you have never heard. This section widens the lens from the benchmark to the fleet. In a real deployment you rarely monitor one machine; you monitor a hundred nominally-identical assets scattered across a dozen sites, and the detector you trained on the pilot unit in Plant A must earn its keep on unit 74 in Plant G without a data scientist flying out to retune it. The uncomfortable truth is that "identical" machines are not identical to a sensor. Casting tolerances, bearing lots, foundation stiffness, sensor mounting torque, ambient temperature, mains frequency, and the sampling clock of the edge box all shift the signal, and every one of those shifts moves the healthy baseline. A detector that hard-codes Plant A's baseline will drown Plant G in false alarms while missing its real faults. This section gives you a taxonomy of where the shift comes from, the vocabulary to name it (covariate versus concept shift), a quantitative meter to measure how far two machines are apart before you trust a shared model, and the fleet-level tactics (per-unit baselining, regime conditioning, domain-invariant features, cohorting) that make one detector portable across the whole population.

This section builds on the leakage-safe splitting discipline of Chapter 5 and the physical origin of measurement variation from Chapter 2. The general theory of distribution shift and test-time adaptation lives in Chapter 66; here we specialize it to condition monitoring, where the shift is between physical assets and the label of interest (healthy versus faulty) is exactly the thing that must stay stable.

Where the shift comes from: a taxonomy for fleets

Debugging a portability failure starts with knowing which knob moved. Cross-machine and cross-site shift decomposes into a small number of physically distinct sources, and the right fix depends entirely on which one you are facing.

The first two are largely static per unit and can be absorbed by a per-unit baseline. The operating regime is dynamic and must be conditioned on, not averaged away. The last is a pipeline problem masquerading as a machine problem, and it is the one that most often gets misdiagnosed as a fleet of failing assets.

Naming the shift: covariate versus concept

Two failure modes hide behind the phrase "domain shift," and they demand opposite responses. Write the healthy input distribution on machine \(m\) as \(P_m(\mathbf{x})\) and the decision you care about as the conditional \(P_m(y \mid \mathbf{x})\), where \(y\) is healthy versus faulty. Covariate shift means \(P_m(\mathbf{x})\) changes across machines while the fault-versus-healthy boundary \(P(y \mid \mathbf{x})\) is stable: the pump sounds different but the meaning of "different from its own normal" transfers. This is the benign, tractable case, and per-unit normalization is designed for it. Concept shift means \(P_m(y \mid \mathbf{x})\) itself changes: the very same spectral signature is healthy on a variable-speed drive and a symptom of cavitation on a fixed-speed one. No amount of input normalization rescues you from concept shift, because the label function moved. The practical rule follows immediately: normalize away covariate shift aggressively, but detect concept shift and refuse to share a model across the boundary where it occurs.

Key Insight

An anomaly detector does not need the healthy distributions of two machines to match. It needs each machine's anomaly boundary to sit in the same place relative to that machine's own normal. This is why per-unit baselining beats fleet-wide pooling so often: a score defined as distance from the unit's own reference bank is invariant to any static covariate shift by construction, whereas a single global threshold inherits every unit-to-unit offset as false-alarm noise. Portability is not about erasing differences between machines; it is about making the score speak each machine's dialect while the threshold speaks a shared language.

Measuring how far two machines are apart

Before you deploy Plant A's model to Plant G, quantify the gap. A cheap, model-agnostic shift meter is a domain classifier: train a simple model to tell machine A's healthy clips from machine G's healthy clips. If it cannot (AUC near 0.5), the two are statistically interchangeable and one model will transfer. If it separates them perfectly (AUC near 1.0), a shared threshold is doomed and you need per-unit adaptation. A complementary continuous meter is the maximum mean discrepancy (MMD), the distance between the two feature distributions in a kernel space:

$$\mathrm{MMD}^2(A, G) = \big\lVert \mathbb{E}_{\mathbf{x}\sim A}[\phi(\mathbf{x})] - \mathbb{E}_{\mathbf{x}\sim G}[\phi(\mathbf{x})] \big\rVert^2_{\mathcal{H}}.$$

Both are computed on healthy data only, so a large value flags a covariate gap to engineer around, not a fault. Run the meter across every pair in the fleet and you get a similarity matrix that directly drives the cohorting decision below.

import numpy as np
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import cross_val_score

def domain_gap_auc(feat_a, feat_g):
    """A2-distance style shift meter on HEALTHY features only.
    AUC ~0.5 => machines interchangeable; ~1.0 => shared threshold will fail."""
    X = np.vstack([feat_a, feat_g])
    y = np.r_[np.zeros(len(feat_a)), np.ones(len(feat_g))]   # domain label, not fault label
    auc = cross_val_score(HistGradientBoostingClassifier(),
                          X, y, cv=5, scoring="roc_auc").mean()
    return auc

def linear_mmd2(feat_a, feat_g):
    """Unbiased linear-kernel MMD^2 between two healthy feature sets."""
    mu = feat_a.mean(0) - feat_g.mean(0)
    return float(mu @ mu)

# gap = domain_gap_auc(healthy_A, healthy_G)
# if gap > 0.8: adapt per unit before sharing a threshold across A and G
A fleet shift meter run on healthy data only. The domain-classifier AUC estimates how distinguishable two machines are (an empirical proxy for the proxy-A-distance); a value near 1.0 says a single global threshold will misfire, so the machines belong in different cohorts or need per-unit baselining. The linear MMD gives a continuous companion score for building a fleet similarity matrix.

Reading the meter turns portability from a hope into a decision. When domain_gap_auc is near chance across a group of units, pool them and share one model; when it saturates, that group needs its own reference or its own cohort.

Making a detector portable

Four tactics, applied in this order, absorb most cross-machine shift. First, per-unit baselining: define the anomaly score as a distance from each unit's own bank of healthy embeddings (the kNN reference idea from Section 37.3), which erases static covariate shift for free. Second, regime conditioning: partition each unit's normal by operating point (load or speed bins read from the machine's own telemetry) and score against the matching regime, so a half-load conveyor is compared to half-load normal rather than a blended average. Third, domain-invariant representation: train the encoder so its embedding predicts machine state but not machine identity, using a gradient-reversal or adversarial head that penalizes the network for being able to guess which unit produced a clip. Fourth, threshold transfer: set thresholds in a normalized score space (a per-unit robust z-score or percentile) rather than in raw units, so one policy such as "flag the 99th percentile" holds fleet-wide. The feature standardization behind steps one and four is the practical hinge, and it connects to the normalization and dimensionality-reduction toolkit of Chapter 8.

Library Shortcut

Implementing a domain-invariant encoder from scratch (a feature extractor, a task head, a domain-discriminator head, the gradient-reversal layer, and the balancing of the two losses) is roughly 150 to 200 lines of careful PyTorch. Libraries such as skorch plus a published gradient-reversal layer, or a domain-adaptation toolkit like ADAPT and pytorch-adapt, reduce it to configuring an estimator and one adversarial callback, on the order of 20 lines. They handle the reversal autograd trick, the loss scheduling, and the training loop so you configure the shift objective instead of debugging sign errors in a custom backward pass.

Practical Example: One Model, Twelve Wind Farms

An operator runs the same turbine gearbox model across twelve sites. A drivetrain vibration detector trained on the two pilot farms scored a clean AUC there and was rolled out fleet-wide with a single global threshold. Within a week, three coastal farms generated a wall of false alarms while an inland farm missed an early planet-bearing fault entirely. The shift meter told the story: the domain-classifier AUC between coastal and inland healthy data was 0.93, driven by salt-air-related damping and a different foundation type, pure covariate shift. The fix used no new labels. The team switched to per-unit baselining (each turbine scored against its own first month of healthy embeddings), added a regime split on wind-speed bins, and set thresholds on a per-turbine percentile. False alarms at the coastal farms fell by an order of magnitude and the inland bearing fault surfaced two weeks before the scheduled inspection. The models were never retrained; only the baseline and the score normalization became local.

Fleet strategy: cohort, share, or isolate

The final decision is architectural: how many models does a fleet need? Three regimes answer it. Isolate (one model per unit) is safest against shift but wastes data and cannot flag a fault it has never personally seen; it suits small numbers of high-value, idiosyncratic assets. Share (one model for everything) is data-efficient and catches rare faults across the population but collapses under any real covariate gap. Cohort is the pragmatic middle: cluster units by the fleet similarity matrix your shift meter produced, train one model per cohort of statistically interchangeable machines, and give each unit its own local baseline and threshold on top. Cohorting turns the false dichotomy of "specialist versus generalist" into a data-driven grouping, and it is the pattern that scales; managing the resulting family of models, their versioning, and the per-unit baselines across a live fleet is the operations problem taken up in Chapter 69, and it feeds directly into the remaining-useful-life estimates of Chapter 36.

Exercise

Take a multi-unit condition-monitoring dataset (MIMII-DG sections, or several machines from a public bearing dataset). (a) Extract healthy features and compute the domain_gap_auc and linear MMD for every pair of machines; render the result as a fleet similarity heatmap. (b) Cluster the machines from that matrix into cohorts and, for one cohort and one outlier machine, compare three detectors: an isolated per-unit model, a single shared model, and a cohort model with per-unit baselining. Report false-alarm rate on healthy target data and partial AUC on faults. (c) Identify one machine pair whose gap is driven by a telemetry or sampling difference rather than physics, and explain why normalization alone cannot fix it.

Self-Check

  1. Distinguish covariate shift from concept shift across two machines, and state which one per-unit normalization can absorb and which one it cannot.
  2. Your fleet shift meter reports a domain-classifier AUC of 0.95 between two nominally-identical pumps, computed on healthy data only. What does this tell you, and what does it explicitly not tell you about faults?
  3. Why does defining the anomaly score as a distance from each unit's own reference bank make a single fleet-wide threshold more portable than pooling all units into one global model?

What's Next

In Section 37.7, we turn from making detectors portable to scoring them fairly. You will meet TSB-AD and the point-adjustment trap, and see why a shift-robust detector can still post a fraudulently high number if you let the evaluation protocol cheat on your behalf, the exact mirror image of the fleet false-alarm problem you just learned to defuse.