Process Engineering Pipeline¤
From Azure Blob timeseries to setpoint adherence, startup detection, control loop health, and process stability scores.
Signals needed:
| Role | UUID example | Type | Description |
|---|---|---|---|
| Setpoint | temperature_setpoint |
value_double |
Target value from recipe/PLC |
| Actual value | temperature_actual |
value_double |
Measured process value (PV) |
| Controller output | temperature_output |
value_double |
Control valve position / PID output (optional) |
Modules used: AzureBlobParquetLoader | MetadataJsonLoader | ContextEnricher | DataHarmonizer | SetpointChangeEvents | StartupDetectionEvents | SteadyStateDetectionEvents | ControlLoopHealthEvents | ProcessStabilityIndex
Prerequisites¤
AZURE_CONNECTION = "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=..."
CONTAINER = "timeseries-data"
UUID_LIST = [
"temperature_setpoint", # double: target value
"temperature_actual", # double: process value (PV)
"temperature_output", # double: controller output (optional)
]
START = "2024-06-01"
END = "2024-06-08"
METADATA_PATH = "config/signal_metadata.json"
# Process specifications
TARGET_VALUE = 100.0
UPPER_SPEC = 105.0
LOWER_SPEC = 95.0
Step 1: Load Data from Azure¤
from ts_shape.loader.timeseries.azure_blob_loader import AzureBlobParquetLoader
loader = AzureBlobParquetLoader(
connection_string=AZURE_CONNECTION,
container_name=CONTAINER,
)
df = loader.load_files_by_time_range_and_uuids(
start_timestamp=START,
end_timestamp=END,
uuid_list=UUID_LIST,
)
print(f"Loaded {len(df):,} rows, {df['uuid'].nunique()} signals")
Step 2: Enrich with Metadata¤
from ts_shape.loader.metadata.metadata_json_loader import MetadataJsonLoader
from ts_shape.loader.context.context_enricher import ContextEnricher
meta = MetadataJsonLoader.from_file(METADATA_PATH)
enricher = ContextEnricher(df)
df = enricher.enrich_with_metadata(meta.to_df(), columns=["description", "unit", "area"])
Step 3: Validate Data Quality & Harmonize¤
from ts_shape.transform.harmonization import DataHarmonizer
harmonizer = DataHarmonizer(df, value_column="value_double")
# Check for gaps in both signals
gaps = harmonizer.detect_gaps(threshold="10s")
if not gaps.empty:
print("Signal gaps:")
print(gaps.groupby("uuid")["gap_duration"].agg(["count", "max"]))
# Resample to uniform 1-second grid for clean alignment
df_uniform = harmonizer.resample_to_uniform(freq="1s", method="linear")
print(f"Uniform grid: {len(df_uniform)} rows")
Why harmonize?
Setpoint and actual value signals often arrive at different rates (setpoint changes only on recipe switch, PV updates every second). Resampling to a uniform grid ensures correct SP-PV alignment for control loop analysis.
# Align setpoint and actual for side-by-side analysis
aligned = harmonizer.align_asof(
left_uuid="temperature_setpoint",
right_uuid="temperature_actual",
tolerance="2s",
direction="nearest",
)
print(aligned.head())
Step 4: Detect Setpoint Changes¤
from ts_shape.events.engineering.setpoint_events import SetpointChangeEvents
sp_events = SetpointChangeEvents(
dataframe=df,
setpoint_uuid="temperature_setpoint",
)
# Detect step changes (magnitude >= 1.0 degree)
steps = sp_events.detect_setpoint_steps(min_delta=1.0, min_hold="30s")
print(f"Setpoint steps detected: {len(steps)}")
if not steps.empty:
print(steps[["start", "magnitude", "prev_level", "new_level"]].head())
# Measure how well the process follows setpoint changes
settling = sp_events.settling_time(
actual_uuid="temperature_actual",
min_delta=1.0,
band_pct=0.02, # 2% settling band
max_window="5min",
)
print(f"Average settling time: {settling['settling_seconds'].mean():.1f}s")
# Overshoot analysis
overshoot = sp_events.overshoot(
actual_uuid="temperature_actual",
min_delta=1.0,
max_window="5min",
)
print(f"Average overshoot: {overshoot['overshoot_pct'].mean():.1f}%")
Step 5: Startup Detection¤
from ts_shape.events.engineering.startup_events import StartupDetectionEvents
startup = StartupDetectionEvents(
dataframe=df,
target_uuid="temperature_actual",
)
# Detect startups by threshold crossing
startups = startup.detect_startup_by_threshold(
threshold=50.0, # process considered "started" above 50 degrees
min_above="60s", # must stay above for 1 minute
)
print(f"Startup events: {len(startups)}")
if not startups.empty:
print(startups[["start", "end"]].head())
Startup vs steady state
Startup detection identifies when the process begins. Combine with steady-state detection (next step) to find when the process stabilizes after startup.
Step 6: Steady-State Detection¤
from ts_shape.events.engineering.steady_state_detection import SteadyStateDetectionEvents
steady = SteadyStateDetectionEvents(
dataframe=df,
signal_uuid="temperature_actual",
)
# Find steady-state intervals (low variance periods)
steady_intervals = steady.detect_steady_state(
window="60s",
threshold=0.5, # rolling std below 0.5 = steady
min_duration="120s", # must be steady for at least 2 minutes
)
print(f"Steady-state intervals: {len(steady_intervals)}")
# Find transient (dynamic) periods
transients = steady.detect_transient_periods(
window="60s",
threshold=0.5,
)
print(f"Transient periods: {len(transients)}")
# Summary statistics
stats = steady.steady_state_statistics(window="60s", threshold=0.5)
print(f"Steady-state time: {stats['steady_pct']:.1f}%")
print(f"Transient time: {stats['transient_pct']:.1f}%")
Step 7: Control Loop Health¤
from ts_shape.events.engineering.control_loop_health import ControlLoopHealthEvents
loop = ControlLoopHealthEvents(
dataframe=df,
setpoint_uuid="temperature_setpoint",
actual_uuid="temperature_actual",
output_uuid="temperature_output", # optional: controller output
)
# Error integrals per shift (8-hour windows)
integrals = loop.error_integrals(window="8h")
print("--- Error Integrals per Shift ---")
print(integrals)
# Columns: window_start, IAE, ISE, ITAE, bias
# Detect sustained oscillation in the error signal
oscillation = loop.detect_oscillation(
window="30min",
min_cycles=3,
)
print(f"Oscillation events: {len(oscillation)}")
# Check for valve saturation (output pegged at 0% or 100%)
if "temperature_output" in UUID_LIST:
saturation = loop.output_saturation(
low_pct=2.0, # below 2% = saturated low
high_pct=98.0, # above 98% = saturated high
min_duration="60s",
)
print(f"Saturation events: {len(saturation)}")
# Shift-level report card
summary = loop.loop_health_summary(window="8h")
print("\n--- Loop Health Summary ---")
print(summary)
Step 8: Process Stability Score¤
from ts_shape.events.engineering.process_stability_index import ProcessStabilityIndex
stability = ProcessStabilityIndex(
dataframe=df,
signal_uuid="temperature_actual",
target=TARGET_VALUE,
upper_spec=UPPER_SPEC,
lower_spec=LOWER_SPEC,
)
# Composite 0-100 stability score per shift
scores = stability.stability_score(window="8h")
print("--- Stability Scores ---")
print(scores)
# Is stability improving or degrading?
trend = stability.score_trend(window="8h")
print(f"Trend direction: {trend['direction'].iloc[-1]}")
# Worst periods for investigation
worst = stability.worst_periods(window="8h", n=3)
print("--- Worst 3 Periods ---")
print(worst)
Results¤
| Output | Description | Use case |
|---|---|---|
steps |
Setpoint change events with magnitude | Recipe tracking |
settling |
Time-to-settle per setpoint change | Tuning assessment |
overshoot |
Overshoot % per change | Control quality |
startups |
Equipment startup intervals | Startup optimization |
steady_intervals |
Steady-state vs transient periods | Process efficiency |
integrals |
IAE/ISE/ITAE per window | Loop performance KPIs |
oscillation |
Oscillation detection events | Tuning issues |
scores |
0-100 stability score per shift | Daily process health |
Next Steps¤
- Correlate setpoint changes with Quality & SPC to find which changes cause quality issues
- Use stability scores alongside OEE Dashboard for a complete production overview
- Feed startup times into Cycle Time Analysis to exclude warm-up from cycle statistics