Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion .github/skills/neqsim-dynamic-simulation/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: neqsim-dynamic-simulation
description: "Dynamic simulation guidance for NeqSim. USE WHEN: running transient simulations, modeling startup/shutdown, tuning PID controllers, analyzing pressure/level dynamics, performing blowdown/depressurization, or setting up measurement devices and control loops. Covers runTransient, DynamicProcessHelper, controller tuning, and dynamic equipment configuration."
last_verified: "2026-08-31"
last_verified: "2026-09-01"
---

# Dynamic Simulation Guidance
Expand Down Expand Up @@ -620,6 +620,26 @@ TransferFunctionBlock leadLag = new TransferFunctionBlock();

## Common Pitfalls

Before relying on a mixed transient flowsheet, inspect its state-ownership inventory separately from runtime activation
and physics qualification:

```java
neqsim.process.dynamics.DynamicCapabilityReport report =
neqsim.process.dynamics.DynamicCapabilityReport.from(process);
if (!report.isFullyAudited()) {
throw new IllegalStateException(report.getReviewItems().toString());
}
report.assertStrictTransientReady();
```

All built-in process-element declarations of `runTransient(double, UUID)` are mapped, and CI rejects a new built-in
override unless it is classified or cites an explicit repository ADR. Custom/downstream overrides remain
`UNCLASSIFIED_DYNAMIC` until reviewed. `isFullyAudited()` is an inventory signal only: it does not certify numerical
stability, conservation, benchmark parity, controls performance, or safety suitability. In particular,
`PipeBeggsAndBrills` has distributed profile state but no conservative line-pack/storage term; do not use its capability
label to claim severe-slugging or liquid-rich transient validity. Route those studies to the qualified `TwoFluidPipe`
path and apply the relevant numerical and public-benchmark gates.

1. **Always run steady state first**: Call `process.run()` before `runTransient()`
2. **Timestep size**: Start with 1.0 s, reduce if oscillating (0.1-0.5 s)
3. **Liquid-outlet level-controller direction**: use `setReverseActing(false)` so level up drives the outlet valve further open. Reverse acting on this configuration drives the loop the wrong way.
Expand Down
50 changes: 38 additions & 12 deletions docs/process/dynamic-capability-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ report.getActivationCounts();
report.getExecutionIssues();
report.getBlockingIssues();
report.getReviewItems();
report.isFullyAudited();
report.getUnverifiedActivationElements();
report.getInactiveAuditedDynamicElements();
String json = report.toJson();
Expand Down Expand Up @@ -257,18 +258,43 @@ the report as a complete nested-unit inventory.
Identity-based de-duplication prevents the same process element or nested `ProcessSystem` object from being counted
repeatedly and prevents accidental recursive container cycles from causing unbounded traversal.

## Initial audited mapping
## Built-in audited mapping

Every built-in `ProcessElementInterface` class that declares the standard two-argument `runTransient(double, UUID)`
boundary has an audited state-ownership category:

- algebraic: standard `Stream` execution, composite module containers, `EnergyNetworkSolver`, ISO-5167 `Orifice`,
`WellFlow` IPR pressure-flow relations, `Heater`, `Mixer`, `Splitter`, `MembraneSeparator`, and the quasi-steady
`AdiabaticPipe`;
- lumped: separators, tanks and `VesselDepressurization`, two-stream heat exchangers, compressors/expanders, pumps,
throttling/control/safety valves, `EnergyConverter` families, `BatteryStorage`, `Filter`,
`CommittedEnergyGenerator`, and `Electrolyzer`;
- distributed: `OnePhasePipeLine`, `TwoFluidPipe`, drift-flux `TransientPipe`, `WaterHammerPipe`, the generic
`Pipeline` family (including `MultiphasePipe` and `PipeBeggsAndBrills`), `DistillationColumn`, `AdsorptionBed`,
`MercuryRemovalBed`, `PipeFlowNetwork`, and `WellFlowlineNetwork`;
- boundary: `SimpleReservoir` and `IronSulfideOxidationSource`;
- control: registered controllers and measurement devices.

The initial contract intentionally classifies only core implementations whose current source contains clear stored-state
semantics:
`DynamicCapabilityReport.isFullyAudited()` is true when a concrete report contains no
`UNCLASSIFIED_DYNAMIC` entries. It is intentionally separate from `isStrictPreflightReady()`: a fully classified
inventory can still have an unsupported runtime request, incomplete activation, unsafe execution mode, or inadequate
numerical/benchmark evidence.

