Chapter 7
11 min read
Section 27 of 121

Sliding-Window Sequences (length = 30)

Sequences, RUL Cap & Health Labels

Why Windows, Not Whole Histories

We have already discussed (Section 1.2) why a model takes a WINDOW of past cycles instead of the entire history: old cycles rarely matter for current prognosis, attention scales poorly with sequence length, and the windowed view turns a single run-to-failure trajectory into many supervised samples. This section formalises the construction.

The standard. Window length W=30W = 30; stride S=1S = 1 for training (every cycle yields a window); stride S1S \ge 1 for evaluation, where you only need the LAST window per engine.

Output Count and Stride

Given an engine with NN cycles, window length WW, and stride SS, the number of valid windows is

nwindows  =  NWS+1.n_{\text{windows}} \;=\; \left\lfloor \frac{N - W}{S} \right\rfloor + 1.

With N=200,W=30,S=1N = 200, W = 30, S = 1 we get 171 windows; with S=5S = 5 we get 35. The per-window RUL target is tfailtendt_{\text{fail}} - t_{\text{end}} where tendt_{\text{end}} is the LAST cycle of the window.

Interactive: Slide Across One Engine

The viewer from Section 1.2, reproduced - drag the cursor and watch the (X, y) pair update.

Loading window explorer…

Python: Build Pairs From One Engine

Stride parameterisation; downsampled training sets
🐍build_pairs.py
1import numpy as np

NumPy is Python's foundational numerical-computing library. It provides ndarray (N-dimensional array) — a fast, memory-efficient matrix type backed by compiled C code. We use it here for two reasons: (1) np.random.randn to fabricate a fake sensor matrix, and (2) np.stack / np.array to glue per-window slices and per-window RUL targets into single tensors at the end.

EXECUTION STATE
numpy = Provides ndarray, linear algebra, broadcasting math, and pseudo-random number generation. All operations execute as compiled C code, not Python loops.
as np = Universal Python alias. Lets us write np.stack(...) instead of numpy.stack(...).
3def build_pairs(sensors, failure_cycle, window=30, stride=1):

Generic windowing helper that turns ONE engine's full run-to-failure trajectory into many supervised (X, y) pairs. This is the canonical reference function for the entire book — every loader in later chapters reuses this exact contract.

EXECUTION STATE
⬇ input: sensors (n_cycles, n_sensors) = (200, 14) — one engine's full sensor history. Each row is one cycle's reading from 14 sensors.
→ sensors purpose = The complete time series for ONE engine, from healthy start to failure. We slice this into (window × n_sensors) chunks.
⬇ input: failure_cycle (int) = 200 — the cycle index on which the engine actually died. Used to compute RUL = failure_cycle − end.
→ why needed = RUL ('Remaining Useful Life') depends on knowing when the engine failed. Without it we cannot compute targets.
⬇ input: window = 30 (default) = 30 cycles — the C-MAPSS leaderboard standard. Each model input is a 30×14 matrix. Smaller windows under-fit; larger ones dilute recent signal and slow training.
⬇ input: stride = 1 (default) = 1 = every cycle yields a window (171 windows). 5 = every 5th cycle (35 windows). Larger stride = fewer training samples but cheaper training.
→ type hints = np.ndarray and int are PEP-484 type hints. They document the contract for IDEs and static checkers; Python ignores them at runtime.
⬆ returns = Tuple (X, y). X has shape (n_pairs, window, n_sensors); y has shape (n_pairs,). Ready for direct use as regression training data.
5Docstring: One engine's run-to-failure → (X, y) regression pairs

Documents the contract: input is one engine's full trajectory plus its failure cycle; output is supervised pairs ready for a regression model. The Returns block names the two output shapes so callers don't need to read the body.

12n = len(sensors)

Total cycles in this engine's trajectory. For our toy example, n = 200.

EXECUTION STATE
📚 len() = Python built-in. For a NumPy ndarray, returns the length of the FIRST axis only — not the total element count. For shape (200, 14), len returns 200, not 2800.
⬇ input: sensors = ndarray of shape (200, 14)
⬆ result: n = 200 — the cycle count
13X_list, y_list = [], []

