Skip to content

[AI-Assisted] Add uncertainty-aware thermodynamic benchmark framework - #2632

Draft
EvenSol wants to merge 28 commits into
masterfrom
feature/thermodynamic-benchmark-h2-co2
Draft

[AI-Assisted] Add uncertainty-aware thermodynamic benchmark framework#2632
EvenSol wants to merge 28 commits into
masterfrom
feature/thermodynamic-benchmark-h2-co2

Conversation

@EvenSol

@EvenSol EvenSol commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a reusable, uncertainty-aware experimental-data benchmark framework, a bounded constant-kij fitter, and the first published H2-CO2/CO2-H2-N2 phase-equilibrium dataset.

Capability

  • immutable experimental points with validated state, mole-fraction composition, units, uncertainty, and provenance
  • aggregate AARD, signed bias, RMS relative error, and maximum error
  • uncertainty-normalized residuals when source uncertainty exists
  • packaged machine-readable data from Tables IV-V of Zhang et al. (2026), DOI 10.1063/5.0288386
  • NeqSim prediction adapter for SRK, PR, GERG-2008, and GERG-2008-H2
  • optional constant H2-CO2 kij override for cubic models
  • bounded golden-section scalar regression minimizing mean squared relative pressure error
  • documentation and reference-manual index entry

Dataset and fit

  • binary calibration: 96 mol% CO2 + 4 mol% H2, 12 bubble/dew points
  • ternary holdout: 96 mol% CO2 + 2 mol% H2 + 2 mol% N2, 12 bubble/dew points
  • -30 to 20 degC
  • pointwise source uncertainty is unavailable and remains explicitly NaN
  • CO2-N2 and H2-N2 parameters remain at database values
Model fitted H2-CO2 kij binary RMSRE binary AARD ternary holdout AARD database-kij binary AARD
SRK -0.47728 140.68% 115.64% 89.06% 167.69%
PR -0.37523 120.26% 92.87% 88.03% 142.00%

The fit used bounds [-0.5, 0.2], tolerance 1e-4, and at most 20 objective evaluations. It reduces the selected objective but remains technically unacceptable as a general model. The fitted constants are documented for reproducibility and are deliberately not installed as database defaults.

Validation

  • unit coverage for transcription, provenance, composition, metrics, uncertainty residuals, physical input validation, bounded regression, and model/kij configuration
  • unknown CSV property tokens fail fast
  • documentation API is exercised by the test suite
  • authoritative focused calibration executed on Java 21 in GitHub Actions
  • PaperLab Replication Gate: passed
  • Spotless: passed
  • Pre-commit checks: passed
  • full build/test/Javadoc, CodeQL, and documentation coverage rerunning on the final head

Scope boundary

PRSV is not included because NeqSim currently exposes no verified PRSV system implementation. The poor constant-kij holdout results indicate that point-level residuals, flash formulation, temperature dependence, and independent binary data should be investigated before any interaction-parameter database change.

Model predictions are calculated independently for every experimental state. Experimental pressure is only the numerical initial pressure and is never substituted as a prediction.

Copilot AI review requested due to automatic review settings July 27, 2026 06:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a reusable thermodynamic benchmarking utility to compare NeqSim model predictions against published experimental data, including a first packaged H2–CO2 / H2–N2–CO2 phase-equilibrium dataset and accompanying documentation.

Changes:

  • Introduces ThermodynamicBenchmark infrastructure (immutable points/datasets, per-point rows, and aggregate error metrics including uncertainty-normalized residuals).
  • Adds a published Zhang et al. (2026) dataset as a packaged CSV resource + loader, and a NeqSim phase-equilibrium prediction adapter (SRK/PR/GERG-2008/GERG-2008-H2).
  • Adds JUnit tests and reference-manual documentation/index entry for the new benchmark framework.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/main/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmark.java Core benchmark types (Point/Dataset/Row/Report) and aggregate metric calculation.
src/main/java/neqsim/thermo/util/benchmark/NeqSimPhaseEquilibriumPrediction.java Adapter that computes bubble/dew-point pressures using selected NeqSim EOS configurations.
src/main/java/neqsim/thermo/util/benchmark/H2CO2PhaseEquilibriumData.java Loader for the packaged Zhang 2026 H2–CO2 / H2–N2–CO2 dataset resource.
src/main/resources/data/thermo/benchmark/zhang2026_h2_co2_phase_equilibrium.csv Machine-readable transcription of Tables IV–V experimental bubble/dew pressures.
src/test/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmarkTest.java Unit tests for dataset transcription/provenance, metrics, uncertainty residuals, and adapter configuration.
docs/thermo/thermodynamic_benchmarks.md Documentation for benchmark framework + dataset scope + Java usage example.
docs/REFERENCE_MANUAL_INDEX.md Adds reference-manual index entry for the new benchmarks page.
Comments suppressed due to low confidence (1)

