Skip to the content.

Batch Studies

New to process optimization? Start with the Optimization Overview to understand when to use which optimizer.

This document describes the batch study infrastructure for parallel parameter studies and concept screening.

Document Description
Optimization Overview When to use which optimizer
Multi-Objective Optimization Pareto fronts and trade-offs
Production Optimization Guide ProductionOptimizer examples

Overview

Early-phase engineering requires rapid evaluation of many alternatives. The BatchStudy class provides:

Table of Contents

Usage

Basic Usage

The Java blocks use the imports and base process below. Put executable statements inside public static void main(String[] args) throws Exception; place imports above your class. Later blocks are alternatives continuing from this setup; use separate scopes when reusing variable names. Python blocks run in order in one session with neqsim, jpype1, pandas, numpy, and matplotlib installed.

Java output uses Log4j2. Declare this field inside your example class: private static final org.apache.logging.log4j.Logger logger = org.apache.logging.log4j.LogManager.getLogger("OptimizationExample");.

import java.util.*;
import java.time.Duration;
import neqsim.process.util.optimizer.BatchStudy;
import neqsim.process.util.optimizer.BatchStudy.*;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.compressor.Compressor;
import neqsim.process.equipment.heatexchanger.Heater;
import neqsim.process.equipment.heatexchanger.Cooler;
import neqsim.process.processmodel.ProcessSystem;
import neqsim.thermo.system.SystemSrkEos;

SystemSrkEos fluid = new SystemSrkEos(298.15, 50.0);
fluid.addComponent("methane", 0.85);
fluid.addComponent("ethane", 0.10);
fluid.addComponent("propane", 0.05);
fluid.setMixingRule("classic");
Stream feed = new Stream("feed", fluid);
feed.setFlowRate(10000.0, "kg/hr");
Heater heater = new Heater("heater", feed);
heater.setOutTemperature(350.0, "K");
Compressor compressor = new Compressor("compressor", heater.getOutletStream());
compressor.setOutletPressure(100.0, "bara");
compressor.setIsentropicEfficiency(0.78);
ProcessSystem baseCase = new ProcessSystem();
baseCase.add(feed);
baseCase.add(heater);
baseCase.add(compressor);
baseCase.run();

// BatchStudy temperature parameters are in degrees Celsius.
BatchStudy study = BatchStudy.builder(baseCase)
    .vary("heater.outletTemperature", 30.0, 100.0, 5)
    .vary("compressor.outletPressure", 80.0, 120.0, 6)
    .addObjective("power", Objective.MINIMIZE,
        proc -> ((Compressor) proc.getUnit("compressor")).getPower("kW"))
    .addObjective("throughput", Objective.MAXIMIZE,
        proc -> ((Stream) proc.getUnit("feed")).getFlowRate("kg/hr"))
    // Illustrative purchased-electricity factor: 0.2 kg CO2e/kWh.
    .addObjective("emissions", Objective.MINIMIZE,
        proc -> 0.2 * ((Compressor) proc.getUnit("compressor")).getPower("kW"))
    .parallelism(4)
    .name("HeaterCompressorStudy")
    .stopOnFailure(false)
    .build();
BatchStudyResult result = study.run();
logger.info(result.getSummary());
if (result.getFailureCount() != 0) {
    throw new IllegalStateException("Inspect failed cases before ranking the study");
}
result.exportToCSV("batch_results.csv");

Convenience Method on ProcessSystem

// Continue from Basic Usage.
BatchStudy.Builder studyBuilder = baseCase.createBatchStudy();

Parameter Variation Methods

// Range: five values [80, 90, 100, 110, 120] bara.
BatchStudy rangeStudy = BatchStudy.builder(baseCase)
    .vary("compressor.outletPressure", 80.0, 120.0, 5).build();
// Explicit values: use an array to avoid selecting the range overload.
BatchStudy explicitStudy = BatchStudy.builder(baseCase)
    .vary("compressor.outletPressure", new double[] {80.0, 100.0, 120.0}).build();
// A single case uses the explicit-values overload (a range needs >= 2 steps).
BatchStudy singleStudy = BatchStudy.builder(baseCase)
    .vary("compressor.outletPressure", new double[] {100.0}).build();