Two empty Python lists that accumulate per-window slices and their RUL targets. We use lists (not pre-allocated arrays) because list.append is amortised O(1) and we don't need to compute n_pairs ahead of time.

EXECUTION STATE
X_list = [] — will collect ndarray slices of shape (30, 14), one per window
y_list = [] — will collect scalar RUL ints, one per window
Tuple unpacking = Python destructures the right-hand tuple ([] , []) into two simultaneous assignments. Same as: X_list = []; y_list = []
14for end in range(window, n + 1, stride):

Slide the window across the trajectory. `end` is the EXCLUSIVE upper bound of each window — the cycle just past the window's last observation. The window covers cycles [end - window, end).

EXECUTION STATE
📚 range(start, stop, step) = Python built-in. Generates integers from start (inclusive) to stop (exclusive), incrementing by step. Memory-efficient — generates lazily, not all at once.
⬇ arg 1: start = window = 30 = First valid value of `end`. We need at least 30 cycles of history before the first window can close.
⬇ arg 2: stop = n + 1 = 201 = EXCLUSIVE upper bound. The +1 lets `end` reach n=200, so the last window covers the engine's final 30 cycles [170, 200).
→ why +1? = range stops one BEFORE `stop`. range(30, 200) ends at end=199, missing the final window. range(30, 201) ends at end=200 — the off-by-one fix.
⬇ arg 3: step = stride = 1 = Slide one cycle at a time. With stride=5, we'd jump 5 cycles per iteration, getting only every 5th window.
Iteration-count formula = n_windows = floor((n − window) / stride) + 1 = floor((200−30)/1) + 1 = 171
LOOP TRACE · 7 iterations
1st iter (stride=1)
end = 30 — window covers cycles [0, 30)
2nd iter
end = 31 — window covers cycles [1, 31)
3rd iter
end = 32 — window covers cycles [2, 32)
...
end = ...slides by stride each iteration
170th iter
end = 199 — window covers cycles [169, 199)
171st iter (last)
end = 200 — window covers cycles [170, 200), the engine's final 30 cycles
stride=5 alternative
end values = 30, 35, 40, ..., 195, 200 → 35 windows total
15X_list.append(sensors[end - window:end])

Slice the past `window` cycles ending at (but not including) `end`. NumPy slicing returns a VIEW into the original array — no data copy — so this is essentially free.

EXECUTION STATE
📚 ndarray[start:stop] = NumPy slice along the first axis. Returns a VIEW that shares memory with the original. Modifying the slice modifies sensors.
end − window = The window's INCLUSIVE start cycle. For end=30, window=30: start = 0. For end=200: start = 170.
Slice shape = sensors[end-window : end] has shape (30, 14) — 30 cycles × 14 sensors
📚 list.append(x) = Python list method. Adds one element to the end. Amortised O(1). We collect 171 slices total before stacking.
Example: end=30 = Appends sensors[0:30] — the first 30 cycles
Example: end=200 = Appends sensors[170:200] — the engine's final 30 cycles, just before failure
16y_list.append(failure_cycle - end)

RUL ('Remaining Useful Life') after the window closes. Convention: with `end` exclusive, the window's last observation is at cycle end−1, and RUL counts cycles from end forward until failure_cycle.

EXECUTION STATE
failure_cycle − end = Cycles between the window's exclusive end and engine death.
Why end (not end−1)? = RUL at time t means 'cycles remaining AFTER observing through t'. Using `end` makes the last window's target equal 0 — the engine dies right at end=failure_cycle.
LOOP TRACE · 7 iterations
end=30
y = failure_cycle − end = 200 − 30 = 170 — 170 cycles to go
end=31
y = 200 − 31 = 169
end=32
y = 200 − 32 = 168
...
y = decreases by 1 each step (stride=1)
end=199
y = 200 − 199 = 1 — engine dies on the next cycle
end=200
y = 200 − 200 = 0 — RUL bottoms out at 0
stride=5
y values = [170, 165, 160, 155, 150, ...] — drops by 5 each step
17return np.stack(X_list), np.array(y_list)

