Skip to content

PID Level Control

In this tutorial you will put a PID controller on the tank of the previous page so that the outlet valve holds the level at a setpoint, in DWSIM's Classic UI. You will fill the tank to the setpoint, store that state, then hit the loop with a feed change and a setpoint change and watch what proportional and integral action each do. You will also see what happens when the controller is in manual, when the gains are too high and when they are too low.

What you will learn

  • How to wire a PID controller: controlled variable, manipulated variable, setpoint, direction, output limits, offset and span
  • What proportional-only control leaves behind (an offset) and what integral action removes
  • How to store a flowsheet state and start a schedule from it, how to switch a controller to manual, and what the PID tuning tool does

Prerequisites

  • Completed From Steady State to Dynamic: the tank, the two valves, the integrator and the schedule are reused as they are
  • The PID equation, at least in words: the output is a gain times the error, plus a gain times the integral of the error

Process Overview

The tank of the previous tutorial with a level controller closed around it:

  1. Inlet delivers 10 kg/s (Flow specification), later stepped to 15 kg/s
  2. T-01 is the 2 m³ tank; its liquid level is the controlled variable
  3. LIC-01 is the PID controller, reading the level and writing the opening of V-02
  4. V-02 is the outlet valve, Kv 400, the manipulated variable
  5. Outlet is held at 1 atm

Process Flow Diagram

graph LR
    IN["Inlet<br/>10 kg/s → 15 kg/s"] --> V1["V-01"]
    V1 --> T["T-01<br/>(Tank)"]
    T --> V2["V-02<br/>opening = MV"]
    V2 --> OUT["Outlet<br/>1 atm"]
    T -. level (PV) .-> LIC["LIC-01<br/>SP 1.0 m"]
    LIC -. opening (MV) .-> V2

Key Design Parameters

Parameter Value Unit
Controlled variable (PV) T-01 Liquid Level m
Manipulated variable (MV) V-02 Opening %
Setpoint 1.0 m
Action Reverse (level up, valve open) -
Output limits 0 to 100 %
Offset (MV at zero output) 29 %
Manipulated variable span 100 %
Kp, Ki, Kd (base tuning) 2, 0.1, 0 -
Integration step, duration 5, 10 s, min

Step-by-Step in the Classic UI

1. Open the tank flowsheet

Open the saved file of the previous tutorial, dynamic_tank_open_loop.dwxmz, and check that Dynamics > Dynamic Mode Active is on (the file opens with it off). The integrator Int1 and the schedule Sch1 exist. This Sch1 starts from the stored state NewState1, saved with the tank empty, so the fill of step 3 starts from an empty tank; its numbers are measured on this file.

2. Insert the controller

Drag a PID Controller from the Object Palette (Logical Blocks) to the canvas and rename it LIC-01. Open it:

  • Controlled Object T-01, Controlled Property Liquid Level, Units m
  • Manipulated Object V-02, Manipulated Property Opening, Units %
  • Set-Point: 1.0
  • Reverse Acting: ticked
  • Min./Max Output (Absolute): 0 / 100
  • Offset (Bias): 29
  • Kp / Ki / Kd: 2 / 0.1 / 0
  • Manipulated Variable Span (Advanced tab): 100

The PID controller editor

Direction, offset and span

The controller computes a dimensionless error (PV − SP)/|SP| and an output = Kp·error + Ki·∫error + Kd·d(error)/dt. With a span set, the valve opening is Offset + Output × Span for a reverse-acting controller. Reverse acting is what a level loop on an outlet valve needs: the level rises above the setpoint, the error is positive, the valve opens further. The offset is the opening the valve should have when the error is zero; 29 % is what V-02 needs to pass 10 kg/s with 1 m of head. Left at zero, the span switches the controller to its older arithmetic, in which the output is scaled by the setpoint; that only makes sense when the manipulated and the controlled variable are numbers of the same size.

3. Fill the tank to the setpoint

Run Sch1 from Integrator Controls (10 minutes). The tank starts empty. The controller shuts V-02 within 10 s and keeps it nearly closed while the level is far below the setpoint, then opens it as the level approaches 1 m. The level passes 1 m at 115 s, peaks at 1.022 m at 120 s and holds 1.000 m from about 3 min, with V-02 at 28.8 %.

Filling from your own flowsheet

If you continue with your own flowsheet of the previous page (Outlet back at 101325 Pa, V-02 at 50 %, 5 s step), its Sch1 starts from the current state and empties the tank before the run. The fill then goes a little differently: V-02 shuts at the first step, the level passes 1 m at 105 s, peaks at 1.039 m at 115 s and holds 1.000 m from about 3 min. It still ends at 1.000 m with V-02 at 28.8 %, and every run from step 5 on starts from the state stored in step 4, so the rest of the page gives the same numbers.