- algebraic: standard `Stream` execution, composite module containers, `EnergyNetworkSolver`, ISO-5167 `Orifice`, and
`WellFlow` IPR pressure-flow relations;
- lumped: separators, tanks, two-stream heat exchangers, compressors/expanders, pumps, throttling/control valves,
`EnergyConverter`-based motors/generators/gearboxes/inverters/transformers, and `BatteryStorage`;
- distributed: `OnePhasePipeLine`, `TwoFluidPipe`, drift-flux `TransientPipe`, and `WaterHammerPipe`;
- boundary: `SimpleReservoir`;
- control: registered controllers and measurement devices.
### Built-in source-inventory CI gate

`DynamicCapabilityBuiltInInventoryTest` scans production process sources for declarations of the standard transient
boundary. A new built-in `ProcessElementInterface` override fails CI unless its declaring class has an explicit mapping
in `DynamicCapabilityResolver` or cites an existing repository-relative Markdown ADR in the resolver's exemption
registry. The WS6 closure has no ADR exemptions. A user or downstream subclass that overrides an audited built-in method
still resolves fail-closed to `UNCLASSIFIED_DYNAMIC`; a subclass that merely inherits the built-in method inherits its
audited category.

An audit category states what kind of state the implementation owns. It does **not** establish conservation,
timestep/mesh independence, transient stability, benchmark parity, restart, rollback, controls, or safety maturity.
In particular, `PipeBeggsAndBrills` owns spatially distributed transient profile state but has no conservative
mass-storage/line-pack term. Its distributed category must not be used as evidence for severe slugging, liquid-rich
transients, line pack, or another storage-driven claim; route those studies to the separately qualified `TwoFluidPipe`
path and apply the relevant numerical and public-benchmark gates.

The `OnePhasePipeLine` distributed classification is supported by merged ProcessSystem-level quantitative evidence for
the conservative one-phase, positive-flow finite-volume path: the pipeline owns spatial hydraulic/species state,
Expand Down Expand Up @@ -305,9 +331,9 @@ clock contract. `Orifice` is a custom pressure-driven transient relation, but st
relation is re-evaluated for every requested refinement while its local execution clock and calculation ID follow the
same A/refine-A/B physical-step contract as other algebraic equipment, including the negligible-flow early return.

Other custom transient implementations remain `UNCLASSIFIED_DYNAMIC` until their state variables, conservation equations,
Custom transient implementations remain `UNCLASSIFIED_DYNAMIC` until their state variables, conservation equations,
initialization, timestep constraints, event behaviour, snapshot/restart semantics, and quantitative validation are
reviewed. The mapping is expected to expand as that audit is completed.
reviewed. Built-in exceptions require an explicit ADR and are rejected by CI when the cited document is absent.

## Event-scheduler rollback boundary

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,21 @@ public List<String> getReviewItems() {
return Collections.unmodifiableList(review);
}

/**
* Whether every element in this report has an audited capability category.
*
* <p>
* This predicate is deliberately narrower than {@link #isStrictPreflightReady()}. A fully audited report can still
* contain a runtime configuration, activation, or process-execution issue. It is an inventory-completion signal, not
* a numerical-validation, engineering-readiness, or safety certificate.
* </p>
*
* @return true when no entry is classified {@link DynamicCapability#UNCLASSIFIED_DYNAMIC}
*/
public boolean isFullyAudited() {
return getReviewItems().isEmpty();
}