Convert the two Python lists into NumPy tensors. np.stack creates a NEW leading axis (so 171 shape-(30,14) arrays become one shape-(171,30,14) tensor); np.array converts a flat list of ints into a 1D ndarray.

EXECUTION STATE
📚 np.stack(arrays, axis=0) = Joins a sequence of arrays along a NEW axis. Each input array must have the same shape. Default axis=0 stacks them as the leading dimension.
→ vs np.concatenate = stack ADDS a new axis: 171 arrays of (30,14) → (171, 30, 14). concatenate joins along an EXISTING axis: same input → (5130, 14). We need stack here so the per-window dimension is preserved.
⬇ X_list (Python list) = 171 ndarrays, each of shape (30, 14)
⬆ np.stack(X_list) = ndarray of shape (171, 30, 14) — (n_pairs, window, n_sensors)
📚 np.array(object) = Converts any sequence (list, tuple, generator) into an ndarray. For a flat list of Python ints, returns a 1D int64 ndarray.
⬇ y_list = 171-element list of ints: [170, 169, 168, ..., 1, 0]
⬆ np.array(y_list) = ndarray of shape (171,), dtype int64 — [170, 169, 168, ..., 1, 0]
→ return as tuple = Python implicit tuple-packing: `return a, b` is equivalent to `return (a, b)`. Caller unpacks via `X, y = build_pairs(...)`.
21np.random.seed(0)

Sets NumPy's global random state to a fixed seed so np.random.randn produces the SAME numbers on every run. Critical for reproducible examples and reproducible book figures.

EXECUTION STATE
📚 np.random.seed(seed) = Initialises NumPy's pseudo-random number generator. Same seed → identical sequence of subsequent random calls. Without this, every run would yield different sensor data.
⬇ arg: 0 = Arbitrary non-negative integer. Convention in tutorials: 0 or 42.
22n_cycles, n_sensors = 200, 14

Tuple unpacking — two assignments in one line. Defines the toy engine's dimensions.

EXECUTION STATE
n_cycles = 200 — engine runs for 200 cycles before failure
n_sensors = 14 — number of informative sensors after C-MAPSS feature selection (drop the 7 constant-value sensors from the 21 raw)
23sensors = np.random.randn(n_cycles, n_sensors).astype(np.float32) * 5 + 100

Generates fake sensor data for the toy example. Read right-to-left: sample standard normals, cast to float32, scale by 5 (std=5), shift by 100 (mean=100).

EXECUTION STATE
📚 np.random.randn(d0, d1, ...) = Samples from the standard normal distribution N(0, 1). The shape arguments give the output shape. Returns float64 by default.
⬇ args: (200, 14) = Output shape — 200 rows × 14 columns of N(0,1) samples
📚 .astype(np.float32) = ndarray method. Casts dtype. randn returns float64; PyTorch prefers float32 (half the memory, identical numerical accuracy for ML).
* 5 = Element-wise multiply. Stretches std from 1 → 5, simulating realistic sensor variance.
+ 100 = Element-wise add. Shifts mean from 0 → 100, simulating a sensor centred around 100 (e.g., a temperature reading in °C).
⬆ result: sensors = ndarray of shape (200, 14), dtype float32, values ≈ N(100, 25)
25X1, y1 = build_pairs(sensors, failure_cycle=200, window=30, stride=1)

Dense windowing — every cycle yields a training window. Maximises training samples (171) from a single engine.

EXECUTION STATE
⬇ args = sensors (200,14), failure_cycle=200, window=30, stride=1
n_windows formula = floor((200 − 30) / 1) + 1 = 170 + 1 = 171
⬆ X1.shape = (171, 30, 14) — 171 windows × 30 cycles × 14 sensors
⬆ y1.shape = (171,) — one scalar RUL per window
y1[:5] = [170, 169, 168, 167, 166] — RUL drops by 1 each window (because stride=1)
y1[-5:] = [4, 3, 2, 1, 0] — final five windows count down to engine death
26X5, y5 = build_pairs(sensors, failure_cycle=200, window=30, stride=5)