src/test/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmarkTest.java:107

  • NeqSimPhaseEquilibriumPrediction's main behavior (predict) isn't exercised by the test suite yet (only the getter is). A small smoke test on one published point would catch API/signature regressions and basic model setup issues without asserting a specific numeric value.

Comment on lines +68 to +71
if (!Double.isFinite(temperatureK) || temperatureK <= 0.0
|| !Double.isFinite(experimentalValue)) {
throw new IllegalArgumentException("Temperature and experimental value must be finite");
}
Comment on lines +148 to +151
if (name == null || name.trim().isEmpty() || citation == null || doi == null
|| license == null || points == null || points.isEmpty()) {
throw new IllegalArgumentException("Dataset metadata and points are required");
}
Comment on lines +70 to +73
Property property =
"bubble".equals(values[2])
? Property.BUBBLE_POINT_PRESSURE
: Property.DEW_POINT_PRESSURE;
Comment on lines +45 to +52
```java
ThermodynamicBenchmark.Dataset dataset = H2CO2PhaseEquilibriumData.load();
NeqSimPhaseEquilibriumPrediction prediction =
new NeqSimPhaseEquilibriumPrediction(
NeqSimPhaseEquilibriumPrediction.Model.GERG_2008_H2);

ThermodynamicBenchmark.Report report =
ThermodynamicBenchmark.run("GERG-2008-H2", dataset, prediction);
Copilot AI review requested due to automatic review settings July 27, 2026 07:00
if (values.length != 8) {
throw new IOException("Expected 8 CSV columns but found " + values.length);
}
double temperatureK = Double.parseDouble(values[1]) + 273.15;
throw new IOException("Expected 8 CSV columns but found " + values.length);
}
double temperatureK = Double.parseDouble(values[1]) + 273.15;
double pressureBara = Double.parseDouble(values[3]) * 10.0;
Map<String, Double> composition = new LinkedHashMap<String, Double>();
composition.put("CO2", Double.parseDouble(values[4]));
composition.put("hydrogen", Double.parseDouble(values[5]));
double nitrogenFraction = Double.parseDouble(values[6]);
double temperatureK = Double.parseDouble(values[1]) + 273.15;
double pressureBara = Double.parseDouble(values[3]) * 10.0;
Map<String, Double> composition = new LinkedHashMap<String, Double>();
composition.put("CO2", Double.parseDouble(values[4]));
double pressureBara = Double.parseDouble(values[3]) * 10.0;
Map<String, Double> composition = new LinkedHashMap<String, Double>();
composition.put("CO2", Double.parseDouble(values[4]));
composition.put("hydrogen", Double.parseDouble(values[5]));

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (4)

src/main/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmark.java:70

  • Point currently validates temperature and experimentalValue, but does not validate pressureBara (can be NaN/<=0) or standardUncertainty, and allows experimentalValue==0 even though Row computes relative errors by dividing by experimentalValue (→ Infinity/NaN). Consider enforcing finite positive pressure, non-zero experimentalValue, and non-negative (or NaN) uncertainty to keep reports well-defined.
      if (!Double.isFinite(temperatureK) || temperatureK <= 0.0 || !Double.isFinite(experimentalValue)) {
        throw new IllegalArgumentException("Temperature and experimental value must be finite");
      }
      double compositionSum = 0.0;

src/main/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmark.java:149

  • Dataset constructor rejects nulls and empty name, but allows blank citation/doi/license strings (e.g., ""). Since these fields are meant for provenance/auditability, it’s safer to enforce non-blank values consistently.
    public Dataset(String name, String citation, String doi, String license, List<Point> points) {
      if (name == null || name.trim().isEmpty() || citation == null || doi == null || license == null || points == null
          || points.isEmpty()) {
        throw new IllegalArgumentException("Dataset metadata and points are required");
      }

src/main/java/neqsim/thermo/util/benchmark/H2CO2PhaseEquilibriumData.java:68

  • CSV parsing currently treats any non-"bubble" token as dew-point, which can silently misclassify data if the file contains typos/whitespace/unexpected values. It’s safer to trim and validate explicitly, failing fast on unknown property values.
        Property property = "bubble".equals(values[2]) ? Property.BUBBLE_POINT_PRESSURE : Property.DEW_POINT_PRESSURE;
        points.add(new Point(property, temperatureK, pressureBara, pressureBara, Double.NaN, "bara", composition));

docs/thermo/thermodynamic_benchmarks.md:56

  • This documentation adds a Java code example, but it doesn’t appear to be exercised by the existing documentation-compilation tests. In this repo, Java doc examples are typically kept compiling/running via DocExamplesCompilationTest; without that, the snippet can silently rot.
```java
ThermodynamicBenchmark.Dataset dataset = H2CO2PhaseEquilibriumData.load();
NeqSimPhaseEquilibriumPrediction prediction =
    new NeqSimPhaseEquilibriumPrediction(
        NeqSimPhaseEquilibriumPrediction.Model.GERG_2008_H2);

