- name
- neqsim-dynamic-simulation
- description
- Dynamic simulation guidance for NeqSim. USE WHEN: running transient simulations, modeling startup/shutdown, tuning PID controllers, analyzing pressure/level dynamics, performing blowdown/depressurization, or setting up measurement devices and control loops. Covers runTransient, DynamicProcessHelper, controller tuning, and dynamic equipment configuration.
- last_verified
- 2026-09-07
# Dynamic Simulation Guidance
Guide for transient/dynamic process simulation in NeqSim.
## When to Use Dynamic Simulation
- Startup and shutdown sequences
- Controller tuning and loop analysis
- Pressure relief / blowdown scenarios
- Level and pressure dynamics
- Compressor surge analysis
- Pipeline transients (slug flow)
- Emergency depressurization (EDP/ESD)
- P&ID-derived valve actions where pressure, level, controller response, or inventory release changes with time
For valve-action studies that start from P&ID symbols and plant data, also load
`neqsim-pid-process-operations` to define the process graph, valve semantics,
historian tag mapping, and event schedule before running `runTransient`.
## Dynamic Simulation Architecture
NeqSim dynamic simulation advances a `ProcessSystem` with `runTransient()`
(using the configured timestep) or `runTransient(double dt, UUID id)` (using an
explicit timestep and calculation identifier).
Pass a finite, positive timestep. Process and model entry points reject zero,
negative, NaN, and infinite values before any area or equipment state changes.
Adaptive stepping rejects invalid requests rather than clamping them.
Each timestep:
1. Flowsheet-wide settings are applied. Each setter unit receives exactly one
transient call, applying its specification and advancing its own clock once.
2. Simulation time advances, then due events and field inputs are applied.
3. Equipment updates, including its thermodynamic calculations, run in insertion
order or dependency-aware graph levels when parallel transient execution is
enabled. Setter units are excluded from these explicit, semi-implicit, and
parallel equipment passes.
Steady-state algebraic equipment still evaluates on every requested pass,
but its local clock advances once per non-null timestep calculation identifier.
Reusing an identifier may refine a recycle without advancing physical time;
use a new identifier for the next physical timestep.
4. Standalone controllers run; controllers actually executed inside equipment
retain their equipment-specific timing for compatibility. Execution is
coalesced by object identity and the timestep calculation identifier. Merely
attaching a controller to equipment that does not execute it must not suppress
the standalone update; repeated standalone registration runs once per step.
`ControllerDeviceBaseClass` also makes repeated calls with one identifier
idempotent, including semi-implicit equipment passes. Custom controllers
integrated by equipment must implement the same identifier contract through
`hasRunTransient(UUID)`.
5. Measurement devices are sampled, alarms are evaluated, and history is stored.
A non-null UUID therefore identifies **one physical timestep**, not an entire
outer time-marching simulation. Refinements of that physical step reuse the
same physical-step UUID; the next accepted physical step must use a different
UUID. For ordinary loops `runTransient()` is safe because it creates a fresh
UUID each call. Deterministic safety/OTS workflows can use
`TransientStepIdentifier.deterministicPhysicalStep(scope, stepIndex)`.
`ProcessSystem.runTransientAdaptive(...)` is **not yet a transactional adaptive
integrator**. The current implementation validates the request, advances trial
state directly, estimates a temperature-change heuristic, and chooses a
subsequent timestep. It does not yet provide rejected-step process/event/control
rollback or a full-step versus two-half-step error estimate. Do not use it as
evidence of qualified stiff/adaptive professional dynamics until the #2911
transactional-step work is complete.
## Basic Dynamic Setup
```java
import neqsim.process.processmodel.ProcessSystem;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.valve.ThrottlingValve;
import neqsim.process.controllerdevice.ControllerDeviceInterface;
import neqsim.process.controllerdevice.ControllerDeviceBaseClass;
import neqsim.process.measurementdevice.LevelTransmitter;
import neqsim.process.measurementdevice.PressureTransmitter;
// Build steady-state process first
SystemInterface fluid = new SystemSrkEos(273.15 + 25.0, 50.0);
fluid.addComponent("methane", 0.80);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.addComponent("n-pentane", 0.05);
fluid.setMixingRule("classic");
Stream feed = new Stream("feed", fluid);
feed.setFlowRate(10000.0, "kg/hr");
Separator sep = new Separator("HP Sep", feed);
sep.setInternalDiameter(2.0); // m — for dynamic simulation, set directly for level dynamics
sep.setSeparatorLength(6.0); // m — for design purposes, use SeparatorMechanicalDesign instead
ThrottlingValve gasValve = new ThrottlingValve("gas valve", sep.getGasOutStream());
gasValve.setOutletPressure(20.0, "bara");
ThrottlingValve liqValve = new ThrottlingValve("liq valve", sep.getLiquidOutStream());
liqValve.setOutletPressure(10.0, "bara");
ProcessSystem process = new ProcessSystem();
process.add(feed);
process.add(sep);
process.add(gasValve);
process.add(liqValve);
// Run steady state first
process.run();
```
## Adding Measurement Devices
```java
// Pressure transmitter
PressureTransmitter PT100 = new PressureTransmitter("PT-100", sep);
PT100.setUnit("bara");
PT100.setMaximumValue(100.0);
PT100.setMinimumValue(0.0);
process.add(PT100);
// Level transmitter
LevelTransmitter LT100 = new LevelTransmitter("LT-100", sep);
LT100.setUnit("m");
process.add(LT100);
// Temperature transmitter
TemperatureTransmitter TT100 = new TemperatureTransmitter("TT-100", sep);
TT100.setUnit("C");
process.add(TT100);
// Flow transmitter
VolumeFlowTransmitter FT100 = new VolumeFlowTransmitter("FT-100", feed);
FT100.setUnit("kg/hr");
process.add(FT100);
```
## Controller Configuration
### PID Controller
```java
// Level controller on liquid OUTLET valve — DIRECT acting (reverseActing = false):
// level up (error > 0) -> output up -> outlet valve opens -> level falls. Setting
// reverseActing(true) here inverts the sign and makes the loop unstable (runaway).
ControllerDeviceInterface LC100 = new ControllerDeviceBaseClass();
LC100.setControllerSetPoint(1.0); // Target level = 1.0 m
LC100.setTransmitter(LT100); // Controlled variable
LC100.setReverseActing(false); // liquid-outlet level valve is direct acting
LC100.setControllerParameters(0.5, 100.0, 10.0); // Kp, Ti (s), Td (s)
// Attach controller to valve
liqValve.addController("LC-100", LC100);
// Pressure controller on gas valve
ControllerDeviceInterface PC100 = new ControllerDeviceBaseClass();
PC100.setControllerSetPoint(50.0); // Target pressure = 50 bara
PC100.setTransmitter(PT100);
PC100.setReverseActing(false); // Pressure up -> valve opens more
PC100.setControllerParameters(1.0, 50.0, 0.0);
gasValve.addController("PC-100", PC100);
```
### Controller Tuning Guidelines
| Loop Type | Typical Kp | Typical Ti (s) | Typical Td (s) |
|-----------|-----------|----------------|-----------------|
| Level (averaging) | 0.5-2.0 | 60-300 | 0 |
| Level (tight) | 2.0-5.0 | 30-60 | 0-10 |
| Pressure (gas) | 0.5-2.0 | 20-100 | 0-5 |
| Flow | 0.3-1.0 | 5-30 | 0 |
| Temperature | 0.5-2.0 | 60-600 | 10-60 |
### Controller Deadband (SP-PV)
`ControllerDeviceBaseClass` supports a native SP-PV deadband via
`setDeadBand(double)` / `getDeadBand()`. While the absolute control error stays
inside the band the controller output is **frozen** (holds the last valve
position) and the integral term does **not** accumulate; default 0 disables it.
The deadband is in the controller error unit (percent in the default percent
mode, else the configured engineering unit). This is the standard DCS averaging-
level deadband used to stop valve cycling.
```java
levelController.setDeadBand(0.5); // hold the valve while |PV - SP| <= 0.5 %
```
**Beware the deadband limit cycle.** On an integrating (level) process a
deadband delays correction until the level reaches the band edge; the delayed
correction then overshoots and the cycle repeats, giving a square-wave valve
trace. Removing (or shrinking) the deadband is the usual fix. If the installed
pip `neqsim` predates `setDeadBand`, emulate it by toggling controller mode
each step: `setMode(ControllerMode.MANUAL)` while `|PV%-SP%| <= deadband` (holds
output) and `setMode(ControllerMode.AUTO)` otherwise (bumpless resume) - this is
numerically identical to the native deadband.
### Dynamic level-loop recipe (get the sequence right)
A dynamic separator level loop only responds if the vessel is switched out of
steady-state mode **and** the liquid outlet valve is direct acting. The exact,
easy-to-get-wrong sequence is:
```java
// 1. Build and solve the steady state first (sets inventory, flows, holdup).
process.run();
// 2. Switch the vessel (and its outlet valve) to dynamic mode. If this is left
// on, the separator recomputes steady state every step and the level is
// pinned at its default (0.5 fraction) no matter what the controller does.
sep.setCalculateSteadyState(false);
liqValve.setCalculateSteadyState(false);
// 3. Set the physical geometry and the starting liquid level (as a 0..1 fraction
// of the vessel). Do this AFTER run() so it is not overwritten by steady state.
sep.setInternalDiameter(2.0); // m — drives holdup volume / level dynamics
sep.setSeparatorLength(6.0); // m
sep.setLiquidLevel(0.30); // start at 30 %
// 4. Direct-acting level controller on the liquid OUTLET valve (see above).
LevelTransmitter LT100 = new LevelTransmitter("LT-100", sep);
LT100.setUnit("m");
ControllerDeviceInterface LC100 = new ControllerDeviceBaseClass();
LC100.setTransmitter(LT100);
LC100.setControllerSetPoint(0.30 * sep.getInternalDiameter()); // SP in the LT unit
LC100.setReverseActing(false); // liquid-outlet level valve = direct acting
LC100.setControllerParameters(1.0, 300.0, 0.0); // averaging level: loose Kp, long Ti, no Td
liqValve.addController("LC-100", LC100);
// 5. Advance the transient with a fresh physical-step identity each step.
for (long step = 0; step < 600; step++) {
java.util.UUID physicalStepId =
neqsim.process.dynamics.TransientStepIdentifier.deterministicPhysicalStep("level-loop", step);
process.runTransient(1.0, physicalStepId); // dt = 1 s
}
```
**Gotchas:**
- **Level pinned at 0.5** — the vessel is still in steady-state mode; call
`setCalculateSteadyState(false)` on the separator (and its outlet valve).
- **Level runs away** — the liquid-outlet level controller is reverse acting; it
must be `setReverseActing(false)` (direct acting). A gas-outlet pressure valve
is also direct acting (`false`); reverse acting is for cases where more output
reduces the measured value (e.g. a controller manipulating an inlet/feed valve).
- **Set `setLiquidLevel` after `run()`** — a steady-state solve resets the level,
so set the starting level and geometry after the first `run()`.
- **One UUID per physical step** — do not create one UUID before the outer time
loop and reuse it. Built-in controllers/equipment intentionally treat repeated
calls with one UUID as refinements of the same physical timestep.
- For an averaging level loop, use a loose `Kp` and long `Ti` (see the tuning
table) and consider an SP-PV deadband only with care (see the limit-cycle note
above).
After the run, use `ControllerPerformanceMetrics.fromEventLog(LC100.getEventLog())`
(or `LC100.getPerformanceMetrics()`) to score the tuning (IAE/ISE/ITAE, PV
variability, valve travel and reversals, settling time) — see the KPI section below.
### Loop-tuning KPIs (ControllerPerformanceMetrics)
`ControllerPerformanceMetrics`
(`neqsim.process.controllerdevice.ControllerPerformanceMetrics`) computes the
standard loop-tuning KPIs from a controller event log (or from raw time / PV / SP
/ output arrays) so tuning studies report consistent numbers without
re-implementing the definitions. It is the preferred way to compare two PID
tunings on the same disturbance.
Metrics: `getIntegralAbsoluteError()` (IAE), `getIntegralSquaredError()` (ISE),
`getIntegralTimeAbsoluteError()` (ITAE, time referenced to the first sample),
`getProcessValueStandardDeviation()` (PV variability), `getPeakAbsoluteError()`,
`getControllerOutputTravel()` (total valve travel), `getControllerOutputReversals()`
(valve direction reversals), and `getSettlingTime()` (time of the last sample
outside the settling band, default 2 % of max(|SP|, 1)).
```java
// After a runTransient loop with a logging controller:
ControllerPerformanceMetrics kpi = LC100.getPerformanceMetrics(); // from getEventLog()
// or, explicitly / with a custom settling band:
ControllerPerformanceMetrics kpi2 =
ControllerPerformanceMetrics.fromEventLog(LC100.getEventLog(), 0.05); // 5 % band
double iae = kpi.getIntegralAbsoluteError();
double valveTravel = kpi.getControllerOutputTravel();
int reversals = kpi.getControllerOutputReversals();
double settlingTime = kpi.getSettlingTime();
logger.info("IAE={} travel={} reversals={} settle={} s", iae, valveTravel, reversals, settlingTime);
// Or build directly from arrays (e.g. PV/OP pulled from a historian):
ControllerPerformanceMetrics kpi3 =
ControllerPerformanceMetrics.fromArrays(time, pv, sp, op);
```
- Integral criteria use trapezoidal integration over the sample intervals, so
irregular time steps are handled correctly.
- A **lower** IAE/ISE/ITAE means tighter regulation; **lower** valve travel and
reversals means gentler actuator duty. Tuning trade-offs usually pit the two
against each other (tighter control costs more valve movement).
- `resetEventLog()` on the controller before the disturbance so the KPIs cover
only the window of interest.
### Canonical controls qualification suite
Use `ControlsBenchmarkSuite` when a request needs a deterministic regression
check across NeqSim's canonical level, pressure, cascade, split-range,
anti-surge, and compressor speed/recycle control structures:
```java
import neqsim.process.controllerdevice.ControlsBenchmarkSuite;
ControlsBenchmarkSuite.Report report = ControlsBenchmarkSuite.runCanonicalSuite();
boolean qualified = report.isPassed();
```
Inspect `report.getCases()` for the uniform time/PV/SP/output traces,
`ControllerPerformanceMetrics`, physical acceptance detail, and final errors.
The report also exposes the dedicated `AgentBenchmarkSuite` verdict through
`getAgentBenchmarkReport()`. See
[`docs/benchmarks/controls_benchmark.md`](../../../docs/benchmarks/controls_benchmark.md)
for equations, declared challenges, numerical gates, and current CI reference
values.
The suite qualifies deterministic control execution and KPI reporting against
transparent surrogate plants. It is not a substitute for field tuning, vendor
compressor data, severe-slugging qualification, a safety-instrumented function,
or commissioning validation.
### Anti-Surge Control (dynamic)
`AntiSurgeController` (`neqsim.process.controllerdevice.AntiSurgeController`) is a
purpose-built **reverse-acting PI** controller that reads the compressor
`getDistanceToSurge()` and drives a recycle (spill-back) `ThrottlingValve` open
when the margin falls below the set point, then closes it again on recovery.
```java
import neqsim.process.controllerdevice.AntiSurgeController;
// distance to surge ~ (operating flow / surge flow - 1); only meaningful once a
// compressor chart with an active surge curve exists.
AntiSurgeController asc = new AntiSurgeController("anti-surge", compressor, recycleValve);
asc.setSurgeMarginSetPoint(0.10); // protect a 10% distance-to-surge margin
asc.setProportionalGain(400.0); // percent opening per unit margin error
asc.setIntegralTime(20.0); // s
asc.setOpeningRange(0.0, 100.0); // valve opening clamp (%) with anti-windup
asc.setActive(true);
recycleValve.addController("anti-surge", asc);
```
Control law each transient step: `error = setPoint - distanceToSurge`,
`integral += Kp/Ti * error * dt`, `opening = clamp(Kp*error + integral)` with
anti-windup; the controller applies the opening directly to the recycle valve.
**Reproducible benchmark.** `AntiSurgeDynamicBenchmark`
(`neqsim.process.util.scenario.AntiSurgeDynamicBenchmark`) drives the real
controller against a transparent first-order gas-path surrogate
`m_{k+1} = m_k - d*dt + a*(u/100)*dt` (m = distance to surge, d = disturbance
rate, a = recycle authority, u = valve opening %). It is deterministic and
always converges, so it is the preferred way to verify or tune the control law:
```java
import neqsim.process.util.scenario.AntiSurgeDynamicBenchmark;
AntiSurgeDynamicBenchmark bench = new AntiSurgeDynamicBenchmark();
bench.setInitialMargin(0.30);
bench.setDisturbanceRate(0.020); // flow loss erodes the margin (/s)
Voir sur GitHub