Pressure Control Using a Pinch Valve: Non-Linear Control Output

  1. Overview
  2. Running the Scripts
  3. Code Example 1: Pressure Model Simulation
  4. Code Example 2: Complex PID Controller for the Pinch Valve

Overview

This guide aims to demonstrate how to implement a pressure control system using a Pinch Valve. The complexity lies in the unique characteristics of a Pinch Valve, which squeezes an elastomeric tube to regulate back pressure. The valve's flow coefficient (Cv) varies in a non-linear fashion with the valve's plunger position (percentage of travel, from full close to full open). This adds an extra layer of complexity to the control system.

As the valve pinches the tube, the effective opening size and shape change non-linearly. Consequently, the Cv (valve flow coefficient) also changes non-linearly with the percentage of valve opening.

Addressing Non-Linearity through Complex PID Scheduling

To manage this non-linearity, the controller must be designed to operate differently in various regions of operation. Specifically, when the total percentage of the valve's opening is small, the maximum control output change (delta_limit) has to be very small to avoid overshooting or other adverse effects.

The PID controller code includes a scheduling system that changes the control parameters based on the operating conditions. Here's a snippet of that part:

# Define multiple schedules with different controller settings
for error_range, control_range, delta_limit, dead_zone in [
    ((-50, 50), None, 0.00005, 10),
    ((-250, 250), None, 0.0005, None),
    ((-10000, 0), None, 0.05, None),
    (None, (0, 0.3), 0.020, None),
    (None, None, 0.050, None),
]:
    params = ScheduleParameters()
    params.kp = -1.0
    params.dead_zone = dead_zone
    params.delta_limit = delta_limit
    constraints = ScheduleConstraints()
    constraints.error = error_range
    constraints.control = control_range
    sched = Schedule(params, constraints)
    p.add_schedule(sched)

This scheduling system allows the controller to operate with different settings, including a dead zone, delta limits, and control ranges, depending on the state of the system. This makes it adaptable and more accurate, particularly when dealing with the non-linear characteristics of the Pinch Valve.

Running the Scripts

To run the simulation and control, you'll need to execute two separate Python scripts in sequence:

  1. Pressure Model Simulation (model.py): First, run the model.py script to simulate the pressure control system. Execute this command in your terminal:

    python3 examples/pid/pinch_valve/model.py -r 0
    
  2. Complex PID Controller (pid.py): Once model.py is running, execute the pid.py script. This will set up a PID controller to regulate the Pinch Valve based on the pressure readings. Run this command:

    python3 -i examples/pid/pinch_valve/pid.py
    

Note: It's crucial to run model.py before executing pid.py to ensure the simulation environment is initialized correctly.

pid_controller_pinch_valve

Code Example 1: Pressure Model Simulation

Imports

import argparse
import time
from aqueduct.core.aq import Aqueduct
from aqueduct.core.aq import InitParams
from aqueduct.devices.base.utils import DeviceTypes
from aqueduct.devices.pressure.transducer import PressureTransducer
from aqueduct.devices.pressure.transducer import PressureUnits
from aqueduct.devices.pump.peristaltic import PeristalticPump
from aqueduct.devices.valve.pinch import PinchValve

The PressureModel Class

The PressureModel class simulates a pressure control system using various devices such as a pump, pinch valve, and pressure transducer.

class PressureModel:
    def __init__(self, pump: PeristalticPump, pinch_valve: PinchValve, transducer: PressureTransducer, aqueduct: "Aqueduct"):
        self.pump = pump
        self.pv = pinch_valve
        self.tdcr = transducer
        self.aq = aqueduct
    # ...

Here's the full code for the model:

"""
Demo code demonstrating pressure estimation using a simple model for Aqueduct devices.
"""
# Import necessary modules
import argparse
import time

from aqueduct.core.aq import Aqueduct
from aqueduct.core.aq import InitParams
from aqueduct.devices.base.utils import DeviceTypes
from aqueduct.devices.pressure.transducer import PressureTransducer
from aqueduct.devices.pressure.transducer import PressureUnits
from aqueduct.devices.pump.peristaltic import PeristalticPump
from aqueduct.devices.valve.pinch import PinchValve


class PressureModel:
    """
    Simple model for estimating pressures in a filtration process using Aqueduct devices.
    """

    filtration_start_time: float = None
    filter_cv_retentate: float = 60

    def __init__(
        self,
        pump: PeristalticPump,
        pinch_valve: PinchValve,
        transducer: PressureTransducer,
        aqueduct: "Aqueduct",
    ):
        self.pump = pump
        self.pv = pinch_valve
        self.tdcr = transducer
        self.aq = aqueduct

    @staticmethod
    def calc_pv_cv(PV) -> float:
        """
        Calculate the Cv of the pinch valve.

        :param PV: Pinch valve position.
        :type PV: float

        :return: Cv of the pinch valve.
        :rtype: float
        """
        if PV < 0.35:
            return max(100 - (1 / PV**2), 1)
        else:
            return 100

    def calc_p1(self, R1, PV) -> float:
        """
        Calculate the pressure drop between retentate and permeate.

        :param R1: Flow rate in the pass-through leg of the TFF filter.
        :type R1: float

        :param PV: Pinch valve position.
        :type PV: float

        :return: Pressure drop between retentate and permeate.
        :rtype: float
        """
        try:
            return 1 / (PressureModel.calc_pv_cv(PV) * 0.865 / R1) ** 2
        except ZeroDivisionError:
            return 0

    def calc_pressures(self):
        """
        Calculate and update the pressures using the model equations.
        """
        p1 = self.calc_p1(self.pump.live[0].ml_min, self.pv.live[0].pct_open)
        p1 = min(p1, 50)
        self.tdcr.set_sim_values(values=(p1,), units=PressureUnits.PSI)


