diff --git a/docs/development/TASK_LOG.md b/docs/development/TASK_LOG.md index f1934014642..b59d31253a5 100644 --- a/docs/development/TASK_LOG.md +++ b/docs/development/TASK_LOG.md @@ -36,6 +36,12 @@ requirement`, or `confidential compressor route`. +### 2026-10-14 — Coordinate shared-slate characterization with mixing-rule isolation +**Type:** A (Property) +**Keywords:** shared reference slate, quantile projection, shared mixing handler, accepted-point replay, 4330, 4333 +**Solution:** Stack PR #4330 on the shared classic mixing-rule accumulator foundation while preserving its characterization implementation, regression tests and mathematical documentation byte-for-byte. Regenerate engineering coverage for the combined source tree. +**Notes:** The unchanged field-to-facility acceptance class passed three fresh-JVM runs on both the original head and the foundation overlay (six tests per variant, none failed or skipped). A read-only fixture probe confirmed the parallel HP/LP branches share phase-specific mixing handlers. Java 8 dependencies and release-8 bytecode were tested on Linux Java 17. This validates the shared dependency; it does not establish the cause of the earlier Windows Java 8 accepted-point replay failure, which remains a hosted CI gate. No solver tolerance or execution setting changed. + ### 2026-10-14 — Isolate shared classic mixing-rule accumulators **Type:** A (Property) **Keywords:** classic mixing rule, concurrency, cloned fluids, heat capacity, compressor pressure replay, SRK, PR, 4326, 4306 diff --git a/docs/pvtsimulation/fluid_characterization_mathematics.md b/docs/pvtsimulation/fluid_characterization_mathematics.md index 648c9ceab62..87b884e2cda 100644 --- a/docs/pvtsimulation/fluid_characterization_mathematics.md +++ b/docs/pvtsimulation/fluid_characterization_mathematics.md @@ -966,7 +966,7 @@ SystemInterface matched = PseudoComponentCombiner.characterizeToReference( | `inheritReferenceProperties` | Inherit the reference lump properties (molar mass, density, critical constants). When `false`, lump properties are recomputed from the source mass on the reference cut grid | `true` | | `delumpBeforeRecharacterization` | Split each coarse source lump into finer single-carbon-number sub-fractions before re-binning onto the reference cuts (Pedersen Ch. 5 delumping). Conserves source moles and mass exactly | `false` | | `delumpResolution` | Number of sub-fractions per source lump when delumping (also the fine grid for equal-mass reference boundaries). Values ≤ 1 disable splitting | `12` | -| `sharedImaginaryBoundaries` | Place reference cut boundaries as carbon-number **equal-mass** cut points on the reference's imaginary (delumped) composition (Pedersen Ch. 5.6, Eqs. 5.58–5.59) instead of boiling-point midpoints | `false` | +| `sharedImaginaryBoundaries` | Use discrete mass-quantile candidates from the reference's imaginary (delumped) composition, projected into adjacent reference gaps; final cut masses need not be equal | `false` | #### Property inheritance vs. recomputation (`inheritReferenceProperties`) @@ -978,7 +978,11 @@ When a field's native lumps already sit close to the reference grid, the naive s #### Equal-mass reference boundaries (`sharedImaginaryBoundaries`) -By default the reference cut boundaries are the arithmetic boiling-point midpoints between adjacent reference lumps, which ignores how much mass each lump represents. With `sharedImaginaryBoundaries(true)` the reference is first rebuilt into a fine single-carbon-number "imaginary" composition (using `delumpResolution`) and the cut points are placed so each cut carries an **equal mass fraction** — the reference-only (NFLUID = 1) form of the Pedersen Ch. 5.6 common-slate rule (Eqs. 5.58–5.59 with §5.3 cut criterion). Each equal-mass cut is then *clamped* into the gap between the two adjacent reference pseudo-components, guaranteeing every reference lump stays inside its own cut so the one-to-one property inheritance is preserved even when the reference lumps carry unequal mass. With `delumpResolution ≤ 1` it falls back to boiling-point midpoints. +By default the reference cut boundaries are the arithmetic boiling-point midpoints between adjacent reference lumps. With `sharedImaginaryBoundaries(true)`, the reference is first rebuilt into a fine imaginary composition using `delumpResolution`. The discrete cumulative-mass quantiles provide candidate boundaries on the sorting-key axis (normal boiling point, falling back to molar mass). + +Each finite candidate is projected into the gap between the corresponding adjacent reference keys: a candidate below the gap moves to the next representable value above its lower key, while a candidate above the gap moves to the next representable value below its upper key. A candidate already inside the gap is retained. This keeps each distinct reference lump in its own cut when a representable interior exists, without discarding an out-of-gap candidate in favor of the midpoint. Missing or non-finite candidates, equal/non-finite keys, gaps with no representable interior, and `delumpResolution ≤ 1` retain the legacy midpoint fallback. Equal keys cannot define separate ordered cuts. + +These are **constrained mass-quantile boundaries**, not a guarantee of equal final cut masses: both projection and the finite imaginary grid can prevent exact equality. Source lumps are assigned whole unless source delumping is enabled, so changing a boundary only changes the result when a source contribution crosses it. With `inheritReferenceProperties(false)`, redistribution preserves source moles and mass; inheriting reference molar masses can change source mass. The synthetic delumping distribution is an approximation and is not independently validated by the cut-placement regression tests. > **Why reference-only here?** `characterizeToReference` inherits the trusted reference's lump properties one-to-one, so the reference fluid is the correct authority for the grid. The pooled multi-fluid imaginary composition of Eqs. 5.58–5.59 remains used by `characterizeToCommonSlate` (free new slate); applying it to the inherit-from-fixed-reference path would break the inherit alignment. @@ -1051,7 +1055,7 @@ System.out.println(report.toReportString()); When matching a source fluid to a reference PC structure: 1. **Component Mapping**: Discrete components (C1, C2, CO₂, etc.) are mapped directly -2. **Cut boundaries**: The reference lumps define cut boundaries on the carbon-number (boiling-point) axis — boiling-point midpoints by default, or carbon-number **equal-mass** cut points with `sharedImaginaryBoundaries(true)` +2. **Cut boundaries**: The reference lumps define cut boundaries on the carbon-number (boiling-point) axis — boiling-point midpoints by default, or constrained mass-quantile cut points with `sharedImaginaryBoundaries(true)` 3. **PC binning**: Each source pseudo-component (optionally first delumped into single-carbon-number sub-fractions, see `delumpBeforeRecharacterization`) is assigned to the reference cut whose boundary range contains its boiling point; the lump moles and mass are accumulated per cut 4. **Lump properties**: Either inherited from the reference (`inheritReferenceProperties(true)`) or recomputed as molar mass = mass / moles per cut (`inheritReferenceProperties(false)`) 5. **Mass Conservation**: Total source pseudo-fraction moles and mass are preserved through the binning diff --git a/docs/thermo/characterization/fluid_characterization_combining.md b/docs/thermo/characterization/fluid_characterization_combining.md index c94792f128b..aa6f1ef1f70 100644 --- a/docs/thermo/characterization/fluid_characterization_combining.md +++ b/docs/thermo/characterization/fluid_characterization_combining.md @@ -350,6 +350,8 @@ Given a reference fluid with $N$ pseudo-components having boiling points $T_{b,1 **Step 1**: Calculate boundary boiling points: $$T_{boundary,k} = \frac{T_{b,k} + T_{b,k+1}}{2}, \quad k = 1, ..., N-1$$ +The optional `sharedImaginaryBoundaries` setting instead uses discrete mass-quantile candidates from a delumped reference, projected strictly inside each adjacent reference gap when possible. Projection preserves reference ordering but does not guarantee equal final cut masses. See [constrained mass-quantile reference boundaries](../../pvtsimulation/fluid_characterization_mathematics.md#equal-mass-reference-boundaries-sharedimaginaryboundaries) for the fallback cases and conservation limits. + **Step 2**: Assign source pseudo-components to bins: - Bin 1: all components with $T_b \leq T_{boundary,1}$ - Bin $k$: all components with $T_{boundary,k-1} < T_b \leq T_{boundary,k}$ diff --git a/src/main/java/neqsim/thermo/characterization/CharacterizationOptions.java b/src/main/java/neqsim/thermo/characterization/CharacterizationOptions.java index 807e98eb8ba..11b3d522a42 100644 --- a/src/main/java/neqsim/thermo/characterization/CharacterizationOptions.java +++ b/src/main/java/neqsim/thermo/characterization/CharacterizationOptions.java @@ -157,20 +157,18 @@ public int getDelumpResolution() { } /** - * Whether the reference cut boundaries should be placed as carbon-number-based equal-mass cuts on the reference - * fluid's imaginary (fine-resolution) composition, instead of the simple boiling-point midpoints between adjacent - * reference pseudo-components. + * Whether the reference cut boundaries should use constrained mass quantiles of the reference fluid's imaginary + * composition instead of boiling-point midpoints. * *
- * Disabled by default (boiling-point midpoints are used). When {@code true}, the reference is the single - * representative composition (NFLUID = 1) of the Pedersen et al. (Chapter 5.6) common-slate scheme: each reference - * pseudo-component is delumped into {@link #getDelumpResolution()} single-carbon-number sub-fractions to rebuild the - * imaginary molar composition (Eqs. 5.58-5.59), and the cut points are then placed so each cut carries an equal mass - * fraction (Section 5.3). Each boundary is clamped to lie strictly between the two adjacent reference - * pseudo-component sorting keys, so the strict one-to-one property-inheritance mapping is preserved even when the - * reference lumps are not equal in mass. + * Disabled by default. When enabled, each reference pseudo-component is delumped into {@link #getDelumpResolution()} + * synthetic sub-fractions. Discrete mass quantiles provide the candidate cut points. A finite candidate outside its + * reference gap is projected to the nearest representable value strictly inside that gap, preserving the one-to-one + * property-inheritance mapping for distinct keys with a representable interior. These constraints and the discrete + * sub-fractions mean the final cuts need not contain equal masses. Missing/non-finite candidates, degenerate gaps and + * resolutions of 1 or less retain the boiling-point midpoint fallback. * - * @return true if equal-mass cut points on the reference imaginary composition should be used + * @return true if constrained mass-quantile reference boundaries should be used */ public boolean isSharedImaginaryBoundaries() { return sharedImaginaryBoundaries; @@ -336,18 +334,15 @@ public Builder delumpResolution(int resolution) { } /** - * Set whether the reference cut boundaries should be placed as carbon-number-based equal-mass cuts on the reference - * fluid's imaginary (fine-resolution) composition, instead of the simple boiling-point midpoints between adjacent - * reference pseudo-components. + * Set whether reference cut boundaries use constrained mass quantiles instead of boiling-point midpoints. * *
- * Disabled by default. Enable to follow the Pedersen et al. (Chapter 5.6) common-slate cut-point rule (Eqs.
- * 5.58-5.59 with NFLUID = 1, Section 5.3 equal-mass lumping) for the reference-only path. The
- * {@link #delumpResolution(int) delump resolution} controls how finely each reference lump is split when rebuilding
- * the imaginary composition. Boundaries are clamped between adjacent reference pseudo-component sorting keys so the
- * property-inheritance mapping stays one-to-one.
+ * Disabled by default. The {@link #delumpResolution(int) delump resolution} controls the reference grid used to
+ * rebuild the imaginary composition. Finite candidates are projected into the representable interior of each
+ * reference gap; clamping may prevent equal final cut masses. See
+ * {@link CharacterizationOptions#isSharedImaginaryBoundaries()} for the ordering constraint and fallback cases.
*
- * @param shared true to use equal-mass cut points on the reference imaginary composition
+ * @param shared true to use constrained mass-quantile reference boundaries
* @return this builder
*/
public Builder sharedImaginaryBoundaries(boolean shared) {
diff --git a/src/main/java/neqsim/thermo/characterization/PseudoComponentCombiner.java b/src/main/java/neqsim/thermo/characterization/PseudoComponentCombiner.java
index ee8b37237c2..580ea0e5036 100644
--- a/src/main/java/neqsim/thermo/characterization/PseudoComponentCombiner.java
+++ b/src/main/java/neqsim/thermo/characterization/PseudoComponentCombiner.java
@@ -182,8 +182,8 @@ public static SystemInterface characterizeToReference(SystemInterface source, Sy
* reference cuts
* @param delumpResolution number of sub-fractions per source lump when {@code delump} is {@code true}; values of 1 or
* less disable splitting
- * @param sharedImaginaryBoundaries whether the reference cut boundaries are placed as equal-mass cuts on the
- * reference's imaginary (delumped) composition instead of boiling-point midpoints
+ * @param sharedImaginaryBoundaries whether the reference cut boundaries use mass quantiles from the reference's
+ * imaginary (delumped) composition projected into reference gaps instead of boiling-point midpoints
* @return characterized fluid containing pseudo components compatible with the reference fluid
*/
private static SystemInterface characterizeToReferenceCore(SystemInterface source, SystemInterface reference,
@@ -1056,29 +1056,23 @@ private static List
- * Each equal-mass cut is finally clamped to lie strictly between the sorting keys of the two adjacent reference
- * pseudo-components. This guarantees that every reference pseudo-component remains inside its own cut, preserving the
- * strict one-to-one ordering on which the property inheritance in {@link #characterizeToReferenceCore} relies, even
- * when the reference lumps are not equal in mass (in which case an unclamped equal-mass cut could fall across a lump
- * and mis-bin the source). When clamping cannot recover a valid in-gap value the boiling-point midpoint is used as a
- * fallback.
+ * Each finite candidate is projected to the nearest representable value strictly between its adjacent reference keys.
+ * This preserves one-to-one ordering for distinct reference keys with a representable interior. Projection and the
+ * discrete grid mean that final cuts need not contain equal masses. Missing/non-finite candidates and gaps with no
+ * representable interior retain the legacy midpoint fallback; equal keys cannot define separate ordered cuts.
*
* @param referenceContributions the reference pseudo-components, sorted ascending by sorting key
- * @param resolution number of single-carbon-number sub-fractions per reference lump used to rebuild the imaginary
- * composition; values of 1 or less fall back to boiling-point midpoints
- * @return the clamped equal-mass cut boundaries (size {@code referenceContributions.size() - 1})
+ * @param resolution number of synthetic sub-fractions per reference lump used to rebuild the imaginary composition;
+ * values of 1 or less fall back to boiling-point midpoints
+ * @return the projected mass-quantile cut boundaries (size {@code referenceContributions.size() - 1})
*/
private static List
- * When the flag is on, the reference cut boundaries are placed as carbon-number-based equal-mass cut points on the
- * reference fluid's imaginary (delumped) composition (Pedersen et al., Chapter 5.6, Eqs. 5.58-5.59, with the reference
- * as the sole weighted fluid), instead of arithmetic boiling-point midpoints. Each equal-mass cut is clamped into the
- * gap between the two adjacent reference pseudo-components so the strict one-to-one property-inheritance ordering is
- * preserved even for a reference whose lumps carry unequal mass.
+ * The imaginary composition supplies mass-quantile candidates that are constrained to the gaps between reference
+ * sorting keys. Tests verify observable redistribution, boundary inclusion, conservation and legacy fallbacks.
+ *
+ * @author ESOL
+ * @version 1.0
*/
class CharacterizeToReferenceSharedSlateTest {
- /** Reference with equal moles but increasing molar mass, so the per-cut mass is unequal. */
+ /**
+ * Reference with equal moles but increasing molar mass, so the per-cut mass is unequal.
+ *
+ * @param cuts number of reference cuts, from one to six
+ * @return reference fluid
+ */
private static SystemInterface referenceFluid(int cuts) {
SystemInterface fluid = new SystemPrEos(298.15, 60.0);
fluid.addComponent("methane", 0.6);
@@ -34,7 +39,11 @@ private static SystemInterface referenceFluid(int cuts) {
return fluid;
}
- /** A graded source spanning the same carbon-number range as the reference. */
+ /**
+ * A graded source spanning the same carbon-number range as the reference.
+ *
+ * @return source fluid
+ */
private static SystemInterface gradedSource() {
SystemInterface fluid = new SystemPrEos(298.15, 60.0);
fluid.addComponent("methane", 0.5);
@@ -44,6 +53,12 @@ private static SystemInterface gradedSource() {
return fluid;
}
+ /**
+ * Count pseudo-component moles.
+ *
+ * @param fluid fluid to inspect
+ * @return total pseudo-component moles
+ */
private static double pseudoMoles(SystemInterface fluid) {
double moles = 0.0;
for (int i = 0; i < fluid.getNumberOfComponents(); i++) {
@@ -55,6 +70,12 @@ private static double pseudoMoles(SystemInterface fluid) {
return moles;
}
+ /**
+ * Calculate pseudo-component mass.
+ *
+ * @param fluid fluid to inspect
+ * @return total pseudo-component mass in kg
+ */
private static double pseudoMass(SystemInterface fluid) {
double mass = 0.0;
for (int i = 0; i < fluid.getNumberOfComponents(); i++) {
@@ -66,6 +87,12 @@ private static double pseudoMass(SystemInterface fluid) {
return mass;
}
+ /**
+ * Count populated pseudo-components.
+ *
+ * @param fluid fluid to inspect
+ * @return number of populated pseudo-components
+ */
private static int validPseudoComponents(SystemInterface fluid) {
int count = 0;
for (int i = 0; i < fluid.getNumberOfComponents(); i++) {
@@ -77,6 +104,7 @@ private static int validPseudoComponents(SystemInterface fluid) {
return count;
}
+ /** Check that shared boundaries remain opt-in. */
@Test
@DisplayName("sharedImaginaryBoundaries defaults off")
void testDefaultOff() {
@@ -84,6 +112,7 @@ void testDefaultOff() {
assertTrue(!options.isSharedImaginaryBoundaries(), "sharedImaginaryBoundaries should default to false");
}
+ /** Compare the complete flag-off result with the no-options overload. */
@Test
@DisplayName("flag off reproduces the no-options reference characterization exactly")
void testBackwardCompatible() {
@@ -96,6 +125,7 @@ void testBackwardCompatible() {
.sharedImaginaryBoundaries(false).build();
SystemInterface flagged = PseudoComponentCombiner.characterizeToReference(source, reference, options);
+ assertSameComposition(base, flagged);
assertEquals(validPseudoComponents(base), validPseudoComponents(flagged),
"flag off must not change the number of populated cuts");
assertEquals(pseudoMoles(base), pseudoMoles(flagged), pseudoMoles(base) * 1e-9,
@@ -104,6 +134,7 @@ void testBackwardCompatible() {
"flag off must not change the total pseudo mass");
}
+ /** Verify inventory conservation with source-derived properties. */
@Test
@DisplayName("equal-mass boundaries conserve total pseudo-fraction moles and mass")
void testMassConservation() {
@@ -121,6 +152,7 @@ void testMassConservation() {
assertEquals(srcMass, pseudoMass(result), srcMass * 1e-6, "total pseudo mass conserved");
}
+ /** Preserve each reference lump and its inherited properties. */
@Test
@DisplayName("equal-mass boundaries preserve the one-to-one inherit mapping for an identical source")
void testPositionalIdentityPreserved() {
@@ -132,6 +164,7 @@ void testPositionalIdentityPreserved() {
.sharedImaginaryBoundaries(true).build();
SystemInterface result = PseudoComponentCombiner.characterizeToReference(source, reference, options);
+ assertSameComposition(reference, result);
assertEquals(6, validPseudoComponents(result),
"every reference cut must remain populated (no cut collapses or doubles up)");
@@ -149,6 +182,7 @@ void testPositionalIdentityPreserved() {
}
}
+ /** Retain midpoint behavior when the imaginary grid is disabled. */
@Test
@DisplayName("resolution <= 1 falls back to boiling-point midpoints (no equal-mass shift)")
void testResolutionFallback() {
@@ -163,5 +197,152 @@ void testResolutionFallback() {
assertEquals(pseudoMoles(base), pseudoMoles(fallback), pseudoMoles(base) * 1e-9,
"resolution<=1 must reproduce the midpoint boundaries");
+ assertSameComposition(base, fallback);
+ }
+
+ /** Sparse whole source lumps may stay in the same bins even after reference boundaries move. */
+ @Test
+ void testSparseSourceCanRetainItsBins() {
+ SystemInterface source = gradedSource();
+ SystemInterface reference = referenceFluid(6);
+ CharacterizationOptions options = CharacterizationOptions.builder().inheritReferenceProperties(false)
+ .sharedImaginaryBoundaries(true).build();
+ SystemInterface result = PseudoComponentCombiner.characterizeToReference(source, reference, options);
+ SystemInterface midpoint = PseudoComponentCombiner.characterizeToReference(source, reference);
+
+ assertSameComposition(midpoint, result);
+ assertEquals(pseudoMoles(source), pseudoMoles(result), 1e-12);
+ assertEquals(pseudoMass(source), pseudoMass(result), 1e-12);
+ }
+
+ /** Quantiles above both gaps must project to just below the upper reference keys. */
+ @Test
+ void testHeavyReferenceProjectsToUpperGapEdges() {
+ SystemInterface reference = controlledReference(1.0, 1.0, 100.0);
+ assertProbeCut(reference, 450.0, "R1_PC");
+ assertProbeCut(reference, Math.nextDown(500.0), "R1_PC");
+ assertProbeCut(reference, 500.0, "R2_PC");
+ assertProbeCut(reference, 650.0, "R2_PC");
+ assertProbeCut(reference, Math.nextDown(700.0), "R2_PC");
+ assertProbeCut(reference, 700.0, "R3_PC");
+ }
+
+ /** Quantiles below both gaps must project to just above the lower reference keys. */
+ @Test
+ void testLightReferenceProjectsToLowerGapEdges() {
+ SystemInterface reference = controlledReference(100.0, 1.0, 1.0);
+ assertProbeCut(reference, 300.0, "R1_PC");
+ assertProbeCut(reference, Math.nextUp(300.0), "R1_PC");
+ assertProbeCut(reference, 350.0, "R2_PC");
+ assertProbeCut(reference, 500.0, "R2_PC");
+ assertProbeCut(reference, Math.nextUp(500.0), "R2_PC");
+ assertProbeCut(reference, 550.0, "R3_PC");
+ }
+
+ /** Valid interior quantiles retain their positions rather than moving to gap edges. */
+ @Test
+ void testInteriorQuantilesAreRetained() {
+ SystemInterface reference = controlledReference(1.0, 1.0, 1.0);
+ // The discrete 12-point mass quantiles are 399.7979157 K and 625.3249451 K.
+ assertProbeCut(reference, 399.7, "R1_PC");
+ assertProbeCut(reference, 399.9, "R2_PC");
+ assertProbeCut(reference, 625.2, "R2_PC");
+ assertProbeCut(reference, 625.4, "R3_PC");
+ }
+
+ /** Shared reference cuts also conserve inventory when source delumping is enabled. */
+ @Test
+ void testConservationWithSourceDelumping() {
+ SystemInterface source = gradedSource();
+ CharacterizationOptions options = CharacterizationOptions.builder().inheritReferenceProperties(false)
+ .sharedImaginaryBoundaries(true).delumpBeforeRecharacterization(true).build();
+ SystemInterface result = PseudoComponentCombiner.characterizeToReference(source, referenceFluid(6), options);
+ assertEquals(pseudoMoles(source), pseudoMoles(result), 1e-12);
+ assertEquals(pseudoMass(source), pseudoMass(result), 1e-12);
+ assertEquals(0.5, result.getComponent("methane").getNumberOfmoles(), 1e-12);
+ CharacterizationOptions midpointOptions = CharacterizationOptions.builder().inheritReferenceProperties(false)
+ .delumpBeforeRecharacterization(true).build();
+ SystemInterface midpoint = PseudoComponentCombiner.characterizeToReference(source, referenceFluid(6),
+ midpointOptions);
+ double largestMoleChange = 0.0;
+ for (int i = 0; i < result.getNumberOfComponents(); i++) {
+ ComponentInterface component = result.getComponent(i);
+ ComponentInterface previous = midpoint.getComponent(component.getComponentName());
+ double previousMoles = previous == null ? 0.0 : previous.getNumberOfmoles();
+ largestMoleChange = Math.max(largestMoleChange, Math.abs(component.getNumberOfmoles() - previousMoles));
+ }
+ assertTrue(largestMoleChange > 1e-3, "the fine source grid must respond to the shifted reference boundaries");
+ }
+
+ /** Equal keys and gaps with no interior double retain the legacy fallback. */
+ @Test
+ void testDegenerateGapsRetainMidpointFallback() {
+ for (double upper : new double[] {300.0, Math.nextUp(300.0)}) {
+ SystemInterface reference = controlledReference(1.0, 1.0, 1.0);
+ reference.getComponent("R2_PC").setNormalBoilingPoint(upper);
+ reference.getComponent("R3_PC").setNormalBoilingPoint(upper);
+ CharacterizationOptions options = CharacterizationOptions.builder().inheritReferenceProperties(false)
+ .sharedImaginaryBoundaries(true).build();
+ SystemInterface midpoint = PseudoComponentCombiner.characterizeToReference(reference, reference);
+ SystemInterface shared = PseudoComponentCombiner.characterizeToReference(reference, reference, options);
+ assertSameComposition(midpoint, shared);
+ }
+ }
+
+ /**
+ * Build a synthetic reference with explicit boiling points and controlled mass distribution.
+ *
+ * @param light light reference moles
+ * @param middle middle reference moles
+ * @param heavy heavy reference moles
+ * @return three-cut reference at 300, 500 and 700 K
+ */
+ private static SystemInterface controlledReference(double light, double middle, double heavy) {
+ SystemInterface fluid = new SystemPrEos(298.15, 60.0);
+ double[] moles = {light, middle, heavy};
+ for (int i = 0; i < moles.length; i++) {
+ String name = "R" + (i + 1);
+ fluid.addTBPfraction(name, moles[i], 0.1, 0.75);
+ fluid.getComponent(name + "_PC").setNormalBoilingPoint(300.0 + 200.0 * i);
+ }
+ return fluid;
+ }
+
+ /**
+ * Locate a cut through the public API, including the inclusive boundary convention.
+ *
+ * @param reference reference defining the cut grid
+ * @param boilingPoint source sorting key in K
+ * @param expectedName expected populated reference cut
+ */
+ private static void assertProbeCut(SystemInterface reference, double boilingPoint, String expectedName) {
+ SystemInterface source = new SystemPrEos(298.15, 60.0);
+ source.addTBPfraction("probe", 1.0, 0.1, 0.75);
+ source.getComponent("probe_PC").setNormalBoilingPoint(boilingPoint);
+ CharacterizationOptions options = CharacterizationOptions.builder().inheritReferenceProperties(false)
+ .sharedImaginaryBoundaries(true).build();
+ SystemInterface result = PseudoComponentCombiner.characterizeToReference(source, reference, options);
+ assertEquals(1, validPseudoComponents(result));
+ assertEquals(expectedName, result.getComponent(0).getComponentName(), "source key " + boilingPoint);
+ assertEquals(1.0, pseudoMoles(result), 1e-12);
+ assertEquals(0.1, pseudoMass(result), 1e-12);
+ }
+
+ /**
+ * Compare every populated component so a changed cut cannot hide behind conserved totals.
+ *
+ * @param expected expected fluid
+ * @param actual fluid to check
+ */
+ private static void assertSameComposition(SystemInterface expected, SystemInterface actual) {
+ assertEquals(expected.getNumberOfComponents(), actual.getNumberOfComponents());
+ for (int i = 0; i < expected.getNumberOfComponents(); i++) {
+ ComponentInterface a = expected.getComponent(i);
+ ComponentInterface b = actual.getComponent(i);
+ assertEquals(a.getComponentName(), b.getComponentName());
+ assertEquals(a.getNumberOfmoles(), b.getNumberOfmoles(), 1e-12);
+ assertEquals(a.getMolarMass(), b.getMolarMass(), 1e-12);
+ assertEquals(a.getNormalLiquidDensity(), b.getNormalLiquidDensity(), 1e-12);
+ }
}
}