External Publication
Visit Post

PLC-Based Cyclic Pressure Test Sequencing: Profile Design and Fatigue Data Analysis

DEV Community [Unofficial] June 24, 2026
Source

Designing a cyclic pressure test programme requires more than just "pressurise and depressurise repeatedly." Ramp rate, hold time, and cycle count interact with the specific fatigue mechanism being investigated. Here's the implementation approach.

Pressure Profile State Machine

from enum import Enum
import time

class TestState(Enum):
    IDLE = 0
    FILLING = 1
    RAMPING_UP = 2
    HOLDING = 3
    DEPRESSURIZING = 4
    CYCLE_COMPLETE = 5
    TEST_COMPLETE = 6
    FAULT = 7

class CyclicPressureTest:
    def __init__(self, target_pressure_bar, ramp_time_s,
                 hold_time_s, total_cycles, depress_time_s=30):
        self.target_pressure = target_pressure_bar
        self.ramp_time = ramp_time_s
        self.hold_time = hold_time_s
        self.total_cycles = total_cycles
        self.depress_time = depress_time_s
        self.current_cycle = 0
        self.state = TestState.IDLE
        self.cycle_log = []

    def calculate_ramp_setpoint(self, elapsed_s):
        """Linear ramp profile — could substitute s-curve for gentler loading"""
        if elapsed_s >= self.ramp_time:
            return self.target_pressure
        return (elapsed_s / self.ramp_time) * self.target_pressure

    def run_cycle(self, pressure_sensor_read_fn, valve_control_fn):
        """
        Execute one complete pressure cycle.
        pressure_sensor_read_fn: callback returning current pressure
        valve_control_fn: callback to set valve position/PID output
        """
        cycle_start = time.time()
        cycle_data = {'cycle_num': self.current_cycle + 1, 'pressures': [], 'timestamps': []}

        # RAMP UP phase
        self.state = TestState.RAMPING_UP
        ramp_start = time.time()
        while (time.time() - ramp_start) < self.ramp_time:
            elapsed = time.time() - ramp_start
            setpoint = self.calculate_ramp_setpoint(elapsed)
            valve_control_fn(setpoint)
            actual = pressure_sensor_read_fn()
            cycle_data['pressures'].append(actual)
            cycle_data['timestamps'].append(time.time() - cycle_start)
            time.sleep(0.1)  # 10Hz sampling

        # HOLD phase
        self.state = TestState.HOLDING
        hold_start = time.time()
        while (time.time() - hold_start) < self.hold_time:
            valve_control_fn(self.target_pressure)
            actual = pressure_sensor_read_fn()
            cycle_data['pressures'].append(actual)
            cycle_data['timestamps'].append(time.time() - cycle_start)

            # Check for sudden pressure drop = potential failure
            if len(cycle_data['pressures']) > 5:
                recent_drop = cycle_data['pressures'][-5] - actual
                if recent_drop > self.target_pressure * 0.05:  # 5% sudden drop
                    self.state = TestState.FAULT
                    return {'status': 'FAULT', 'reason': 'Sudden pressure drop detected',
                            'cycle': self.current_cycle, 'data': cycle_data}
            time.sleep(0.1)

        # DEPRESSURIZE phase
        self.state = TestState.DEPRESSURIZING
        valve_control_fn(0)
        time.sleep(self.depress_time)

        self.current_cycle += 1
        self.cycle_log.append(cycle_data)

        if self.current_cycle >= self.total_cycles:
            self.state = TestState.TEST_COMPLETE
            return {'status': 'TEST_COMPLETE', 'cycles_run': self.current_cycle}

        self.state = TestState.CYCLE_COMPLETE
        return {'status': 'CYCLE_OK', 'cycle': self.current_cycle}

Fatigue Data Analysis After Test Completion

import numpy as np

def analyze_cycle_consistency(cycle_log):
    """
    Check if peak pressure and ramp time drift over the course of the test —
    drift can indicate developing leaks or actuator degradation.
    """
    peak_pressures = []
    ramp_durations = []

    for cycle in cycle_log:
        peak_pressures.append(max(cycle['pressures']))
        # Find time to reach 95% of target (proxy for ramp performance)
        target_95 = max(cycle['pressures']) * 0.95
        ramp_idx = next((i for i, p in enumerate(cycle['pressures']) if p >= target_95), None)
        if ramp_idx:
            ramp_durations.append(cycle['timestamps'][ramp_idx])

    return {
        'peak_pressure_trend': {
            'mean': np.mean(peak_pressures),
            'std': np.std(peak_pressures),
            'first_10_mean': np.mean(peak_pressures[:10]),
            'last_10_mean': np.mean(peak_pressures[-10:]),
            'drift_pct': (np.mean(peak_pressures[-10:]) - np.mean(peak_pressures[:10]))
                         / np.mean(peak_pressures[:10]) * 100
        },
        'ramp_time_trend': {
            'mean_s': np.mean(ramp_durations),
            'std_s': np.std(ramp_durations)
        }
    }

def generate_fatigue_report(component_id, cycle_log, target_pressure,
                            total_cycles_completed, standard='Customer-specified'):
    consistency = analyze_cycle_consistency(cycle_log)

    report = {
        'component_id': component_id,
        'test_standard': standard,
        'target_pressure_bar': target_pressure,
        'cycles_completed': total_cycles_completed,
        'cycles_log_summary': {
            'total_recorded': len(cycle_log)
        },
        'consistency_analysis': consistency,
        'result': 'PASS' if abs(consistency['peak_pressure_trend']['drift_pct']) < 5
                  else 'REVIEW REQUIRED — Pressure drift detected'
    }
    return report

The Neometrix PLC Controlled Autoclave Pressure Tester implements this state-machine architecture for automated, unattended cyclic pressure testing with full data logging for fatigue qualification programmes. → https://neometrixgroup.com/products/PLC-controlled-autoclave-pressure-tester

Discussion in the ATmosphere

Loading comments...