ThermodynamicBenchmark.Report report =
    ThermodynamicBenchmark.run("GERG-2008-H2", dataset, prediction);

double aardPercent = report.getAverageAbsoluteRelativeDeviationPercent();
double biasPercent = report.getBiasPercent();
</details>

Comment on lines +66 to +72
double pressureBara = system.getPressure("bara");
if (!Double.isFinite(pressureBara) || pressureBara <= 0.0) {
throw new IllegalStateException(
model + " returned invalid " + point.getProperty() + " at " + point.getTemperatureK() + " K");
}
return pressureBara;
}
Dataset dataset = new Dataset("uncertainty test", "test citation", "10.0000/test", "test data",
java.util.Collections.singletonList(point));

Report report = ThermodynamicBenchmark.run("test model", dataset, value -> 37.5);
Comment on lines +75 to +82
@Test
void exposesConfiguredNeqSimModel() {
NeqSimPhaseEquilibriumPrediction prediction = new NeqSimPhaseEquilibriumPrediction(
NeqSimPhaseEquilibriumPrediction.Model.GERG_2008_H2);

assertEquals(NeqSimPhaseEquilibriumPrediction.Model.GERG_2008_H2, prediction.getModel());
}
}
Copilot AI review requested due to automatic review settings July 27, 2026 07:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (3)

src/main/java/neqsim/thermo/util/benchmark/H2CO2PhaseEquilibriumData.java:68

  • CSV property parsing currently treats any value other than the exact string "bubble" as dew-point. That can silently misclassify data if the column contains unexpected values (e.g., whitespace, capitalization, typos). Parse both bubble/dew explicitly and throw an IOException for unknown values.
        Property property = "bubble".equals(values[2]) ? Property.BUBBLE_POINT_PRESSURE : Property.DEW_POINT_PRESSURE;
        points.add(new Point(property, temperatureK, pressureBara, pressureBara, Double.NaN, "bara", composition));

src/main/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmark.java:69

  • Point validation does not check pressure/uncertainty, and allows an experimental value of 0.0 even though the benchmark metrics (relative error) divide by the experimental value. This can lead to invalid states (NaN/Inf relative errors or invalid initial pressures). Validate pressure as finite/positive, require experimentalValue to be non-zero, and require standardUncertainty to be positive when provided.
      if (!Double.isFinite(temperatureK) || temperatureK <= 0.0 || !Double.isFinite(experimentalValue)) {
        throw new IllegalArgumentException("Temperature and experimental value must be finite");
      }

docs/thermo/thermodynamic_benchmarks.md:49

  • This page adds a runnable Java example, but there is no corresponding compilation/execution coverage in the documentation example test suite (DocExamplesCompilationTest). Without a test, the snippet can drift and break without CI catching it. Add a DocExamplesCompilationTest case that exercises the same API calls shown here.
