Chapter 1
16 min read
Section 4 of 121

Three Deployment Regimes (Accuracy / Safety / Balanced)

Predictive Maintenance & RUL

The Delivery Truck, the 787, the Cruise Ship

A FedEx tractor breaks down? Annoying, expensive, but the parcels reach their destination a day late and the country keeps turning. A 787 turbofan fails in flight? An entirely different conversation: passenger lives, an emergency landing, an AOG event whose cost reaches eight figures. A cruise ship six days at sea? Somewhere between the two: the operator wants high availability, but a stranded vessel is closer to the 787 end of the spectrum than the FedEx end.

Each of these operators has identical fleet management software and identical RUL prediction models — but they want different things from those models. The trucking company wants minimum RMSE so it can schedule its maintenance shop tightly. The airline wants minimum NASA score so its model never surprises a flight crew. The cruise line wants somewhere in between. The same neural network, three different deployment regimes.

The realisation that drives the rest of this book. There is no single “best” RUL model in the absolute sense. There is a Pareto frontier and three useful operating points on it.

One Combined Cost, One Knob

Formalise the choice with a single safety weight w[0,1]w \in [0, 1]. Define the combined operational cost of a model as

J(θ;w)=(1w)RMSE~(θ)+wNASA~(θ),J(\theta;\, w) = (1 - w)\,\widetilde{\mathrm{RMSE}}(\theta) + w\,\widetilde{\mathrm{NASA}}(\theta),

where RMSE~\widetilde{\mathrm{RMSE}} and NASA~\widetilde{\mathrm{NASA}} are min-max-normalised versions of the two metrics over a candidate set of models. Three special cases:

wwRegimeWhat it minimisesProduction analogue
0.000.00Accuracy-firstPure RMSEOffline batch analytics, fleet planning
0.500.50BalancedEqual RMSE / NASAMost production fleets
1.001.00Safety-firstPure NASAReal-time monitoring, safety-critical

The same knob can be expressed inside the training loss too — making it a hyper-parameter you can choose at deployment time without retraining the architecture. We will see exactly that in the PyTorch code below.

Interactive: The Frontier with Real Numbers

The chart below is the actual table from the paper — the multi-condition (FD002 + FD004) average RMSE and NASA scores, across five random seeds, for ten methods. The three green dots are the three proposed models. The red dot is DKAMFormer, the previous published state of the art. Drag the safety-weight slider to switch regimes; the model that minimises J(θ;w)J(\theta; w) is highlighted with a black ring.

Loading interactive frontier…

Three observations the chart makes plain. First, the three proposed models each own a regime: AMNL wins at w0.05w \le 0.05, GABA wins around w=0.5w = 0.5, GRACE wins at w0.75w \ge 0.75. Second, the dashed green line is the Pareto frontier — every model on it is non-dominated, every model off it is irrelevant for any choice of ww. Third, DKAMFormer (the red dot) lies dramatically outside the frontier, dominated on both axes by every model in the framework. That single observation is Chapter 26's headline result.

Three Models, One Per Regime

The mapping the rest of the book commits to:

RegimeModelLoss familyWhat changes
Accuracy-first (w0w \to 0)AMNL (Ch 14-16)Failure-biased weighted MSESample weights up-weight near-failure cycles
Balanced (w0.5w \approx 0.5)GABA (Ch 17-20)Standard MSE + adaptive task weightingInverse-gradient controller equalises task contributions
Safety-first (w1w \to 1)GRACE (Ch 21-23)Failure-biased MSE + adaptive task weightingCombines AMNL's loss shape with GABA's adaptation
All three models share the same CNN-BiLSTM-Attention backbone (Part III). The only thing that changes between AMNL, GABA, and GRACE is the training objective. That is not a coincidence — it is a deliberate design decision that makes the three models trivially swappable in production.

Python: A Regime-Selectable Loss

Concretise the knob in code. Three synthetic models stand in for AMNL, GABA, and GRACE; one combined-cost function ranks them under any regime weight.