/**
* Returns elements with explicit dynamic capability whose type-specific runtime activation is still unaudited.
*
Expand Down
154 changes: 118 additions & 36 deletions src/main/java/neqsim/process/dynamics/DynamicCapabilityResolver.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,55 @@
package neqsim.process.dynamics;

import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import neqsim.process.ProcessElementInterface;
import neqsim.process.SimulationInterface;
import neqsim.process.controllerdevice.ControllerDeviceInterface;
import neqsim.process.equipment.adsorber.AdsorptionBed;
import neqsim.process.equipment.adsorber.MercuryRemovalBed;
import neqsim.process.equipment.battery.BatteryStorage;
import neqsim.process.equipment.compressor.Compressor;
import neqsim.process.equipment.diffpressure.Orifice;
import neqsim.process.equipment.distillation.DistillationColumn;
import neqsim.process.equipment.electrolyzer.Electrolyzer;
import neqsim.process.equipment.energy.CommittedEnergyGenerator;
import neqsim.process.equipment.energy.EnergyConverter;
import neqsim.process.equipment.energy.EnergyNetworkSolver;
import neqsim.process.equipment.energy.Inverter;
import neqsim.process.equipment.expander.Expander;
import neqsim.process.equipment.filter.Filter;
import neqsim.process.equipment.heatexchanger.HeatExchanger;
import neqsim.process.equipment.heatexchanger.Heater;
import neqsim.process.equipment.membrane.MembraneSeparator;
import neqsim.process.equipment.mixer.Mixer;
import neqsim.process.equipment.network.PipeFlowNetwork;
import neqsim.process.equipment.network.WellFlowlineNetwork;
import neqsim.process.equipment.pipeline.AdiabaticPipe;
import neqsim.process.equipment.pipeline.MultiphasePipe;
import neqsim.process.equipment.pipeline.OnePhasePipeLine;
import neqsim.process.equipment.pipeline.PipeBeggsAndBrills;
import neqsim.process.equipment.pipeline.Pipeline;
import neqsim.process.equipment.pipeline.TwoFluidPipe;
import neqsim.process.equipment.pipeline.WaterHammerPipe;
import neqsim.process.equipment.pipeline.twophasepipe.TransientPipe;
import neqsim.process.equipment.pump.Pump;
import neqsim.process.equipment.reactor.IronSulfideOxidationSource;
import neqsim.process.equipment.reservoir.SimpleReservoir;
import neqsim.process.equipment.reservoir.WellFlow;
import neqsim.process.equipment.separator.Separator;
import neqsim.process.equipment.separator.ThreePhaseSeparator;
import neqsim.process.equipment.splitter.Splitter;
import neqsim.process.equipment.stream.Stream;
import neqsim.process.equipment.tank.Tank;
import neqsim.process.equipment.tank.VesselDepressurization;
import neqsim.process.equipment.valve.BlowdownValve;
import neqsim.process.equipment.valve.ESDValve;
import neqsim.process.equipment.valve.HIPPSValve;
import neqsim.process.equipment.valve.PSDValve;
import neqsim.process.equipment.valve.RuptureDisk;
import neqsim.process.equipment.valve.SafetyValve;
import neqsim.process.equipment.valve.ThrottlingValve;
import neqsim.process.measurementdevice.MeasurementDeviceInterface;
import neqsim.process.processmodel.ModuleInterface;
Expand All @@ -38,9 +67,19 @@
* </p>
*
* @author Even Solbraa
* @version 1.0
* @version 1.1
*/
public final class DynamicCapabilityResolver {
/**
* Explicit ADR exemptions for built-in transient implementations that intentionally remain unclassified.
*
* <p>
* Entries must map a declaring class to an existing repository-relative Markdown ADR. The source-inventory test
* rejects absent files. There are no exemptions after the WS6 audit closure.
* </p>
*/
private static final Map<Class<?>, String> UNCLASSIFIED_BUILT_IN_ADRS = Collections.emptyMap();

/** Utility class. */
private DynamicCapabilityResolver() {
}
Expand All @@ -64,57 +103,89 @@ public static DynamicCapability resolve(ProcessElementInterface element) {
return DynamicCapability.ALGEBRAIC;
}

if (element instanceof EnergyNetworkSolver || element instanceof Orifice || element instanceof WellFlow) {
return DynamicCapability.ALGEBRAIC;
Class<?> transientOwner = getTransientImplementationClass(element);
DynamicCapability audited = resolveAuditedBuiltInClass(transientOwner);
if (audited != null) {
return audited;
}

if (element instanceof OnePhasePipeLine || element instanceof TwoFluidPipe || element instanceof TransientPipe
|| element instanceof WaterHammerPipe) {
return DynamicCapability.DYNAMIC_DISTRIBUTED;
if (transientOwner != null && transientOwner != SimulationInterface.class) {
return DynamicCapability.UNCLASSIFIED_DYNAMIC;
}

if (element instanceof SimpleReservoir) {
return DynamicCapability.BOUNDARY_DYNAMIC;
return DynamicCapability.ALGEBRAIC;
}

/**
* Resolve one built-in class that declares the standard two-argument transient boundary.
*
* <p>
* Classification is based on the class that declares the effective method, not merely the runtime subtype. A custom
* subclass that inherits an audited built-in implementation keeps that implementation's category. A subclass that
* overrides the method becomes {@link DynamicCapability#UNCLASSIFIED_DYNAMIC} until its own implementation is
* audited.
* </p>
*
* @param type class declaring {@code runTransient(double, UUID)}
* @return audited capability, or null when the declaring class has no audit mapping
*/
static DynamicCapability resolveAuditedBuiltInClass(Class<?> type) {
if (type == null) {
return null;
}

if (element instanceof Separator || element instanceof Tank || element instanceof HeatExchanger
|| element instanceof Compressor || element instanceof Pump || element instanceof ThrottlingValve
|| element instanceof EnergyConverter || element instanceof BatteryStorage) {
return DynamicCapability.DYNAMIC_LUMPED;
if (ControllerDeviceInterface.class.isAssignableFrom(type)
|| MeasurementDeviceInterface.class.isAssignableFrom(type)) {
return DynamicCapability.CONTROL_DYNAMIC;
}

if (element instanceof Stream && usesStandardStreamTransientBoundary(element)) {
if (ModuleInterface.class.isAssignableFrom(type)) {
return DynamicCapability.ALGEBRAIC;
}

if (hasCustomTransientImplementation(element)) {
return DynamicCapability.UNCLASSIFIED_DYNAMIC;
if (isOneOf(type, EnergyNetworkSolver.class, Orifice.class, WellFlow.class, Stream.class, Heater.class, Mixer.class,
Splitter.class, MembraneSeparator.class, AdiabaticPipe.class)) {
return DynamicCapability.ALGEBRAIC;
}

return DynamicCapability.ALGEBRAIC;
if (isOneOf(type, Separator.class, ThreePhaseSeparator.class, Tank.class, VesselDepressurization.class,
HeatExchanger.class, Compressor.class, Expander.class, Pump.class, ThrottlingValve.class, BlowdownValve.class,
ESDValve.class, HIPPSValve.class, PSDValve.class, RuptureDisk.class, SafetyValve.class, EnergyConverter.class,
Inverter.class, BatteryStorage.class, Filter.class, CommittedEnergyGenerator.class, Electrolyzer.class)) {
return DynamicCapability.DYNAMIC_LUMPED;
}

if (isOneOf(type, OnePhasePipeLine.class, TwoFluidPipe.class, TransientPipe.class, WaterHammerPipe.class,
Pipeline.class, MultiphasePipe.class, PipeBeggsAndBrills.class, DistillationColumn.class, AdsorptionBed.class,
MercuryRemovalBed.class, PipeFlowNetwork.class, WellFlowlineNetwork.class)) {
return DynamicCapability.DYNAMIC_DISTRIBUTED;
}

if (isOneOf(type, SimpleReservoir.class, IronSulfideOxidationSource.class)) {
return DynamicCapability.BOUNDARY_DYNAMIC;
}

return null;
}

/**
* Whether a stream uses NeqSim's established algebraic transient boundary.
* Repository ADR for an intentionally unclassified built-in transient implementation.
*
* <p>
* {@link Stream#runTransient(double, UUID)} re-evaluates the stream and advances its execution clock, but it does not
* integrate stored physical state. Stream subclasses that inherit that method remain algebraic; subclasses that
* override it continue through the conservative custom-implementation audit below.
* </p>
*
* @param element stream element to inspect
* @return true when the effective transient method is declared by {@link Stream}
* @param type class declaring the transient implementation
* @return repository-relative Markdown path, or null when no exemption is recorded
*/
private static boolean usesStandardStreamTransientBoundary(ProcessElementInterface element) {
try {
Method method = element.getClass().getMethod("runTransient", Double.TYPE, UUID.class);
return method.getDeclaringClass() == Stream.class;
} catch (NoSuchMethodException ex) {
return false;
} catch (SecurityException ex) {
return false;
static String getUnclassifiedBuiltInAdr(Class<?> type) {
return UNCLASSIFIED_BUILT_IN_ADRS.get(type);
}

/** Compare a class against a compact exact-class inventory. */
private static boolean isOneOf(Class<?> type, Class<?>... candidates) {
for (Class<?> candidate : candidates) {
if (type == candidate) {
return true;
}
}
return false;
}

/**
Expand All @@ -133,13 +204,24 @@ static boolean hasCustomTransientImplementation(ProcessElementInterface element)
if (!(element instanceof SimulationInterface)) {
return false;
}
Class<?> transientOwner = getTransientImplementationClass(element);
return transientOwner != null && transientOwner != SimulationInterface.class;
}

/**
* Class that declares the effective standard transient boundary.
*
* @param element process element to inspect
* @return declaring class, or null when reflection cannot resolve the boundary
*/
private static Class<?> getTransientImplementationClass(ProcessElementInterface element) {
try {
Method method = element.getClass().getMethod("runTransient", Double.TYPE, UUID.class);
return method.getDeclaringClass() != SimulationInterface.class;
return method.getDeclaringClass();
} catch (NoSuchMethodException ex) {
return false;
return null;
} catch (SecurityException ex) {
return false;
return null;
}
}
}
Loading
Loading