Sparse windowing — slide 5 cycles at a time. 5× fewer windows. Useful when training data is overwhelming or compute is constrained.

EXECUTION STATE
n_windows formula = floor((200 − 30) / 5) + 1 = 34 + 1 = 35
⬆ X5.shape = (35, 30, 14) — 35 windows
⬆ y5.shape = (35,)
y5[:5] = [170, 165, 160, 155, 150] — RUL drops by 5 each window (because stride=5)
Trade-off = 5× less data → 5× faster training but the model sees fewer near-failure examples. For evaluation, the convention is one window per engine — the LAST possible window.
28print("stride 1: X.shape", X1.shape, " y[:5]", y1[:5].tolist())

Verify dense-windowing shapes and RUL targets at runtime.

EXECUTION STATE
📚 ndarray.shape = Tuple attribute holding the array's dimensions. For X1 it's (171, 30, 14). Not a function — no parentheses.
📚 .tolist() = ndarray method. Converts to a nested Python list. Used here only so print doesn't wrap output in numpy's `array(...)` cosmetic noise.
⬇ y1[:5] = ndarray slice — first 5 elements: array([170, 169, 168, 167, 166])
⬆ Output = stride 1: X.shape (171, 30, 14) y[:5] [170, 169, 168, 167, 166]
29print("stride 5: X.shape", X5.shape, " y[:5]", y5[:5].tolist())

Verify sparse-windowing shapes. Note RUL jumps by 5 — the visible signature of stride>1.

EXECUTION STATE
⬆ Output = stride 5: X.shape ( 35, 30, 14) y[:5] [170, 165, 160, 155, 150]
16 lines without explanation
1import numpy as np
2
3def build_pairs(sensors: np.ndarray, failure_cycle: int,
4                window: int = 30, stride: int = 1):
5    """One engine's run-to-failure -> (X, y) regression pairs.
6
7    Returns
8    -------
9    X : (n_pairs, window, n_sensors)  - sliding-window inputs
10    y : (n_pairs,)                    - per-window scalar RUL target
11    """
12    n = len(sensors)
13    X_list, y_list = [], []
14    for end in range(window, n + 1, stride):
15        X_list.append(sensors[end - window:end])
16        y_list.append(failure_cycle - end)
17    return np.stack(X_list), np.array(y_list)
18
19
20# ----- Run on one synthetic engine -----
21np.random.seed(0)
22n_cycles, n_sensors = 200, 14
23sensors = np.random.randn(n_cycles, n_sensors).astype(np.float32) * 5 + 100
24
25X1, y1 = build_pairs(sensors, failure_cycle=200, window=30, stride=1)
26X5, y5 = build_pairs(sensors, failure_cycle=200, window=30, stride=5)
27
28print("stride 1: X.shape", X1.shape, " y[:5]", y1[:5].tolist())
29print("stride 5: X.shape", X5.shape, " y[:5]", y5[:5].tolist())
30
31# stride 1: X.shape (171, 30, 14)  y[:5] [170, 169, 168, 167, 166]
32# stride 5: X.shape ( 35, 30, 14)  y[:5] [170, 165, 160, 155, 150]

PyTorch: A Sliding-Window Dataset

Lazy single-engine Dataset; stride controls density
🐍sliding_window_dataset.py
1import numpy as np

We still need NumPy here as the source format for sensor data. PyTorch's torch.from_numpy is the standard zero-copy bridge — the ndarray's memory becomes the tensor's storage, no duplication.

EXECUTION STATE
Why both numpy and torch? = Real PdM pipelines load CSVs into NumPy first (pandas → ndarray), then convert once at the Dataset boundary. Avoids unnecessary copies and keeps preprocessing in the fast NumPy ecosystem.
2import torch

PyTorch root module. Provides torch.Tensor (the GPU-aware n-dimensional array), torch.from_numpy, torch.tensor, torch.manual_seed, and the autograd engine. Everything in this Dataset stores its data as torch.Tensors so the DataLoader can collate batches and move them to GPU.