Supported Parameter Paths

Parameters are specified as equipment.property:

Property Equipment Types Example
duty Heaters, Coolers heater.duty
pressure Valves, Compressors, Pumps valve.pressure
outletPressure Valves, Compressors, Pumps compressor.outletPressure
opening Valves valve.opening
percentValveOpening Valves valve.percentValveOpening
cv Valves valve.cv
outletTemperature Heaters, Coolers heater.outletTemperature
polytropicEfficiency Compressors compressor.polytropicEfficiency
isentropicEfficiency Compressors compressor.isentropicEfficiency
temperature Streams stream.temperature
flowRate Streams stream.flowRate
internalDiameter Separators separator.internalDiameter

Temperatures in temperature and outletTemperature paths are °C, pressures are bara, duty is W, flow is kg/hr, diameter is m, opening is %, and efficiencies are fractions. A separator pressure is set by its inlet boundary; separator.pressure is not a supported variation. Unknown equipment/property paths produce failed cases instead of silently leaving the process unchanged.

Range variation requires at least two steps. For one value or an explicit list, pass double[] (Python: JArray(JDouble)) to select the varargs overload.

Result Analysis

BatchStudyResult

// Summary statistics
int total = result.getTotalCases();
int completed = result.getSuccessCount();
int failed = result.getFailureCount();
String summary = result.getSummary();

// Find best cases
CaseResult bestByPower = result.getBestCase("power");
CaseResult bestByEmissions = result.getBestCase("emissions");

// Get all results
List<CaseResult> allResults = result.getAllResults();

// Filter successful cases
List<CaseResult> successful = result.getSuccessfulResults();

// Export
result.exportToCSV("results.csv");
result.exportToJSON("results.json");
String json = result.toJson();  // Timestamps/durations are ISO-8601 strings

// Pareto front analysis (non-dominated solutions)
List<CaseResult> paretoFront = result.getParetoFront("power", "emissions");

CaseResult

CaseResult caseResult = result.getBestCase("power");
if (caseResult == null) {
    throw new IllegalStateException("No successful finite power objective");
}

// Parameter values used
Map<String, Double> params = caseResult.parameters.values;

// Check status
boolean failed = caseResult.failed;
String error = caseResult.errorMessage;

// Objective values
Map<String, Double> objectives = caseResult.objectiveValues;
double power = objectives.get("power");

// Runtime
Duration caseRuntime = caseResult.runtime;

Multi-Objective Analysis

// Illustrative economic screening, not vendor CAPEX estimates.
// Power: kW; CAPEX proxy: currency; OPEX proxy: currency/year;
// purchased-electricity emissions: kg CO2e/hour; throughput: kg/hour.
BatchStudy economicStudy = BatchStudy.builder(baseCase)
    .vary("feed.flowRate", 5000.0, 15000.0, 5)
    .addObjective("capex", Objective.MINIMIZE,
        proc -> 1000.0 * Math.pow(
            ((Compressor) proc.getUnit("compressor")).getPower("kW"), 0.7))
    .addObjective("opex", Objective.MINIMIZE,
        proc -> 8000.0 * 0.10 * ((Compressor) proc.getUnit("compressor")).getPower("kW"))
    .addObjective("emissions", Objective.MINIMIZE,
        proc -> 0.20 * ((Compressor) proc.getUnit("compressor")).getPower("kW"))
    .addObjective("throughput", Objective.MAXIMIZE,
        proc -> ((Stream) proc.getUnit("feed")).getFlowRate("kg/hr"))
    .build();
BatchStudyResult economicResult = economicStudy.run();
List<CaseResult> economicFront = economicResult.getParetoFront("opex", "throughput");

Integration Examples

With Emissions Tracking

// Add a purchased-electricity emissions objective to a new builder.
// Replace this illustrative factor with the applicable electricity inventory.
BatchStudy.Builder emissionsStudy = BatchStudy.builder(baseCase)
    .addObjective("co2e_kg_hr", Objective.MINIMIZE,
        proc -> 0.20 * ((Compressor) proc.getUnit("compressor")).getPower("kW"));

With Safety Scenarios

