Skip to content

Custom Controllers and Networks

In this tutorial you will open two of the dynamic samples shipped with DWSIM in the Classic UI: the water tank whose level is held by a controller written in Python in place of a PID, and a district water supply network whose tank level is held by the setting pressure of a pressure-reducing valve. The first shows what the Python Controller block gives you when a PID is not the right law; the second shows a control loop whose manipulated variable lives inside a pipe network.

What you will learn

  • What the Python Controller block receives on every step and what it has to return
  • How a model-based controller uses the valve equation and the tank balance directly
  • How a PID can manipulate a variable inside a Pipe Network unit, and what the bias and span mean when that variable is a pressure

Prerequisites

  • Completed PID Level Control: the tank, the valve equation and the loop vocabulary are reused
  • Basic Python: assignments, if, arithmetic

Process Overview

Two flowsheets from the Samples list:

  1. Dynamic Simulation - Water Tank Level with Custom Controller: the tank of the earlier tutorials (inlet 10 kg/s at 1.3 bar, V-01 with fixed Kv 100, T-01 of 2 m³, V-02 with Kv 400), with a Python Controller PC-1 in place of the PID, setpoint 1.7 m, and an event set that steps the inlet to 15, 7.5, 30 and back to 10 kg/s at 2, 7, 10 and 15 min.
  2. Pipe Network - District Supply with Tank Level Control (Dynamic): a header at 8 bar, a 2 km main, a pressure-reducing valve set to 3.41 bar, a 400 m feeder and a delivery at 1.2 bar, all inside one Pipe Network unit; downstream, a 24 m³ tank and a flow-control valve to the district. A PID LIC-01 holds the tank level at 3 m by moving the PRV setting pressure.

Process Flow Diagram

graph LR
    IN["inlet<br/>10 kg/s (steps)"] --> V1["V-01"]
    V1 --> T["T-01<br/>2 m³"]
    T --> V2["V-02<br/>Kv 400"]
    V2 --> OUT["outlet"]
    T -. level .-> PC["PC-1<br/>(Python)<br/>SP 1.7 m"]
    PC -. opening .-> V2
graph LR
    H["header<br/>8 bar"] --> NET["WATERNET<br/>main 2 km → PRV-01 → feeder 400 m"]
    NET --> D["delivered<br/>1.2 bar"]
    D --> TK["TK-01<br/>24 m³, 6 m"]
    TK --> FCV["FCV-01<br/>Kv 170"]
    FCV --> DIS["district"]
    TK -. level .-> LIC["LIC-01<br/>SP 3 m"]
    LIC -. PRV setting .-> NET

Key Design Parameters

Parameter Tank with Python controller District network
Property package Steam Tables Steam Tables
Feed 10 kg/s, 1.3 bar, Flow spec 34.7 kg/s at 8 bar (header, Pressure spec inside the network)
Holdup T-01: 2 m³, 2 m, level 1.7 m TK-01: 24 m³, 6 m, level 3.0 m
Manipulated variable V-02 opening (%) PRV-01 setting pressure (Pa)
Setpoint 1.7 m 3.0 m
Controller Python script, see below PID Kp 3, Ki 0.004, direct acting, bias 341,435 Pa, span 200,000 Pa, limits 1.6 to 6.5 bar
Integrator 1 s step, 30 min 5 s step, 60 min
Events inlet 15 kg/s at 2 min, 7.5 at 7, 30 at 10, 10 at 15 LIC-01 setpoint 3.6 m at 30 min

Step-by-Step in the Classic UI

1. Open the custom controller sample

File > Samples (or the Samples list on the welcome screen), Dynamic Simulation - Water Tank Level with Custom Controller. Turn Dynamic Mode on if it is off.

2. Read the script

Double-click PC-1 and open its script. On every integration step DWSIM runs it with three names already defined: Flowsheet (the flowsheet object), Me (the controller) and PV (the current value of the controlled variable). The script has to leave two names set when it ends: MV, the value written to the manipulated variable, and SP, the setpoint it worked to.

This one is a model-based controller. It reads the tank level and volume, the pressures on both sides of V-02 and the current inlet flow, and computes the opening in three parts:

  • a base opening abt0 = 23.8 %, the opening that passes 10 kg/s at the design level;
  • a feedforward correction from the flow change, straight from the valve equation: the extra opening needed to pass wnew − w0 under the current pressure drop;
  • a feedback correction from the level error, sized so the level moves back at a rate that grows with the square of the error.

The sum is clipped to 0 to 100 % and returned as MV. No gains to tune: the valve equation and the tank area are the model.

The Python controller script

3. Run the schedule

Dynamics > Integrator Controls, Sch1, Run. Thirty minutes at 1 s take a minute or two. The event set Perturbation steps the inlet four times.

4. Read the results

The level stays at 1.71 m through the whole run, within 0.04 m of the setpoint even when the inlet triples to 30 kg/s. V-02 answers each step at once: 31.7 % at the start, 29.9 % once the loop has settled, 44.8 % from 2 min (15 kg/s), 22.4 % from 7 min (7.5 kg/s), 89.6 % from 10 min (30 kg/s) and back to 29.9 % after 15 min. The feedforward term does most of the work: the opening jumps with the flow and the feedback term only trims the small level error left behind.

Level and opening with the Python controller

5. Open the district network sample

Pipe Network - District Supply with Tank Level Control (Dynamic). This sample needs the Pipe Network unit, which ships with the Patreon edition, and its water pipes and pressure-reducing valve need Patreon level 3 for the steady state and for the dynamic run; below that level the error names the blocks, the level needed and the current level.

6. Read the loop