4. Store the steady state

Press Store Current in the Flowsheet States group of the flowsheet toolbar and name the state Steady. A stored state holds every object, the tank content and the controller included, so a schedule that starts from it always starts from the same 1.000 m and 28.8 %.

5. A feed disturbance

In the Dynamics Manager:

  • Event Sets: add FeedStep with one event at 00:02:00: object Inlet, property Mass Flow, value 15 kg/s, step change.
  • Schedules: add Disturbance with integrator Int1, event set FeedStep and initial state Steady.

Run Disturbance. The level holds at 1.000 m for two minutes, rises to 1.062 m 15 s after the step, and is back at 1.000 m by 4 min; V-02 goes from 28.8 % to 43.3 % and stays there, passing the new 15 kg/s.

Feed step with PI control

6. Proportional only

Add an event set POnly with two events: at 00:00:05 set LIC-01 Ki to 0, and the same feed step at 00:02:00. Add a schedule POnly from Steady with it and run.

The level rises to 1.070 m and settles at 1.065 m. It never comes back: with proportional action alone, the valve only opens further while there is an error, and passing 15 kg/s takes 41.9 % of opening, which needs an error of 0.065 m to produce. That standing error is the offset of proportional control. Raise Kp and it shrinks; only integral action removes it.

Why change the gain through an event

A schedule that starts from a stored state restores the controller with that state, tuning included. Setting Ki = 0 in the editor before the run would be undone the moment the run starts from Steady. An event at 5 s changes it inside the run; the alternative is to change the tuning, then store a new state.

7. Manual mode

Add an event set Manual with an event at 00:00:05 setting LIC-01 ManualOverride to 1, plus the feed step at 2 min. Run a schedule from Steady with it.

V-02 stays at 28.8 %. After the step the level climbs 0.26 m in the first minute, passes the top of the tank (2 m) at 9.4 min and reads 2.03 m at 10 min. When it passes 2 m the tank writes a warning to the log: the tank has no overflow in this model, so the level goes on rising. This is the open-loop response the controller was hiding: 5 kg/s more in than out is 5 L/s into a 1 m² tank, 0.3 m per minute at first, slowing down as the growing head pushes more water through V-02.

Moving the valve in manual

In manual the controller writes its output to V-02 at every step, so an event on the V-02 opening is undone at the next step. To move the valve with the loop in manual, put the event on LIC-01 OutputAbs, the output in %.

8. Too much and too little

Two more event sets, each with the feed step at 2 min and gain changes at 5 s:

  • Aggressive: Kp 20, Ki 1. The loop turns unstable as soon as the gains change at 5 s, before the feed step: the level swings between 0.84 and 1.13 m for the rest of the run, V-02 slams between 0 and 100 %, and the integral of the absolute error (IAE) is 36.3 m·s.
  • Sluggish: Kp 0.5, Ki 0.005. The level rises to 1.158 m and is still at 1.060 m after 10 min; IAE 40.3 m·s.

The base tuning (Kp 2, Ki 0.1) gave a peak of 1.062 m and an IAE of 1.7 m·s. Both errors are more than twenty times worse.

9. Setpoint change

Add an event set SPStep with one event at 00:01:00: LIC-01 SetPointAbs = 1.5. Run from Steady. V-02 shuts completely to build level, the level reaches 1.49 m at 2 min, overshoots to 1.53 m and settles at 1.500 m by 4 min with V-02 back at 23.6 %: less opening than before, because 1.5 m of head pushes the same 10 kg/s through a smaller Kv.

10. The PID tuning tool and real time

Dynamics > PID Controller Tuning runs a schedule repeatedly with a simplex search on the gains, minimising the sum of the absolute errors of the selected controllers. Point it at Disturbance and LIC-01 to see whether it finds anything better than the base tuning.

To watch the loop in real time, switch the flowsheet to Control Panel Mode, pick a schedule in Integrator Controls and press Real-Time (the clock button). A real-time run starts from the schedule's initial state and plays its events, so Disturbance brings the feed step two minutes in. Change the setpoint from the controller's face plate while it runs.

Results and Validation

Case Peak level Final level Final V-02 Unit
Fill from empty, PI 1.022 1.000 28.8 m, m, %
Feed step, PI (Kp 2, Ki 0.1) 1.062 1.000 43.3 m, m, %
Feed step, P only (Kp 2) 1.070 1.065 41.9 m, m, %
Feed step, manual 2.03 at 10 min (above the 2 m top from 9.4 min) 2.03, still rising 28.8 m, m, %
Feed step, Kp 20, Ki 1 1.13, oscillating 0.84 to 1.13, still swinging 0 to 100 m, m, %
Feed step, Kp 0.5, Ki 0.005 1.158 1.060 (still moving) - m, m
Setpoint 1.0 → 1.5 m, PI 1.53 1.500 23.6 m, m, %

Expected results

With the base tuning the loop rejects a 50 % feed increase with a 6 cm peak and no offset, and follows a 50 cm setpoint change with 5 % overshoot. Proportional action alone leaves 6.5 cm of offset. In manual the level passes the top of the tank in under ten minutes.

Understanding the Results

A level loop on an outlet valve is the simplest loop there is, and it shows every feature of PID control. Proportional action moves the valve in proportion to the error, so it needs an error to hold a new steady state: the offset. Integral action keeps moving the valve while any error remains, so the offset goes to zero; too much of it, and the valve keeps moving after the error has changed sign, which is the oscillation of the aggressive case. Derivative action was left at zero here; on a noisy measurement it does more harm than good, and a tank level has no dynamics fast enough to need it.

The manual case is the one to remember. With V-02 fixed, the outflow still grows with the square root of the head, so this tank does find a new balance, at about 2.25 m (2.246 m after 30 min), above its top. The controller was hiding how far, and how slowly, the level goes when nothing moves the valve. A tank drained by a pump, whose flow does not depend on the level, has no balance at all: it fills or empties at a constant rate. That is why level loops are always closed, and why a level controller in manual is the first thing an operator looks for when a tank alarm sounds.

Bumpless transfer

When the controller goes from manual back to automatic, DWSIM initialises the integral term so that the output starts from the current valve position, with no jump. Try it: an event ManualOverride = 0 at 5 min in the Manual schedule brings the loop back with no jump at the switch; what follows is the controller's normal answer to a 0.6 m error.

Automating This Tutorial

Files in this repository

import os
import sys

import pythonnet
pythonnet.load("netfx")              # DWSIM is a .NET Framework application
import clr  # noqa: E402

DWSIM_BIN = os.environ.get("DWSIM_BIN")
if not DWSIM_BIN:
    raise SystemExit("Set DWSIM_BIN to the DWSIM installation folder before running.")
for sub in ("", "unitops", "ppacks", "extenders"):
    sys.path.append(os.path.join(DWSIM_BIN, sub))
clr.AddReference("DWSIM.Automation")
clr.AddReference("DWSIM.Automation.FluentAPI")
clr.AddReference("DWSIM.UnitOperations")

from DWSIM.Automation.FluentAPI import Flowsheet, PropertyPackages, Q
from DWSIM.UnitOperations.UnitOperations import Valve
from System import TimeSpan

fs = (Flowsheet.Create("TankLevelControl")
      .WithCompound("Water").WithPropertyPackage(PropertyPackages.SteamTables))
inlet = (fs.AddMaterialStream("Inlet").At(Q.Kelvin(298.15), Q.Pascal(130000.0))
         .WithMassFlow(Q.KgPerSecond(10.0)).AsFlowSpec())
s1 = fs.AddMaterialStream("S-1").AsPressureSpec()
s2 = fs.AddMaterialStream("S-2").AsPressureSpec()
outlet = fs.AddMaterialStream("Outlet").AsPressureSpec()
(fs.AddValve("V-01").WithCalcMode(Valve.CalculationMode.Kv_Liquid).WithKv(100.0)
   .WithOpeningPercent(50.0).ConnectFeed(inlet, 0).ConnectProduct(s1, 0))
tank = (fs.AddTank("T-01").WithVolume(Q.CubicMeters(2.0)).WithHeight(Q.Meters(2.0))
        .ConnectFeed(s1, 0).ConnectProduct(s2, 0))
(fs.AddValve("V-02").WithCalcMode(Valve.CalculationMode.Kv_Liquid).WithKv(400.0)
   .WithOpeningKvRelationship().WithOpeningPercent(50.0)
   .ConnectFeed(s2, 0).ConnectProduct(outlet, 0))
fs.AutoLayout()
fs.Solve()
outlet.Object.Phases[0].Properties.pressure = 101325.0

lic = (fs.AddPIDController("LIC-01")
       .Controls("T-01", "Liquid Level", "m")
       .Manipulates("V-02", "PROP_VA_5", "")
       .WithSetPoint(1.0).WithTuning(2.0, 0.1, 0.0)
       .WithOutputLimits(0.0, 100.0).WithOffset(29.0).ReverseActing(True))
lic.Object.ManipulatedVariableSpan = 100.0