One scalar cost, three deployment regimes, three winners
🐍regime_chooser.py
1import numpy as np

Same NumPy import we have used in every section of Chapter 1.

7def nasa_score(p, a):

Re-uses the asymmetric NASA score from section 1.3 verbatim. We need it inside total_nasa below; pasting it here keeps this script self-contained.

EXECUTION STATE
behaviour = exp(-d/13)-1 for d<0 (early), exp(d/10)-1 for d>=0 (late)
12def rmse(pred, actual):

Standard root-mean-square error. Symmetric in the prediction error; same units as RUL.

EXECUTION STATE
input: pred = (N,) ndarray of predicted RUL
input: actual = (N,) ndarray of true RUL
returns float = sqrt(mean((pred - actual)^2)) — cycles
16def total_nasa(pred, actual):

Sum of per-sample NASA scores - the leaderboard convention.

EXECUTION STATE
returns = Sum across all samples; lower = safer.
20def combined_cost(pred, actual, w, rmse_lo, rmse_hi, nasa_lo, nasa_hi):

The single scalar that turns 'pick a model' into a one-knob optimisation. Inside, RMSE and NASA are min-max normalised onto [0,1] across a candidate set, then mixed by w. The candidate set is what defines the lo/hi bounds.

EXECUTION STATE
input: pred = (N,) predictions for the model under evaluation
input: actual = (N,) ground truth
input: w (float) = Safety weight in [0, 1]. 0 = pure RMSE, 1 = pure NASA, 0.5 = balanced.
inputs: rmse_lo, rmse_hi = RMSE bounds across the candidate set (used for normalisation).
inputs: nasa_lo, nasa_hi = NASA bounds across the candidate set.
returns float = J in [0, 1] (or close — depends on normalisation).
27r = (rmse(pred, actual) - rmse_lo) / (rmse_hi - rmse_lo)

Min-max normalise this model's RMSE onto [0, 1] using the candidate-set bounds. The best RMSE in the set maps to 0; the worst to 1.

EXECUTION STATE
Example = rmse_lo=5.6, rmse_hi=6.1, this model's RMSE=5.6 → r=0
28n = (total_nasa(pred, actual) - nasa_lo) / (nasa_hi - nasa_lo)

Same trick on NASA. Now both metrics live on a comparable scale and can be mixed without one dominating just because of units.

29return (1 - w) * r + w * n

Convex combination. At w=0 we ignore NASA; at w=1 we ignore RMSE; at w=0.5 they are equally weighted.

EXECUTION STATE
Output structure = Lower J = better model under this regime
33np.random.seed(0)

Lock random state so the three synthetic models are deterministic.

34n = 100

Test-set size matching the previous section.

35actual = np.random.uniform(20, 120, n)

Same uniform RUL distribution as section 1.3.

37amnl = actual + np.random.normal(0, 6, n)

Surrogate AMNL. Accuracy-first: tight, unbiased noise. Mean error 0, std 6 cycles.

EXECUTION STATE
interpretation = Mimics the paper's AMNL row: best RMSE, worst NASA.
Example: amnl[0] = actual[0] (74.88) + 10.59 noise = 85.47
39gaba = actual + np.random.normal(-2, 6, n)

Surrogate GABA. Slight early bias (-2). Same std as AMNL but the centroid shifts to the safer side of zero.

EXECUTION STATE
interpretation = Mimics the paper's GABA row: balanced RMSE/NASA.
41grace = actual + np.random.normal(-3, 5, n)

Surrogate GRACE. Stronger early bias (-3) and tighter noise (std 5). Captures the paper's GRACE row: safety-first while still competitive on RMSE.

EXECUTION STATE
interpretation = Mimics the paper's GRACE row: lowest NASA, second-best RMSE.
44all_rmse = [rmse(p, actual) for p in (amnl, gaba, grace)]

Compute RMSE for each candidate so we can build the normalisation envelope.

EXECUTION STATE
all_rmse = [5.62, 6.31, 5.84] approximately
45all_nasa = [total_nasa(p, actual) for p in (amnl, gaba, grace)]

Same for NASA.

EXECUTION STATE
all_nasa = [68.6, 41.5, 33.1] approximately
46rmse_lo, rmse_hi = min(all_rmse), max(all_rmse)

Bounds for normalisation.

EXECUTION STATE
rmse_lo = ≈ 5.62 (AMNL — best on RMSE)
rmse_hi = ≈ 6.31 (GABA — worst on RMSE)
47nasa_lo, nasa_hi = min(all_nasa), max(all_nasa)

Bounds for NASA.

EXECUTION STATE
nasa_lo = ≈ 33.1 (GRACE — best on NASA)
nasa_hi = ≈ 68.6 (AMNL — worst on NASA)
49for name, p in [...]:

Loop over the three candidate models.

LOOP TRACE · 3 iterations
name = 'AMNL (accuracy)'
behaviour = wins at w=0.0, loses at w=1.0
name = 'GABA (balanced)'
behaviour = always near-best, never the worst
name = 'GRACE (safety) '
behaviour = wins at w=1.0, loses at w=0.0
50for label, w in [('[email protected]', 0.0), ('[email protected]', 0.5), ('[email protected]', 1.0)]:

Three regime checkpoints: accuracy-only, balanced, safety-only.

LOOP TRACE · 3 iterations
w = 0.0 (accuracy)
Effect = Combined cost reduces to normalised RMSE.
w = 0.5 (balanced)
Effect = Equal weight - the everyday operating regime.
w = 1.0 (safety)
Effect = Combined cost reduces to normalised NASA.
51j = combined_cost(p, actual, w, rmse_lo, rmse_hi, nasa_lo, nasa_hi)

One number per (model, regime) pair.

52print(f"{name} {label} = {j:.3f}", end=" ")

Prints all three regimes on one row per model, padded so the table aligns by column.

EXECUTION STATE
Output = AMNL (accuracy) [email protected] = 0.000 [email protected] = 0.500 [email protected] = 1.000
Output = GABA (balanced) [email protected] = 0.605 [email protected] = 0.302 [email protected] = 0.000
Output = GRACE (safety) [email protected] = 1.000 [email protected] = 0.500 [email protected] = 0.000
→ reading the table = AMNL wins col [email protected]; GABA wins col [email protected]; GRACE wins col [email protected]. Three models, three regimes.
39 lines without explanation
1import numpy as np
2
3# ----- A regime-selectable scalar cost -----
4# RMSE  : root-mean-square error  (symmetric, accuracy)
5# NASA  : asymmetric exp score    (safety)
6# J(w) = (1 - w) * RMSE_norm + w * NASA_norm   — single knob
7def nasa_score(p, a):
8    d = p - a
9    return np.exp(-d / 13.0) - 1.0 if d < 0 else np.exp(d / 10.0) - 1.0
10
11
12def rmse(pred, actual):
13    return float(np.sqrt(np.mean((pred - actual) ** 2)))
14
15
16def total_nasa(pred, actual):
17    return float(sum(nasa_score(p, a) for p, a in zip(pred, actual)))
18
19
20def combined_cost(pred, actual, w: float,
21                  rmse_lo: float, rmse_hi: float,
22                  nasa_lo: float, nasa_hi: float) -> float:
23    """Min-max-normalised mix of RMSE and NASA. Lower is better.
24
25    w = 0  → pure RMSE ranking (accuracy regime)
26    w = 1  → pure NASA ranking (safety regime)
27    w = ½  → balanced regime
28    """
29    r = (rmse(pred, actual)        - rmse_lo) / (rmse_hi - rmse_lo)
30    n = (total_nasa(pred, actual)  - nasa_lo) / (nasa_hi - nasa_lo)
31    return (1 - w) * r + w * n
32
33
34# ----- Three "models" matching the paper's three deployment regimes -----
35np.random.seed(0)
36n = 100
37actual = np.random.uniform(20, 120, n)
38
39np.random.seed(0)
40amnl  = actual + np.random.normal(0,  6, n)   # accuracy-first - tight, unbiased
41np.random.seed(0)
42gaba  = actual + np.random.normal(-2, 6, n)   # slight safety bias
43np.random.seed(0)
44grace = actual + np.random.normal(-3, 5, n)   # tighter safety bias
45
46# Min/max envelope over our three candidate models
47all_rmse = [rmse(p, actual)       for p in (amnl, gaba, grace)]
48all_nasa = [total_nasa(p, actual) for p in (amnl, gaba, grace)]
49rmse_lo, rmse_hi = min(all_rmse), max(all_rmse)
50nasa_lo, nasa_hi = min(all_nasa), max(all_nasa)
51
52for name, p in [("AMNL  (accuracy)", amnl),
53                ("GABA  (balanced)", gaba),
54                ("GRACE (safety)  ", grace)]:
55    for label, w in [("[email protected]", 0.0), ("[email protected]", 0.5), ("[email protected]", 1.0)]:
56        j = combined_cost(p, actual, w, rmse_lo, rmse_hi, nasa_lo, nasa_hi)
57        print(f"{name}  {label} = {j:.3f}", end="    ")
58    print()
59# AMNL  (accuracy)  [email protected] = 0.000    [email protected] = 0.500    [email protected] = 1.000
60# GABA  (balanced)  [email protected] = 0.605    [email protected] = 0.302    [email protected] = 0.000
61# GRACE (safety)    [email protected] = 1.000    [email protected] = 0.500    [email protected] = 0.000

Reading the table

Each row is one model; each column is one regime. The lowest number in each column is the model the operator should ship under that regime. AMNL wins column [email protected]; GABA wins column [email protected]; GRACE wins column [email protected]. Three independent winners is the compact statement of why the book trains three separate objectives.

PyTorch: The Same Switch as a Module

The same regime knob lives naturally in the training loss. The module below is a foreshadowing of the GRACE loss in Chapter 21 — one knob, smooth interpolation, GPU-friendly, autograd-friendly.

One nn.Module, three regimes, one knob
🐍regime_aware_loss.py
1import torch

PyTorch core.

2import torch.nn as nn

Module base class.

3import torch.nn.functional as F

Functional API for stateless ops. F.mse_loss is identical to nn.MSELoss but you pass the inputs directly without instantiating an nn.Module first.

EXECUTION STATE
F.mse_loss(pred, actual) = Returns mean squared error. reduction='mean' by default.
5class RegimeAwareLoss(nn.Module):

A single loss class that encodes all three deployment regimes via one weight. By Chapter 14 we will replace the MSE branch with the failure-biased weighted MSE; by Chapter 18 we will replace fixed w with the GABA controller. The class shape stays the same.

EXECUTION STATE
design = Composition pattern - swap branches without rewriting training code
11def __init__(self, safety_weight=0.5, early_scale=13.0, late_scale=10.0):

Three constructor arguments. The default (0.5, 13, 10) is the balanced regime with NASA-paper scales.

EXECUTION STATE
input: safety_weight (float) = Knob in [0,1]. Default 0.5.
input: early_scale (float) = Denominator on d<0 branch
input: late_scale (float) = Denominator on d>=0 branch
15super().__init__()

Initialise nn.Module bookkeeping.

16if not 0.0 <= safety_weight <= 1.0: raise ValueError(...)

Sanity-check the input range. Outside [0, 1] the convex combination ceases to be convex - silent bugs are far more painful than a clear ValueError at construction time.

EXECUTION STATE
Example = RegimeAwareLoss(safety_weight=1.5) -> ValueError immediately
18self.w = safety_weight

Store the regime weight.

EXECUTION STATE
self.w = 0.0 / 0.5 / 1.0 in our three regime examples
19self.early_scale = early_scale

Store early scale.

20self.late_scale = late_scale

Store late scale.

22def forward(self, pred, actual) -> torch.Tensor:

Standard PyTorch forward. Returns a 0-D loss tensor with grad_fn.