// Pressure-boundary scenarios; this is steady-state screening, not a relief study.
for (double dischargePressure : new double[] {90.0, 100.0, 110.0}) {
    ProcessSystem scenarioCase = baseCase.copy();
    ((Compressor) scenarioCase.getUnit("compressor"))
        .setOutletPressure(dischargePressure, "bara");
    BatchStudy scenarioStudy = BatchStudy.builder(scenarioCase)
        .vary("feed.flowRate", 5000.0, 15000.0, 5)
        .addObjective("power_margin_kW", Objective.MAXIMIZE,
            proc -> 1000.0 - ((Compressor) proc.getUnit("compressor")).getPower("kW"))
        .build();
    BatchStudyResult scenarioResult = scenarioStudy.run();
    logger.info(scenarioResult.getSummary());
}

Concept Screening Example

// Compare 1-4 stages at the same 30 bara suction and 150 bara discharge.
// Vary flow in every concept; keep the final pressure identical.
Map<Integer, BatchStudyResult> conceptResults = new LinkedHashMap<>();
for (int stages = 1; stages <= 4; stages++) {
    ProcessSystem concept = new ProcessSystem();
    Stream conceptFeed = new Stream("feed", fluid.clone());
    conceptFeed.setPressure(30.0, "bara");
    conceptFeed.setFlowRate(10000.0, "kg/hr");
    concept.add(conceptFeed);
    neqsim.process.equipment.stream.StreamInterface inlet = conceptFeed;
    for (int stage = 1; stage <= stages; stage++) {
        Compressor stageCompressor = new Compressor("stage" + stage, inlet);
        stageCompressor.setOutletPressure(30.0 * Math.pow(5.0, (double) stage / stages));
        stageCompressor.setIsentropicEfficiency(0.78);
        concept.add(stageCompressor);
        inlet = stageCompressor.getOutletStream();
        if (stage < stages) {
            Cooler intercooler = new Cooler("cooler" + stage, inlet);
            intercooler.setOutTemperature(308.15, "K");
            concept.add(intercooler);
            inlet = intercooler.getOutletStream();
        }
    }
    BatchStudy conceptStudy = BatchStudy.builder(concept)
        .vary("feed.flowRate", 5000.0, 15000.0, 3)
        .addObjective("power", Objective.MINIMIZE, proc -> {
            double powerKW = 0.0;
            for (neqsim.process.equipment.ProcessEquipmentInterface unit : proc.getUnitOperations()) {
                if (unit instanceof Compressor) {
                    powerKW += ((Compressor) unit).getPower("kW");
                }
            }
            return powerKW;
        })
        .parallelism(2).build();
    conceptResults.put(stages, conceptStudy.run());
}
for (Map.Entry<Integer, BatchStudyResult> entry : conceptResults.entrySet()) {
    CaseResult best = entry.getValue().getBestCase("power");
    if (best == null) {
        throw new IllegalStateException("No successful cases for " + entry.getKey());
    }
    logger.info(String.format("%d stages: %.1f kW at %.0f kg/hr%n", entry.getKey(),
        best.objectiveValues.get("power"), best.parameters.values.get("feed.flowRate")));
}

Performance Considerations

Factor Recommendation
Parallelism Start with CPU cores, adjust based on memory
Case Count Thousands OK, millions need distribution
Memory Each case clones the process system
Timeout Consider case-level timeouts for robustness

Best Practices

  1. Start Small: Test with few cases before large sweeps
  2. Log Progress: Monitor completion for long studies
  3. Handle Failures: Decide continue vs stop strategy
  4. Export Results: Always save before analysis
  5. Version Control: Track study configurations

Python Usage (via JPype)

BatchStudy is fully accessible from Python using neqsim-python.

Basic Setup

from neqsim.neqsimpython import jneqsim
import jpype
from jpype import JImplements, JOverride
import pandas as pd
import json

# Import classes
ProcessSystem = jneqsim.process.processmodel.ProcessSystem
Stream = jneqsim.process.equipment.stream.Stream
Compressor = jneqsim.process.equipment.compressor.Compressor
Heater = jneqsim.process.equipment.heatexchanger.Heater
SystemSrkEos = jneqsim.thermo.system.SystemSrkEos

BatchStudy = jneqsim.process.util.optimizer.BatchStudy
Objective = BatchStudy.Objective