(fs.Dynamics.DefineIntegrator("Int1")
   .WithIntegrationStep(TimeSpan.FromSeconds(5)).WithDuration(TimeSpan.FromMinutes(10))
   .Monitor("T-01", "Liquid Level", "m", "level")
   .Monitor("V-02", "PROP_VA_5", "", "V-02 opening")
   .Monitor("Inlet", "PROP_MS_2", "kg/s", "inlet flow"))
fs.Dynamics.WithHistorian(True, 5000)

# fill the tank to the setpoint, then keep that state
fs.Dynamics.DefineSchedule("Fill").WithIntegrator("Int1").UseCurrentStateAsInitial(True).MakeCurrent()
fs.RunDynamics("Fill").Execute()
fs.Dynamics.StoreCurrentStateAs("Steady")

# a feed step from the stored state
(fs.Dynamics.DefineEventSet("FeedStep")
   .AddStepChange("Inlet", "PROP_MS_2", 15.0, Q.Seconds(120.0), "kg/s", "feed step"))
(fs.Dynamics.DefineSchedule("Disturbance").WithIntegrator("Int1")
   .WithEventSet("FeedStep").WithInitialState("Steady").MakeCurrent())
result = fs.RunDynamics("Disturbance").Execute()
level = result.GetSeries("level")
print(f"peak = {level.Max:.3f} m, final = {level.Final:.3f} m, IAE = {level.IAE(1.0):.2f} m.s")
print(f"V-02 final = {result.GetSeries('V-02 opening').Final:.1f} %")

# proportional only: change the gain inside the run, because the stored state restores it
(fs.Dynamics.DefineEventSet("POnly")
   .AddStepChange("LIC-01", "Ki", 0.0, Q.Seconds(5.0), "", "Ki off")
   .AddStepChange("Inlet", "PROP_MS_2", 15.0, Q.Seconds(120.0), "kg/s", "feed step"))
(fs.Dynamics.DefineSchedule("POnly").WithIntegrator("Int1")
   .WithEventSet("POnly").WithInitialState("Steady").MakeCurrent())
level = fs.RunDynamics("POnly").Execute().GetSeries("level")
print(f"P only: offset = {level.Final - 1.0:.3f} m")

# save the settled tank at 10 kg/s with the base tuning
fs.Inner.LoadProcessData(fs.Inner.StoredSolutions["Steady"])
fs.Dynamics.Schedule("Fill").MakeCurrent()
fs.Save("dynamic_tank_level_control.dwxmz")

Add the controller with dwsim.unitop.add (type PID Controller) and set its properties with dwsim.unitop.set; define event sets and schedules with the dwsim.dynamics.* tools, store the state, and run each schedule.

Output may vary

Results depend on the LLM's reasoning quality and tool-use accuracy. Always verify the simulation matches your intent before relying on the numbers.

Open the water tank dynamic flowsheet (tank between two Kv valves, inlet 10 kg/s
flow spec, outlet 1 atm) in DWSIM via the MCP server and:

- Add a PID controller "LIC-01": controlled variable T-01 liquid level (m),
  manipulated variable V-02 opening (%), setpoint 1.0 m, reverse acting,
  output limits 0-100 %, offset 29 %, manipulated variable span 100,
  Kp 2, Ki 0.1, Kd 0
- Run the existing schedule (5 s step, 10 min) to fill the tank to the setpoint,
  then store the flowsheet state as "Steady"
- Define an event set with a step of the inlet mass flow to 15 kg/s at 2 min and
  a schedule "Disturbance" that starts from "Steady" with it; run it
- Report the peak level, the final level and the final V-02 opening
- Repeat with an extra event at 5 s that sets the controller's Ki to 0, and report
  the final level (the proportional offset)

Exercises

  1. With proportional action only, double Kp to 4 and then to 8. How does the offset change? What happens to the valve movement?
  2. Add Kd = 5 to the base tuning and rerun the feed step. Does the peak change? Look at the valve opening in the first minute after the step.
  3. Put an event ManualOverride = 0 at 5 min in the Manual schedule. Where does the level end, and how long does the controller take to bring it back?

Further Reading

Selected references from the DWSIM technical bibliography. Click the DOI link to access each paper.

  • K. J. Åström & T. Hägglund. (2006). Advanced PID Control. ISA
  • D. E. Seborg, T. F. Edgar, D. A. Mellichamp & F. J. Doyle. (2016). Process Dynamics and Control. Wiley

Next Steps

In Separator Pressure and Level Control, two loops share one vessel: a pressure controller on the gas outlet and a level controller on the liquid outlet, with setpoint changes scheduled as events.