Extending NeqSim with New Process Equipment
This guide explains how to add new process equipment to NeqSim. Whether you’re creating a specialized reactor, a custom separator design, or a novel unit operation, you’ll need to understand the equipment architecture and implement the required interfaces.
Table of Contents
- Architecture Overview
- Creating a Simple Equipment Class
- Implementing the run() Method
- Stream Connections
- Adding Mechanical Design
- Validation Framework
- Complete Example: Static Mixer
- Testing Your Equipment
- Python Integration
- Best Practices
Architecture Overview
Equipment Hierarchy
All process equipment in NeqSim follows this inheritance hierarchy:
ProcessElementInterface (marker — extends NamedInterface, Serializable)
└── ProcessEquipmentInterface (extends SimulationInterface)
└── ProcessEquipmentBaseClass (abstract)
├── TwoPortEquipment (single inlet/outlet)
│ ├── Heater, Cooler
│ ├── Compressor, Pump
│ ├── ThrottlingValve
│ └── AdiabaticPipe
├── MultiPortEquipment (abstract — multiple inlets/outlets)
│ └── Your Custom Multi-Port Equipment
├── Separator (multiple outlets)
│ ├── ThreePhaseSeparator
│ └── GasScrubber
├── Mixer, Splitter
└── Your Custom Equipment
Key Interfaces
| Interface | Purpose |
|---|---|
ProcessElementInterface |
Common marker for all process system elements (equipment, controllers, sensors) |
ProcessEquipmentInterface |
Core interface all equipment must implement — extends ProcessElementInterface |
SimulationInterface |
Defines run(), getName(), solved() |
ControllerDeviceInterface |
Feedback controllers — also extends ProcessElementInterface |
MeasurementDeviceInterface |
Sensors and transmitters — also extends ProcessElementInterface |
CapacityConstrainedEquipment |
Optional: for capacity-constrained equipment |
StateVectorProvider |
Optional: for ML/digital twin integration |
Required Methods
Your equipment class must implement or inherit:
| Method | Purpose |
|---|---|
run(UUID id) |
Main calculation logic |
getThermoSystem() |
Return fluid state |
getOutletStream() |
Return outlet stream(s) |
getMechanicalDesign() |
Return mechanical design object |
Creating a Simple Equipment Class
Step 1: Create the Package and Class
Create your class in the appropriate package under neqsim.process.equipment:
package neqsim.process.equipment.mixer;
import java.util.UUID;
import neqsim.process.equipment.ProcessEquipmentBaseClass;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.stream.StreamInterface;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
/**
* StaticMixer performs inline mixing of a main stream with an injection stream.
*
* @author YourName
* @version 1.0
*/
public class StaticMixer extends ProcessEquipmentBaseClass {
/** Serialization version UID. */
private static final long serialVersionUID = 1000;
/** Main process stream. */
private StreamInterface mainStream;
/** Injection stream (chemical, water, etc.). */
private StreamInterface injectionStream;
/** Mixed outlet stream. */
private StreamInterface outletStream;
/** Internal thermodynamic system for calculations. */
private SystemInterface thermoSystem;
/** Pressure drop across mixer in bar. */
private double pressureDrop = 0.0;
/** Mixing efficiency (0-1). */
private double mixingEfficiency = 1.0;
/**
* Constructor for StaticMixer.
*
* @param name Equipment name
*/
public StaticMixer(String name) {
super(name);
}
/**
* Constructor for StaticMixer with main stream.
*
* @param name Equipment name
* @param mainStream Main process stream
*/
public StaticMixer(String name, StreamInterface mainStream) {
this(name);
setMainStream(mainStream);
}
}
Step 2: Add Stream Setters and Getters
/**
* Sets the main process stream.
*
* @param mainStream the main stream to set
*/
public void setMainStream(StreamInterface mainStream) {
this.mainStream = mainStream;
}
/**
* Sets the injection stream.
*
* @param injectionStream the injection stream to set
*/
public void setInjectionStream(StreamInterface injectionStream) {
this.injectionStream = injectionStream;
}
/**
* Gets the outlet stream.
*
* @return the mixed outlet stream
*/
public StreamInterface getOutletStream() {
return outletStream;
}
/** {@inheritDoc} */
@Override
public SystemInterface getThermoSystem() {
return thermoSystem;
}
Implementing the run() Method
The run(UUID id) method is where all calculations happen. This method is called by ProcessSystem when executing the flowsheet.
Basic Pattern
/** {@inheritDoc} */
@Override
public void run(UUID id) {
// Step 1: Validate inputs
if (mainStream == null) {
throw new RuntimeException("Main stream not set for " + getName());
}
// Step 2: Clone inlet fluid(s) to avoid modifying upstream
thermoSystem = mainStream.getThermoSystem().clone();
// Step 3: Mix streams if injection is present
if (injectionStream != null) {
SystemInterface injectionFluid = injectionStream.getThermoSystem();
// Add components from injection stream
for (int i = 0; i < injectionFluid.getNumberOfComponents(); i++) {
String compName = injectionFluid.getComponent(i).getName();
double moles = injectionFluid.getComponent(i).getNumberOfmoles();
if (thermoSystem.hasComponent(compName)) {
thermoSystem.addComponent(compName, moles * mixingEfficiency);
} else {
thermoSystem.addComponent(compName, moles * mixingEfficiency);
}
}
}
// Step 4: Apply pressure drop
if (pressureDrop > 0) {
thermoSystem.setPressure(thermoSystem.getPressure() - pressureDrop);
}
// Step 5: Run thermodynamic flash
ThermodynamicOperations ops = new ThermodynamicOperations(thermoSystem);
ops.TPflash();
thermoSystem.initProperties();
// Step 6: Create/update outlet stream
if (outletStream == null) {
outletStream = new Stream("Mixed Stream", thermoSystem);
} else {
outletStream.setThermoSystem(thermoSystem);
}
outletStream.run(id);
// Step 7: Mark as solved
setCalculationIdentifier(id);
}
Handling Multiple Outlets
For equipment with multiple outlets (like separators):
/** {@inheritDoc} */
@Override
public void run(UUID id) {
// ... inlet processing ...
// Run flash calculation
ThermodynamicOperations ops = new ThermodynamicOperations(thermoSystem);
ops.TPflash();
thermoSystem.initProperties();
// Create phase-specific outlet streams
if (thermoSystem.hasPhaseType("gas")) {
gasSystem = thermoSystem.phaseToSystem("gas");
gasOutStream.setThermoSystem(gasSystem);
gasOutStream.run(id);
}
if (thermoSystem.hasPhaseType("oil") || thermoSystem.hasPhaseType("aqueous")) {
liquidSystem = thermoSystem.phaseToSystem(1); // liquid phase
liquidOutStream.setThermoSystem(liquidSystem);
liquidOutStream.run(id);
}
setCalculationIdentifier(id);
}
Stream Connections
Input Streams
Equipment receives input through stream setters:
// In your equipment class
public void setInletStream(StreamInterface inletStream) {
this.inletStream = inletStream;
// Initialize internal systems based on inlet
thermoSystem = inletStream.getThermoSystem().clone();
}
// For multiple inlets, use a Mixer internally
private Mixer inletMixer = new Mixer("Internal Mixer");
public void addStream(StreamInterface newStream) {
inletMixer.addStream(newStream);
numberOfInputStreams++;
}
Output Streams
Provide getters for downstream equipment:
public StreamInterface getOutletStream() {
return outletStream;
}
// For multiple outlets
public StreamInterface getGasOutStream() {
return gasOutStream;
}
public StreamInterface getLiquidOutStream() {
return liquidOutStream;
}
Stream Introspection (getInletStreams / getOutletStreams)
Every equipment class should override getInletStreams() and getOutletStreams() so that generic tools (DEXPI export, graph builders, topology analysis) can discover the process topology without casting to specific equipment types.
TwoPortEquipment already provides default overrides returning single-element lists, so subclasses like Heater, Compressor, and Valve get this for free. Custom equipment with multiple ports must override explicitly:
@Override
public List<StreamInterface> getInletStreams() {
// For equipment with an internal Mixer, delegate:
return inletMixer.getInletStreams();
}
@Override
public List<StreamInterface> getOutletStreams() {
List<StreamInterface> outlets = new ArrayList<>(2);
if (gasOutStream != null) {
outlets.add(gasOutStream);
}
if (liquidOutStream != null) {
outlets.add(liquidOutStream);
}
return Collections.unmodifiableList(outlets);
}
The returned lists must be unmodifiable. Returning Collections.emptyList() is correct when a stream is not yet connected.
MultiPortEquipment Base Class
For new equipment with arbitrary numbers of inlets and outlets, extend MultiPortEquipment instead of ProcessEquipmentBaseClass:
import neqsim.process.equipment.MultiPortEquipment;
public class HeatExchangerNetwork extends MultiPortEquipment {
public HeatExchangerNetwork(String name) {
super(name);
}
public void addHotStream(StreamInterface stream) {
addInletStream(stream); // inherited from MultiPortEquipment
}
public void addColdStream(StreamInterface stream) {
addInletStream(stream); // inherited
}
// getInletStreams() and getOutletStreams() are already implemented
}
MultiPortEquipment manages inletStreams and outletStreams lists and returns unmodifiable views from the getter methods. Existing equipment classes (Separator, Mixer, Splitter) were not refactored to extend this class to preserve backward compatibility.
Using in ProcessSystem
ProcessSystem process = new ProcessSystem();
Stream feed = new Stream("Feed", fluid);
process.add(feed);
StaticMixer mixer = new StaticMixer("Chemical Injection", feed);
mixer.setInjectionStream(chemicalStream);
mixer.setPressureDrop(0.5);
process.add(mixer);
Separator sep = new Separator("HP Sep", mixer.getOutletStream());
process.add(sep);
process.run();
Adding Mechanical Design
Create a MechanicalDesign Subclass
package neqsim.process.mechanicaldesign.mixer;
import neqsim.process.equipment.ProcessEquipmentInterface;
import neqsim.process.mechanicaldesign.MechanicalDesign;
/**
* Mechanical design for static mixer equipment.
*/
public class StaticMixerMechanicalDesign extends MechanicalDesign {
private double pipeDiameter = 0.1; // meters
private int numberOfElements = 6;
private String materialGrade = "316SS";
public StaticMixerMechanicalDesign(ProcessEquipmentInterface equipment) {
super(equipment);
}
@Override
public void calcDesign() {
// Calculate design parameters based on process conditions
double flowRate = getProcessEquipment().getFluid()
.getFlowRate("m3/hr");
// Size based on velocity (typical 1-3 m/s)
double velocity = 2.0; // m/s target
double area = flowRate / 3600.0 / velocity;
pipeDiameter = Math.sqrt(4.0 * area / Math.PI);
// Number of elements based on mixing requirements
numberOfElements = (int) Math.ceil(pipeDiameter * 20);
}
// Getters and setters...
}
Wire Up in Equipment Class
public class StaticMixer extends ProcessEquipmentBaseClass {
private StaticMixerMechanicalDesign mechanicalDesign;
@Override
public void initMechanicalDesign() {
mechanicalDesign = new StaticMixerMechanicalDesign(this);
}
@Override
public StaticMixerMechanicalDesign getMechanicalDesign() {
if (mechanicalDesign == null) {
initMechanicalDesign();
}
return mechanicalDesign;
}
}
Validation Framework
Implement validateSetup() to catch configuration errors early:
import neqsim.util.validation.ValidationResult;
public class StaticMixer extends ProcessEquipmentBaseClass {
/**
* Validates equipment setup before running.
*
* @return validation result with any errors or warnings
*/
@Override
public ValidationResult validateSetup() {
ValidationResult result = new ValidationResult(getName());
// Check required inputs
if (mainStream == null) {
result.addError("mainStream",
"Main stream not connected",
"Call setMainStream() with a valid stream");
}
// Check parameter ranges
if (pressureDrop < 0) {
result.addError("pressureDrop",
"Pressure drop cannot be negative: " + pressureDrop,
"Set pressureDrop to a positive value or zero");
}
if (mixingEfficiency < 0 || mixingEfficiency > 1) {
result.addWarning("mixingEfficiency",
"Efficiency outside 0-1 range: " + mixingEfficiency,
"Consider setting efficiency between 0 and 1");
}
return result;
}
}
Complete Example: Static Mixer
Here’s the complete implementation:
package neqsim.process.equipment.mixer;
import java.util.UUID;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import neqsim.process.equipment.ProcessEquipmentBaseClass;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.stream.StreamInterface;
import neqsim.process.mechanicaldesign.mixer.StaticMixerMechanicalDesign;
import neqsim.thermo.system.SystemInterface;
import neqsim.thermodynamicoperations.ThermodynamicOperations;
import neqsim.util.validation.ValidationResult;
/**
* StaticMixer performs inline mixing of a main stream with an injection stream.
*
* <p>Static mixers use fixed internal elements to create turbulent mixing without
* moving parts. This class models the thermodynamic mixing and pressure drop.</p>
*
* @author NeqSim Development Team
* @version 1.0
*/
public class StaticMixer extends ProcessEquipmentBaseClass {
/** Serialization version UID. */
private static final long serialVersionUID = 1000;
/** Logger object for class. */
static Logger logger = LogManager.getLogger(StaticMixer.class);
private StreamInterface mainStream;
private StreamInterface injectionStream;
private StreamInterface outletStream;
private SystemInterface thermoSystem;
private double pressureDrop = 0.0;
private double mixingEfficiency = 1.0;
private StaticMixerMechanicalDesign mechanicalDesign;
/**
* Constructor for StaticMixer.
*
* @param name Equipment name
*/
public StaticMixer(String name) {
super(name);
}
/**
* Constructor for StaticMixer with main stream.
*
* @param name Equipment name
* @param mainStream Main process stream
*/
public StaticMixer(String name, StreamInterface mainStream) {
this(name);
setMainStream(mainStream);
}
/**
* Sets the main process stream.
*
* @param mainStream the main stream to set
*/
public void setMainStream(StreamInterface mainStream) {
this.mainStream = mainStream;
}
/**
* Sets the injection stream.
*
* @param injectionStream the injection stream to set
*/
public void setInjectionStream(StreamInterface injectionStream) {
this.injectionStream = injectionStream;
}
/**
* Gets the outlet stream.
*
* @return the mixed outlet stream
*/
public StreamInterface getOutletStream() {
return outletStream;
}
/**
* Sets the pressure drop across the mixer.
*
* @param pressureDrop pressure drop in bar
*/
public void setPressureDrop(double pressureDrop) {
this.pressureDrop = pressureDrop;
}
/**
* Gets the pressure drop across the mixer.
*
* @return pressure drop in bar
*/
public double getPressureDrop() {
return pressureDrop;
}
/**
* Sets the mixing efficiency.
*
* @param efficiency mixing efficiency (0-1)
*/
public void setMixingEfficiency(double efficiency) {
this.mixingEfficiency = efficiency;
}
/** {@inheritDoc} */
@Override
public SystemInterface getThermoSystem() {
return thermoSystem;
}
/** {@inheritDoc} */
@Override
public void initMechanicalDesign() {
mechanicalDesign = new StaticMixerMechanicalDesign(this);
}
/** {@inheritDoc} */
@Override
public StaticMixerMechanicalDesign getMechanicalDesign() {
if (mechanicalDesign == null) {
initMechanicalDesign();
}
return mechanicalDesign;
}
/** {@inheritDoc} */
@Override
public ValidationResult validateSetup() {
ValidationResult result = new ValidationResult(getName());
if (mainStream == null) {
result.addError("mainStream",
"Main stream not connected",
"Call setMainStream() with a valid stream");
}
if (pressureDrop < 0) {
result.addError("pressureDrop",
"Pressure drop cannot be negative: " + pressureDrop,
"Set pressureDrop to a positive value or zero");
}
if (mixingEfficiency < 0 || mixingEfficiency > 1) {
result.addWarning("mixingEfficiency",
"Efficiency outside 0-1 range: " + mixingEfficiency,
"Consider setting efficiency between 0 and 1");
}
return result;
}
/** {@inheritDoc} */
@Override
public void run(UUID id) {
// Validate setup
ValidationResult validation = validateSetup();
if (validation.hasErrors()) {
throw new RuntimeException("Validation failed: " +
validation.getErrors().get(0).getMessage());
}
// Clone inlet fluid
thermoSystem = mainStream.getThermoSystem().clone();
// Mix streams if injection is present
if (injectionStream != null) {
SystemInterface injectionFluid = injectionStream.getThermoSystem();
for (int i = 0; i < injectionFluid.getNumberOfComponents(); i++) {
String compName = injectionFluid.getComponent(i).getName();
double moles = injectionFluid.getComponent(i).getNumberOfmoles();
thermoSystem.addComponent(compName, moles * mixingEfficiency);
}
}
// Apply pressure drop
if (pressureDrop > 0) {
thermoSystem.setPressure(thermoSystem.getPressure() - pressureDrop);
}
// Run thermodynamic flash
ThermodynamicOperations ops = new ThermodynamicOperations(thermoSystem);
ops.TPflash();
thermoSystem.initProperties();
// Create/update outlet stream
if (outletStream == null) {
outletStream = new Stream(getName() + " outlet", thermoSystem);
} else {
outletStream.setThermoSystem(thermoSystem);
}
outletStream.run(id);
setCalculationIdentifier(id);
}
}
Testing Your Equipment
Unit Test Example
package neqsim.process.equipment.mixer;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;
public class StaticMixerTest {
private SystemSrkEos mainFluid;
private SystemSrkEos injectionFluid;
@BeforeEach
void setUp() {
mainFluid = new SystemSrkEos(298.15, 50.0);
mainFluid.addComponent("methane", 0.9);
mainFluid.addComponent("ethane", 0.1);
mainFluid.setMixingRule("classic");
injectionFluid = new SystemSrkEos(298.15, 50.0);
injectionFluid.addComponent("MEG", 1.0);
injectionFluid.setMixingRule("classic");
}
@Test
void testBasicMixing() {
ProcessSystem process = new ProcessSystem();
Stream mainStream = new Stream("Main", mainFluid);
mainStream.setFlowRate(1000.0, "kg/hr");
process.add(mainStream);
Stream injection = new Stream("Injection", injectionFluid);
injection.setFlowRate(10.0, "kg/hr");
process.add(injection);
StaticMixer mixer = new StaticMixer("Chemical Injection", mainStream);
mixer.setInjectionStream(injection);
mixer.setPressureDrop(0.5);
process.add(mixer);
process.run();
// Verify outlet contains all components
assertTrue(mixer.getOutletStream().getFluid().hasComponent("MEG"));
assertTrue(mixer.getOutletStream().getFluid().hasComponent("methane"));
// Verify pressure drop
assertEquals(49.5, mixer.getOutletStream().getPressure(), 0.01);
// Verify mass balance
double inMass = mainStream.getFlowRate("kg/hr") + injection.getFlowRate("kg/hr");
double outMass = mixer.getOutletStream().getFlowRate("kg/hr");
assertEquals(inMass, outMass, 0.1);
}
@Test
void testValidation() {
StaticMixer mixer = new StaticMixer("Test");
// No main stream set
var result = mixer.validateSetup();
assertTrue(result.hasErrors());
}
}
Python Integration
Using Your Equipment from Python
Once your Java equipment is compiled into NeqSim, you can use it from Python via neqsim-python:
from neqsim import jneqsim
# Import your custom equipment
StaticMixer = jneqsim.process.equipment.mixer.StaticMixer
# Create fluid
fluid = jneqsim.thermo.system.SystemSrkEos(298.15, 50.0)
fluid.addComponent("methane", 0.9)
fluid.addComponent("ethane", 0.1)
fluid.setMixingRule("classic")
# Create injection fluid
injection_fluid = jneqsim.thermo.system.SystemSrkEos(298.15, 50.0)
injection_fluid.addComponent("MEG", 1.0)
injection_fluid.setMixingRule("classic")
# Build process
ProcessSystem = jneqsim.process.processmodel.ProcessSystem
Stream = jneqsim.process.equipment.stream.Stream
process = ProcessSystem()
main_stream = Stream("Main Feed", fluid)
main_stream.setFlowRate(1000.0, "kg/hr")
process.add(main_stream)
injection = Stream("MEG Injection", injection_fluid)
injection.setFlowRate(10.0, "kg/hr")
process.add(injection)
mixer = StaticMixer("Chemical Injection", main_stream)
mixer.setInjectionStream(injection)
mixer.setPressureDrop(0.5)
process.add(mixer)
# Run and get results
process.run()
print(f"Outlet temperature: {mixer.getOutletStream().getTemperature() - 273.15:.1f} °C")
print(f"Outlet pressure: {mixer.getOutletStream().getPressure():.1f} bara")
print(f"MEG in outlet: {mixer.getOutletStream().getFluid().hasComponent('MEG')}")
Implementing Java Interfaces from Python
For callbacks and custom objective functions, you can implement Java interfaces in Python using JPype:
from jpype import JImplements, JOverride
@JImplements("neqsim.process.util.optimizer.ObjectiveFunction")
class CustomObjective:
@JOverride
def evaluate(self, process):
mixer = process.getUnit("Chemical Injection")
return mixer.getPressureDrop()
@JOverride
def getName(self):
return "Mixer Pressure Drop"
@JOverride
def getDirection(self):
return jneqsim.process.util.optimizer.ObjectiveFunction.Direction.MINIMIZE
Best Practices
1. Always Clone Input Fluids
// CORRECT - clone to avoid modifying upstream
thermoSystem = inletStream.getThermoSystem().clone();
// WRONG - modifies upstream equipment
thermoSystem = inletStream.getThermoSystem();
thermoSystem.setPressure(newPressure); // Changes inlet!
2. Handle Missing Phases Gracefully
if (thermoSystem.hasPhaseType("gas")) {
gasOutStream.setThermoSystem(thermoSystem.phaseToSystem("gas"));
} else {
// Create empty gas stream or handle appropriately
logger.warn("No gas phase in " + getName());
}
3. Support Unit Conversions
public double getPressureDrop(String unit) {
if (unit.equals("bar") || unit.equals("bara")) {
return pressureDrop;
} else if (unit.equals("psi") || unit.equals("psia")) {
return pressureDrop * 14.5038;
}
throw new IllegalArgumentException("Unknown unit: " + unit);
}
4. Implement Serialization
Ensure all fields are serializable for ProcessEquipmentBaseClass.copy():
// Mark non-serializable fields as transient
private transient SomeNonSerializableClass tempObject;
// Or ensure custom classes implement Serializable
private MyCustomClass config; // Must implement Serializable
5. Add Logging for Debugging
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class StaticMixer extends ProcessEquipmentBaseClass {
static Logger logger = LogManager.getLogger(StaticMixer.class);
@Override
public void run(UUID id) {
logger.debug("Running {} with inlet T={} K, P={} bar",
getName(),
mainStream.getTemperature(),
mainStream.getPressure());
// ... calculations ...
logger.debug("{} completed: outlet T={} K, P={} bar",
getName(),
outletStream.getTemperature(),
outletStream.getPressure());
}
}
6. Document with JavaDoc
/**
* Calculates the outlet conditions after mixing.
*
* <p>The mixing process is adiabatic unless heat input is specified.
* Pressure drop is applied after mixing.</p>
*
* @param id Calculation identifier for tracking
* @throws RuntimeException if main stream is not connected
* @see ProcessEquipmentBaseClass#run(UUID)
*/
@Override
public void run(UUID id) {
// ...
}
See Also
- Process Simulation Introduction
- Equipment Hierarchy
- Mechanical Design Framework
- Validation Framework
- Python Extension Patterns
Document last updated: February 2026