3from torch.utils.data import Dataset

Imports the abstract base class for ALL PyTorch map-style datasets. Subclassing Dataset and implementing __len__ + __getitem__ is the only contract a DataLoader needs — it then handles batching, shuffling, multiprocessing, and pinning automatically.

EXECUTION STATE
📚 torch.utils.data.Dataset = Abstract base class. Two required methods: __len__(self) → int (sample count) and __getitem__(self, idx) → (X, y) (one sample). Anything subclassing this works with DataLoader.
6class SlidingWindowDataset(Dataset):

Lazy single-engine windowing dataset. Unlike the NumPy helper that materialises ALL 171 windows up front, this class stores ONLY the (200,14) sensor matrix plus a 171-element list of start indices, and slices a window on-demand inside __getitem__. Saves memory when you have many engines.

EXECUTION STATE
Eager vs Lazy = Eager (NumPy helper): allocates 171×30×14 = 71,820 floats up front. Lazy (this class): allocates 171 ints (starts) + reuses the 200×14 base. ~25× less memory per engine.
Inheritance = (Dataset) tells Python this class IS-A Dataset, so PyTorch's DataLoader will accept it.
7Docstring: Lazy windowing — one engine, sliding stride.

One-line summary describing the strategy: lazy (no upfront materialisation) and single-engine (one trajectory per Dataset instance). For multi-engine training you'd wrap this in torch.utils.data.ConcatDataset.

9def __init__(self, sensors, failure_cycle, window=30, stride=1):

Constructor. Same four arguments as the NumPy helper, but this version stores them and pre-computes start indices instead of building windows.

EXECUTION STATE
⬇ input: self = The fresh instance Python is constructing. By the end of __init__, it will have four attributes: self.sensors, self.failure_cycle, self.window, self.starts.
⬇ input: sensors (np.ndarray) = Shape (200, 14). The engine's full sensor history as a NumPy ndarray.
⬇ input: failure_cycle (int) = 200 — engine's death cycle. Stashed for RUL computation later.
⬇ input: window = 30 = 30-cycle window length (C-MAPSS standard).
⬇ input: stride = 1 = 1 = dense (171 windows). 5 = sparse (35 windows). Controls how many samples the dataset exposes via __len__.
11self.sensors = torch.from_numpy(sensors).float()

Zero-copy bridge from ndarray to torch.Tensor, then cast to float32. The new tensor SHARES memory with the original ndarray — modifying one modifies the other.

EXECUTION STATE
📚 torch.from_numpy(ndarray) = Creates a torch.Tensor that shares the SAME underlying memory as the ndarray. Zero data copy. Inherited dtype matches the ndarray's dtype (here float32).
→ vs torch.tensor(...) = torch.tensor() COPIES the data; torch.from_numpy() shares memory. For large sensor arrays the difference is gigabytes — always prefer from_numpy at the I/O boundary.
📚 .float() = Tensor method. Equivalent to .to(torch.float32). PyTorch's nn layers expect float32 by default; mismatched dtypes raise RuntimeError at the first matmul.
⬇ input: sensors (ndarray) = shape (200, 14), dtype float32 (already, from the .astype(np.float32) earlier)
⬆ self.sensors = torch.Tensor of shape torch.Size([200, 14]), dtype torch.float32, device cpu
12self.failure_cycle = failure_cycle

Stash the failure cycle on the instance so __getitem__ can compute RUL = failure_cycle − e at sample-fetch time. Plain int — no tensor conversion needed because we'll wrap the per-sample y as a tensor inside __getitem__.

EXECUTION STATE
self.failure_cycle = 200 (Python int)
13self.window = window

Stash window length. Used in __getitem__ to compute the slice end e = s + self.window.

EXECUTION STATE
self.window = 30 (Python int)
14self.starts = list(range(0, len(sensors) - window + 1, stride))

Pre-compute every valid window start index as a Python list. With stride=1 we get [0, 1, 2, ..., 170] (171 entries); with stride=5 we get [0, 5, 10, ..., 170] (35 entries). __getitem__ later indexes into this list to find s.

