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.
Related Documentation
| 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:
- Parameter Sweeps: Vary design variables systematically
- Parallel Execution: Utilize multiple CPU cores
- Multi-Objective Ranking: Compare by cost, emissions, efficiency
- Result Aggregation: Collect and analyze efficiently
Table of Contents
- Usage
- Parameter Variation Methods
- Supported Parameter Paths
- Result Analysis
- Multi-Objective Analysis
- Concept Screening Example
- Performance Considerations
- Best Practices
- Python Usage (via JPype)
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
- Start Small: Test with few cases before large sweeps
- Log Progress: Monitor completion for long studies
- Handle Failures: Decide continue vs stop strategy
- Export Results: Always save before analysis
- 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")
Related Documentation
- Optimization Package - General optimization capabilities
- Multi-Objective Optimization - Pareto fronts
- Python Optimization Tutorial - SciPy integration
- Safety Scenario Generation - Generate scenarios for batch studies
- Future Infrastructure Overview - Full infrastructure overview