if __name__ == "__main__":

    # Parse the initialization parameters from the command line
    params = InitParams.parse()

    # Initialize the Aqueduct instance with the provided parameters
    aq = Aqueduct(
        params.user_id,
        params.ip_address,
        params.port,
        register_process=params.register_process,
    )

    # Perform system initialization if specified
    aq.initialize(params.init)

    # Set a delay between sending commands to the pump
    aq.set_command_delay(0.05)

    parser = argparse.ArgumentParser()

    parser.add_argument(
        "-c",
        "--clear",
        type=int,
        help="clear and create the setup (either 0 or 1)",
        default=1,
    )

    args, _ = parser.parse_known_args()
    clear = bool(args.clear)

    # Define names for devices
    PUMP_NAME = "PP"
    XDCR_NAME = "TDCR"
    PV_NAME = "PV"

    if clear:
        # Clear the existing setup and add devices
        aq.clear_setup()

        aq.add_device(DeviceTypes.PERISTALTIC_PUMP, PUMP_NAME, 1)
        aq.add_device(DeviceTypes.PRESSURE_TRANSDUCER, XDCR_NAME, 1)
        aq.add_device(DeviceTypes.PINCH_VALVE, PV_NAME, 1)

    # Retrieve the setup to confirm the added devices
    aq.get_setup()

    # Retrieve device instances
    pp: PeristalticPump = aq.devices.get(PUMP_NAME)
    tdcr: PressureTransducer = aq.devices.get(XDCR_NAME)
    pv: PinchValve = aq.devices.get(PV_NAME)

    # Create an instance of the PressureModel
    model = PressureModel(pp, pv, tdcr, aq)

    # Continuous pressure calculation loop
    while True:
        model.calc_pressures()
        time.sleep(0.1)

Code Example 2: Complex PID Controller for the Pinch Valve

Imports

from aqueduct.core.aq import Aqueduct
from aqueduct.core.aq import InitParams
from aqueduct.core.pid import Pid
from aqueduct.core.pid import Schedule
from aqueduct.core.pid import ScheduleConstraints
from aqueduct.core.pid import ScheduleParameters
from aqueduct.devices.pressure.transducer import PressureTransducer
from aqueduct.devices.pump.peristaltic import PeristalticPump
from aqueduct.devices.valve.pinch import PinchValve

Device Retrieval

pp: PeristalticPump = aq.devices.get(PUMP_NAME)
tdcr: PressureTransducer = aq.devices.get(XDCR_NAME)
pv: PinchValve = aq.devices.get(PV_NAME)

PID Controller Setup

The PID controller is set up with multiple schedules to adapt to the system's non-linear nature.

# Define PID controller parameters
process = tdcr.to_pid_process_value(index=0)
control = pv.to_pid_control_output(index=0)
p = Pid(500)

Here's the full code for PID control:

"""
Demonstration of setting up a PID controller with Aqueduct devices.
"""
# Import necessary modules
from aqueduct.core.aq import Aqueduct
from aqueduct.core.aq import InitParams
from aqueduct.core.pid import Pid
from aqueduct.core.pid import Schedule
from aqueduct.core.pid import ScheduleConstraints
from aqueduct.core.pid import ScheduleParameters
from aqueduct.devices.pressure.transducer import PressureTransducer
from aqueduct.devices.pump.peristaltic import PeristalticPump
from aqueduct.devices.valve.pinch import PinchValve

# Parse the initialization parameters from the command line
params = InitParams.parse()

# Initialize the Aqueduct instance with the provided parameters
aq = Aqueduct(params.user_id, params.ip_address, params.port)

# Perform system initialization if specified
aq.initialize(params.init)

# Set a delay between sending commands to the pump
aq.set_command_delay(0.05)

# Define names for devices
PUMP_NAME = "PP"
XDCR_NAME = "TDCR"
PV_NAME = "PV"

# Retrieve the setup to confirm the added devices
aq.get_setup()

# Retrieve device instances
pp: PeristalticPump = aq.devices.get(PUMP_NAME)
tdcr: PressureTransducer = aq.devices.get(XDCR_NAME)
pv: PinchValve = aq.devices.get(PV_NAME)

# Define PID controller parameters
process = tdcr.to_pid_process_value(index=0)
control = pv.to_pid_control_output(index=0)
p = Pid(500)

# Define multiple schedules with different controller settings
for error_range, control_range, delta_limit, dead_zone in [
    ((-50, 50), None, 0.00005, 10),
    ((-250, 250), None, 0.0005, None),
    ((-10000, 0), None, 0.05, None),
    (None, (0, 0.3), 0.020, None),
    (None, None, 0.050, None),
]:
    params = ScheduleParameters()
    params.kp = -1.0
    params.dead_zone = dead_zone
    params.delta_limit = delta_limit
    constraints = ScheduleConstraints()
    constraints.error = error_range
    constraints.control = control_range
    sched = Schedule(params, constraints)
    p.add_schedule(sched)

# Set output limits for the PID controller
p.output_limits = (0.1, 1)

# Create a PID controller instance using Aqueduct
pid = aq.pid_controller("pinch_valve_control", process, control, p)