EXECUTION STATE
📚 range(start, stop, step) = Python built-in. Lazy integer generator from start (incl) to stop (excl), step `step`.
⬇ arg 1: start = 0 = Earliest possible window start — cycle 0 (engine begin).
⬇ arg 2: stop = len(sensors) − window + 1 = 200 − 30 + 1 = 171. EXCLUSIVE upper bound. The +1 lets the last start be 170, so the last window covers cycles [170, 200) — exactly the engine's final 30 cycles.
→ why +1? = range(0, 200 − 30) stops at start=169, missing the final window. range(0, 171) reaches start=170 — the off-by-one fix mirrored from the NumPy helper.
⬇ arg 3: step = stride = 1 = every cycle, 5 = every 5th. Controls density of starts.
📚 list(iterable) = Python built-in. Materialises a lazy iterable (range object) into a concrete list. We need a list because __len__ and __getitem__ index into it.
⬆ self.starts (stride=1) = [0, 1, 2, 3, ..., 168, 169, 170] — 171 entries
⬆ self.starts (stride=5) = [0, 5, 10, 15, ..., 160, 165, 170] — 35 entries
16def __len__(self) -> int:

PyTorch DataLoader contract method #1. Returns the total number of samples the dataset exposes. DataLoader uses this to compute number of batches and to bound the sampler's index range.

EXECUTION STATE
→ -> int return hint = PEP-484 type hint that this method returns an int. PyTorch's DataLoader actually requires this — Python's len() built-in expects an int.
17return len(self.starts)

The number of valid window starts equals the number of available samples. 171 for stride=1, 35 for stride=5.

EXECUTION STATE
⬆ return = 171 (stride=1) or 35 (stride=5)
19def __getitem__(self, idx: int):

PyTorch DataLoader contract method #2. Given a sample index in [0, len(self)), return one (X, y) pair. DataLoader calls this once per sample then collates results into a batch.

EXECUTION STATE
⬇ input: self = The dataset instance (provides self.sensors, self.starts, self.window, self.failure_cycle).
⬇ input: idx (int) = Sample index, 0 ≤ idx < len(self). For stride=1: 0 fetches the first window, 170 fetches the last.
⬆ returns = Tuple (X, y) where X is a (30, 14) tensor and y is a scalar tensor. DataLoader stacks these into batches.
20s = self.starts[idx]

Look up the start cycle for this sample.

EXECUTION STATE
Example: idx=0, stride=1 = s = self.starts[0] = 0
Example: idx=170, stride=1 = s = self.starts[170] = 170
Example: idx=0, stride=5 = s = self.starts[0] = 0
Example: idx=34, stride=5 = s = self.starts[34] = 170
21e = s + self.window

Exclusive upper bound of the slice. With s=0 and window=30, e=30 — slice covers cycles [0, 30).

EXECUTION STATE
Example: s=0 = e = 0 + 30 = 30 — slice covers [0, 30)
Example: s=170 = e = 170 + 30 = 200 — slice covers [170, 200), the engine's final 30 cycles
22X = self.sensors[s:e]

Tensor slice along the first axis. Like NumPy slicing, returns a VIEW into self.sensors — no data copy. The DataLoader's collate_fn later stacks batch_size such views into a (B, 30, 14) batch tensor.

EXECUTION STATE
📚 tensor[s:e] = PyTorch slice indexing. Returns a view sharing storage with the parent tensor. Cheap (O(1)) until you call .clone() or send to device.
⬇ self.sensors = torch.Tensor of shape (200, 14)
Slice indices: [s:e] = For idx=0, stride=1: [0:30] → first 30 cycles
⬆ X = torch.Tensor of shape torch.Size([30, 14]), dtype float32 — one window of sensor readings
23y = torch.tensor(float(self.failure_cycle - e))

Build the scalar RUL target as a 0-D tensor. We must cast through Python float to ensure a clean torch.float32 result; passing a Python int directly would yield a torch.int64 which mismatches the float32 X and the typical MSE-loss expectation.