Double-click LIC-01. The controlled variable is TK-01 Liquid Level; the manipulated variable is PRV-01: Setting Pressure inside WATERNET, in pascals. The controller is direct acting: a rising level lowers the PRV setting, less water reaches the tank, the level falls back. The bias is 341,435 Pa (the setting at the design point) and the span is 200,000 Pa: one unit of controller output moves the setting by 2 bar. Without a span the controller would compute its output in metres and pin the valve at a limit on the first step; this is the case the span was made for.

7. Run the schedule (Patreon)

Integrator Controls, Sch1, Run: one hour at 5 s. At 30 min the event set steps the setpoint to 3.6 m; the PRV setting rises, the feeder passes more, and the level climbs to the new setpoint over the next minutes while FCV-01 keeps delivering to the district.

Results and Validation

Variable Expected Unit
Tank level with the Python controller, whole run 1.67 to 1.75 m
V-02 opening at 15, 7.5, 30 and 10 kg/s 44.8, 22.4, 89.6, 29.9 %
District tank level before the setpoint step 3.0 m
PRV setting at the design point 3.41 bar

Expected results

The Python controller holds the level within 4 cm through feed steps of −25 % to +200 % because it knows the valve equation; a PID with the tuning of the previous tutorial would show a peak of several centimetres at each step. The network loop holds a level with a pressure: a manipulated variable in pascals needs a bias and a span in pascals.

Understanding the Results

A PID knows nothing about the process; that is its strength and its limit. The Python controller here knows two things, the valve equation and the tank cross-section, and uses them to compute the opening that balances the flows before the level has moved. That is feedforward, and it is why the level barely twitches when the feed triples. The feedback term is still there for what the model does not know: the density, the exact Kv, the outlet pressure drifting. Every model-based controller has this shape, a prediction plus a correction, and the Python block is the place to write one when the built-in PID and MPC blocks do not fit.

The district network shows the other half of the loop vocabulary. The manipulated variable is a pressure setting deep inside a network of pipes; the process gain from that pressure to the tank level goes through the feeder hydraulics, and its size is what the span encodes. Reading a loop starts with the units of its two ends.

Script rules

The script runs once per step, so keep it short: no loops that wait, no file access. Use Flowsheet.GetObject("tag") to reach any object, GetDynamicProperty("...") for level and pressure, GetMassFlow() and GetPressure() on streams. Set MV and SP before the last line; leaving either unset stops the run with an error naming the controller. Me.ResetRequested is true on the first step of a run, which is where the script clears its history.

Automating This Tutorial

Files in this repository

  • Python script: examples/dynamics/06_run_samples.py
  • In DWSIM: Samples list on the welcome screen, Dynamic Simulation - Water Tank Level with Custom Controller and Pipe Network - District Supply with Tank Level Control (Dynamic)
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

# the samples ship in the "samples" folder of the DWSIM installation; DWSIM_SAMPLES overrides it
samples = os.environ.get("DWSIM_SAMPLES", os.path.join(DWSIM_BIN, "samples"))

# the custom controller sample, run as it ships
fs = Flowsheet.Load(os.path.join(samples, "Dynamic Simulation - Water Tank Level with Custom Controller.dwxmz"))
fs.Solve()
result = fs.RunDynamics("Sch1").FromCurrentState(True).Execute()
level = result.GetSeries("Level")
opening = result.GetSeries("V2")
print(f"level: min {level.Min:.3f} m, max {level.Max:.3f} m")
for t in (60.0, 300.0, 540.0, 720.0, 1200.0):
    print(f"t = {t:5.0f} s  V-02 = {opening.ValueAt(t):5.1f} %")

# the district network (Patreon): the same call; the run reports the licence
# level it needs in result.Errors when the key does not cover it. The sample ships
# with the Patreon edition only, so the open edition skips this part.
network = os.path.join(samples, "Pipe Network - District Supply with Tank Level Control (Dynamic).dwxmz")
if os.path.isfile(network):
    net = Flowsheet.Load(network)
    r = net.RunDynamics("Sch1").FromCurrentState(True).Execute()
    print(r.Completed, [str(e)[:80] for e in r.Errors])
    if r.Completed:
        print(r.GetSeries("TK-01 Liquid Level").Final)
else:
    print("district network sample not found (Patreon edition only), skipped")

Open the sample with dwsim.flowsheet.open, run the schedule with the dwsim.dynamics.* tools and read the monitored variables from the result.

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 DWSIM sample "Dynamic Simulation - Water Tank Level with Custom
Controller" via the MCP server, run its schedule "Sch1" (30 min), and report the
minimum and maximum tank level and the V-02 opening at 1, 5, 9, 12 and 20 minutes.
Then explain, from the Python script of the controller PC-1, which term of the
opening changes when the inlet flow changes and which term changes when the
level drifts from 1.7 m.

Exercises

  1. Replace PC-1 with a PID (Kp 2, Ki 0.1, offset 30 %, span 100) and rerun the same event set. Compare the level peaks at each step.
  2. In the script, set the feedforward correction to zero (deltaabtF = 0.0) and rerun. How far does the level move at the 30 kg/s step?
  3. Write a controller that opens V-02 fully whenever the level passes 1.8 m and otherwise holds abt0. Is it stable? What does the valve do?

Further Reading

Selected references from the DWSIM technical bibliography.

  • D. E. Seborg, T. F. Edgar, D. A. Mellichamp & F. J. Doyle. (2016). Process Dynamics and Control. Wiley
  • T. E. Marlin. (2000). Process Control: Designing Processes and Control Systems for Dynamic Performance. McGraw-Hill

Next Steps

This closes the Dynamics track. The Operator Training pages take these plant models into an instructor-led simulator with malfunctions, scenarios and scoring.