Creating a Base Process

# Create fluid
fluid = SystemSrkEos(298.15, 50.0)
fluid.addComponent("methane", 0.85)
fluid.addComponent("ethane", 0.10)
fluid.addComponent("propane", 0.05)
fluid.setMixingRule("classic")

# Build base process
base_process = ProcessSystem()

feed = Stream("feed", fluid)
feed.setFlowRate(10000.0, "kg/hr")
feed.setPressure(50.0, "bara")
base_process.add(feed)

heater = Heater("heater", feed)
heater.setOutTemperature(350.0, "K")
base_process.add(heater)

compressor = Compressor("compressor", heater.getOutletStream())
compressor.setOutletPressure(100.0, "bara")
base_process.add(compressor)

base_process.run()

Defining Objective Functions in Python

# Define objective functions using Java interface
@JImplements("java.util.function.Function")
class PowerObjective:
    @JOverride
    def apply(self, proc):
        comp = proc.getUnit("compressor")
        return comp.getPower("kW") if comp else 0.0

@JImplements("java.util.function.Function")
class ThroughputObjective:
    @JOverride
    def apply(self, proc):
        return proc.getUnit("feed").getFlowRate("kg/hr")

@JImplements("java.util.function.Function")
class EfficiencyObjective:
    @JOverride
    def apply(self, proc):
        comp = proc.getUnit("compressor")
        return comp.getPolytropicEfficiency() * 100 if comp else 0.0

Building and Running Batch Study

# Build batch study using builder pattern
study = BatchStudy.builder(base_process) \
    .name("HeaterCompressorStudy") \
    .vary("heater.outletTemperature", 30.0, 100.0, 5) \
    .vary("compressor.outletPressure", 80.0, 120.0, 5) \
    .addObjective("power", Objective.MINIMIZE, PowerObjective()) \
    .addObjective("throughput", Objective.MAXIMIZE, ThroughputObjective()) \
    .parallelism(4) \
    .stopOnFailure(False) \
    .build()

# Run the study
result = study.run()

# Print summary
print(f"Total cases: {result.getTotalCases()}")
print(f"Completed: {result.getSuccessCount()}")
print(f"Failed: {result.getFailureCount()}")
print(str(result.getSummary()))

Analyzing Results

# Get best cases
best_power = result.getBestCase("power")
best_throughput = result.getBestCase("throughput")

print(f"\nBest by power: {best_power.objectiveValues.get('power'):.1f} kW")
print(f"Best by throughput: {best_throughput.objectiveValues.get('throughput'):.0f} kg/hr")

# Get all successful results
successful = result.getSuccessfulResults()
print(f"\nSuccessful cases: {len(list(successful))}")

# Get Pareto front for two objectives
pareto_front = result.getParetoFront("power", "throughput")
print(f"Pareto front size: {len(list(pareto_front))}")

Exporting Results

# Export to CSV
result.exportToCSV("batch_results.csv")

# Export to JSON
result.exportToJSON("batch_results.json")

# Get JSON string directly
json_str = result.toJson()
data = json.loads(str(json_str))

Converting to Pandas DataFrame

import pandas as pd

# Build DataFrame from results
rows = []
for case_result in result.getAllResults():
    row = {
        'failed': case_result.failed,
        'error': case_result.errorMessage if case_result.failed else None
    }

    # Add parameters
    for name, value in case_result.parameters.values.items():
        row[f'param_{name}'] = value

    # Add objectives (if successful)
    if not case_result.failed:
        for name, value in case_result.objectiveValues.items():
            row[f'obj_{name}'] = value

    rows.append(row)

df = pd.DataFrame(rows)
print(df.head())

# Filter successful cases
df_success = df[~df['failed']]
print(f"\nSuccessful cases: {len(df_success)}")

# Find optimal
idx_min_power = df_success['obj_power'].idxmin()
print(f"\nMinimum power case:")
print(df_success.loc[idx_min_power])

Visualizing Results

import matplotlib.pyplot as plt
import numpy as np

