Microstop Thresholds for Packaging Line Changeovers
A high-speed packaging line stopping for 14 minutes to swap a bottle-size changeparts kit is not a downtime event at all — it is scheduled non-production time that should never touch the Availability denominator — while a 12-second label-web break on the same line during a run is exactly the kind of microstop that threshold tuning for microstops exists to isolate. Conflating the two is the single most common OEE-corrupting bug on packaging lines: teams either bury real changeover time inside “downtime” and tank Availability on every SKU change, or they let the changeover’s own startup ramp mask genuine microstops as normal variance. This page covers the packaging-specific fix inside Downtime Classification & OEE Calculation — a three-way taxonomy, state-gated exclusion of planned changeover from the Availability formula, and duration thresholds tuned for a line that stops far more often, and far more briefly, than a machining center ever does.
Packaging lines run at 100–400 cycles per minute on fillers and cappers, so a handful of seconds of infeed starvation or a web-splice stall is routine — but only while the line is in a RUN state. The state a stop occurs in, not just its duration, decides which OEE bucket it belongs to.
Changeover vs microstop vs downtime taxonomy Permalink to this section
Three state categories exist on a packaging line and each has a different relationship to the OEE formula’s time base. RUN intervals contribute Run Time, the numerator of Availability. Planned CHANGEOVER — a format-part swap, label-roll change, or sanitation break scheduled on the production calendar — is not downtime at all; it is subtracted from All Time before Planned Production Time is even computed, so it never enters the Availability ratio in either direction. Everything else that stops the line while it should be running is unplanned, and duration alone then decides whether it is a microstop (Performance loss) or logged downtime (Availability loss).
from __future__ import annotations
from dataclasses import dataclass
from enum import Enum
class LineState(str, Enum):
RUN = "RUN"
CHANGEOVER = "CHANGEOVER"
MICROSTOP = "MICROSTOP"
DOWN = "DOWN"
@dataclass(frozen=True)
class StateInterval:
state: LineState
duration_sec: float
is_scheduled: bool # False for CHANGEOVER: never in Planned Production Time
def classify_interval(
state: LineState,
duration_sec: float,
*,
min_threshold_sec: float = 5.0,
max_threshold_sec: float = 90.0,
) -> LineState:
"""Resolve an unplanned stop interval into MICROSTOP or DOWN.
Only intervals already tagged as unplanned stops reach this function —
CHANGEOVER is resolved earlier, from the HMI/MES format-change flag,
never from duration.
"""
if state is not LineState.DOWN:
raise ValueError("classify_interval expects a raw unplanned-stop interval")
if duration_sec < min_threshold_sec:
raise ValueError("interval shorter than debounce floor should not reach classification")
return LineState.MICROSTOP if duration_sec < max_threshold_sec else LineState.DOWN
The is_scheduled flag is the load-bearing detail: it is set from the changeover state itself, never inferred from how long the stop lasted. A changeover that runs long because a changepart is missing is still a changeover, not downtime — the corrective action is a maintenance-planning conversation, not a reclassification of the interval.
State-gated exclusion of planned changeover from Availability Permalink to this section
Excluding changeover correctly means removing it from both sides of the Availability ratio, not subtracting it from Downtime. A common bug treats CHANGEOVER as a Downtime category with its own reason code inside the numerator’s subtraction — which is arithmetically identical to counting it as Run Time, since
is unaffected by what you call time that was never Planned Production Time to begin with. The correct contract computes Planned Production Time as All Time minus every category of scheduled non-production time, changeover included, before Availability is evaluated at all:
import pandas as pd
def compute_availability(
intervals: pd.DataFrame,
*,
all_time_sec: float,
) -> dict[str, float]:
"""Compute Availability with CHANGEOVER excluded from Planned Production Time.
`intervals` has columns `state` (LineState value) and `duration_sec`,
covering the full reporting window with no gaps.
"""
total = intervals["duration_sec"].sum()
if abs(total - all_time_sec) > 1.0:
raise ValueError(
f"intervals sum to {total:.1f}s, expected {all_time_sec:.1f}s "
"(no gaps/overlaps allowed before this computation)"
)
changeover_sec = intervals.loc[intervals["state"] == "CHANGEOVER", "duration_sec"].sum()
down_sec = intervals.loc[intervals["state"] == "DOWN", "duration_sec"].sum()
run_sec = intervals.loc[intervals["state"].isin(["RUN", "MICROSTOP"]), "duration_sec"].sum()
planned_production_sec = all_time_sec - changeover_sec
if planned_production_sec <= 0:
raise ValueError("changeover time cannot equal or exceed all_time_sec")
# Run Time excludes MICROSTOP dwell — that time is lost to Performance,
# not Availability, so it must not appear in the Availability numerator.
microstop_sec = intervals.loc[intervals["state"] == "MICROSTOP", "duration_sec"].sum()
run_time_sec = run_sec - microstop_sec
availability = run_time_sec / planned_production_sec
return {
"planned_production_sec": planned_production_sec,
"run_time_sec": run_time_sec,
"down_sec": down_sec,
"availability": round(availability, 4),
}
Note the second, subtler exclusion inside run_time_sec: MICROSTOP dwell is bundled into the RUN/MICROSTOP bucket at the interval level (the line was scheduled to run) but must be stripped back out before the Availability numerator, because microstop time is Performance loss, not Run Time. Skipping that second subtraction is the mirror-image bug to mishandling changeover — it silently inflates Availability by letting Performance-loss seconds masquerade as uptime, which OEE formula validation should catch as an internally inconsistent factor split.
Short-stall thresholds on a packaging line Permalink to this section
Threshold values only apply while the line is gated into RUN — a stop cannot become a microstop candidate during CHANGEOVER, so the detector must check line state before it ever measures a dwell. Packaging equipment cycles far faster than a CNC cell or a conveyor motor, so its microstop window sits lower and narrower: a min_threshold_sec of roughly 5 s filters normal infeed hesitation and web-splice pauses, while a max_threshold_sec near 90 s is generous enough to cover a manual label-roll reload without letting a real jam hide inside Performance loss for a minute and a half.
def detect_run_state_microstops(
df: pd.DataFrame,
*,
debounce_hold_sec: float = 1.0,
min_threshold_sec: float = 5.0,
max_threshold_sec: float = 90.0,
) -> pd.DataFrame:
"""Flag microstops only among stops that occur inside a RUN-gated window.
`df` must carry `timestamp`, `line_state` ('RUN'/'CHANGEOVER'), and
`stopped` (bool, True while the line is not producing).
"""
required = {"timestamp", "line_state", "stopped"}
missing = required - set(df.columns)
if missing:
raise KeyError(f"detect_run_state_microstops: missing columns {sorted(missing)}")
df = df.sort_values("timestamp").copy()
# A stop is only threshold-eligible while the line was gated RUN —
# changeover-window stops are never microstop candidates regardless
# of how short they are.
eligible = df["stopped"] & (df["line_state"] == "RUN")
span_id = (eligible != eligible.shift()).cumsum()
spans = (
df[eligible]
.groupby(span_id[eligible])
.agg(start=("timestamp", "min"), end=("timestamp", "max"))
)
spans["dwell_sec"] = (spans["end"] - spans["start"]).dt.total_seconds()
spans = spans[spans["dwell_sec"] >= debounce_hold_sec].copy()
spans["is_microstop"] = (spans["dwell_sec"] >= min_threshold_sec) & (
spans["dwell_sec"] < max_threshold_sec
)
spans["is_downtime"] = spans["dwell_sec"] >= max_threshold_sec
return spans.reset_index(drop=True)
Any stop shorter than debounce_hold_sec — typically an infeed photoeye flicker at 100+ cycles/min — never becomes a span at all, which keeps the ledger free of sub-second noise that would otherwise dwarf the real microstop count on a fast line.
Gotchas & anti-patterns Permalink to this section
- Logging changeover as a downtime reason code. Even with a dedicated
CHANGEOVERcode, if it still runs through thePlanned − Downtimesubtraction it is arithmetically indistinguishable from counting it as uptime. Exclude it from Planned Production Time directly. - Duration-only changeover detection. Inferring “this is probably a changeover” from a long stop duration instead of reading the HMI/MES format-change flag misclassifies a genuine 20-minute mechanical breakdown as scheduled time.
- One threshold set for every product family. A line running small vials at 380/min and the same line running large jugs at 60/min have different infeed dynamics; a shared
min_threshold_seceither buries real vial-line jams or over-counts jug-line hesitation. - Forgetting the startup ramp after changeover. The first minute after a changeover ends is often a slow ramp to rate, not steady
RUN; if that ramp is gated into threshold evaluation immediately, its natural slowdown reads as a string of microstops. - Double-subtracting microstop dwell. Some pipelines subtract
MICROSTOPtime from both Planned Production Time and Run Time. It belongs only in the Performance factor’s loss term — see OEE formula validation for the bounds check that catches this.
Quick reference Permalink to this section
| Line state | Typical duration | OEE treatment | Detection basis |
|---|---|---|---|
RUN |
— | Run Time (Availability + Performance) | HMI/MES state flag |
CHANGEOVER |
8–25 min | Excluded from Planned Production Time | HMI/MES format-change flag, never duration |
MICROSTOP (in RUN) |
5–90 s | Performance loss | Duration threshold, RUN-gated |
DOWN (in RUN) |
≥90 s | Availability loss | Duration threshold, RUN-gated |
| Startup ramp post-changeover | ~60 s | Performance loss (slow cycles, not stops) | Rate comparison, not stop detection |
Related Permalink to this section
- Threshold Tuning for Microstops — the parent subsystem and shared parameter contract
- Microstop Thresholds for Conveyor Motor Jams — the sibling case for dual-signal jam detection
- Adjusting Performance Thresholds for Legacy CNC Machines — a single-signal, rolling-median variant of threshold tuning
- Shift Boundary Logic — handling a changeover that straddles a crew change