EXECUTION STATE
self.failure_cycle − e = Python int subtraction. For e=30: 200−30 = 170. For e=200: 200−200 = 0.
📚 float(x) = Python built-in. Casts an int to a Python float. Necessary so torch.tensor produces a float32 scalar (not int64).
📚 torch.tensor(data) = Constructs a NEW tensor by COPYING data. Unlike torch.from_numpy this copies, but for a single scalar the copy is a single 4-byte write — irrelevant.
→ vs torch.from_numpy = from_numpy: zero-copy, requires ndarray input. tensor: copies, accepts any Python scalar/list/ndarray. Use tensor for scalars and small constants; from_numpy for large preprocessed arrays.
⬆ y = 0-dimensional tensor — torch.tensor(170.0), shape torch.Size([]), dtype float32
24return X, y

DataLoader contract: __getitem__ returns one sample as a tuple. Default collate_fn stacks the X tensors into (batch, 30, 14) and the y tensors into (batch,) automatically.

EXECUTION STATE
⬆ return = Tuple (X (30,14) float32, y scalar float32)
Implicit packing = `return X, y` is shorthand for `return (X, y)`. Caller unpacks: `X, y = ds[idx]`
28torch.manual_seed(0)

Sets PyTorch's CPU random seed. Belt-and-braces with np.random.seed earlier — different libraries have independent RNGs, so reproducibility requires seeding each one used.

EXECUTION STATE
📚 torch.manual_seed(seed) = Initialises PyTorch's CPU random state. For full reproducibility you'd also call torch.cuda.manual_seed_all and set torch.backends.cudnn.deterministic=True.
⬇ arg: 0 = Same seed value the rest of the book uses. Convention.
29sensors = np.random.randn(200, 14).astype(np.float32) * 5 + 100

Same fake sensor matrix as the NumPy block — same seed-controlled values. Pre-converted to float32 so torch.from_numpy gives a float32 tensor without an extra cast.

EXECUTION STATE
⬆ sensors = ndarray (200, 14) float32, values ≈ N(100, 25)
31ds_dense = SlidingWindowDataset(sensors, 200, window=30, stride=1)

Dense windowing — every cycle yields a sample. The constructor pre-computes self.starts = [0..170] and stores the (200,14) tensor. No windows materialised yet.

EXECUTION STATE
Constructor args = sensors=(200,14), failure_cycle=200, window=30, stride=1
len(ds_dense) = 171 — exposed via __len__
Memory cost = self.sensors = 200×14×4 bytes = 11,200 B; self.starts = 171 ints. Total ~12 KB per engine. Compare to the eager NumPy version's 171×30×14×4 = 287 KB.
32ds_sparse = SlidingWindowDataset(sensors, 200, window=30, stride=5)

Sparse windowing — only every 5th cycle yields a sample. self.starts = [0, 5, 10, ..., 170]. Same self.sensors tensor — only the starts list shrinks.

EXECUTION STATE
len(ds_sparse) = 35
self.starts = [0, 5, 10, 15, 20, 25, ..., 160, 165, 170]
34print("dense size :", len(ds_dense))

Verifies __len__ wiring. Python's len() built-in calls Dataset's __len__(self) under the hood.

EXECUTION STATE
📚 len(obj) = Python built-in that calls obj.__len__(). Works on any class that implements __len__ — that's why we had to define it.
⬆ Output = dense size : 171
35print("sparse size :", len(ds_sparse))

Verifies stride=5 yields exactly 35 samples — matches floor((200−30)/5)+1.

EXECUTION STATE
⬆ Output = sparse size : 35
37X, y = ds_dense[0]

Subscript notation triggers __getitem__(self, idx=0). The class returns a (X, y) tuple; Python unpacks it into two variables.

EXECUTION STATE
📚 obj[idx] sugar = Python desugars `obj[idx]` to `obj.__getitem__(idx)`. That's why our class works with bracket indexing despite being a custom type.
Trace: idx=0 = s = self.starts[0] = 0 → e = 0 + 30 = 30 → X = self.sensors[0:30] (30,14) → y = torch.tensor(200.0 − 30.0) = 170.0
⬆ X = torch.Tensor shape (30, 14), dtype float32 — first window
⬆ y = torch.tensor(170.0), 0-D float32 — RUL after observing first 30 cycles
38print("X.shape:", tuple(X.shape), "y:", float(y))