# Create scatter plot of parameter study
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Plot 1: Power vs parameters
ax1 = axes[0]
if 'param_heater.outletTemperature' in df_success.columns:
    scatter = ax1.scatter(
        df_success['param_heater.outletTemperature'],
        df_success['param_compressor.outletPressure'],
        c=df_success['obj_power'],
        cmap='viridis',
        s=100
    )
    plt.colorbar(scatter, ax=ax1, label='Power (kW)')
    ax1.set_xlabel('Heater Outlet Temperature (°C)')
    ax1.set_ylabel('Compressor Outlet Pressure (bara)')
    ax1.set_title('Power Consumption Heat Map')

# Plot 2: Pareto front
ax2 = axes[1]
ax2.scatter(df_success['obj_power'], df_success['obj_throughput'],
            s=100, alpha=0.6, label='All cases')

# Highlight Pareto front
pareto_rows = []
for case in result.getParetoFront("power", "throughput"):
    pareto_rows.append({
        'power': case.objectiveValues.get('power'),
        'throughput': case.objectiveValues.get('throughput')
    })
df_pareto = pd.DataFrame(pareto_rows)
if not df_pareto.empty:
    ax2.scatter(df_pareto['power'], df_pareto['throughput'],
                s=150, c='red', marker='*', label='Pareto front')

ax2.set_xlabel('Power (kW)')
ax2.set_ylabel('Throughput (kg/hr)')
ax2.set_title('Pareto Front: Power vs Throughput')
ax2.legend()
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('batch_study_results.png', dpi=150)
plt.show()

Using Explicit Parameter Values

# Vary with explicit values instead of range
study = BatchStudy.builder(base_process) \
    .name("ExplicitValuesStudy") \
    .vary("compressor.outletPressure", jpype.JArray(jpype.JDouble)([80.0, 100.0, 120.0])) \
    .vary("heater.outletTemperature", jpype.JArray(jpype.JDouble)([40.0, 60.0, 80.0])) \
    .addObjective("power", Objective.MINIMIZE, PowerObjective()) \
    .parallelism(2) \
    .build()

result = study.run()
print(f"Evaluated {result.getTotalCases()} combinations")

Concept Screening Example

Every concept has 30 bara suction and 150 bara final discharge. The flow sweep is common to all concepts, so the power comparison uses equivalent boundaries.

def create_staged_compressor(num_stages, fluid):
    """Create a compressor train with specified stages"""
    process = ProcessSystem()

    feed = Stream("feed", fluid)
    feed.setFlowRate(10000.0, "kg/hr")
    feed.setPressure(30.0, "bara")
    process.add(feed)

    inlet_stream = feed
    total_ratio = 5.0  # Total pressure ratio
    stage_ratio = total_ratio ** (1.0 / num_stages)

    for i in range(num_stages):
        comp = Compressor(f"stage{i+1}", inlet_stream)
        outlet_p = 30.0 * (stage_ratio ** (i + 1))
        comp.setOutletPressure(outlet_p, "bara")
        comp.setIsentropicEfficiency(0.78)
        process.add(comp)

        if i < num_stages - 1:  # Add intercooler
            cooler = jneqsim.process.equipment.heatexchanger.Cooler(
                f"cooler{i+1}", comp.getOutletStream())
            cooler.setOutTemperature(308.15)  # 35°C
            process.add(cooler)
            inlet_stream = cooler.getOutletStream()
        else:
            inlet_stream = comp.getOutletStream()

    process.run()
    return process

# Screen 1, 2, 3, 4 stage options
concept_results = {}
for stages in range(1, 5):
    concept = create_staged_compressor(stages, fluid.clone())

    @JImplements("java.util.function.Function")
    class TotalPowerObj:
        @JOverride
        def apply(self, proc):
            total = 0.0
            for unit in proc.getUnitOperations():
                if unit.getClass().getSimpleName() == "Compressor":
                    total += unit.getPower("kW")
            return total

    study = BatchStudy.builder(concept) \
        .name(f"Concept-{stages}-stages") \
        .vary("feed.flowRate", 5000.0, 15000.0, 3) \
        .addObjective("totalPower", Objective.MINIMIZE, TotalPowerObj()) \
        .parallelism(2) \
        .build()

    result = study.run()
    concept_results[stages] = result

    best = result.getBestCase("totalPower")
    print(f"{stages} stages: Best power = {best.objectiveValues.get('totalPower'):.1f} kW")