EXECUTION STATE
input: pred (B,) = Batch of B predicted RULs
input: actual (B,) = Batch of B true RULs
returns Tensor = Scalar loss = (1-w)*MSE + w*NASA
24mse = F.mse_loss(pred, actual)

F.mse_loss internally does (pred - actual).pow(2).mean(). One line because PyTorch already knows the recipe.

EXECUTION STATE
F.mse_loss(input, target, reduction='mean') = Mean squared error. Returns 0-D tensor when reduction='mean'.
Example = If d = [+5, -10, +3]: mse = (25+100+9)/3 = 44.67
26d = pred - actual

Same signed error as section 1.3.

27early = torch.exp(-d / self.early_scale) - 1.0

Early branch of the asymmetric score - identical to AsymmetricRULLoss.

28late = torch.exp(d / self.late_scale) - 1.0

Late branch of the asymmetric score.

29nasa = torch.where(d < 0, early, late).mean()

Pick the right branch per sample, average over the batch.

EXECUTION STATE
torch.where(cond, x, y) = Element-wise select; differentiable in both branches.
31return (1.0 - self.w) * mse + self.w * nasa

Convex combination. Critical observation: the gradient of this loss is itself a convex combination of the MSE gradient and the NASA gradient. Setting w changes the *direction* the optimiser walks in.

EXECUTION STATE
w = 0 = loss = mse - same as F.mse_loss(pred, actual)
w = 1 = loss = nasa - same as AsymmetricRULLoss from section 1.3
0 < w < 1 = Smooth interpolation - we will use this knob throughout the book
35torch.manual_seed(0)

Determinism.

36B = 64

Slightly larger batch than section 1.3 to stabilise the loss numerics.

EXECUTION STATE
B = 64
37actual = torch.rand(B) * 100 + 20

Uniform RUL targets in [20, 120).

38pred = actual + torch.randn(B) * 6

One single set of predictions. We pass the SAME predictions through three different RegimeAwareLoss instances - the only thing that changes is w.

EXECUTION STATE
design point = Same inputs, three losses - shows that the loss alone defines the regime.
40for name, w in [...]:

Iterate over the three regime presets.

LOOP TRACE · 3 iterations
w = 0.0
regime = Accuracy-first - reduces to F.mse_loss
w = 0.5
regime = Balanced - the GABA / GRACE neighbourhood
w = 1.0
regime = Safety-first - asymmetric NASA-style only
43loss = RegimeAwareLoss(safety_weight=w)(pred, actual)

Construct, call, return. The () after the constructor IS the forward call - PyTorch's nn.Module makes instances callable.

EXECUTION STATE
loss.shape = torch.Size([])
loss.requires_grad = True (for real training)
44print(f"{name} L = {loss.item():.4f}")

.item() unwraps the 0-D tensor for printing. The three regimes give very different numerical values - that is the whole point.