Verify shapes and the RUL value of the first sample.

EXECUTION STATE
📚 X.shape = torch.Size attribute. tuple() conversion turns torch.Size([30, 14]) into the cleaner (30, 14) when printing.
📚 float(y) = Built-in cast that calls y.item() under the hood for 0-D tensors. Pulls the scalar out of the tensor wrapper for clean printing.
⬆ Output = X.shape: (30, 14) y: 170.0
12 lines without explanation
1import numpy as np
2import torch
3from torch.utils.data import Dataset
4
5
6class SlidingWindowDataset(Dataset):
7    """Lazy windowing - one engine, sliding stride."""
8
9    def __init__(self, sensors: np.ndarray, failure_cycle: int,
10                 window: int = 30, stride: int = 1):
11        self.sensors = torch.from_numpy(sensors).float()
12        self.failure_cycle = failure_cycle
13        self.window = window
14        self.starts = list(range(0, len(sensors) - window + 1, stride))
15
16    def __len__(self) -> int:
17        return len(self.starts)
18
19    def __getitem__(self, idx: int):
20        s = self.starts[idx]
21        e = s + self.window
22        X = self.sensors[s:e]                               # (window, n_sensors)
23        y = torch.tensor(float(self.failure_cycle - e))     # scalar RUL
24        return X, y
25
26
27# ----- Use it -----
28torch.manual_seed(0)
29sensors = np.random.randn(200, 14).astype(np.float32) * 5 + 100
30
31ds_dense  = SlidingWindowDataset(sensors, 200, window=30, stride=1)
32ds_sparse = SlidingWindowDataset(sensors, 200, window=30, stride=5)
33
34print("dense  size :", len(ds_dense))    # 171
35print("sparse size :", len(ds_sparse))   #  35
36
37X, y = ds_dense[0]
38print("X.shape:", tuple(X.shape), "y:", float(y))   # (30, 14) 170.0
Train vs evaluation discipline. Use stride=1 for training (maximises data). Use stride that produces ONE window per test engine (the last possible window) for evaluation - the standard C-MAPSS leaderboard convention.

Sliding Windows in Other Domains

DomainWindowStrideNotes
RUL (this book)30 cycles1 (train) / last (eval)Window in cycles
Speech recognition25 ms frames10 ms (75% overlap)Hop length = stride
EEG seizure detection1-second windows0.5 s (50% overlap)Continuous classification
Network IDS60 packets1 packet (heavy overlap)Stream-mode windowing
Stock-trading signals60 minutes5 minutesHourly indicators
Wearable activity recognition200 samples (~2 s)100 samples (50% overlap)Ditto

Three Window-Construction Pitfalls

Pitfall 1: Off-by-one in n_windows. The +1 in the formula matters. Forgetting it produces 170 windows instead of 171 - silent test-time drift if the loader expects fixed counts.
Pitfall 2: Engines shorter than W. Some FD003 engines fail in ~120 cycles; with W=30 you still get 91 windows, but with W=50 you get 71. Always check min(engine_length) when picking W.
Pitfall 3: Aligning RUL to the wrong cycle. The target is the RUL at the WINDOW'S LAST cycle, not its first. Reversing this is one of the most common bugs - the model appears to overfit because it is predicting the past instead of the future.
The point. One run-to-failure engine becomes 100+ training samples via windowing. The model never sees more than 30 cycles at a time. The RUL target is always the cycles-to-go at the window's last cycle.

Takeaway

  • Window length 30 is the C-MAPSS standard. Stride 1 for training, last-window-only for evaluation.
  • n_windows = floor((N - W)/S) + 1. Memorise this formula; it is the foundation of every loader in this book.
  • RUL is computed at the window's LAST cycle. Reversing this aligns prediction to the past, not the future.
Loading comments...