Process Controllers and Logic
Use steady-state utilities to satisfy process specifications and dynamic controllers to update actuators from measured signals. These have different execution and connection requirements.
Table of Contents
- Overview
- Named Controller Map
- Adjusters
- Recycles
- Automatic recycle insertion
- Setters
- Calculators
- PID Controllers
- Native Dynamic Control Blocks
- Process Logic
Overview
| Class | Package | Purpose |
|---|---|---|
Adjuster |
neqsim.process.equipment.util |
Iterate a manipulated variable toward a target |
Recycle |
neqsim.process.equipment.util |
Update a tear stream until a recycle converges |
Setter |
neqsim.process.equipment.util |
Apply supported constant pressure or temperature specifications |
Calculator |
neqsim.process.equipment.util |
Execute a Java calculation callback |
ControllerDeviceBaseClass |
neqsim.process.controllerdevice |
PID control using a measurement device |
TransferFunctionBlock, LogicBlock |
neqsim.process.controllerdevice |
Signal dynamics and Boolean decisions |
Each Java block below is a complete Java 8 program. Save it using its public class name and run
with NeqSim and its dependencies on the classpath. Enable assertions with java -ea to check the
stated results. ControllersAndWellsDocumentationTest compiles and executes these exact blocks.
Named Controller Map
Equipment supports tagged controller registration through addController(tag, controller),
getController(tag), and getControllers(). setController(controller) selects the legacy
primary controller and also registers it under its name. The first addController call also
selects a primary controller when none exists.
The map records associations; it does not implement arbitration between two outputs that both
request the same valve position. ThrottlingValve executes its primary controller. Use an
explicit control structure when several measurements must determine one actuator command.
Registering a controller with ProcessSystem.add(controller) makes it available to the
system-level transient scan. Equipment-owned controllers that already executed for the step’s
UUID are not integrated a second time. System-level registration alone does not connect the
controller response to an actuator. The PID example demonstrates both the
actuator connection and registration.
Adjusters
An adjuster is a steady-state specification solver. Register it after the equipment whose
result it measures. For a direct temperature specification, a heater’s setOutTemperature
is sufficient. To solve for the required duty, use explicit getter, setter, and measurement
callbacks:
import neqsim.process.equipment.heatexchanger.Heater;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.util.Adjuster;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
public class HeaterDutyAdjusterExample {
public static void main(String[] args) {
SystemSrkEos fluid = new SystemSrkEos(298.15, 20.0);
fluid.addComponent("methane", 1.0);
fluid.setMixingRule("classic");
Stream feed = new Stream("feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
Heater heater = new Heater("heater", feed);
heater.setDuty(10000.0); // W
Adjuster adjuster = new Adjuster("outlet temperature specification");
adjuster.setAdjustedEquipment(heater);
adjuster.setTargetEquipment(heater);
adjuster.setAdjustedValueGetter(() -> heater.getDuty());
adjuster.setAdjustedValueSetter(value -> heater.setDuty(value));
adjuster.setTargetValueCalculator(() -> heater.getOutletStream().getTemperature("C"));
adjuster.setTargetValue(80.0); // C, matching the measurement callback
adjuster.setMinAdjustedValue(0.0); // W, matching the manipulated variable
adjuster.setMaxAdjustedValue(100000.0);
adjuster.setTolerance(1.0e-5); // C
ProcessSystem process = new ProcessSystem("heater specification");
process.add(feed);
process.add(heater);
process.add(adjuster);
process.run();
// Check the physical target too: an adjuster can stop at a bound.
assert adjuster.solved();
assert Math.abs(heater.getOutletStream().getTemperature("C") - 80.0) < 1.0e-4;
assert heater.getDuty() > 0.0 && heater.getDuty() < 100000.0;
assert Math.abs(heater.getOutletStream().getFlowRate("kg/hr") - 1000.0) < 1.0e-6;
}
}
The string-based API supports selected stream properties; it is not general property reflection.
For example, adjusted "flow" needs an explicit flow unit. A target "temperature" does not
select a built-in temperature measurement in the current Adjuster, so the callback above is
essential. Use callbacks for heater duty, component fractions, and other custom quantities.
See Adjusters for the supported property contract.
Recycles
Connect the recycle outlet to a stream that already feeds the upstream mixer. A mixer itself
is not a valid argument to Recycle.setOutletStream. This example recycles 25% of the mixed
flow and exports 75%. At convergence, export equals the fresh feed and recycle is one third of
that fresh feed.
import neqsim.process.equipment.mixer.Mixer;
import neqsim.process.equipment.splitter.Splitter;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.util.AccelerationMethod;
import neqsim.process.equipment.util.Recycle;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
public class MaterialRecycleExample {
public static void main(String[] args) {
SystemSrkEos fluid = new SystemSrkEos(298.15, 20.0);
fluid.addComponent("methane", 1.0);
fluid.setMixingRule("classic");
Stream feed = new Stream("fresh feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
Stream tear = new Stream("recycle guess", fluid.clone());
tear.setFlowRate(100.0, "kg/hr");
Mixer mixer = new Mixer("mixer");
mixer.addStream(feed);
mixer.addStream(tear);
Splitter splitter = new Splitter("product and recycle", mixer.getOutletStream());
splitter.setSplitFactors(new double[] {0.75, 0.25});
Recycle recycle = new Recycle("recycle");
recycle.addStream(splitter.getSplitStream(1));
recycle.setOutletStream(tear);
recycle.setTolerance(1.0e-8);
recycle.setMaxIterations(100);
recycle.setAccelerationMethod(AccelerationMethod.DIRECT_SUBSTITUTION);
ProcessSystem process = new ProcessSystem("material recycle");
process.add(feed);
process.add(tear);
process.add(mixer);
process.add(splitter);
process.add(recycle);
process.run();
assert recycle.solved();
assert Math.abs(splitter.getSplitStream(0).getFlowRate("kg/hr") - 1000.0) < 1.0e-3;
assert Math.abs(tear.getFlowRate("kg/hr") - 1000.0 / 3.0) < 1.0e-3;
assert Math.abs(tear.getPressure("bara") - 20.0) < 1.0e-6;
}
}
DIRECT_SUBSTITUTION is the default acceleration method. WEGSTEIN and BROYDEN are opt-in
alternatives; their benefit depends on the coupled variables and flowsheet. Current Wegstein
acceleration operates on composition, so it does not accelerate the pure-methane flow balance
above. Its default q bounds are -5 to 0, with a two-iteration warm-up. Both ProcessSystem
and ProcessModel also provide setRecycleAccelerationMethod to update all their recycle units.
See Recycle Acceleration for tuning and diagnostics.
Automatic recycle insertion
A loop wired straight back into an upstream mixer - no Recycle unit anywhere in it - is an
implicit tear. It still converges, because the surrounding sweep keeps re-evaluating it, but it
has no tolerance, no acceleration and no convergence report of its own. In a ProcessModel that is
worse: a stream produced by an area that runs after its consumer can only be closed by the outer
Gauss-Seidel pass, which has no relaxation setting, so the plant residual sits on a floor no
tolerance setting can reach.
makeRecycles() finds those loops and closes them:
| Method | Scope |
|---|---|
ProcessSystem.makeRecycles() |
Strongly connected components of one flowsheet |
ProcessModel.makeRecycles() |
Cross-area feedback streams, then every area |
makeRecycles(double tolerance) |
Same, with an explicit relative tear tolerance (default 1e-2) |
setAutoRecycles(boolean) |
Do it automatically on run() / runUntilConverged(...) |
For each loop the inlet with the smallest recycle ratio - tear flow divided by the total flow
into the consuming unit - is swapped for a tear stream seeded from the current loop fluid, and a
Recycle is registered to close it. That ratio governs how fast a direct-substitution tear
contracts, so tearing a small side stream into a large mixer converges in a few passes where
tearing the main line would make the loop iterate on its own throughput. One edge is torn per
round and the loop structure is recomputed afterwards, so nested cycles get the tears they need
and no more - every extra tear is another sub-iteration at run time.
Each generated recycle starts on direct substitution with setAdaptiveAcceleration(true), so it
upgrades itself to Wegstein only once its flow error stops contracting, and gets an absolute flow
tolerance at 1e-6 of the largest flow in its area, so a tear on a near-zero leg converges on its
absolute change instead of a relative error it can never meet.
Only inlets that can be rewired are torn - Mixer and Manifold expose
replaceStream(int, StreamInterface). A loop that closes on any other equipment type is logged and
left untouched rather than silently mis-wired; route it through a mixer to make it tearable.
import java.util.List;
import neqsim.process.equipment.mixer.Mixer;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.splitter.Splitter;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.util.Recycle;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
public class AutomaticRecycleExample {
public static void main(String[] args) {
SystemSrkEos fluid = new SystemSrkEos(273.15 + 30.0, 50.0);
fluid.addComponent("methane", 0.8);
fluid.addComponent("ethane", 0.1);
fluid.addComponent("n-heptane", 0.1);
fluid.setMixingRule("classic");
Stream feed = new Stream("feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
Mixer mixer = new Mixer("inlet mixer");
mixer.addStream(feed);
Separator separator = new Separator("separator", mixer.getOutletStream());
Splitter splitter = new Splitter("gas splitter", separator.getGasOutStream());
splitter.setSplitFactors(new double[] {0.9, 0.1});
ProcessSystem process = new ProcessSystem("automatic recycle");
process.add(feed);
process.add(mixer);
process.add(separator);
process.add(splitter);
process.run();
// Close the loop implicitly: the recycle branch goes straight back to the mixer.
mixer.addStream(splitter.getSplitStream(1));
List<Recycle> created = process.makeRecycles();
assert created.size() == 1;
assert created.get(0).isAdaptiveAcceleration();
assert created.get(0).getAbsoluteFlowTolerance() > 0.0;
// The mixer now reads the generated tear stream, not the splitter outlet.
assert mixer.getInletStreams().get(1) == created.get(0).getOutletStream();
process.run();
assert process.solved();
double products = splitter.getSplitStream(0).getFlowRate("kg/hr")
+ separator.getLiquidOutStream().getFlowRate("kg/hr");
assert Math.abs(products - 1000.0) < 1.0;
// Idempotent: the loop is closed, so nothing more is inserted.
assert process.makeRecycles().isEmpty();
}
}
makeRecycles() seeds itself: if the flowsheet has streams without a fluid it runs once first, so
the call order is not the caller’s problem. To drop the explicit call entirely, enable auto mode -
the first run then seeds and closes every implicit loop, and later runs use the generated tears:
plant.setAutoRecycles(True)
plant.runUntilConverged(30)
Auto mode is off by default, because inserting a tear changes how an existing flowsheet iterates and would silently move results in models that rely on the implicit-loop pass.
Setters
Setter applies supported constant specifications with addTargetEquipment and addParameter.
It does not flash the target after changing its inputs: run the target afterward. Assign flow
directly with Stream.setFlowRate. Define feed composition on the thermodynamic system, in
component insertion order when using setMolarComposition.
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.util.Setter;
import neqsim.thermo.system.SystemSrkEos;
public class ConstantSpecificationExample {
public static void main(String[] args) {
SystemSrkEos fluid = new SystemSrkEos(298.15, 20.0);
fluid.addComponent("methane", 0.90);
fluid.addComponent("CO2", 0.10);
fluid.setMixingRule("classic");
fluid.setMolarComposition(new double[] {0.98, 0.02});
Stream feed = new Stream("feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
Setter setter = new Setter("feed specifications");
setter.addTargetEquipment(feed);
setter.addParameter("temperature", "C", 40.0);
setter.addParameter("pressure", "bara", 30.0);
setter.run();
feed.run();
assert Math.abs(feed.getTemperature("C") - 40.0) < 1.0e-8;
assert Math.abs(feed.getPressure("bara") - 30.0) < 1.0e-8;
assert Math.abs(feed.getFlowRate("kg/hr") - 1000.0) < 1.0e-6;
assert Math.abs(feed.getFluid().getComponent("CO2").getz() - 0.02) < 1.0e-10;
}
}
Composition specification changes the input definition. To model physical injection or removal,
include the corresponding material streams and equipment in the flowsheet. The available
MoleFractionControllerUtil modifies component inventory and is described in
Calculators and Setters; there is no MoleFractionSetter class.
Calculators
Use Calculator.setCalculationMethod with a Java callback. Register whole equipment objects
as inputs and output; setExpression and property-name overloads are not part of this API.
Here a separate auxiliary-feed specification is set to 10% of a main-feed mass rate.
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.util.Calculator;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
public class FlowSpecificationCalculatorExample {
public static void main(String[] args) {
SystemSrkEos fluid = new SystemSrkEos(298.15, 20.0);
fluid.addComponent("methane", 1.0);
fluid.setMixingRule("classic");
Stream mainFeed = new Stream("main feed", fluid);
mainFeed.setFlowRate(1000.0, "kg/hr");
Stream auxiliaryFeed = new Stream("auxiliary feed", fluid.clone());
auxiliaryFeed.setFlowRate(1.0, "kg/hr");
Calculator calculator = new Calculator("auxiliary feed specification");
calculator.addInputVariable(mainFeed);
calculator.setOutputVariable(auxiliaryFeed);
calculator.setCalculationMethod((inputs, output) -> {
Stream source = (Stream) inputs.get(0);
Stream target = (Stream) output;
target.setFlowRate(0.10 * source.getFlowRate("kg/hr"), "kg/hr");
target.run();
});
ProcessSystem process = new ProcessSystem("calculated feed specification");
process.add(mainFeed);
process.add(calculator);
process.run();
assert Math.abs(auxiliaryFeed.getFlowRate("kg/hr") - 100.0) < 1.0e-6;
assert Math.abs(mainFeed.getFlowRate("kg/hr") - 1000.0) < 1.0e-6;
assert Math.abs(auxiliaryFeed.getTemperature("C") - 25.0) < 1.0e-6;
}
}
This callback establishes the rate of an independent source; splitting one feed into products
requires a Splitter. Registered calculator inputs and outputs also describe graph dependencies.
Validate callback results explicitly, because callback exceptions are logged by Calculator.
PID Controllers
Use ControllerDeviceBaseClass with a transmitter. setControllerParameters(Kp, Ti, Td) takes
gain, integral time in seconds, and derivative time in seconds. Ti is not an integral gain.
With an explicit engineering unit, the controller uses measurement minus set point as its error;
setReverseActing(true) reverses the output response. Select the action from the actual process
and actuator response.
The example initializes a valve at 50% opening, then performs one dynamic step with pressure above set point. The controller opens the valve to 51%. The imposed feed pressure stays fixed: this verifies wiring and action, while pressure regulation requires upstream inventory dynamics.
import java.util.UUID;
import neqsim.process.controllerdevice.ControllerDeviceBaseClass;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.valve.ThrottlingValve;
import neqsim.process.measurementdevice.PressureTransmitter;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
public class PressureControllerWiringExample {
public static void main(String[] args) {
SystemSrkEos fluid = new SystemSrkEos(298.15, 25.0);
fluid.addComponent("methane", 1.0);
fluid.setMixingRule("classic");
Stream feed = new Stream("feed", fluid);
feed.setFlowRate(1000.0, "kg/hr");
ThrottlingValve valve = new ThrottlingValve("pressure valve", feed);
valve.setOutletPressure(10.0, "bara");
valve.setPercentValveOpening(50.0);
PressureTransmitter transmitter = new PressureTransmitter("PT-100", feed);
transmitter.setUnit("bara");
ControllerDeviceBaseClass controller = new ControllerDeviceBaseClass("PC-100");
controller.setTransmitter(transmitter);
controller.setControllerSetPoint(20.0, "bara");
controller.setControllerParameters(2.0, 10.0, 0.0);
controller.setReverseActing(false);
controller.setOutputLimits(0.0, 100.0);
valve.setController(controller);
ProcessSystem process = new ProcessSystem("pressure controller wiring");
process.add(feed);
process.add(valve);
process.add(controller);
process.run(); // Initialize outlet state and valve sizing before the dynamic step.
valve.setCalculateSteadyState(false);
process.runTransient(1.0, UUID.randomUUID());
assert valve.getController("PC-100") == controller;
assert valve.getControllers().contains(controller);
assert Math.abs(controller.getMeasuredValue("bara") - 25.0) < 1.0e-8;
assert Math.abs(controller.getResponse() - 51.0) < 1.0e-8;
assert Math.abs(valve.getPercentValveOpening() - 51.0) < 1.0e-8;
assert valve.getOutletStream().getFlowRate("kg/hr") > 0.0;
}
}
For time series, call process.runTransient(dt, UUID.randomUUID()) once per physical step,
with dt in seconds. Read the measured value, getControllerSetPoint, and getResponse for
results. Reuse a UUID only for repeated evaluations of the same physical step.
Native Dynamic Control Blocks
TransferFunctionBlock supplies first-order lag, lead-lag, dead-time, and second-order signal
dynamics. LogicBlock evaluates threshold, fixed, or chained Boolean inputs. Both read a
transmitter’s configured measurement unit, so set that unit explicitly.
import java.util.UUID;
import neqsim.process.controllerdevice.LogicBlock;
import neqsim.process.controllerdevice.TransferFunctionBlock;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.measurementdevice.PressureTransmitter;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
public class DynamicControlBlocksExample {
public static void main(String[] args) {
SystemSrkEos fluid = new SystemSrkEos(298.15, 20.0);
fluid.addComponent("methane", 1.0);
fluid.setMixingRule("classic");
Stream feed = new Stream("measured stream", fluid);
feed.setFlowRate(1000.0, "kg/hr");
PressureTransmitter transmitter = new PressureTransmitter("PT-100", feed);
transmitter.setUnit("bara");
TransferFunctionBlock lag = new TransferFunctionBlock(
"pressure filter", TransferFunctionBlock.Type.FIRST_ORDER_LAG);
lag.setTransmitter(transmitter);
lag.setLagTime(5.0); // s
LogicBlock highPressure = new LogicBlock("high pressure", LogicBlock.Operator.AND);
highPressure.addInput(transmitter, 30.0, LogicBlock.Comparator.GREATER_EQUAL);
ProcessSystem process = new ProcessSystem("signal dynamics");
process.add(feed);
process.add(lag);
process.add(highPressure);
process.run();
process.runTransient(1.0, UUID.randomUUID());
assert Math.abs(lag.getOutput() - 20.0) < 1.0e-8;
assert !highPressure.getOutputBoolean();
feed.setPressure(32.0, "bara");
process.runTransient(1.0, UUID.randomUUID());
// Backward-Euler first-order lag: 20 + (32 - 20) * 1 / (5 + 1) = 22 bara.
assert Math.abs(lag.getOutput() - 22.0) < 1.0e-8;
assert highPressure.getOutputBoolean();
}
}
Both concrete block classes participate in ProcessSystem and multi-area ProcessModel
transient-step transactions. A rejected step restores dynamic state, delay buffers, output,
calculation identity, configuration, and original transmitter/input bindings. Replaying that
physical-step identifier after rollback reproduces the control-block continuation. Repeated
evaluation with an already accepted identifier is ignored. TransferFunctionBlock.reset() also
clears the remembered identifier so a run can restart from its initial state.
Subclasses must supply snapshots for their own mutable state to obtain transaction coverage. The transaction mechanism does not validate tuning or safety integrity, or defer external side effects from callbacks.
Process Logic
A LogicBlock produces a Boolean signal; it does not automatically shut a valve. Connect the
signal to an explicit action or sequence. The implemented neqsim.process.logic subpackages
contain StartupLogic, ShutdownLogic, conditions, and actions for these workflows. See
Advanced Process Logic for sequence integration.
Alarm Integration
Configure limits on a measurement device using AlarmConfig, then register that measurement
with ProcessAlarmManager. Alarm values and thresholds must use the same engineering unit.
There is no equipment/property overload of addAlarm as previously shown on this page.
See the Alarm System Guide for configuration, evaluation,
acknowledgment, and history.
Related Documentation
- ProcessSystem - Named controllers and process execution
- Dynamic Simulation Guide - Equipment dynamics and controller scans
- Dynamic Simulation Helper - Instrumentation for dynamic simulation
- Adjusters - Supported property names and callbacks
- Recycles - Tear streams and convergence
- Calculators and Setters - Calculation callbacks and specifications