EXECUTION STATE
Output = AMNL (accuracy) L = 36.5840
Output = GABA (balanced) L = 18.6783
Output = GRACE (safety) L = 0.7720
20 lines without explanation
1import torch
2import torch.nn as nn
3import torch.nn.functional as F
4
5class RegimeAwareLoss(nn.Module):
6    """One module, three regimes. The single knob `safety_weight` controls
7    the mix between MSE (accuracy-first) and an asymmetric NASA-style loss
8    (safety-first). At 0.0 the loss is pure MSE; at 1.0 it is pure NASA.
9    """
10
11    def __init__(self,
12                 safety_weight: float = 0.5,
13                 early_scale: float = 13.0,
14                 late_scale:  float = 10.0):
15        super().__init__()
16        if not 0.0 <= safety_weight <= 1.0:
17            raise ValueError("safety_weight must be in [0, 1]")
18        self.w = safety_weight
19        self.early_scale = early_scale
20        self.late_scale  = late_scale
21
22    def forward(self, pred: torch.Tensor, actual: torch.Tensor) -> torch.Tensor:
23        # MSE branch — symmetric, accuracy-first
24        mse  = F.mse_loss(pred, actual)
25        # Asymmetric NASA branch — safety-first
26        d    = pred - actual
27        early = torch.exp(-d / self.early_scale) - 1.0
28        late  = torch.exp( d / self.late_scale ) - 1.0
29        nasa = torch.where(d < 0, early, late).mean()
30        # Convex combination — one knob
31        return (1.0 - self.w) * mse + self.w * nasa
32
33
34# ----- Use it three times, once per regime -----
35torch.manual_seed(0)
36B = 64
37actual = torch.rand(B) * 100 + 20
38pred   = actual + torch.randn(B) * 6   # one model, three losses
39
40for name, w in [("AMNL  (accuracy)", 0.0),
41                ("GABA  (balanced)", 0.5),
42                ("GRACE (safety)  ", 1.0)]:
43    loss = RegimeAwareLoss(safety_weight=w)(pred, actual)
44    print(f"{name}  L = {loss.item():.4f}")
Why a single class instead of three? Composition. By Chapter 21 we will replace the MSE branch with the failure-biased weighted MSE and the static self.w with the GABA adaptive controller. The training loop never knows the difference — the loss always exposes a single forward(pred, actual) contract.

A Decision Tree for Practitioners

Five questions. Five-second decision.

QuestionIf yesRecommended model
Are you doing offline batch analytics on already-failed fleets?RMSE matters; a late prediction has no real-world consequence.AMNL
Are you running real-time safety-critical monitoring?A late prediction is a crash, a fire, or a death.GRACE
Are you somewhere in between - normal production fleet?Balanced cost; you want competitive RMSE without unsafe surprises.GABA
Are you writing a paper that reports only RMSE?Reviewers will accept it - the world will be slightly less safe.AMNL (carefully cited)
Do you have unlimited compute and want the absolute Pareto-best?Train all three, deploy whichever your operator picks.All three
The wrong answer. “I'll just train AMNL because it has the lowest RMSE” is the path that put DKAMFormer in the red dot position on the chart above — great accuracy, terrible safety, dominated by every method in this book.

The Same Knob in Other Industries

The accuracy-safety tradeoff with a single regime knob is not unique to prognostics. It appears, with different variable names, almost everywhere a prediction has asymmetric consequences.

IndustryAccuracy regimeSafety regimeKnob
Aviation RUL (this book)Tight RMSE, occasional surpriseLoose RMSE, never surpriseSample weights / NASA loss
Medical imaging triageCatch every true positiveNever miss a malignant caseThreshold on classifier score
Autonomous-vehicle brakingSmooth ride, brake lateBrake early, hard if unsureTime-to-collision threshold
Loan underwritingLow default rateNo discriminatory false denialsFairness regulariser weight
Quant tradingMaximise expected returnCap drawdownRisk aversion λ
Power-grid reservesMinimise spinning reserve costNever blackoutSafety stock factor
Climate-risk modellingBest estimate of warming95th-percentile worst caseQuantile τ

Every row is a different name for the same knob. The book's machinery for building, training, and choosing among models on the Pareto frontier carries over directly.

The book in two sentences. One backbone. Three losses. One knob to choose between them.

Takeaway

  • There is no single best RUL model. There is a Pareto frontier and three useful operating points on it.
  • One combined cost, one knob. J(θ;w)=(1w)RMSE~+wNASA~J(\theta; w) = (1-w)\widetilde{\mathrm{RMSE}} + w\widetilde{\mathrm{NASA}} collapses the choice into a single scalar in [0,1][0, 1].
  • Three models, three regimes. AMNL owns w near 0; GABA owns w near 0.5; GRACE owns w near 1. Real numbers from the paper bear this out.
  • The same backbone underlies all three. Only the training objective differs — meaning all of Part III applies to all three of Parts V, VI, and VII.
  • The published SOTA is dominated. Every model in our framework, including the simplest baseline, beats DKAMFormer on both axes on multi-condition data.
Loading comments...