diff --git a/docs/dexpi-reader.md b/docs/dexpi-reader.md index d9be10804e..75a2d84110 100644 --- a/docs/dexpi-reader.md +++ b/docs/dexpi-reader.md @@ -3,8 +3,8 @@ The `DexpiXmlReader` utility converts [DEXPI](https://dexpi.org/) XML P&ID exports into [`ProcessSystem`](../src/main/java/neqsim/process/processmodel/ProcessSystem.java) models. It recognises major equipment such as pumps, heat exchangers, tanks and control valves as well as -piping segments, which are imported as runnable `DexpiStream` units tagged with the source line -number. +complex reactors, compressors and inline analysers. Piping segments are imported as runnable +`DexpiStream` units tagged with the source line number. ## Usage @@ -34,8 +34,20 @@ Each imported equipment item is represented as a lightweight `DexpiProcessUnit` original DEXPI class together with the mapped `EquipmentEnum` category and contextual information like line numbers or fluid codes. Piping segments become `DexpiStream` objects that clone the pressure, temperature and flow settings from the template stream (or a built-in methane/ethane -fallback), allowing the resulting `ProcessSystem` to perform full thermodynamic calculations when -`run()` is invoked. +fallback). When available, the reader honours the recommended metadata exported by NeqSim so +pressure, temperature and flow values embedded in DEXPI documents override the template defaults. +The resulting `ProcessSystem` can therefore perform full thermodynamic calculations when `run()` is +invoked without requiring downstream tooling to remap metadata. + +### Metadata conventions + +Both the reader and writer share the [`DexpiMetadata`](../src/main/java/neqsim/process/processmodel/DexpiMetadata.java) +constants that describe the recommended generic attributes for DEXPI exchanges. Equipment exports +include tag names, line numbers and fluid codes, while piping segments also carry segment numbers +and operating pressure/temperature/flow triples (together with their units). Downstream tools can +consult `DexpiMetadata.recommendedStreamAttributes()` and +`DexpiMetadata.recommendedEquipmentAttributes()` to understand the minimal metadata sets guaranteed +by NeqSim. ### Exporting back to DEXPI @@ -54,8 +66,29 @@ The writer groups all discovered `DexpiStream` segments by line number (or fluid not available) to generate simple `` elements with associated `` children. Equipment and valves are exported as `` and `` elements that preserve the original tag names, line numbers and fluid codes via -`GenericAttribute` entries. The resulting XML focuses on the metadata required to rehydrate the -process structure and is intentionally compact to ease downstream tooling consumption. +`GenericAttribute` entries. Stream metadata is enriched with operating pressure, temperature and flow +values (stored in the default NeqSim units, but accompanied by explicit `Unit` annotations) so that +downstream thermodynamic simulators can reproduce NeqSim's state without bespoke mappings. + +Each piping network is also labelled with a `NeqSimGroupingKey` generic attribute so that +visualisation libraries—such as [pyDEXPI](https://github.com/process-intelligence-research/pyDEXPI) +or Graphviz exports—can easily recreate line-centric layouts without additional heuristics. + +### Round-trip profile + +To codify the minimal metadata required for reliable imports/exports NeqSim exposes the +[`DexpiRoundTripProfile`](../src/main/java/neqsim/process/processmodel/DexpiRoundTripProfile.java) +utility. The `minimalRunnableProfile` validates that a process contains runnable `DexpiStream` +segments (with line/fluid references and operating conditions), tagged equipment and at least one +piece of equipment alongside the piping network. Regression tests enforce this profile on the +reference training case and the re-imported export artefacts to guarantee round-trip fidelity. + +### Security considerations + +Both the reader and writer configure their XML factories with hardened defaults: secure-processing +is enabled, external entity resolution is disabled and `ACCESS_EXTERNAL_DTD` / +`ACCESS_EXTERNAL_SCHEMA` properties are cleared. These guardrails mirror the guidance in the +regression tests and should be preserved if the parsing/serialisation logic is extended. ## Tested example @@ -65,8 +98,9 @@ training case provided by the [DEXPI Training Test Cases repository](https://gitlab.com/dexpi/TrainingTestCases/-/tree/master/dexpi%201.3/example%20pids) and verifies that the expected equipment (two heat exchangers, two pumps, a tank, valves and piping segments) are discovered. The regression additionally seeds the import with an example NeqSim feed -stream and confirms that the generated streams remain active after `process.run()`. A companion test -exports the imported process with `DexpiXmlWriter`, then parses the generated XML with a hardened DOM -builder to confirm that the document contains equipment, piping components and -`PipingNetworkSystem`/`PipingNetworkSegment` structures ready for downstream DEXPI tooling such as -pyDEXPI. +stream and confirms that the generated streams remain active after `process.run()`. Companion +assertions enforce the `DexpiRoundTripProfile` and check that exported metadata (pressure, +temperature, flow and units) survives a round-trip reload. A companion test exports the imported +process with `DexpiXmlWriter`, then parses the generated XML with a hardened DOM builder to confirm +that the document contains equipment, piping components and `PipingNetworkSystem`/ +`PipingNetworkSegment` structures ready for downstream DEXPI tooling such as pyDEXPI. diff --git a/docs/equipment_factory.md b/docs/equipment_factory.md new file mode 100644 index 0000000000..0726a14ce1 --- /dev/null +++ b/docs/equipment_factory.md @@ -0,0 +1,34 @@ +# Equipment factory usage + +The `EquipmentFactory` provides a single entry-point for instantiating process equipment that can +be automatically wired into a `ProcessSystem`. The factory supports every value listed in +`EquipmentEnum`, including the energy storage and production classes (`WindTurbine`, +`BatteryStorage`, and `SolarPanel`). + +## Basic creation + +```java +ProcessEquipmentInterface pump = EquipmentFactory.createEquipment("pump1", EquipmentEnum.Pump); +ProcessEquipmentInterface stream = EquipmentFactory.createEquipment("feed", "stream"); +``` + +The string based overload is tolerant of the common aliases that existed historically (for example +`valve` and `separator_3phase`). Unknown identifiers now throw an exception instead of silently +creating the wrong equipment. + +## Equipment with mandatory collaborators + +Some equipment types cannot be instantiated without additional collaborators. The factory now +prevents creation of partially initialised objects and exposes dedicated helpers instead: + +```java +StreamInterface motive = new Stream("motive"); +StreamInterface suction = new Stream("suction"); +Ejector ejector = EquipmentFactory.createEjector("ej-1", motive, suction); + +SystemInterface reservoirFluid = new SystemSrkEos(273.15, 100.0); +ReservoirCVDsim cvd = EquipmentFactory.createReservoirCVDsim("cvd", reservoirFluid); +``` + +Attempting to create these units through the generic method now results in an informative exception +message that points to the correct helper method. diff --git a/src/main/java/neqsim/process/equipment/EquipmentEnum.java b/src/main/java/neqsim/process/equipment/EquipmentEnum.java index f93802ab6d..d2f294b486 100644 --- a/src/main/java/neqsim/process/equipment/EquipmentEnum.java +++ b/src/main/java/neqsim/process/equipment/EquipmentEnum.java @@ -12,7 +12,8 @@ public enum EquipmentEnum { Splitter, Reactor, Column, ThreePhaseSeparator, Recycle, Ejector, GORfitter, Adjuster, SetPoint, FlowRateAdjuster, Calculator, Expander, SimpleTEGAbsorber, Tank, ComponentSplitter, ReservoirCVDsim, ReservoirDiffLibsim, VirtualStream, ReservoirTPsim, SimpleReservoir, Manifold, - Flare, FlareStack, FuelCell, CO2Electrolyzer, Electrolyzer; + Flare, FlareStack, FuelCell, CO2Electrolyzer, Electrolyzer, WindTurbine, BatteryStorage, + SolarPanel; /** {@inheritDoc} */ @Override diff --git a/src/main/java/neqsim/process/equipment/EquipmentFactory.java b/src/main/java/neqsim/process/equipment/EquipmentFactory.java index c06b21250a..d055da9f22 100644 --- a/src/main/java/neqsim/process/equipment/EquipmentFactory.java +++ b/src/main/java/neqsim/process/equipment/EquipmentFactory.java @@ -1,5 +1,6 @@ package neqsim.process.equipment; +import java.util.Objects; import neqsim.process.equipment.absorber.SimpleTEGAbsorber; import neqsim.process.equipment.battery.BatteryStorage; import neqsim.process.equipment.compressor.Compressor; @@ -7,23 +8,27 @@ import neqsim.process.equipment.electrolyzer.CO2Electrolyzer; import neqsim.process.equipment.electrolyzer.Electrolyzer; import neqsim.process.equipment.expander.Expander; +import neqsim.process.equipment.flare.Flare; +import neqsim.process.equipment.flare.FlareStack; import neqsim.process.equipment.heatexchanger.Cooler; import neqsim.process.equipment.heatexchanger.HeatExchanger; import neqsim.process.equipment.heatexchanger.Heater; import neqsim.process.equipment.manifold.Manifold; import neqsim.process.equipment.mixer.Mixer; +import neqsim.process.equipment.powergeneration.FuelCell; import neqsim.process.equipment.powergeneration.SolarPanel; +import neqsim.process.equipment.powergeneration.WindTurbine; import neqsim.process.equipment.pump.Pump; import neqsim.process.equipment.reservoir.ReservoirCVDsim; import neqsim.process.equipment.reservoir.ReservoirDiffLibsim; import neqsim.process.equipment.reservoir.ReservoirTPsim; import neqsim.process.equipment.reservoir.SimpleReservoir; -import neqsim.process.equipment.powergeneration.WindTurbine; import neqsim.process.equipment.separator.Separator; import neqsim.process.equipment.separator.ThreePhaseSeparator; import neqsim.process.equipment.splitter.ComponentSplitter; import neqsim.process.equipment.splitter.Splitter; import neqsim.process.equipment.stream.Stream; +import neqsim.process.equipment.stream.StreamInterface; import neqsim.process.equipment.stream.VirtualStream; import neqsim.process.equipment.tank.Tank; import neqsim.process.equipment.util.Adjuster; @@ -33,116 +38,190 @@ import neqsim.process.equipment.util.Recycle; import neqsim.process.equipment.util.SetPoint; import neqsim.process.equipment.valve.ThrottlingValve; -import neqsim.process.equipment.flare.Flare; -import neqsim.process.equipment.flare.FlareStack; +import neqsim.thermo.system.SystemInterface; /** - *

- * EquipmentFactory class. - *

- * - * @author esol + * Factory for creating process equipment. */ -public class EquipmentFactory { +public final class EquipmentFactory { + + private EquipmentFactory() {} + /** - *

- * createEquipment. - *

+ * Creates a piece of equipment based on the provided type. * - * @param name a {@link java.lang.String} object - * @param equipmentType a {@link java.lang.String} object - * @return a {@link neqsim.process.equipment.ProcessEquipmentInterface} object + * @param name name to assign to the equipment + * @param equipmentType equipment type identifier + * @return the created equipment instance */ public static ProcessEquipmentInterface createEquipment(String name, String equipmentType) { if (equipmentType == null || equipmentType.trim().isEmpty()) { throw new IllegalArgumentException("Equipment type cannot be null or empty"); } - String normalizedType = equipmentType.trim().toLowerCase(); - switch (normalizedType) { - case "throttlingvalve": + String normalized = equipmentType.trim().toLowerCase(); + switch (normalized) { case "valve": + return createEquipment(name, EquipmentEnum.ThrottlingValve); + case "separator_3phase": + case "separator3phase": + case "threephaseseparator": + return createEquipment(name, EquipmentEnum.ThreePhaseSeparator); + case "co₂electrolyzer": + case "co2electrolyser": + case "co2electrolyzer": + return createEquipment(name, EquipmentEnum.CO2Electrolyzer); + case "windturbine": + return createEquipment(name, EquipmentEnum.WindTurbine); + case "batterystorage": + return createEquipment(name, EquipmentEnum.BatteryStorage); + case "solarpanel": + return createEquipment(name, EquipmentEnum.SolarPanel); + default: + EquipmentEnum enumType = resolveEquipmentEnum(equipmentType); + return createEquipment(name, enumType); + } + } + + /** + * Creates a piece of equipment based on {@link EquipmentEnum}. + * + * @param name name to assign + * @param equipmentType {@link EquipmentEnum} + * @return the created equipment + */ + public static ProcessEquipmentInterface createEquipment(String name, EquipmentEnum equipmentType) { + Objects.requireNonNull(equipmentType, "equipmentType"); + + switch (equipmentType) { + case ThrottlingValve: return new ThrottlingValve(name); - case "stream": + case Stream: return new Stream(name); - case "compressor": + case Compressor: return new Compressor(name); - case "pump": + case Pump: return new Pump(name); - case "separator": + case Separator: return new Separator(name); - case "heatexchanger": + case HeatExchanger: return new HeatExchanger(name); - case "mixer": + case Mixer: return new Mixer(name); - case "splitter": + case Splitter: return new Splitter(name); - case "cooler": + case Cooler: return new Cooler(name); - case "heater": + case Heater: return new Heater(name); - case "recycle": + case Recycle: return new Recycle(name); - case "threephaseseparator": - case "separator_3phase": + case ThreePhaseSeparator: return new ThreePhaseSeparator(name); - case "ejector": - // Requires motiveStream and suctionStream, placeholders added - return new Ejector(name, null, null); - case "gorfitter": - // Requires stream, placeholder added - return new GORfitter(name, null); - case "adjuster": + case Ejector: + throw new IllegalArgumentException( + "Ejector requires motive and suction streams. Use createEjector instead."); + case GORfitter: + throw new IllegalArgumentException( + "GORfitter requires an inlet stream. Use createGORfitter instead."); + case Adjuster: return new Adjuster(name); - case "setpoint": + case SetPoint: return new SetPoint(name); - case "flowrateadjuster": + case FlowRateAdjuster: return new FlowRateAdjuster(name); - case "calculator": + case Calculator: return new Calculator(name); - case "expander": + case Expander: return new Expander(name); - case "simpletegabsorber": + case SimpleTEGAbsorber: return new SimpleTEGAbsorber(name); - case "tank": + case Tank: return new Tank(name); - case "componentsplitter": + case ComponentSplitter: return new ComponentSplitter(name); - case "reservoircvdsim": - // Requires reservoirFluid, placeholder added - return new ReservoirCVDsim(name, null); - case "reservoirdifflibsim": - // Requires reservoirFluid, placeholder added - return new ReservoirDiffLibsim(name, null); - case "virtualstream": + case ReservoirCVDsim: + throw new IllegalArgumentException( + "ReservoirCVDsim requires a reservoir fluid. Use createReservoirCVDsim instead."); + case ReservoirDiffLibsim: + throw new IllegalArgumentException( + "ReservoirDiffLibsim requires a reservoir fluid. Use createReservoirDiffLibsim instead."); + case VirtualStream: return new VirtualStream(name); - case "reservoirtpsim": - // Requires reservoirFluid, placeholder added - return new ReservoirTPsim(name, null); - case "simplereservoir": + case ReservoirTPsim: + throw new IllegalArgumentException( + "ReservoirTPsim requires a reservoir fluid. Use createReservoirTPsim instead."); + case SimpleReservoir: return new SimpleReservoir(name); - case "manifold": + case Manifold: return new Manifold(name); - case "flare": + case Flare: return new Flare(name); - case "flarestack": + case FlareStack: return new FlareStack(name); - case "electrolyzer": - return new Electrolyzer(name); - case "co2electrolyzer": - case "co₂electrolyzer": - case "co2electrolyser": + case FuelCell: + return new FuelCell(name); + case CO2Electrolyzer: return new CO2Electrolyzer(name); - case "windturbine": + case Electrolyzer: + return new Electrolyzer(name); + case WindTurbine: return new WindTurbine(name); - case "batterystorage": + case BatteryStorage: return new BatteryStorage(name); - case "solarpanel": + case SolarPanel: return new SolarPanel(name); - - // Add other equipment types here default: - throw new IllegalArgumentException("Unknown equipment type: " + equipmentType); + throw new IllegalArgumentException( + "Unsupported equipment type: " + equipmentType.name()); + } + } + + private static EquipmentEnum resolveEquipmentEnum(String equipmentType) { + String sanitized = equipmentType.replaceAll("[\\s_-]", ""); + for (EquipmentEnum value : EquipmentEnum.values()) { + if (value.name().equalsIgnoreCase(equipmentType) + || value.name().equalsIgnoreCase(sanitized)) { + return value; + } + } + throw new IllegalArgumentException("Unknown equipment type: " + equipmentType); + } + + public static Ejector createEjector(String name, StreamInterface motiveStream, + StreamInterface suctionStream) { + if (motiveStream == null || suctionStream == null) { + throw new IllegalArgumentException("Ejector requires both motive and suction streams"); + } + return new Ejector(name, motiveStream, suctionStream); + } + + public static GORfitter createGORfitter(String name, StreamInterface stream) { + if (stream == null) { + throw new IllegalArgumentException("GORfitter requires a non-null inlet stream"); + } + return new GORfitter(name, stream); + } + + public static ReservoirCVDsim createReservoirCVDsim(String name, SystemInterface reservoirFluid) { + if (reservoirFluid == null) { + throw new IllegalArgumentException("ReservoirCVDsim requires a reservoir fluid"); + } + return new ReservoirCVDsim(name, reservoirFluid); + } + + public static ReservoirDiffLibsim createReservoirDiffLibsim(String name, + SystemInterface reservoirFluid) { + if (reservoirFluid == null) { + throw new IllegalArgumentException("ReservoirDiffLibsim requires a reservoir fluid"); + } + return new ReservoirDiffLibsim(name, reservoirFluid); + } + + public static ReservoirTPsim createReservoirTPsim(String name, SystemInterface reservoirFluid) { + if (reservoirFluid == null) { + throw new IllegalArgumentException("ReservoirTPsim requires a reservoir fluid"); } + return new ReservoirTPsim(name, reservoirFluid); } } diff --git a/src/main/java/neqsim/process/equipment/distillation/DistillationColumn.java b/src/main/java/neqsim/process/equipment/distillation/DistillationColumn.java index 5139e73bb9..6ae489b69e 100644 --- a/src/main/java/neqsim/process/equipment/distillation/DistillationColumn.java +++ b/src/main/java/neqsim/process/equipment/distillation/DistillationColumn.java @@ -1,6 +1,7 @@ package neqsim.process.equipment.distillation; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -69,6 +70,14 @@ public enum SolverType { /** Relaxation factor used when {@link SolverType#DAMPED_SUBSTITUTION} is active. */ private double relaxationFactor = 0.5; + /** Minimum relaxation factor used when adaptive damping scales down the step. */ + private double minAdaptiveRelaxation = 0.1; + /** Maximum relaxation factor allowed by the adaptive controller. */ + private double maxAdaptiveRelaxation = 1.0; + /** Factor used to expand the relaxation factor when residuals shrink. */ + private double relaxationIncreaseFactor = 1.2; + /** Factor used to shrink the relaxation factor when residuals grow. */ + private double relaxationDecreaseFactor = 0.5; Mixer feedmixer = new Mixer("temp mixer"); double bottomTrayPressure = -1.0; @@ -88,6 +97,17 @@ public enum SolverType { */ private double err = 1.0e10; + /** Last number of iterations executed by the active solver. */ + private int lastIterationCount = 0; + /** Last recorded average temperature residual in Kelvin. */ + private double lastTemperatureResidual = 0.0; + /** Last recorded relative mass balance residual. */ + private double lastMassResidual = 0.0; + /** Last recorded relative enthalpy residual. */ + private double lastEnergyResidual = 0.0; + /** Duration of the latest solve step in seconds. */ + private double lastSolveTimeSeconds = 0.0; + /** * Instead of Map<Integer,StreamInterface>, we store a list of feed streams per tray number. * This allows multiple feeds to the same tray. @@ -169,6 +189,20 @@ public void addFeedStream(StreamInterface inputStream, int feedTrayNumber) { setDoInitializion(true); } + /** + * Return the feed streams connected to a given tray. + * + * @param feedTrayNumber tray index where feeds are connected + * @return immutable view of feed streams connected to the tray + */ + public List getFeedStreams(int feedTrayNumber) { + List feeds = feedStreams.get(feedTrayNumber); + if (feeds == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(feeds); + } + /** * Prepare the column for calculation by estimating tray temperatures and linking streams between * trays. @@ -188,6 +222,7 @@ public void init() { // If feedStreams is empty, nothing to do if (feedStreams.isEmpty()) { + resetLastSolveMetrics(); return; } @@ -288,25 +323,31 @@ public void init() { *

* Solve the column until tray temperatures converge. * - * The method applies sequential substitution. Pressures are set linearly between bottom and top. - * Each iteration performs an upward sweep where liquid flows downward followed by a downward - * sweep where vapour flows upward. The sum of absolute temperature changes is used as error - * measure. + * The method applies sequential substitution with an adaptive relaxation controller. Pressures + * are set linearly between bottom and top. Each iteration performs an upward sweep where liquid + * flows downward followed by a downward sweep where vapour flows upward. Tray temperatures and + * inter-tray stream flow rates are relaxed if the combined temperature, mass and energy residuals + * grow, providing basic line-search behaviour. *

*/ @Override public void run(UUID id) { - if (solverType == SolverType.DAMPED_SUBSTITUTION) { - runDamped(id); - return; - } + double initialRelaxation = + solverType == SolverType.DAMPED_SUBSTITUTION ? relaxationFactor : 1.0; + solveSequential(id, initialRelaxation); + } + /** + * Execute the sequential substitution solver with an adaptive relaxation controller. + * + * @param id calculation identifier + * @param initialRelaxation relaxation factor applied to the first iteration + */ + private void solveSequential(UUID id, double initialRelaxation) { if (feedStreams.isEmpty()) { - // no feeds, nothing to do return; } - // Find the *lowest* tray number among feed trays, for reference. int firstFeedTrayNumber = feedStreams.keySet().stream().min(Integer::compareTo).get(); if (bottomTrayPressure < 0) { @@ -320,16 +361,11 @@ public void run(UUID id) { if (numberOfTrays > 1) { dp = (bottomTrayPressure - topTrayPressure) / (numberOfTrays - 1.0); } - // Set tray pressures linearly from bottom to top for (int i = 0; i < numberOfTrays; i++) { trays.get(i).setPressure(bottomTrayPressure - i * dp); } - // numberOfFeedsUsed[i] will track how many streams we have assigned - // to tray i so we call getStream(...) index properly int[] numeroffeeds = new int[numberOfTrays]; - - // For each tray in feedStreams, pass each feed into that tray for (Entry> entry : feedStreams.entrySet()) { int feedTrayNumber = entry.getKey(); List trayFeeds = entry.getValue(); @@ -340,7 +376,6 @@ public void run(UUID id) { } } - // If there's only one tray total, just run it if (numberOfTrays == 1) { trays.get(0).run(id); gasOutStream.setThermoSystem(trays.get(0).getGasOutStream().getThermoSystem().clone()); @@ -348,95 +383,143 @@ public void run(UUID id) { gasOutStream.setCalculationIdentifier(id); liquidOutStream.setCalculationIdentifier(id); setCalculationIdentifier(id); + lastIterationCount = 1; + lastTemperatureResidual = 0.0; + lastMassResidual = 0.0; + lastEnergyResidual = 0.0; + lastSolveTimeSeconds = 0.0; return; } - // If we haven't done an init or we added feeds, do it now if (isDoInitializion()) { this.init(); } err = 1.0e10; - double errOld; int iter = 0; double massErr = 1.0e10; double energyErr = 1.0e10; + double previousCombinedResidual = Double.POSITIVE_INFINITY; + + long startTime = System.nanoTime(); - // We'll use this array to measure temperature changes in each iteration double[] oldtemps = new double[numberOfTrays]; + StreamInterface[] previousGasStreams = new StreamInterface[numberOfTrays]; + StreamInterface[] previousLiquidStreams = new StreamInterface[numberOfTrays]; + + double relaxation = Math.max(minAdaptiveRelaxation, + Math.min(maxAdaptiveRelaxation, initialRelaxation)); - // Make sure feed tray is up to date trays.get(firstFeedTrayNumber).run(id); - // Start the iterative solution + int iterationLimit = Math.max(maxNumberOfIterations, numberOfTrays * 3); + do { iter++; - errOld = err; - err = 0.0; - // Snapshot old temperatures + for (int i = 0; i < numberOfTrays; i++) { oldtemps[i] = trays.get(i).getThermoSystem().getTemperature(); } - // Move upward (bottom to feed tray) + StreamInterface[] currentGasStreams = new StreamInterface[numberOfTrays]; + StreamInterface[] currentLiquidStreams = new StreamInterface[numberOfTrays]; + for (int i = firstFeedTrayNumber; i > 1; i--) { - int replaceStream1 = trays.get(i - 1).getNumberOfInputStreams() - 1; - trays.get(i - 1).replaceStream(replaceStream1, trays.get(i).getLiquidOutStream()); - trays.get(i - 1).setPressure(bottomTrayPressure - (i - 1) * dp); + int replaceStream = trays.get(i - 1).getNumberOfInputStreams() - 1; + StreamInterface relaxedLiquid = applyRelaxation(previousLiquidStreams[i], + trays.get(i).getLiquidOutStream(), relaxation); + trays.get(i - 1).replaceStream(replaceStream, relaxedLiquid); + currentLiquidStreams[i] = relaxedLiquid.clone(); trays.get(i - 1).run(id); } - // reboiler tray hooking up to tray 1 int streamNumb = trays.get(0).getNumberOfInputStreams() - 1; - trays.get(0).setPressure(bottomTrayPressure); - trays.get(0).replaceStream(streamNumb, trays.get(1).getLiquidOutStream()); + StreamInterface reboilerFeed = applyRelaxation(previousLiquidStreams[1], + trays.get(1).getLiquidOutStream(), relaxation); + trays.get(0).replaceStream(streamNumb, reboilerFeed); + currentLiquidStreams[1] = reboilerFeed.clone(); trays.get(0).run(id); - // Move downward (feed tray to top) for (int i = 1; i <= numberOfTrays - 1; i++) { - // The top tray has 1 input less int replaceStream = trays.get(i).getNumberOfInputStreams() - 2; if (i == (numberOfTrays - 1)) { replaceStream = trays.get(i).getNumberOfInputStreams() - 1; } - trays.get(i).replaceStream(replaceStream, trays.get(i - 1).getGasOutStream()); + StreamInterface relaxedGas = applyRelaxation(previousGasStreams[i - 1], + trays.get(i - 1).getGasOutStream(), relaxation); + trays.get(i).replaceStream(replaceStream, relaxedGas); + currentGasStreams[i - 1] = relaxedGas.clone(); trays.get(i).run(id); } - // Then from top minus 1 down to feed tray for (int i = numberOfTrays - 2; i >= firstFeedTrayNumber; i--) { int replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - trays.get(i).replaceStream(replaceStream, trays.get(i + 1).getLiquidOutStream()); + StreamInterface relaxedLiquid = applyRelaxation(previousLiquidStreams[i + 1], + trays.get(i + 1).getLiquidOutStream(), relaxation); + trays.get(i).replaceStream(replaceStream, relaxedLiquid); + currentLiquidStreams[i + 1] = relaxedLiquid.clone(); trays.get(i).run(id); } - // Sum up changes in temperature + double temperatureResidual = 0.0; + double effectiveRelaxation = Math.max(0.0, Math.min(1.0, relaxation)); for (int i = 0; i < numberOfTrays; i++) { - err += Math.abs(oldtemps[i] - trays.get(i).getThermoSystem().getTemperature()); + double updated = trays.get(i).getThermoSystem().getTemperature(); + double newTemp = oldtemps[i] + effectiveRelaxation * (updated - oldtemps[i]); + trays.get(i).setTemperature(newTemp); + temperatureResidual += Math.abs(newTemp - oldtemps[i]); } + temperatureResidual /= Math.max(1, numberOfTrays); + err = temperatureResidual; massErr = getMassBalanceError(); energyErr = getEnergyBalanceError(); - logger.info("error iteration = " + iter + " err = " + err + " massErr= " + massErr - + " energyErr= " + energyErr); + double combinedResidual = Math.max( + Math.max(err / temperatureTolerance, massErr / massBalanceTolerance), + energyErr / enthalpyBalanceTolerance); + + if (combinedResidual > previousCombinedResidual * 1.05) { + relaxation = Math.max(minAdaptiveRelaxation, relaxation * relaxationDecreaseFactor); + } else if (combinedResidual < previousCombinedResidual * 0.95) { + relaxation = Math.min(maxAdaptiveRelaxation, relaxation * relaxationIncreaseFactor); + } + + previousCombinedResidual = combinedResidual; + + for (int i = 0; i < numberOfTrays; i++) { + if (currentGasStreams[i] != null) { + previousGasStreams[i] = currentGasStreams[i]; + } + if (currentLiquidStreams[i] != null) { + previousLiquidStreams[i] = currentLiquidStreams[i]; + } + } + + logger.info("iteration {} relaxation={} tempErr={} massErr={} energyErr={}", iter, relaxation, + err, massErr, energyErr); } while ((err > temperatureTolerance || massErr > massBalanceTolerance - || energyErr > enthalpyBalanceTolerance) && err < errOld && iter < maxNumberOfIterations); + || energyErr > enthalpyBalanceTolerance) && iter < iterationLimit); + + lastIterationCount = iter; + lastTemperatureResidual = err; + lastMassResidual = massErr; + lastEnergyResidual = energyErr; + lastSolveTimeSeconds = (System.nanoTime() - startTime) / 1.0e9; - // Once converged, fill final gasOut/liquidOut streams gasOutStream .setThermoSystem(trays.get(numberOfTrays - 1).getGasOutStream().getThermoSystem().clone()); gasOutStream.setCalculationIdentifier(id); liquidOutStream.setThermoSystem(trays.get(0).getLiquidOutStream().getThermoSystem().clone()); liquidOutStream.setCalculationIdentifier(id); - // Mark everything as solved for (int i = 0; i < numberOfTrays; i++) { trays.get(i).setCalculationIdentifier(id); } setCalculationIdentifier(id); } + /** * Solve the column using a simple Broyden mixing of tray temperatures. * @@ -444,6 +527,7 @@ public void run(UUID id) { */ public void runBroyden(UUID id) { if (feedStreams.isEmpty()) { + resetLastSolveMetrics(); return; } @@ -479,6 +563,8 @@ public void runBroyden(UUID id) { trays.get(firstFeedTrayNumber).run(id); + long startTime = System.nanoTime(); + do { iter++; errOld = err; @@ -529,6 +615,12 @@ public void runBroyden(UUID id) { } while ((err > temperatureTolerance || massErr > massBalanceTolerance || energyErr > enthalpyBalanceTolerance) && err < errOld && iter < maxNumberOfIterations); + lastIterationCount = iter; + lastTemperatureResidual = err; + lastMassResidual = massErr; + lastEnergyResidual = energyErr; + lastSolveTimeSeconds = (System.nanoTime() - startTime) / 1.0e9; + gasOutStream .setThermoSystem(trays.get(numberOfTrays - 1).getGasOutStream().getThermoSystem().clone()); gasOutStream.setCalculationIdentifier(id); @@ -542,115 +634,15 @@ public void runBroyden(UUID id) { } /** - * Solve the column using a damped sequential substitution scheme. - * - *

- * This method implements a simple relaxation (or damping) of the tray temperatures between - * iterations. Literature on tray based distillation modelling (e.g. Seader, Henley and Roper, - * Separation Process Principles) recommends damping to avoid oscillations and improve - * convergence when applying the classic inside-out algorithm. A relaxation factor of 1.0 - * reproduces the behaviour of {@link #run(UUID)} while smaller factors provide additional - * stability. - *

+ * Solve the column using the adaptive sequential substitution scheme with a damped starting step. * * @param id calculation identifier */ private void runDamped(UUID id) { - if (feedStreams.isEmpty()) { - return; - } - - int firstFeedTrayNumber = feedStreams.keySet().stream().min(Integer::compareTo).get(); - - if (bottomTrayPressure < 0) { - bottomTrayPressure = getTray(firstFeedTrayNumber).getStream(0).getPressure(); - } - if (topTrayPressure < 0) { - topTrayPressure = getTray(firstFeedTrayNumber).getStream(0).getPressure(); - } - - double dp = 0.0; - if (numberOfTrays > 1) { - dp = (bottomTrayPressure - topTrayPressure) / (numberOfTrays - 1.0); - } - for (int i = 0; i < numberOfTrays; i++) { - trays.get(i).setPressure(bottomTrayPressure - i * dp); - } - - if (isDoInitializion()) { - this.init(); - } - - err = 1.0e10; - double errOld; - int iter = 0; - double massErr = 1.0e10; - double energyErr = 1.0e10; - - double[] oldtemps = new double[numberOfTrays]; - - trays.get(firstFeedTrayNumber).run(id); - - do { - iter++; - errOld = err; - err = 0.0; - for (int i = 0; i < numberOfTrays; i++) { - oldtemps[i] = trays.get(i).getThermoSystem().getTemperature(); - } - - for (int i = firstFeedTrayNumber; i > 1; i--) { - int replaceStream1 = trays.get(i - 1).getNumberOfInputStreams() - 1; - trays.get(i - 1).replaceStream(replaceStream1, trays.get(i).getLiquidOutStream()); - trays.get(i - 1).run(id); - } - - int streamNumb = trays.get(0).getNumberOfInputStreams() - 1; - trays.get(0).replaceStream(streamNumb, trays.get(1).getLiquidOutStream()); - trays.get(0).run(id); - - for (int i = 1; i <= numberOfTrays - 1; i++) { - int replaceStream = trays.get(i).getNumberOfInputStreams() - 2; - if (i == (numberOfTrays - 1)) { - replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - } - trays.get(i).replaceStream(replaceStream, trays.get(i - 1).getGasOutStream()); - trays.get(i).run(id); - } - - for (int i = numberOfTrays - 2; i >= firstFeedTrayNumber; i--) { - int replaceStream = trays.get(i).getNumberOfInputStreams() - 1; - trays.get(i).replaceStream(replaceStream, trays.get(i + 1).getLiquidOutStream()); - trays.get(i).run(id); - } - - for (int i = 0; i < numberOfTrays; i++) { - double updated = trays.get(i).getThermoSystem().getTemperature(); - double newTemp = oldtemps[i] + relaxationFactor * (updated - oldtemps[i]); - trays.get(i).setTemperature(newTemp); - err += Math.abs(newTemp - oldtemps[i]); - } - - massErr = getMassBalanceError(); - energyErr = getEnergyBalanceError(); - - logger.info("error iteration = " + iter + " err = " + err + " massErr= " + massErr - + " energyErr= " + energyErr); - } while ((err > temperatureTolerance || massErr > massBalanceTolerance - || energyErr > enthalpyBalanceTolerance) && err < errOld && iter < maxNumberOfIterations); - - gasOutStream - .setThermoSystem(trays.get(numberOfTrays - 1).getGasOutStream().getThermoSystem().clone()); - gasOutStream.setCalculationIdentifier(id); - liquidOutStream.setThermoSystem(trays.get(0).getLiquidOutStream().getThermoSystem().clone()); - liquidOutStream.setCalculationIdentifier(id); - - for (int i = 0; i < numberOfTrays; i++) { - trays.get(i).setCalculationIdentifier(id); - } - setCalculationIdentifier(id); + solveSequential(id, relaxationFactor); } + /** {@inheritDoc} */ @Override @ExcludeFromJacocoGeneratedReport @@ -753,6 +745,78 @@ public boolean solved() { return (err < temperatureTolerance); } + /** + * Retrieve the iteration count of the most recent solve. + * + * @return iteration count + */ + public int getLastIterationCount() { + return lastIterationCount; + } + + /** + * Retrieve the latest average temperature residual in Kelvin. + * + * @return average temperature residual + */ + public double getLastTemperatureResidual() { + return lastTemperatureResidual; + } + + /** + * Retrieve the latest relative mass residual. + * + * @return relative mass balance residual + */ + public double getLastMassResidual() { + return lastMassResidual; + } + + /** + * Retrieve the latest relative enthalpy residual. + * + * @return relative enthalpy residual + */ + public double getLastEnergyResidual() { + return lastEnergyResidual; + } + + /** + * Retrieve the duration of the most recent solve in seconds. + * + * @return solve time in seconds + */ + public double getLastSolveTimeSeconds() { + return lastSolveTimeSeconds; + } + + /** + * Access the configured relative mass balance tolerance. + * + * @return mass balance tolerance + */ + public double getMassBalanceTolerance() { + return massBalanceTolerance; + } + + /** + * Access the configured relative enthalpy balance tolerance. + * + * @return enthalpy balance tolerance + */ + public double getEnthalpyBalanceTolerance() { + return enthalpyBalanceTolerance; + } + + /** + * Access the configured average temperature tolerance. + * + * @return temperature tolerance in Kelvin + */ + public double getTemperatureTolerance() { + return temperatureTolerance; + } + /** *

* Setter for the field maxNumberOfIterations. @@ -935,33 +999,6 @@ public void setEnthalpyBalanceTolerance(double tol) { this.enthalpyBalanceTolerance = tol; } - /** - * Get temperature convergence tolerance. - * - * @return tolerance value - */ - public double getTemperatureTolerance() { - return temperatureTolerance; - } - - /** - * Get mass balance convergence tolerance. - * - * @return tolerance value - */ - public double getMassBalanceTolerance() { - return massBalanceTolerance; - } - - /** - * Get enthalpy balance convergence tolerance. - * - * @return tolerance value - */ - public double getEnthalpyBalanceTolerance() { - return enthalpyBalanceTolerance; - } - /** * Check mass balance for all components. * @@ -1034,11 +1071,9 @@ public boolean componentMassBalanceCheck(String componentName) { } /** - *

- * getMassBalanceError. - *

+ * Calculate the relative mass balance error across the column. * - * @return a double + * @return maximum of tray-wise and overall relative mass imbalance */ public double getMassBalanceError() { double[] massInput = new double[numberOfTrays]; @@ -1054,17 +1089,26 @@ public double getMassBalanceError() { massOutput[i] += trays.get(i).getLiquidOutStream().getFlowRate("kg/hr"); massBalance[i] = massInput[i] - massOutput[i]; } - double massError = 0.0; + double trayRelativeError = 0.0; + double totalInlet = 0.0; + double totalResidual = 0.0; for (int i = 0; i < numberOfTrays; i++) { - massError += Math.abs(massBalance[i]); + double inlet = Math.abs(massInput[i]); + double imbalance = Math.abs(massBalance[i]); + if (inlet > 1e-12) { + trayRelativeError = Math.max(trayRelativeError, imbalance / inlet); + } + totalInlet += inlet; + totalResidual += imbalance; } - return massError; + double columnRelative = totalInlet > 1e-12 ? totalResidual / totalInlet : totalResidual; + return Math.max(trayRelativeError, columnRelative); } /** - * Calculates the total enthalpy imbalance across all trays. + * Calculates the relative enthalpy imbalance across all trays. * - * @return the summed absolute enthalpy imbalance + * @return maximum of tray-wise and overall relative enthalpy imbalance */ public double getEnergyBalanceError() { double[] energyInput = new double[numberOfTrays]; @@ -1080,11 +1124,64 @@ public double getEnergyBalanceError() { energyOutput[i] += trays.get(i).getLiquidOutStream().getFluid().getEnthalpy(); energyBalance[i] = energyInput[i] - energyOutput[i]; } - double energyError = 0.0; + double trayRelativeError = 0.0; + double totalInlet = 0.0; + double totalResidual = 0.0; for (int i = 0; i < numberOfTrays; i++) { - energyError += Math.abs(energyBalance[i]); + double inlet = Math.abs(energyInput[i]); + double imbalance = Math.abs(energyBalance[i]); + if (inlet > 1e-12) { + trayRelativeError = Math.max(trayRelativeError, imbalance / inlet); + } + totalInlet += inlet; + totalResidual += imbalance; + } + double columnRelative = totalInlet > 1e-12 ? totalResidual / totalInlet : totalResidual; + return Math.max(trayRelativeError, columnRelative); + } + + /** + * Blend the current stream update with the previous iterate using the provided relaxation factor. + * + * @param previous stream from the previous iteration (may be {@code null}) + * @param current current iteration stream + * @param relaxation relaxation factor applied to the update + * @return relaxed stream instance to be used in the next tear + */ + private StreamInterface applyRelaxation(StreamInterface previous, StreamInterface current, + double relaxation) { + StreamInterface relaxed = current.clone(); + if (previous == null) { + relaxed.run(); + return relaxed; } - return energyError; + + double step = Math.max(0.0, Math.min(1.0, relaxation)); + double previousFlow = previous.getFlowRate("kg/hr"); + double currentFlow = current.getFlowRate("kg/hr"); + double mixedFlow = previousFlow + step * (currentFlow - previousFlow); + relaxed.setFlowRate(mixedFlow, "kg/hr"); + + double mixedTemperature = previous.getTemperature("K") + + step * (current.getTemperature("K") - previous.getTemperature("K")); + relaxed.setTemperature(mixedTemperature, "K"); + + double mixedPressure = previous.getPressure("bara") + + step * (current.getPressure("bara") - previous.getPressure("bara")); + relaxed.setPressure(mixedPressure, "bara"); + + relaxed.run(); + + return relaxed; + } + + /** Reset cached solve metrics when no calculation is performed. */ + private void resetLastSolveMetrics() { + lastIterationCount = 0; + lastTemperatureResidual = 0.0; + lastMassResidual = 0.0; + lastEnergyResidual = 0.0; + lastSolveTimeSeconds = 0.0; } /** diff --git a/src/main/java/neqsim/process/processmodel/DexpiMetadata.java b/src/main/java/neqsim/process/processmodel/DexpiMetadata.java new file mode 100644 index 0000000000..405885eec4 --- /dev/null +++ b/src/main/java/neqsim/process/processmodel/DexpiMetadata.java @@ -0,0 +1,79 @@ +package neqsim.process.processmodel; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Shared constants describing the recommended DEXPI metadata handled by the reader and writer. + */ +public final class DexpiMetadata { + private DexpiMetadata() {} + + /** Generic attribute containing the tag name of an equipment item. */ + public static final String TAG_NAME = "TagNameAssignmentClass"; + + /** Generic attribute containing a line number reference. */ + public static final String LINE_NUMBER = "LineNumberAssignmentClass"; + + /** Generic attribute containing a fluid code reference. */ + public static final String FLUID_CODE = "FluidCodeAssignmentClass"; + + /** Generic attribute containing the segment number of a piping network segment. */ + public static final String SEGMENT_NUMBER = "SegmentNumberAssignmentClass"; + + /** Generic attribute containing the operating pressure value of a segment. */ + public static final String OPERATING_PRESSURE_VALUE = "OperatingPressureValue"; + + /** Generic attribute containing the unit of the operating pressure value of a segment. */ + public static final String OPERATING_PRESSURE_UNIT = "OperatingPressureUnit"; + + /** Generic attribute containing the operating temperature value of a segment. */ + public static final String OPERATING_TEMPERATURE_VALUE = "OperatingTemperatureValue"; + + /** Generic attribute containing the unit of the operating temperature value of a segment. */ + public static final String OPERATING_TEMPERATURE_UNIT = "OperatingTemperatureUnit"; + + /** Generic attribute containing the operating flow value of a segment. */ + public static final String OPERATING_FLOW_VALUE = "OperatingFlowValue"; + + /** Generic attribute containing the unit of the operating flow value of a segment. */ + public static final String OPERATING_FLOW_UNIT = "OperatingFlowUnit"; + + /** Default pressure unit written to DEXPI documents. */ + public static final String DEFAULT_PRESSURE_UNIT = "bara"; + + /** Default temperature unit written to DEXPI documents. */ + public static final String DEFAULT_TEMPERATURE_UNIT = "C"; + + /** Default volumetric flow unit written to DEXPI documents. */ + public static final String DEFAULT_FLOW_UNIT = "MSm3/day"; + + private static final Set RECOMMENDED_STREAM_ATTRIBUTES = + Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(LINE_NUMBER, FLUID_CODE, + SEGMENT_NUMBER, OPERATING_PRESSURE_VALUE, OPERATING_PRESSURE_UNIT, + OPERATING_TEMPERATURE_VALUE, OPERATING_TEMPERATURE_UNIT, OPERATING_FLOW_VALUE, + OPERATING_FLOW_UNIT))); + + private static final Set RECOMMENDED_EQUIPMENT_ATTRIBUTES = Collections + .unmodifiableSet(new LinkedHashSet<>(Arrays.asList(TAG_NAME, LINE_NUMBER, FLUID_CODE))); + + /** + * Returns the recommended generic attributes that should accompany DEXPI piping segments. + * + * @return immutable set of attribute names + */ + public static Set recommendedStreamAttributes() { + return RECOMMENDED_STREAM_ATTRIBUTES; + } + + /** + * Returns the recommended generic attributes that should accompany DEXPI equipment items. + * + * @return immutable set of attribute names + */ + public static Set recommendedEquipmentAttributes() { + return RECOMMENDED_EQUIPMENT_ATTRIBUTES; + } +} diff --git a/src/main/java/neqsim/process/processmodel/DexpiRoundTripProfile.java b/src/main/java/neqsim/process/processmodel/DexpiRoundTripProfile.java new file mode 100644 index 0000000000..d5d234d916 --- /dev/null +++ b/src/main/java/neqsim/process/processmodel/DexpiRoundTripProfile.java @@ -0,0 +1,123 @@ +package neqsim.process.processmodel; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Describes validation profiles for round-tripping DEXPI data through NeqSim. + */ +public final class DexpiRoundTripProfile { + private final String name; + + private DexpiRoundTripProfile(String name) { + this.name = name; + } + + /** + * Returns a minimal profile guaranteeing that imported data can be executed and exported. + * + * @return profile enforcing stream metadata and equipment tagging + */ + public static DexpiRoundTripProfile minimalRunnableProfile() { + return Holder.MINIMAL_RUNNABLE; + } + + /** + * Validates the supplied process system against the profile. + * + * @param processSystem process system produced from DEXPI data + * @return validation result indicating success and listing violations + */ + public ValidationResult validate(ProcessSystem processSystem) { + Objects.requireNonNull(processSystem, "processSystem"); + List violations = new ArrayList<>(); + + long streamCount = processSystem.getUnitOperations().stream() + .filter(DexpiStream.class::isInstance).count(); + if (streamCount == 0) { + violations.add("Process must contain at least one DexpiStream"); + } + + List streams = processSystem.getUnitOperations().stream() + .filter(DexpiStream.class::isInstance).map(DexpiStream.class::cast).collect(Collectors.toList()); + for (DexpiStream stream : streams) { + if (isBlank(stream.getName())) { + violations.add("DexpiStream is missing a name"); + } + if (isBlank(stream.getLineNumber()) && isBlank(stream.getFluidCode())) { + violations.add("DexpiStream " + stream.getName() + + " requires a line number or fluid code to preserve connectivity"); + } + if (Double.isNaN(stream.getPressure(DexpiMetadata.DEFAULT_PRESSURE_UNIT))) { + violations.add("DexpiStream " + stream.getName() + " is missing operating pressure metadata"); + } + if (Double.isNaN(stream.getTemperature(DexpiMetadata.DEFAULT_TEMPERATURE_UNIT))) { + violations.add("DexpiStream " + stream.getName() + " is missing operating temperature metadata"); + } + if (Double.isNaN(stream.getFlowRate(DexpiMetadata.DEFAULT_FLOW_UNIT))) { + violations.add("DexpiStream " + stream.getName() + " is missing operating flow metadata"); + } + if (!stream.isActive()) { + violations.add("DexpiStream " + stream.getName() + " must be active after simulation"); + } + } + + List units = processSystem.getUnitOperations().stream() + .filter(DexpiProcessUnit.class::isInstance).map(DexpiProcessUnit.class::cast) + .collect(Collectors.toList()); + for (DexpiProcessUnit unit : units) { + if (isBlank(unit.getName())) { + violations.add("DexpiProcessUnit is missing a tag"); + } + if (unit.getMappedEquipment() == null) { + violations.add("DexpiProcessUnit " + unit.getName() + " lacks a mapped equipment enum"); + } + if (isBlank(unit.getDexpiClass())) { + violations.add("DexpiProcessUnit " + unit.getName() + " does not expose its original DEXPI class"); + } + } + + boolean hasEquipment = processSystem.getUnitOperations().stream() + .anyMatch(unit -> unit instanceof DexpiProcessUnit); + if (!hasEquipment) { + violations.add("Process must contain at least one DexpiProcessUnit"); + } + + return new ValidationResult(violations.isEmpty(), Collections.unmodifiableList(violations)); + } + + /** Profile validation result. */ + public static final class ValidationResult { + private final boolean successful; + private final List violations; + + private ValidationResult(boolean successful, List violations) { + this.successful = successful; + this.violations = violations; + } + + /** Indicates whether validation succeeded. */ + public boolean isSuccessful() { + return successful; + } + + /** Detailed violations preventing the process from satisfying the profile. */ + public List getViolations() { + return violations; + } + } + + private static boolean isBlank(String value) { + return value == null || value.trim().isEmpty(); + } + + private static final class Holder { + private static final DexpiRoundTripProfile MINIMAL_RUNNABLE = + new DexpiRoundTripProfile("minimalRunnable"); + + private Holder() {} + } +} diff --git a/src/main/java/neqsim/process/processmodel/DexpiXmlReader.java b/src/main/java/neqsim/process/processmodel/DexpiXmlReader.java index dd1769ae59..30274b8d45 100644 --- a/src/main/java/neqsim/process/processmodel/DexpiXmlReader.java +++ b/src/main/java/neqsim/process/processmodel/DexpiXmlReader.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.function.BiConsumer; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -36,16 +37,30 @@ public final class DexpiXmlReader { static { Map equipmentMap = new HashMap<>(); equipmentMap.put("PlateHeatExchanger", EquipmentEnum.HeatExchanger); + equipmentMap.put("ShellAndTubeHeatExchanger", EquipmentEnum.HeatExchanger); equipmentMap.put("TubularHeatExchanger", EquipmentEnum.HeatExchanger); + equipmentMap.put("AirCooledHeatExchanger", EquipmentEnum.HeatExchanger); equipmentMap.put("CentrifugalPump", EquipmentEnum.Pump); equipmentMap.put("ReciprocatingPump", EquipmentEnum.Pump); + equipmentMap.put("CentrifugalCompressor", EquipmentEnum.Compressor); + equipmentMap.put("ReciprocatingCompressor", EquipmentEnum.Compressor); equipmentMap.put("Tank", EquipmentEnum.Tank); + equipmentMap.put("StirredTankReactor", EquipmentEnum.Reactor); + equipmentMap.put("PlugFlowReactor", EquipmentEnum.Reactor); + equipmentMap.put("PackedBedReactor", EquipmentEnum.Reactor); + equipmentMap.put("InlineAnalyzer", EquipmentEnum.Calculator); + equipmentMap.put("GasAnalyzer", EquipmentEnum.Calculator); + equipmentMap.put("Spectrometer", EquipmentEnum.Calculator); EQUIPMENT_CLASS_MAP = Collections.unmodifiableMap(equipmentMap); Map pipingMap = new HashMap<>(); pipingMap.put("GlobeValve", EquipmentEnum.ThrottlingValve); pipingMap.put("ButterflyValve", EquipmentEnum.ThrottlingValve); pipingMap.put("CheckValve", EquipmentEnum.ThrottlingValve); + pipingMap.put("ControlValve", EquipmentEnum.ThrottlingValve); + pipingMap.put("PressureSafetyValve", EquipmentEnum.ThrottlingValve); + pipingMap.put("PressureReliefValve", EquipmentEnum.ThrottlingValve); + pipingMap.put("PressureReducingValve", EquipmentEnum.ThrottlingValve); PIPING_COMPONENT_MAP = Collections.unmodifiableMap(pipingMap); } @@ -207,7 +222,7 @@ private static void addEquipmentUnits(Document document, ProcessSystem processSy continue; } - String baseName = firstNonEmpty(getGenericAttribute(element, "TagNameAssignmentClass"), + String baseName = firstNonEmpty(attributeValue(element, DexpiMetadata.TAG_NAME), element.getAttribute("ID")); addDexpiUnit(processSystem, element, equipmentEnum, baseName, element.getAttribute("ComponentClass")); @@ -228,8 +243,8 @@ private static void addPipingComponents(Document document, ProcessSystem process } String baseName = firstNonEmpty( - getGenericAttribute(element, "PipingComponentNumberAssignmentClass"), - getGenericAttribute(element, "TagNameAssignmentClass"), element.getAttribute("ID")); + attributeValue(element, "PipingComponentNumberAssignmentClass"), + attributeValue(element, DexpiMetadata.TAG_NAME), element.getAttribute("ID")); addDexpiUnit(processSystem, element, equipmentEnum, baseName, element.getAttribute("ComponentClass")); } @@ -244,7 +259,7 @@ private static void addPipingSegments(Document document, ProcessSystem processSy continue; } Element element = (Element) node; - String baseName = firstNonEmpty(getGenericAttribute(element, "SegmentNumberAssignmentClass"), + String baseName = firstNonEmpty(attributeValue(element, DexpiMetadata.SEGMENT_NUMBER), element.getAttribute("ID")); addDexpiStream(processSystem, element, templateStream, baseName); } @@ -254,9 +269,8 @@ private static void addDexpiStream(ProcessSystem processSystem, Element element, Stream templateStream, String baseName) { String contextualName = prependLineOrFluid(element, baseName); String uniqueName = ensureUniqueName(processSystem, contextualName); - String lineNumber = findAttributeInAncestors(element, "LineNumberAssignmentClass"); - String fluidCode = firstNonEmpty(getGenericAttribute(element, "FluidCodeAssignmentClass"), - findAttributeInAncestors(element, "FluidCodeAssignmentClass")); + String lineNumber = attributeValue(element, DexpiMetadata.LINE_NUMBER); + String fluidCode = attributeValue(element, DexpiMetadata.FLUID_CODE); SystemInterface baseFluid = templateStream.getThermoSystem(); SystemInterface fluid = baseFluid == null ? createDefaultFluid() : baseFluid.clone(); @@ -264,6 +278,14 @@ private static void addDexpiStream(ProcessSystem processSystem, Element element, DexpiStream stream = new DexpiStream(uniqueName, fluid, element.getAttribute("ComponentClass"), lineNumber, fluidCode); stream.setSpecification(templateStream.getSpecification()); + stream.setPressure(templateStream.getPressure(DexpiMetadata.DEFAULT_PRESSURE_UNIT), + DexpiMetadata.DEFAULT_PRESSURE_UNIT); + stream.setTemperature(templateStream.getTemperature(DexpiMetadata.DEFAULT_TEMPERATURE_UNIT), + DexpiMetadata.DEFAULT_TEMPERATURE_UNIT); + stream.setFlowRate(templateStream.getFlowRate(DexpiMetadata.DEFAULT_FLOW_UNIT), + DexpiMetadata.DEFAULT_FLOW_UNIT); + + applyStreamMetadata(element, stream); processSystem.addUnit(uniqueName, stream); } @@ -271,9 +293,8 @@ private static void addDexpiUnit(ProcessSystem processSystem, Element element, EquipmentEnum equipmentEnum, String baseName, String componentClass) { String contextualName = prependLineOrFluid(element, baseName); String uniqueName = ensureUniqueName(processSystem, contextualName); - String lineNumber = findAttributeInAncestors(element, "LineNumberAssignmentClass"); - String fluidCode = firstNonEmpty(getGenericAttribute(element, "FluidCodeAssignmentClass"), - findAttributeInAncestors(element, "FluidCodeAssignmentClass")); + String lineNumber = attributeValue(element, DexpiMetadata.LINE_NUMBER); + String fluidCode = attributeValue(element, DexpiMetadata.FLUID_CODE); DexpiProcessUnit unit = new DexpiProcessUnit(uniqueName, componentClass, equipmentEnum, lineNumber, fluidCode); processSystem.addUnit(uniqueName, unit); @@ -281,12 +302,11 @@ private static void addDexpiUnit(ProcessSystem processSystem, Element element, private static String prependLineOrFluid(Element element, String baseName) { String trimmedBase = baseName == null ? "" : baseName.trim(); - String lineNumber = findAttributeInAncestors(element, "LineNumberAssignmentClass"); + String lineNumber = attributeValue(element, DexpiMetadata.LINE_NUMBER); if (!isBlank(lineNumber)) { return lineNumber.trim() + "-" + trimmedBase; } - String fluidCode = firstNonEmpty(getGenericAttribute(element, "FluidCodeAssignmentClass"), - findAttributeInAncestors(element, "FluidCodeAssignmentClass")); + String fluidCode = attributeValue(element, DexpiMetadata.FLUID_CODE); if (!isBlank(fluidCode)) { return fluidCode.trim() + "-" + trimmedBase; } @@ -358,6 +378,56 @@ private static String getGenericAttribute(Element element, String attributeName) return null; } + private static String attributeValue(Element element, String attributeName) { + return firstNonEmpty(getGenericAttribute(element, attributeName), + findAttributeInAncestors(element, attributeName)); + } + + private static void applyStreamMetadata(Element element, DexpiStream stream) { + applyNumericAttribute(element, DexpiMetadata.OPERATING_PRESSURE_VALUE, + DexpiMetadata.OPERATING_PRESSURE_UNIT, stream::setPressure, + DexpiMetadata.DEFAULT_PRESSURE_UNIT); + applyNumericAttribute(element, DexpiMetadata.OPERATING_TEMPERATURE_VALUE, + DexpiMetadata.OPERATING_TEMPERATURE_UNIT, stream::setTemperature, + DexpiMetadata.DEFAULT_TEMPERATURE_UNIT); + applyNumericAttribute(element, DexpiMetadata.OPERATING_FLOW_VALUE, + DexpiMetadata.OPERATING_FLOW_UNIT, stream::setFlowRate, DexpiMetadata.DEFAULT_FLOW_UNIT); + } + + private static void applyNumericAttribute(Element element, String valueAttribute, + String unitAttribute, BiConsumer consumer, String defaultUnit) { + String valueText = firstNonEmpty(getGenericAttribute(element, valueAttribute), + findAttributeInAncestors(element, valueAttribute)); + Double value = parseNumeric(valueText); + if (value == null) { + return; + } + String unit = firstNonEmpty(getGenericAttribute(element, unitAttribute), + findAttributeInAncestors(element, unitAttribute), defaultUnit); + consumer.accept(value, unit); + } + + private static Double parseNumeric(String valueText) { + if (isBlank(valueText)) { + return null; + } + String trimmed = valueText.trim(); + try { + return Double.parseDouble(trimmed); + } catch (NumberFormatException ex) { + int spaceIndex = trimmed.indexOf(' '); + if (spaceIndex > 0) { + String candidate = trimmed.substring(0, spaceIndex); + try { + return Double.parseDouble(candidate); + } catch (NumberFormatException ignored) { + return null; + } + } + return null; + } + } + private static List directChildElements(Element element, String tagName) { if (element == null) { return Collections.emptyList(); diff --git a/src/main/java/neqsim/process/processmodel/DexpiXmlWriter.java b/src/main/java/neqsim/process/processmodel/DexpiXmlWriter.java index 75691036b4..9ba9d57fc4 100644 --- a/src/main/java/neqsim/process/processmodel/DexpiXmlWriter.java +++ b/src/main/java/neqsim/process/processmodel/DexpiXmlWriter.java @@ -6,6 +6,8 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; import java.time.LocalDate; import java.time.LocalTime; import java.time.format.DateTimeFormatter; @@ -13,13 +15,16 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Pattern; +import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; @@ -37,6 +42,13 @@ */ public final class DexpiXmlWriter { private static final Pattern NON_IDENTIFIER = Pattern.compile("[^A-Za-z0-9_-]"); + private static final ThreadLocal DECIMAL_FORMAT = ThreadLocal.withInitial(() -> { + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(Locale.ROOT); + DecimalFormat format = new DecimalFormat("0.############", symbols); + format.setMaximumFractionDigits(12); + format.setGroupingUsed(false); + return format; + }); private DexpiXmlWriter() {} @@ -105,10 +117,16 @@ public static void write(ProcessSystem processSystem, OutputStream outputStream) private static Document createDocument() throws IOException { try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(false); - factory.setExpandEntityReferences(false); - factory.setXIncludeAware(false); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, ""); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); + factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + factory.setFeature("http://xml.org/sax/features/external-general-entities", false); + factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + factory.setNamespaceAware(false); + factory.setExpandEntityReferences(false); + factory.setXIncludeAware(false); DocumentBuilder builder = factory.newDocumentBuilder(); return builder.newDocument(); } catch (ParserConfigurationException e) { @@ -150,11 +168,11 @@ private static void appendProcessUnit(Document document, Element parent, uniqueIdentifier(elementName, processUnit.getName(), usedIds)); Element genericAttributes = document.createElement("GenericAttributes"); - appendGenericAttribute(document, genericAttributes, "TagNameAssignmentClass", + appendGenericAttribute(document, genericAttributes, DexpiMetadata.TAG_NAME, processUnit.getName()); - appendGenericAttribute(document, genericAttributes, "LineNumberAssignmentClass", + appendGenericAttribute(document, genericAttributes, DexpiMetadata.LINE_NUMBER, processUnit.getLineNumber()); - appendGenericAttribute(document, genericAttributes, "FluidCodeAssignmentClass", + appendGenericAttribute(document, genericAttributes, DexpiMetadata.FLUID_CODE, processUnit.getFluidCode()); if (genericAttributes.hasChildNodes()) { @@ -173,10 +191,11 @@ private static void appendPipingNetworkSystem(Document document, Element parent, Element systemAttributes = document.createElement("GenericAttributes"); String lineNumber = streams.stream().map(DexpiStream::getLineNumber) .filter(value -> !isBlank(value)).findFirst().orElse(null); - appendGenericAttribute(document, systemAttributes, "LineNumberAssignmentClass", lineNumber); + appendGenericAttribute(document, systemAttributes, DexpiMetadata.LINE_NUMBER, lineNumber); String fluidCode = streams.stream().map(DexpiStream::getFluidCode) .filter(value -> !isBlank(value)).findFirst().orElse(null); - appendGenericAttribute(document, systemAttributes, "FluidCodeAssignmentClass", fluidCode); + appendGenericAttribute(document, systemAttributes, DexpiMetadata.FLUID_CODE, fluidCode); + appendGenericAttribute(document, systemAttributes, "NeqSimGroupingKey", key); if (systemAttributes.hasChildNodes()) { systemElement.appendChild(systemAttributes); } @@ -197,12 +216,26 @@ private static void appendPipingNetworkSegment(Document document, Element parent uniqueIdentifier("Segment", stream.getName(), usedIds)); Element genericAttributes = document.createElement("GenericAttributes"); - appendGenericAttribute(document, genericAttributes, "SegmentNumberAssignmentClass", + appendGenericAttribute(document, genericAttributes, DexpiMetadata.SEGMENT_NUMBER, stream.getName()); - appendGenericAttribute(document, genericAttributes, "LineNumberAssignmentClass", + appendGenericAttribute(document, genericAttributes, DexpiMetadata.LINE_NUMBER, stream.getLineNumber()); - appendGenericAttribute(document, genericAttributes, "FluidCodeAssignmentClass", + appendGenericAttribute(document, genericAttributes, DexpiMetadata.FLUID_CODE, stream.getFluidCode()); + appendNumericAttribute(document, genericAttributes, DexpiMetadata.OPERATING_PRESSURE_VALUE, + stream.getPressure(DexpiMetadata.DEFAULT_PRESSURE_UNIT), + DexpiMetadata.DEFAULT_PRESSURE_UNIT); + appendGenericAttribute(document, genericAttributes, DexpiMetadata.OPERATING_PRESSURE_UNIT, + DexpiMetadata.DEFAULT_PRESSURE_UNIT); + appendNumericAttribute(document, genericAttributes, DexpiMetadata.OPERATING_TEMPERATURE_VALUE, + stream.getTemperature(DexpiMetadata.DEFAULT_TEMPERATURE_UNIT), + DexpiMetadata.DEFAULT_TEMPERATURE_UNIT); + appendGenericAttribute(document, genericAttributes, DexpiMetadata.OPERATING_TEMPERATURE_UNIT, + DexpiMetadata.DEFAULT_TEMPERATURE_UNIT); + appendNumericAttribute(document, genericAttributes, DexpiMetadata.OPERATING_FLOW_VALUE, + stream.getFlowRate(DexpiMetadata.DEFAULT_FLOW_UNIT), DexpiMetadata.DEFAULT_FLOW_UNIT); + appendGenericAttribute(document, genericAttributes, DexpiMetadata.OPERATING_FLOW_UNIT, + DexpiMetadata.DEFAULT_FLOW_UNIT); if (genericAttributes.hasChildNodes()) { segmentElement.appendChild(genericAttributes); } @@ -212,15 +245,31 @@ private static void appendPipingNetworkSegment(Document document, Element parent private static void appendGenericAttribute(Document document, Element parent, String name, String value) { + appendGenericAttribute(document, parent, name, value, null); + } + + private static void appendGenericAttribute(Document document, Element parent, String name, + String value, String unit) { if (isBlank(value)) { return; } Element attribute = document.createElement("GenericAttribute"); attribute.setAttribute("Name", name); attribute.setAttribute("Value", value.trim()); + if (!isBlank(unit)) { + attribute.setAttribute("Unit", unit.trim()); + } parent.appendChild(attribute); } + private static void appendNumericAttribute(Document document, Element parent, String name, + double value, String unit) { + if (Double.isNaN(value) || Double.isInfinite(value)) { + return; + } + appendGenericAttribute(document, parent, name, DECIMAL_FORMAT.get().format(value), unit); + } + private static String defaultComponentClass(EquipmentEnum mapped, String elementName) { if (mapped == null) { return elementName; @@ -304,6 +353,9 @@ private static void writeDocument(Document document, OutputStream outputStream) throws IOException { try { TransformerFactory factory = TransformerFactory.newInstance(); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, ""); + factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); + factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true); Transformer transformer = factory.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); @@ -311,6 +363,8 @@ private static void writeDocument(Document document, OutputStream outputStream) transformer.transform(new DOMSource(document), new StreamResult(outputStream)); } catch (TransformerException e) { throw new IOException("Unable to serialize DEXPI document", e); + } catch (TransformerFactoryConfigurationError e) { + throw new IOException("Unable to configure XML transformer", e); } } } diff --git a/src/main/java/neqsim/process/processmodel/ProcessSystem.java b/src/main/java/neqsim/process/processmodel/ProcessSystem.java index 499ef98f64..513f6d38c4 100644 --- a/src/main/java/neqsim/process/processmodel/ProcessSystem.java +++ b/src/main/java/neqsim/process/processmodel/ProcessSystem.java @@ -260,7 +260,7 @@ public int getUnitNumber(String name) { return i; } } - return 0; + return -1; } /** @@ -272,7 +272,17 @@ public int getUnitNumber(String name) { * @param operation a {@link neqsim.process.equipment.ProcessEquipmentBaseClass} object */ public void replaceObject(String unitName, ProcessEquipmentBaseClass operation) { - unitOperations.set(getUnitNumber(name), operation); + Objects.requireNonNull(unitName, "unitName"); + Objects.requireNonNull(operation, "operation"); + + int index = getUnitNumber(unitName); + if (index < 0 || index >= unitOperations.size() || getUnit(unitName) == null) { + throw new IllegalArgumentException( + "No process equipment named '" + unitName + "' exists in this ProcessSystem"); + } + + operation.setName(unitName); + unitOperations.set(index, operation); } /** diff --git a/src/test/java/neqsim/process/equipment/EquipmentFactoryTest.java b/src/test/java/neqsim/process/equipment/EquipmentFactoryTest.java new file mode 100644 index 0000000000..fb809de679 --- /dev/null +++ b/src/test/java/neqsim/process/equipment/EquipmentFactoryTest.java @@ -0,0 +1,78 @@ +package neqsim.process.equipment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import neqsim.process.equipment.ejector.Ejector; +import neqsim.process.equipment.powergeneration.WindTurbine; +import neqsim.process.equipment.reservoir.ReservoirCVDsim; +import neqsim.process.equipment.stream.Stream; +import neqsim.process.equipment.stream.StreamInterface; +import neqsim.process.equipment.util.GORfitter; +import neqsim.process.equipment.valve.ThrottlingValve; +import neqsim.process.equipment.ProcessEquipmentInterface; +import neqsim.thermo.system.SystemInterface; +import neqsim.thermo.system.SystemSrkEos; + +/** + * Tests for {@link EquipmentFactory}. + */ +public class EquipmentFactoryTest extends neqsim.NeqSimTest { + + @Test + public void createEquipmentFromEnum() { + ProcessEquipmentInterface equipment = + EquipmentFactory.createEquipment("valve1", EquipmentEnum.ThrottlingValve); + + assertInstanceOf(ThrottlingValve.class, equipment); + assertEquals("valve1", equipment.getName()); + } + + @Test + public void createEquipmentFromStringAlias() { + ProcessEquipmentInterface equipment = EquipmentFactory.createEquipment("wt", "windturbine"); + + assertInstanceOf(WindTurbine.class, equipment); + assertEquals("wt", equipment.getName()); + } + + @Test + public void ejectorRequiresStreams() { + assertThrows(IllegalArgumentException.class, () -> EquipmentFactory.createEquipment("ej", "ejector")); + + StreamInterface motive = new Stream("motive"); + StreamInterface suction = new Stream("suction"); + + Ejector ejector = EquipmentFactory.createEjector("ej", motive, suction); + assertEquals("ej", ejector.getName()); + } + + @Test + public void gorfitterRequiresInletStream() { + assertThrows(IllegalArgumentException.class, + () -> EquipmentFactory.createEquipment("gor", EquipmentEnum.GORfitter)); + + StreamInterface inlet = new Stream("inlet"); + GORfitter fitter = EquipmentFactory.createGORfitter("gor", inlet); + assertEquals("gor", fitter.getName()); + } + + @Test + public void reservoirSimRequiresFluid() { + assertThrows(IllegalArgumentException.class, + () -> EquipmentFactory.createEquipment("cvd", EquipmentEnum.ReservoirCVDsim)); + + SystemInterface fluid = new SystemSrkEos(273.15, 100.0); + fluid.addComponent("methane", 1.0); + fluid.createDatabase(true); + fluid.setMixingRule(2); + + ReservoirCVDsim simulator = EquipmentFactory.createReservoirCVDsim("cvd", fluid); + assertNotNull(simulator); + assertEquals("cvd", simulator.getName()); + } +} diff --git a/src/test/java/neqsim/process/equipment/distillation/DistillationColumnTest.java b/src/test/java/neqsim/process/equipment/distillation/DistillationColumnTest.java index 8a76dea8e9..37400ecdaf 100644 --- a/src/test/java/neqsim/process/equipment/distillation/DistillationColumnTest.java +++ b/src/test/java/neqsim/process/equipment/distillation/DistillationColumnTest.java @@ -1,6 +1,8 @@ package neqsim.process.equipment.distillation; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.UUID; import org.junit.jupiter.api.Test; import neqsim.process.equipment.stream.Stream; import neqsim.process.equipment.stream.StreamInterface; @@ -212,6 +214,61 @@ public void debutanizerTest() { assertEquals(0.0, massbalance, 0.2); } + @Test + public void adaptiveSolverRecordsSolveMetrics() { + SystemInterface simpleSystem = new SystemSrkEos(298.15, 5.0); + simpleSystem.addComponent("methane", 1.0); + simpleSystem.addComponent("ethane", 1.0); + simpleSystem.createDatabase(true); + simpleSystem.setMixingRule("classic"); + + Stream feed = new Stream("metricsFeed", simpleSystem); + feed.run(); + + DistillationColumn column = new DistillationColumn("metrics column", 1, true, true); + column.addFeedStream(feed, 1); + column.run(); + + DistillationColumn broydenColumn = new DistillationColumn("metrics column broyden", 1, true, true); + Stream broydenFeed = new Stream("metricsFeedBroyden", simpleSystem.clone()); + broydenFeed.run(); + broydenColumn.addFeedStream(broydenFeed, 1); + broydenColumn.runBroyden(UUID.randomUUID()); + + assertTrue(column.getLastIterationCount() > 0); + assertTrue(column.getLastTemperatureResidual() >= 0.0); + assertTrue(Double.isFinite(column.getLastMassResidual())); + assertTrue(Double.isFinite(column.getLastEnergyResidual())); + assertTrue(column.getLastSolveTimeSeconds() >= 0.0); + assertTrue(Double.isFinite(broydenColumn.getLastMassResidual())); + assertTrue(Double.isFinite(broydenColumn.getLastEnergyResidual())); + } + + @Test + public void multipleFeedsOnDifferentTraysAreHandled() { + SystemInterface simpleSystem = new SystemSrkEos(298.15, 5.0); + simpleSystem.addComponent("methane", 1.0); + simpleSystem.addComponent("ethane", 1.0); + simpleSystem.createDatabase(true); + simpleSystem.setMixingRule("classic"); + + Stream feedOne = new Stream("feedOne", simpleSystem.clone()); + feedOne.run(); + Stream feedTwo = new Stream("feedTwo", simpleSystem.clone()); + feedTwo.run(); + Stream feedThree = new Stream("feedThree", simpleSystem.clone()); + feedThree.run(); + + DistillationColumn column = new DistillationColumn("feed tracking", 3, true, true); + column.addFeedStream(feedOne, 1); + column.addFeedStream(feedTwo, 1); + column.addFeedStream(feedThree, 3); + + assertEquals(2, column.getFeedStreams(1).size()); + assertEquals(1, column.getFeedStreams(3).size()); + assertEquals(0, column.getFeedStreams(2).size()); + } + /** * */ diff --git a/src/test/java/neqsim/process/processmodel/DexpiXmlReaderTest.java b/src/test/java/neqsim/process/processmodel/DexpiXmlReaderTest.java index 7479027af9..e85d7cb34a 100644 --- a/src/test/java/neqsim/process/processmodel/DexpiXmlReaderTest.java +++ b/src/test/java/neqsim/process/processmodel/DexpiXmlReaderTest.java @@ -11,6 +11,7 @@ import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import org.w3c.dom.Element; import org.w3c.dom.Document; import org.w3c.dom.NodeList; import org.junit.jupiter.api.Test; @@ -82,6 +83,11 @@ void convertsDexpiExampleIntoRunnableProcess() throws Exception { .filter(DexpiStream.class::isInstance).map(DexpiStream.class::cast) .filter(Stream::isActive).count(); assertTrue(activeStreams > 0, "At least one imported stream should calculate a TP flash"); + + DexpiRoundTripProfile.ValidationResult result = + DexpiRoundTripProfile.minimalRunnableProfile().validate(processSystem); + assertTrue(result.isSuccessful(), + () -> "Minimal runnable profile violations: " + result.getViolations()); } } @@ -113,6 +119,51 @@ void exportsProcessToDexpiXmlForExampleProcess() throws Exception { assertTrue(systems.getLength() > 0, "Export should include piping network systems"); assertTrue(equipment.getLength() + pipingComponents.getLength() > 5, "Export should include multiple pieces of equipment and piping components"); + + Element firstSegment = (Element) pipingSegments.item(0); + Element segmentAttributes = findGenericAttributes(firstSegment); + assertNotNull(segmentAttributes, + "Piping segments should carry generic attributes with operating metadata"); + assertTrue(hasGenericAttribute(segmentAttributes, DexpiMetadata.OPERATING_PRESSURE_VALUE), + "Segments should include operating pressure values"); + assertTrue(hasGenericAttribute(segmentAttributes, DexpiMetadata.OPERATING_TEMPERATURE_VALUE), + "Segments should include operating temperature values"); + assertTrue(hasGenericAttribute(segmentAttributes, DexpiMetadata.OPERATING_FLOW_VALUE), + "Segments should include operating flow values"); + + Element pressureAttribute = getGenericAttribute(segmentAttributes, + DexpiMetadata.OPERATING_PRESSURE_VALUE); + assertNotNull(pressureAttribute, "Pressure attribute must be present"); + assertTrue(pressureAttribute.hasAttribute("Unit"), + "Pressure attribute should declare a unit"); + + Stream roundTripTemplate = createExampleFeedStream(); + roundTripTemplate.setPressure(5.0, DexpiMetadata.DEFAULT_PRESSURE_UNIT); + roundTripTemplate.setTemperature(10.0, DexpiMetadata.DEFAULT_TEMPERATURE_UNIT); + roundTripTemplate.setFlowRate(5.0, DexpiMetadata.DEFAULT_FLOW_UNIT); + + try (InputStream reimportStream = Files.newInputStream(exportPath)) { + ProcessSystem roundTripped = DexpiXmlReader.read(reimportStream, roundTripTemplate); + roundTripped.run(); + + DexpiRoundTripProfile.ValidationResult roundTripValidation = + DexpiRoundTripProfile.minimalRunnableProfile().validate(roundTripped); + assertTrue(roundTripValidation.isSuccessful(), + () -> "Round-trip profile violations: " + roundTripValidation.getViolations()); + + DexpiStream exportedStream = (DexpiStream) roundTripped.getUnit("47121-S1"); + if (exportedStream != null) { + assertEquals(templateStream.getPressure(DexpiMetadata.DEFAULT_PRESSURE_UNIT), + exportedStream.getPressure(DexpiMetadata.DEFAULT_PRESSURE_UNIT), 1e-9, + "Pressure metadata should survive round-trip"); + assertEquals(templateStream.getTemperature(DexpiMetadata.DEFAULT_TEMPERATURE_UNIT), + exportedStream.getTemperature(DexpiMetadata.DEFAULT_TEMPERATURE_UNIT), 1e-9, + "Temperature metadata should survive round-trip"); + assertEquals(templateStream.getFlowRate(DexpiMetadata.DEFAULT_FLOW_UNIT), + exportedStream.getFlowRate(DexpiMetadata.DEFAULT_FLOW_UNIT), 1e-9, + "Flow metadata should survive round-trip"); + } + } } } @@ -131,4 +182,27 @@ private Document parseExport(Path exportPath) throws Exception { return builder.parse(exportStream); } } + + private Element findGenericAttributes(Element element) { + NodeList nodes = element.getElementsByTagName("GenericAttributes"); + if (nodes.getLength() == 0) { + return null; + } + return (Element) nodes.item(0); + } + + private boolean hasGenericAttribute(Element parent, String name) { + return getGenericAttribute(parent, name) != null; + } + + private Element getGenericAttribute(Element parent, String name) { + NodeList attributes = parent.getElementsByTagName("GenericAttribute"); + for (int i = 0; i < attributes.getLength(); i++) { + Element attribute = (Element) attributes.item(i); + if (name.equals(attribute.getAttribute("Name"))) { + return attribute; + } + } + return null; + } } diff --git a/src/test/java/neqsim/process/processmodel/ProcessSystemReplaceObjectTest.java b/src/test/java/neqsim/process/processmodel/ProcessSystemReplaceObjectTest.java new file mode 100644 index 0000000000..5ba70b06b5 --- /dev/null +++ b/src/test/java/neqsim/process/processmodel/ProcessSystemReplaceObjectTest.java @@ -0,0 +1,44 @@ +package neqsim.process.processmodel; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import neqsim.process.equipment.heatexchanger.Cooler; +import neqsim.process.equipment.pump.Pump; +import neqsim.process.equipment.separator.Separator; + +/** + * Tests for {@link ProcessSystem#replaceObject(String, neqsim.process.equipment.ProcessEquipmentBaseClass)}. + */ +public class ProcessSystemReplaceObjectTest extends neqsim.NeqSimTest { + + @Test + public void replaceExistingUnitKeepsPositionAndName() { + ProcessSystem system = new ProcessSystem(); + system.add(new Separator("first")); + system.add(new Cooler("second")); + + Pump replacement = new Pump("replacement"); + + system.replaceObject("second", replacement); + + assertSame(replacement, system.getUnitOperations().get(1)); + assertEquals("second", replacement.getName(), "Replacement should adopt the original unit name"); + assertSame(replacement, system.getUnit("second")); + assertNotNull(system.getUnit("first")); + } + + @Test + public void replaceNonExistingUnitThrows() { + ProcessSystem system = new ProcessSystem(); + system.add(new Separator("first")); + + Pump replacement = new Pump("replacement"); + + assertThrows(IllegalArgumentException.class, () -> system.replaceObject("unknown", replacement)); + } +}