- 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-10-27
# 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)`.
The legacy two-argument `ProcessSystem.runTransientAdaptive(dt, id)` advances one
full step and recommends a following timestep using a temperature-change heuristic.
It does not reject/retry on a full-step versus two-half-step estimate. Its strict
qualification blocker remains; do not infer a different contract from its name.
For opt-in numerical acceptance, both `ProcessSystem` and `ProcessModel` provide
`runTransientAdaptive(requestedDt, parentId, AdaptiveTransientOptions)`. This first
profile requires concrete process/model owners, exactly aligned finite area clocks
and no shared mutable participant across different areas. Use explicit
minimum/maximum timestep, bounded attempts, integration order and a
`TransientErrorStateBinding` for every unique rollback participant, including
attached controllers. Each read-only serializable provider returns a nonempty
vector with stable owner/variable IDs, explicit units, finite values, positive
absolute tolerances and nonnegative relative tolerances. Numerical coverage must
be complete and the schema/tolerances must remain fixed across all trial states.
The caller remains responsible for auditing that the declared variables cover
the relevant physical and control state.
The overload compares one full step with two half steps using maximum normalized
error, restores rejected trials in place, and commits the actual two-half-step
state. Distinct deterministic physical child UUIDs prevent idempotent controllers
from skipping the second half. Use a new parent UUID on the next call; preserve
result `getAcceptedStepSeconds()` versus `getSuggestedNextStepSeconds()` semantics.
Execution/provider/schema failures propagate after rollback; they are not accepted
as numerical progress. Exhausted bounds/attempts leave the step uncommitted.
Use `AdaptiveTransientException` reason (`MINIMUM_STEP`, `ATTEMPT_LIMIT`, or
`UNREPRESENTABLE_INTERVAL`), attempted-trial count, attempted step, normalized error
and limiting-variable evidence for numerical failure reports. A `NaN` error marks
an unavailable comparison; infinity marks an unbounded/overflowing error.
Only this new overload uses start-of-substep event timing and caps the interval at
the earliest future event across all area schedulers. Shared due events execute
before any area advances. Endpoint events remain pending until the following call,
so they cannot alter the interval that precedes their trigger time. Event actions
must have declared complete in-memory mutation scope; external effects are outside
the rollback contract. Callbacks must terminate and avoid due-now cross-scheduler
chains; the underlying scheduler has no callback budget. Newly inserted future
events trigger bounded rollback/localization, while a leftover past event fails
the interval. Keep exclusive ownership during adaptive execution.
Expect three physical evaluations per complete attempt plus snapshot/comparison and
rejected-trial cost. Default/legacy paths gain no adaptive work. The synthetic
quantitative tests establish the numerical acceptance/replay contract, while real
process-control and safety studies still need method/parameter qualification,
conservation/refinement evidence and explicit limits. Read the
[dynamic capability contract](../../../docs/process/dynamic-capability-contract.md#opt-in-numerical-adaptive-acceptance)
for the norm, provider contract, provenance, event and qualification boundaries.
## 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();
```
## TwoFluidPipe conservative thermal recovery
For blowdown or repressurization, the legacy post-step temperature update does not
recover temperature from transported total energy. The experimental
`setUseConservativeEnergyRecovery(true)` option must be selected before `run()`;
it initializes named-component inventories automatically, including pure fluids.
Select coupled pressure-momentum, Euler, energy enabled and slug tracking disabled.
Read [the two-fluid model guide](../../../docs/process/TWOFLUIDPIPE_MODEL.md#opt-in-conservative-transient-thermodynamic-recovery)
for unsupported combinations and qualification limits. Inspect
`getLastConservativeEnergyReport()` together with mass and component reports;
the legacy sensible thermal report is null in this mode. A successful interval
recovers common P/T from occupied volume and internal energy at each transported
phase composition. It does not qualify rich-fluid evaporation rates or asset
blowdown times. Regressions: `TwoFluidPipeConservativeEnergyTest` and
`ConservativeThermodynamicRecoveryTest`. Keep issue #4595 open until the complete
rich-phase-transition and refinement acceptance is demonstrated.
## 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.
### Dynamic pressure-inventory loop recipe
A pressure loop needs a real compressible inventory. A valve between two imposed-pressure
streams verifies action and hydraulics, but it cannot demonstrate pressure regulation. For the
canonical separator chain:
1. Configure separator diameter, length and starting level, then disable steady-state calculation.
2. Put a gas pressure-control valve on the outlet with a lower declared downstream pressure,
actuator lag and physical-step idempotency enabled.
3. Set the PCV embedded controller to manual when an external PID owns the command.
4. Register a physical-step-sampled `PressureTransmitter` on the separator gas outlet and attach an
engineering-unit, direct-acting PID to the PCV.
5. Check each component inventory, the fixed-volume/internal-energy flash residual, pressure and
temperature bounds, held scan order, valve travel and timestep sensitivity.
`SeparatorPressureControlChainTest` is the public reference implementation. Its SRK/classic wet-gas
case uses a 2.2 m by 7.0 m separator, 55 bara initial pressure, 45 bara downstream boundary,
20 to 28 kg/s feed step, one-second scans, `Kp = 6.0`, `Ti = 30 s` and 4 s actuator lag. The
70 s regression limits peak pressure to 56.68 bara, pressure IAE to 84.6 bar-s and PCV travel to
21.38 percentage points; it also requires exact rollback/restart/repeatability and 1.0/0.5 s
refinement. These values qualify only the synthetic numerical chain. Do not reuse the tuning or
limits as a plant design basis.
### 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:
View on GitHub