```java
ThermodynamicBenchmark.Dataset dataset = H2CO2PhaseEquilibriumData.load();
NeqSimPhaseEquilibriumPrediction prediction =
    new NeqSimPhaseEquilibriumPrediction(
        NeqSimPhaseEquilibriumPrediction.Model.GERG_2008_H2);

Comment on lines +140 to +148
throw new AssertionError(
"CALIBRATION SRK kij="
+ srk.getBinaryInteractionParameter()
+ " RMSRE="
+ srk.getRootMeanSquareRelativeErrorPercent()
+ "; PR kij="
+ pr.getBinaryInteractionParameter()
+ " RMSRE="
+ pr.getRootMeanSquareRelativeErrorPercent());
Copilot AI review requested due to automatic review settings July 27, 2026 07:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

src/test/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmarkTest.java:114

  • This test will always fail because it unconditionally throws an AssertionError after running the calibration loop. That breaks CI and also makes the test suite non-deterministic/debug-only.
    throw new AssertionError("CALIBRATION SRK kij=" + srk.getBinaryInteractionParameter() + " RMSRE="
        + srk.getRootMeanSquareRelativeErrorPercent() + "; PR kij=" + pr.getBinaryInteractionParameter() + " RMSRE="
        + pr.getRootMeanSquareRelativeErrorPercent());

src/main/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmark.java:69

  • Point validation allows non-finite/invalid pressures and an experimental value of 0.0. Downstream calculations (relative error in Row and objective()) divide by experimentalValue, so a zero value would yield Infinity/NaN and corrupt aggregate metrics.
      if (!Double.isFinite(temperatureK) || temperatureK <= 0.0 || !Double.isFinite(experimentalValue)) {
        throw new IllegalArgumentException("Temperature and experimental value must be finite");
      }

src/main/java/neqsim/thermo/util/benchmark/H2CO2PhaseEquilibriumData.java:68

  • CSV parsing treats any property value other than the exact string "bubble" as a dew point. That can silently misclassify points if the file contains unexpected values (typos, capitalization) and hide transcription errors.
        Property property = "bubble".equals(values[2]) ? Property.BUBBLE_POINT_PRESSURE : Property.DEW_POINT_PRESSURE;
        points.add(new Point(property, temperatureK, pressureBara, pressureBara, Double.NaN, "bara", composition));

Copilot AI review requested due to automatic review settings July 27, 2026 07:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

src/main/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmark.java:69

  • Point accepts invalid physical state and uncertainty values (e.g., non-finite/negative pressure, negative uncertainty) and allows experimentalValue == 0, which will later cause a division-by-zero when computing relative error in Row. Since Point is the public immutable input type, validate pressureBara and standardUncertainty and reject a zero experimental value up front.
      if (property == null || unit == null || unit.trim().isEmpty() || composition == null || composition.isEmpty()) {
        throw new IllegalArgumentException("Property, unit, and composition are required");
      }
      if (!Double.isFinite(temperatureK) || temperatureK <= 0.0 || !Double.isFinite(experimentalValue)) {
        throw new IllegalArgumentException("Temperature and experimental value must be finite");
      }

src/main/java/neqsim/thermo/util/benchmark/H2CO2PhaseEquilibriumData.java:68

  • CSV parsing maps any non-"bubble" value to dew-point pressure, which can silently mask transcription errors (e.g., unexpected strings, casing, whitespace). Validate the property column explicitly and fail fast with an IOException when it is not "bubble" or "dew".
        Property property = "bubble".equals(values[2]) ? Property.BUBBLE_POINT_PRESSURE : Property.DEW_POINT_PRESSURE;
        points.add(new Point(property, temperatureK, pressureBara, pressureBara, Double.NaN, "bara", composition));

src/test/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmarkTest.java:137

  • This test unconditionally fails via throw new AssertionError(...), which will break mvn test and the new kij-calibration workflow. Replace the unconditional failure with logging (Log4j2) and at least minimal assertions that keep the test meaningful while allowing CI to pass.
        new NeqSimPhaseEquilibriumPrediction(NeqSimPhaseEquilibriumPrediction.Model.SRK));
    Report prDefault = ThermodynamicBenchmark.run("PR default", binary,
        new NeqSimPhaseEquilibriumPrediction(NeqSimPhaseEquilibriumPrediction.Model.PR));

    throw new AssertionError("CALIBRATION SRK kij=" + srk.getBinaryInteractionParameter() + " RMSRE="
        + srk.getRootMeanSquareRelativeErrorPercent() + " trainAARD="
        + srkTrain.getAverageAbsoluteRelativeDeviationPercent() + " validationAARD="

src/test/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmarkTest.java:84

  • The docs claim support for GERG-2008 / GERG-2008-H2 predictions, but the tests only assert the enum value (and that custom kij is rejected). Add a small smoke test that actually runs the GERG-2008-H2 adapter on at least one published point and asserts the prediction is finite, so documentation/API claims are exercised by CI.
  void exposesConfiguredNeqSimModel() {
    NeqSimPhaseEquilibriumPrediction prediction = new NeqSimPhaseEquilibriumPrediction(
        NeqSimPhaseEquilibriumPrediction.Model.GERG_2008_H2);

    assertEquals(NeqSimPhaseEquilibriumPrediction.Model.GERG_2008_H2, prediction.getModel());
  }

Copilot AI review requested due to automatic review settings July 27, 2026 07:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

src/main/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmark.java:69

  • Point validation currently allows non-finite/negative pressure, zero experimental values, and negative standard uncertainty. This can produce NaN/Infinity errors later (relative-error calculations divide by experimentalValue, and prediction adapters rely on a valid state). Consider rejecting invalid pressure, disallowing experimentalValue==0 when using relative metrics, and enforcing non-negative uncertainty when provided.
      if (!Double.isFinite(temperatureK) || temperatureK <= 0.0 || !Double.isFinite(pressureBara)
          || pressureBara <= 0.0 || !Double.isFinite(experimentalValue) || experimentalValue == 0.0) {
        throw new IllegalArgumentException("Temperature, pressure, and experimental value must be physical and finite");

src/main/java/neqsim/thermo/util/benchmark/H2CO2PhaseEquilibriumData.java:68

  • CSV parsing treats any non-"bubble" token as a dew point (including typos/whitespace), which can silently corrupt the dataset. It would be safer to trim + validate the token and fail fast on unexpected values.
        Property property;
        if ("bubble".equals(values[2])) {

src/test/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmarkTest.java:81

  • The documentation and API surface introduce NeqSimPhaseEquilibriumPrediction as a runnable adapter, but the tests only assert configuration (getModel/getKij) and never call predict(). Adding a single-point prediction test would catch regressions in flash setup/mixing-rule selection and ensure the adapter remains executable.
  @Test
  void rejectsInvalidStateValueAndUncertainty() {
    Map<String, Double> composition = new LinkedHashMap<String, Double>();
    composition.put("CO2", 0.96);
    composition.put("hydrogen", 0.04);

    assertThrows(IllegalArgumentException.class,

Copilot AI review requested due to automatic review settings July 27, 2026 07:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (2)

src/main/java/neqsim/thermo/util/benchmark/ThermodynamicBenchmark.java:159

  • Dataset constructor allows empty/blank citation/doi/license strings and does not validate that the points list contains no null entries. That can lead to NPEs later (e.g., in run/report) and contradicts the "metadata and points are required" contract.
    public Dataset(String name, String citation, String doi, String license, List<Point> points) {
      if (name == null || name.trim().isEmpty() || citation == null || doi == null || license == null || points == null
          || points.isEmpty()) {
        throw new IllegalArgumentException("Dataset metadata and points are required");
      }
      this.name = name;
      this.citation = citation;
      this.doi = doi;
      this.license = license;
      this.points = Collections.unmodifiableList(new ArrayList<Point>(points));
    }

docs/thermo/thermodynamic_benchmarks.md:56

  • The documentation includes a Java code example, but it is not covered by the repository’s existing "doc examples compile" convention (src/test/java/neqsim/DocExamplesCompilationTest.java). Adding a dedicated test method there for this snippet would help ensure future API changes don’t silently break the published documentation.
## Java example

```java
ThermodynamicBenchmark.Dataset dataset = H2CO2PhaseEquilibriumData.load();
NeqSimPhaseEquilibriumPrediction prediction =
    new NeqSimPhaseEquilibriumPrediction(
        NeqSimPhaseEquilibriumPrediction.Model.GERG_2008_H2);

ThermodynamicBenchmark.Report report =
    ThermodynamicBenchmark.run("GERG-2008-H2", dataset, prediction);

double aardPercent = report.getAverageAbsoluteRelativeDeviationPercent();
double biasPercent = report.getBiasPercent();
</details>

Comment on lines +54 to +63
String[] values = line.split(",", -1);
if (values.length != 8) {
throw new IOException("Expected 8 CSV columns but found " + values.length);
}
double temperatureK = Double.parseDouble(values[1]) + 273.15;
double pressureBara = Double.parseDouble(values[3]) * 10.0;
Map<String, Double> composition = new LinkedHashMap<String, Double>();
composition.put("CO2", Double.parseDouble(values[4]));
composition.put("hydrogen", Double.parseDouble(values[5]));
double nitrogenFraction = Double.parseDouble(values[6]);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants