Dynamic Process Simulation Enhancements
This document covers the dynamic simulation improvements added in PR #2064
(dynprocess branch). These features bring NeqSim’s transient simulation
capabilities closer to commercial DCS/OTS simulators, covering controller
logic, instrumentation realism, equipment-level dynamics, and numerical
integration infrastructure.
For the base dynamic simulation setup and DynamicProcessHelper, see
Dynamic Simulation with DynamicProcessHelper.
Table of Contents
- 1. Controller Enhancements
- 2. Instrumentation Realism
- 3. Valve Nonlinearities
- 4. Equipment Dynamics
- 5. Numerical Infrastructure
- 6. Test Coverage
- 7. Related Documentation
1. Controller Enhancements
1.1 Controller Modes (AUTO/MANUAL/CASCADE)
Controllers now support the three standard operating modes found in commercial DCS:
| Mode | Behavior |
|---|---|
AUTO |
PID algorithm computes the output (default) |
MANUAL |
Operator-set output; PID is bypassed but error tracking continues |
CASCADE |
Setpoint received from an upstream (primary) controller |
Switching between modes includes bumpless transfer: when transitioning from MANUAL to AUTO, the integral state is back-calculated to match the current manual output so the controller output does not jump.
Package: neqsim.process.controllerdevice
import neqsim.process.controllerdevice.ControllerDeviceInterface.ControllerMode;
// Switch to manual
controller.setMode(ControllerMode.MANUAL);
controller.setManualOutput(45.0); // Set output directly
// Switch back to AUTO with bumpless transfer
controller.setMode(ControllerMode.AUTO);
// Output continues smoothly from 45.0
Key methods on ControllerDeviceInterface:
| Method | Description |
|---|---|
getMode() |
Returns current ControllerMode |
setMode(ControllerMode) |
Switches mode with bumpless transfer |
getManualOutput() |
Returns the manual output value |
setManualOutput(double) |
Sets the manual output value |
1.2 Two-Degree-of-Freedom PID
The PID controller now supports a setpoint weight parameter ($b$) for the proportional term. This decouples the setpoint response from the disturbance response:
\[\text{proportional error} = \text{measurement} - b \cdot \text{setpoint}\]- $b = 1.0$ (default): standard PID — setpoint changes cause a full proportional kick
- $b = 0.0$: proportional term acts only on measurement changes (no setpoint kick)
- $0 < b < 1$: intermediate behavior
controller.setSetpointWeight(0.5); // Reduce setpoint kick by 50%
double b = controller.getSetpointWeight();
1.3 Sequential Function Chart (SFC)
A new SequentialFunctionChart class implements IEC 61131-3 style sequence
logic for modeling startup/shutdown procedures, ESD logic, and batch operations.
Package: neqsim.process.controllerdevice
An SFC consists of steps (with entry/active/exit actions) connected by
transitions (with guard conditions). The chart advances when the active
step’s outgoing transition evaluates to true.
SequentialFunctionChart sfc = new SequentialFunctionChart("Startup Sequence");
// Define steps with entry/active/exit actions
SfcStep idle = new SfcStep("IDLE");
SfcStep pressurize = new SfcStep("PRESSURIZE");
pressurize.setEntryAction(() -> valve.setPercentValveOpening(10.0));
SfcStep running = new SfcStep("RUNNING");
running.setEntryAction(() -> valve.setPercentValveOpening(100.0));
sfc.addStep(idle);
sfc.addStep(pressurize);
sfc.addStep(running);
// Transitions with guards
sfc.addTransition("IDLE", "PRESSURIZE", () -> startButton);
sfc.addTransition("PRESSURIZE", "RUNNING",
() -> separator.getPressure() > 50.0);
// Or timed transitions
sfc.addTimedTransition("RUNNING", "IDLE", 3600.0); // After 1 hour
sfc.start();
// During transient loop
for (int i = 0; i < steps; i++) {
sfc.runTransient(dt);
process.runTransient();
}
// Query state
String current = sfc.getActiveStepName(); // "PRESSURIZE"
boolean isRunning = sfc.isRunning();
List<String> history = sfc.getEventHistory();
Key methods on SequentialFunctionChart:
| Method | Description |
|---|---|
addStep(SfcStep) |
Adds a step (first added becomes initial step) |
addTransition(from, to, guard) |
Adds a conditional transition |
addTimedTransition(from, to, seconds) |
Adds a time-based transition |
start() |
Activates the initial step |
stop() |
Deactivates the chart |
reset() |
Returns to initial step |
runTransient(double dt) |
Advances time and evaluates transitions |
getActiveStepName() |
Returns the currently active step |
getEventHistory() |
Returns the transition event log |
1.4 Override (Selector) Control
The OverrideControllerStructure implements HIGH-SELECT or LOW-SELECT logic
between two controllers. This is commonly used for safety overrides where a
temperature or pressure controller can take over from a flow controller.
Package: neqsim.process.controllerdevice.structure
import neqsim.process.controllerdevice.structure.OverrideControllerStructure;
import neqsim.process.controllerdevice.structure.OverrideControllerStructure.SelectionType;
OverrideControllerStructure override = new OverrideControllerStructure(
flowController, // primary (normal operation)
pressureController, // override (safety)
SelectionType.LOW_SELECT // lowest output wins
);
// In transient loop
override.runTransient(dt);
double output = override.getOutput();
boolean overrideActive = override.isOverrideActive();
1.5 Split-Range Control
The SplitRangeControllerStructure maps a single controller output (0-100%)
to multiple final control elements, each assigned a sub-range.
Package: neqsim.process.controllerdevice.structure
import neqsim.process.controllerdevice.structure.SplitRangeControllerStructure;
// Two valves: valve A operates 0-50%, valve B operates 50-100%
SplitRangeControllerStructure splitRange =
new SplitRangeControllerStructure(controller, 2);
// Or with custom ranges
double[] lows = {0.0, 40.0, 70.0};
double[] highs = {40.0, 70.0, 100.0};
SplitRangeControllerStructure customSplit =
new SplitRangeControllerStructure(controller, lows, highs);
// In transient loop
splitRange.runTransient(dt);
double valve1 = splitRange.getOutput(0); // 0-100% for element 0
double valve2 = splitRange.getOutput(1); // 0-100% for element 1
2. Instrumentation Realism
2.1 Sensor Fault Injection
Transmitters can now simulate common sensor failure modes for testing control system robustness and operator training scenarios.
Package: neqsim.process.measurementdevice
| Fault Type | Behavior |
|---|---|
NONE |
Normal operation (default) |
STUCK_AT_VALUE |
Output frozen at the fault parameter value |
LINEAR_DRIFT |
Output drifts at faultParameter units/second |
BIAS |
Constant offset added to the true value |
NOISE_BURST |
Gaussian noise burst (amplitude = fault parameter) |
SATURATION |
Output clamped at the fault parameter value |
import neqsim.process.measurementdevice.SensorFaultType;
// Inject a stuck-at fault
pressureTransmitter.setFault(SensorFaultType.STUCK_AT_VALUE, 55.0);
// Inject a drift fault (0.1 bar/second)
pressureTransmitter.setFault(SensorFaultType.LINEAR_DRIFT, 0.1);
// Clear the fault
pressureTransmitter.clearFault();
2.2 Transmitter First-Order Filter
Transmitters can apply a first-order exponential filter (low-pass) to smooth noisy readings. This models the signal damping typically configured on industrial transmitters.
The filter equation per sample:
\[y_k = \alpha \cdot x_k + (1 - \alpha) \cdot y_{k-1}\]where $\alpha = 1 - e^{-\Delta t / \tau}$ and $\tau$ is the time constant.
// Set a 2-second filter time constant
pressureTransmitter.setFirstOrderTimeConstant(2.0);
// Disable filtering
pressureTransmitter.setFirstOrderTimeConstant(0.0);
// Query
double tau = pressureTransmitter.getFirstOrderTimeConstant();
Typical industrial transmitter time constants range from 0.5 to 10 seconds. A value of 0 (the default) disables the filter entirely.
2.3 Alarm Shelving
Alarms can be temporarily shelved (suppressed) while the alarm state machine continues to track the process value. This models the operator practice of shelving nuisance alarms during maintenance or known upset conditions.
// Shelve indefinitely with a reason
pressureTransmitter.shelveAlarm("Maintenance in progress");
// Shelve until a specific simulation time (e.g., 3600 seconds)
pressureTransmitter.shelveAlarm("Scheduled work", 3600.0);
// Check status
boolean shelved = pressureTransmitter.isAlarmShelved();
// Remove shelve manually
pressureTransmitter.unshelveAlarm();
While shelved:
- The alarm state machine continues evaluating thresholds internally
- No
AlarmEventobjects are generated - If a timed shelve expires, normal alarm behavior resumes automatically
The underlying AlarmState class provides the shelve(), unshelve(), and
isShelved() methods directly if you work with the alarm system outside of
transmitters.
3. Valve Nonlinearities
The ThrottlingValve now supports realistic valve dynamics that affect
closed-loop control performance:
| Parameter | Method | Description |
|---|---|---|
| Deadband | setValveDeadband(double) |
Minimum signal change required before the valve moves (%) |
| Stiction | setValveStiction(double) |
Static friction — valve sticks until the signal exceeds the breakaway force (%) |
| Hysteresis | setValveHysteresis(double) |
Difference in valve position between increasing and decreasing signal (%) |
ThrottlingValve valve = new ThrottlingValve("CV-101", feed);
valve.setOutletPressure(40.0);
// Configure nonlinearities
valve.setValveDeadband(0.5); // 0.5% deadband
valve.setValveStiction(2.0); // 2% stiction
valve.setValveHysteresis(1.0); // 1% hysteresis
// Query
double db = valve.getValveDeadband();
double st = valve.getValveStiction();
double hy = valve.getValveHysteresis();
These are modeled in the valve’s runTransient() method. The effective valve
position may differ from the commanded position due to these nonlinearities,
which is critical for control loop performance analysis and valve diagnostics
studies.
4. Equipment Dynamics
4.1 Separator Internals
The Separator now models two internal components that affect dynamic behavior:
Weir-Controlled Liquid Outflow
A weir controls the liquid overflow rate using the Francis weir formula:
\[Q_{\text{weir}} = C_d \cdot L_w \cdot (h - h_w)^{3/2}\]where $L_w$ is the weir length, $h$ is the liquid level, $h_w$ is the weir height, and $C_d$ is the discharge coefficient.
Separator sep = new Separator("HP Sep", feed);
sep.setWeirHeight(0.3); // 0.3 m weir height
sep.setWeirLength(1.0); // 1.0 m weir length
// Query the calculated overflow rate (after runTransient)
double qWeir = sep.getWeirOverflowRate(); // m³/s
When both weirHeight and weirLength are set to non-zero values, the weir
overflow rate is applied in runTransient() to modulate the liquid outlet flow.
Mist Eliminator Pressure Drop
A mist eliminator (demister pad) adds a velocity-dependent pressure drop to the gas outlet:
\[\Delta P_{\text{mist}} = K \cdot \rho_g \cdot v_g^2 \cdot t\]where $K$ is the pressure drop coefficient, $\rho_g$ is the gas density, $v_g$ is the gas superficial velocity, and $t$ is the pad thickness.
sep.setMistEliminatorDpCoeff(150.0); // Coefficient K
sep.setMistEliminatorThickness(0.15); // 0.15 m pad thickness
// Query the calculated pressure drop (after runTransient)
double dpMist = sep.getMistEliminatorPressureDrop(); // Pa
Boot Volume
For three-phase separators, a boot (sump) volume can be specified:
sep.setBootVolume(0.5); // 0.5 m³ boot volume
4.2 Heat Exchanger Thermal Model
The HeatExchanger now has a full dynamic thermal model with wall energy
storage and optional fluid holdup volumes.
Wall Energy ODE
When dynamicModelEnabled is true, the heat exchanger tracks wall
temperature using a lumped-parameter energy balance:
where $M_w$ is the wall mass, $C_{p,w}$ is the wall heat capacity, $h_s$ and $h_t$ are the shell-side and tube-side heat transfer coefficients, and $A$ is the heat transfer area.
HeatExchanger hx = new HeatExchanger("E-101", hotStream, coldStream);
hx.setDynamicModelEnabled(true);
// Wall properties
hx.setWallMass(500.0); // kg
hx.setWallCp(500.0); // J/(kg·K)
hx.setHeatTransferArea(25.0); // m²
// Heat transfer coefficients
hx.setShellSideHtc(400.0); // W/(m²·K)
hx.setTubeSideHtc(800.0); // W/(m²·K)
Fluid Holdup (CSTR Model)
When holdup volumes are non-zero, the shell and tube fluids are modeled as well-mixed accumulation volumes (CSTR), adding thermal inertia:
\[\rho V C_p \frac{dT_f}{dt} = \dot{m} C_p (T_{\text{in}} - T_f) + h A (T_w - T_f)\]hx.setShellHoldupVolume(0.5); // m³ shell-side fluid volume
hx.setTubeHoldupVolume(0.3); // m³ tube-side fluid volume
With holdup volume = 0 (default), the model reduces to the wall-only ODE.
Key methods:
| Method | Description |
|---|---|
setDynamicModelEnabled(boolean) |
Enable/disable the dynamic thermal model |
setWallMass(double) |
Wall metal mass in kg |
setWallCp(double) |
Wall heat capacity in J/(kg·K) |
setHeatTransferArea(double) |
Heat transfer area in m² |
setShellSideHtc(double) |
Shell-side HTC in W/(m²·K) |
setTubeSideHtc(double) |
Tube-side HTC in W/(m²·K) |
setShellHoldupVolume(double) |
Shell-side fluid volume in m³ |
setTubeHoldupVolume(double) |
Tube-side fluid volume in m³ |
getWallTemperature() |
Current wall temperature in K |
4.3 Distillation Column MESH Dynamics
The DistillationColumn now supports per-tray dynamic modeling with liquid
holdup, energy balance, and vapor hydraulics.
Tray Liquid Holdup
When dynamicColumnEnabled is true, each tray tracks its liquid holdup using
the Francis weir overflow formula:
DistillationColumn col = new DistillationColumn("Deethanizer", 10, true, true);
col.setDynamicColumnEnabled(true);
col.setTrayWeirHeight(0.05); // 5 cm weir height
col.setTrayWeirLength(1.2); // 1.2 m weir length
// After transient steps, query holdup
double[] holdups = col.getTrayLiquidHoldup(); // m³ per tray
Energy Balance
When dynamicEnergyEnabled is true, per-tray enthalpy is tracked and
vapor flow is driven by the tray pressure drop:
col.setDynamicEnergyEnabled(true);
col.setTrayDryPressureDrop(500.0); // 500 Pa dry tray pressure drop
// After transient steps, query per-tray enthalpy
double[] enthalpies = col.getTrayEnthalpy(); // J per tray
The vapor flow through each tray is calculated from:
\[V = \sqrt{\frac{\Delta P_{\text{tray}}}{\rho_V}} \cdot A_{\text{tray}}\]All dynamic features default to false/0, so existing steady-state
column calculations are unaffected.
5. Numerical Infrastructure
5.1 Adaptive Time Stepping
The ProcessSystem now supports adaptive time stepping that automatically
adjusts $\Delta t$ based on solution stability:
ProcessSystem process = new ProcessSystem();
// ... add equipment ...
process.run();
process.setAdaptiveTimestepEnabled(true);
process.setMinTimestep(0.001); // seconds
process.setMaxTimestep(10.0); // seconds
process.setAdaptiveTimestepTolerance(0.01); // relative tolerance
UUID id = UUID.randomUUID();
double actualDt = process.runTransientAdaptive(1.0, id);
// actualDt may be smaller or larger than the requested 1.0
Every ProcessSystem and ProcessModel transient entry point requires a
finite, positive timestep. Zero, negative, NaN, and infinite values throw
IllegalArgumentException before snapshots, setters, events, process areas,
equipment, controllers, clocks, or identifiers can change. Adaptive stepping
rejects invalid requested values instead of silently clamping them.
A multi-area ProcessModel also requires every child ProcessSystem to start
the step on the same finite simulation clock. A material mismatch throws
IllegalStateException before the first area advances or a shared
EventScheduler fires. Reset or explicitly synchronize restored, copied, or
independently advanced areas before calling ProcessModel.runTransient(...);
this prevents one scheduled event from affecting only the areas that happen to
run later in the insertion order.
The adaptive algorithm compares state changes between the full step and two half-steps, reducing or increasing $\Delta t$ based on the relative error.
Setter units form a dedicated pre-step specification phase. Each setter receives
exactly one runTransient(dt, id) call per process timestep, so its specification
is applied once and its own clock advances by the same $\Delta t$ as the process.
Setters are excluded from the later explicit, semi-implicit, and parallel
equipment passes.
Controller execution is coalesced by object identity and timestep calculation
identifier. Equipment that actually integrates a controller passes the same
calculation identifier to the controller; registering that object separately with
process.add(controller) therefore keeps it discoverable without advancing its
time, integral, derivative, or event-log state twice. Merely attaching a controller
to equipment that does not execute it does not suppress the standalone update.
Repeated standalone registration is also coalesced to one update per timestep;
distinct controller objects remain independent even if they share a name.
ControllerDeviceBaseClass tracks the identifier automatically and makes
repeated calls with one identifier idempotent, including semi-implicit equipment
passes. Custom controller implementations that integrate inside equipment should
implement hasRunTransient(UUID) and the same one-update-per-identifier contract.
Steady-state equipment can be evaluated more than once while a solver refines one
physical timestep. The default SimulationInterface.runTransient(dt, id) still
executes run(id) on every evaluation, so algebraic recycles retain both
semi-implicit passes, but advances the equipment clock only on the first
successful evaluation of a non-null calculation identifier. A new identifier
advances time for the next physical step. This keeps algebraic solver iterations
separate from simulated time, including normal low-flow returns.
5.2 Parallel Transient Execution
For large flowsheets, equipment-level transient calculations can be run in parallel threads:
process.setParallelTransientEnabled(true);
process.setTransientThreadPoolSize(4); // Number of worker threads
// Transient steps now run equipment in parallel
process.runTransient();
The configured workers are reused across timesteps. NeqSim creates the executor
lazily for each ProcessSystem, keeps it out of serialized model state, and
allows idle daemon workers to time out after 60 seconds. Changing
setTransientThreadPoolSize(...) or disabling parallel transient execution
retires the existing pool. This avoids creating and destroying a complete
thread pool for every timestep in long dynamic studies.
Parallel transient execution uses the same cached process graph as steady-state parallel execution. Equipment is partitioned into topological levels: all upstream groups in one level finish before downstream equipment is submitted, while independent groups in the same level can use separate workers. Groups that must share mutable inlet state execute sequentially within one worker. This prevents a downstream unit from observing a partially updated upstream stream merely because both units were submitted in the same timestep.
If the thread calling runTransient(...) is interrupted while waiting for
parallel equipment, NeqSim restores the caller’s interrupt status and stops
waiting. No downstream level is submitted. Later parallel passes in the same
timestep observe that status and do not submit additional equipment work.
Queued futures are cancelled without interrupting equipment already updating
state. Because those running updates are not transactionally interruptible,
NeqSim aborts the remaining controller, measurement, result-storage, and
event-publication phases for that timestep. Callers should still treat an
interrupted timestep as incomplete rather than as an atomic rollback.
Note: Parallel execution is beneficial for flowsheets with many independent branches. Topological ordering covers feed-forward stream dependencies, but it does not define iteration or rollback for recycle loops and other implicit couplings. Keep the option disabled for those cases until their transient convergence contract is explicitly supported. For small flowsheets, the scheduling overhead may also outweigh the benefit.
5.3 Integration Methods
The ProcessSystem supports selectable integration methods:
import neqsim.process.processmodel.ProcessSystem.IntegrationMethod;
process.setIntegrationMethod(IntegrationMethod.EXPLICIT_EULER); // Default
process.setIntegrationMethod(IntegrationMethod.RUNGE_KUTTA_4); // Higher-order
| Method | Order | Stability | Cost |
|---|---|---|---|
EXPLICIT_EULER |
1st | Conditional | 1 evaluation/step |
RUNGE_KUTTA_4 |
4th | Better | 4 evaluations/step |
6. Test Coverage
The features in this guide are covered by eight focused test classes with 85 total tests:
| Test Class | Tests | Coverage |
|---|---|---|
DynamicImprovementsTest |
17 | Controller modes, 2-DOF PID, SFC, control structures, gain scheduling, event logging, performance metrics |
DynamicImprovementsPhase2Test |
27 | Sensor faults, valve nonlinearities, adaptive timestep, parallel transient, integration methods, JSON process builder |
DynamicImprovementsPhase3Test |
16 | Transmitter filter, alarm shelving, separator internals, HX thermal model, distillation MESH dynamics |
ProcessSystemParallelTransientTest |
9 | Worker reuse, copy lifecycle, dependency ordering, independent-level concurrency, and interruption behavior at waits and level boundaries |
ProcessSystemTransientSetterTest |
3 | Single setter application, repeated-step clock advancement, and explicit, semi-implicit, and parallel execution |
ProcessSystemTransientControllerTest |
5 | Equipment execution, semi-implicit passes, attachment-only fallback, duplicate standalone registration, identity semantics, and repeated timesteps |
ProcessSystemTransientAlgebraicTimeTest |
4 | Repeated algebraic evaluation, semi-implicit recycle iteration, low-flow completion identifiers, null-ID compatibility, and one clock advance per timestep |
ProcessSystemTransientTimestepValidationTest |
4 | Finite-positive process, adaptive, and multi-area timestep preflight plus valid nearby steps |
Run all tests:
./mvnw test -Dtest=DynamicImprovementsTest,DynamicImprovementsPhase2Test,DynamicImprovementsPhase3Test,ProcessSystemParallelTransientTest,ProcessSystemTransientSetterTest,ProcessSystemTransientControllerTest,ProcessSystemTransientAlgebraicTimeTest,ProcessSystemTransientTimestepValidationTest
7. Related Documentation
- Dynamic Simulation with DynamicProcessHelper — Base dynamic simulation setup
- Dynamic Simulation Guide — Comprehensive transient simulation guide
- Process Controllers and Logic — PID controllers and adjusters
- Separators — Separator configuration and sizing
- Heat Exchangers — Heat exchanger types and design