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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package cbit.vcell.geometry;

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;

/**
* Pins the output of geometry surface generation against committed goldens.
*
* Why this exists: VCell's regression suites are math-generation centric and mostly non-spatial, so
* a change to region finding, surface tessellation, Taubin smoothing or membrane adjacency could
* alter every spatial model without a single test noticing. The goldens were generated from
* <b>pre-merge master ({@code f35beaddcd})</b> — the behaviour that was deployed before the #2026 /
* #2027 memory work — so they record the old behaviour rather than blessing the new one.
*
* What a failure means: surface generation now produces a different answer than the deployed
* implementation did. That is not automatically wrong — an intentional improvement will fail these
* too — but it must be a decision, with the golden updated deliberately and the diff reviewed.
*
* Regenerating (only after deciding the new output is correct):
* <pre>
* mvn -q -pl vcell-core exec:java -Dexec.classpathScope=test \
* -Dexec.mainClass=cbit.vcell.geometry.GeometrySurfaceGolden
* </pre>
* then read the diff before committing it. See {@link GeometrySurfaceGolden} for what is captured
* and how floating point is handled.
*/
@Tag("Fast")
public class GeometrySurfaceRegressionTest {

private static final String RESOURCE_DIR = "/cbit/vcell/geometry/surface-golden/";

@TestFactory
public List<DynamicTest> surfaceDescriptionsMatchTheDeployedImplementation() {
List<DynamicTest> tests = new ArrayList<>();
for (Map.Entry<String, GeometrySurfaceGolden.GeometryFactory> entry
: GeometrySurfaceGolden.fixtures().entrySet()) {
String fixture = entry.getKey();
tests.add(DynamicTest.dynamicTest(fixture, () -> {
String expected = readGolden(fixture);
String actual = GeometrySurfaceGolden.describe(entry.getValue().create());
assertEquals(expected, actual, () -> describeDifference(fixture, expected, actual));
}));
}
return tests;
}

/**
* Guards the guard. If a fixture is added without a golden, or a golden goes missing, the
* factory above would simply produce fewer tests and the suite would still be green.
*/
@Test
public void everyFixtureHasAGolden() {
List<String> missing = new ArrayList<>();
for (String fixture : GeometrySurfaceGolden.fixtures().keySet()) {
if (GeometrySurfaceRegressionTest.class.getResourceAsStream(RESOURCE_DIR + fixture + ".txt") == null) {
missing.add(fixture);
}
}
assertTrue(missing.isEmpty(),
"fixtures with no committed golden (run GeometrySurfaceGolden.main): " + missing);
assertFalse(GeometrySurfaceGolden.fixtures().isEmpty(), "there must be fixtures to compare");
}

private static String readGolden(String fixture) throws Exception {
try (InputStream in = GeometrySurfaceRegressionTest.class
.getResourceAsStream(RESOURCE_DIR + fixture + ".txt")) {
assertNotNull(in, "no golden for fixture '" + fixture + "'");
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}

/**
* A readable report. assertEquals on two multi-line blocks prints both in full and leaves the
* reader to find the difference; surface descriptions are long enough that this matters.
*/
private static String describeDifference(String fixture, String expected, String actual) {
String[] want = expected.split("\n", -1);
String[] got = actual.split("\n", -1);
StringBuilder sb = new StringBuilder();
sb.append("geometry surface output changed for fixture '").append(fixture).append("'.\n");
sb.append("This is a change against pre-merge master f35beaddcd. If it is intentional, ")
.append("regenerate the golden with GeometrySurfaceGolden.main and review the diff.\n");
int shown = 0;
for (int i = 0; i < Math.max(want.length, got.length) && shown < 12; i++) {
String w = i < want.length ? want[i] : "<missing>";
String g = i < got.length ? got[i] : "<missing>";
if (!w.equals(g)) {
sb.append(" line ").append(i + 1).append('\n')
.append(" golden: ").append(w).append('\n')
.append(" actual: ").append(g).append('\n');
shown++;
}
}
if (shown == 0) {
sb.append(" (no line differs — trailing whitespace or line endings?)\n");
}
return sb.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Geometry surface goldens

Each `.txt` here is a deterministic description of what geometry surface generation produced for one
fixture. `GeometrySurfaceRegressionTest` compares them; `GeometrySurfaceGolden` produces them and
defines the fixtures.

They exist because VCell's regression suites are math-generation centric and mostly non-spatial.
Nothing else pins `RegionImage` or `SurfaceCollection`, so a change to region finding, surface
tessellation, Taubin smoothing or membrane adjacency could alter every spatial model silently.

## Where these came from, and why it matters

**Generated on `f35beaddcd`** — master as it stood before the #2026 / #2027 memory work. They record
the behaviour that was already deployed. They do **not** bless whatever the current code happens to
do.

That distinction is the whole value of the suite, and it is easy to lose. A golden generated by the
same code it is meant to check proves nothing: it just photographs the current answer, including any
regression already present.

## Adding fixtures later — branch from the golden base

**`test/geometry-goldens-base`** exists for this. It is anchored at `f35beaddcd` and carries this
suite, so it is a standing checkout of the old implementation with the harness already on it. Branch
from it, never from current master:

```bash
# 1. branch from the golden base, which still runs the pre-change implementation
git checkout -b test/geometry-more-fixtures test/geometry-goldens-base

# 2. add fixtures to GeometrySurfaceGolden.fixtures(), then generate goldens
# HERE, with the old implementation
mvn -q -pl vcell-core exec:java -Dexec.classpathScope=test \
-Dexec.mainClass=cbit.vcell.geometry.GeometrySurfaceGolden
mvn -o surefire:test -pl vcell-core -Dtest=GeometrySurfaceRegressionTest # must pass here
git commit -am "more geometry fixtures + goldens from the pre-change implementation"

# 3. NOW bring current master in. This merge is the experiment.
git merge master
mvn -o surefire:test -pl vcell-core -Dtest=GeometrySurfaceRegressionTest
```

If step 3 passes, the new fixtures confirm current master matches the older behaviour. If it fails,
the suite has found a real difference and the diff says exactly which quantity moved.

Then cherry-pick the fixture-and-golden commit onto a branch off master and open the PR from
there:

```bash
git checkout -b test/geometry-more-fixtures-pr origin/master
git cherry-pick <the fixture commit>
```

Cherry-picking rather than merging keeps `test/geometry-goldens-base` pinned at the old
implementation instead of dragging master into it, so it stays usable as a generation base for the
next round.

## When a golden legitimately changes

An intentional improvement will fail these tests — that is correct, not a nuisance. Regenerate
deliberately:

```bash
mvn -q -pl vcell-core exec:java -Dexec.classpathScope=test \
-Dexec.mainClass=cbit.vcell.geometry.GeometrySurfaceGolden
git diff -- vcell-core/src/test/resources/cbit/vcell/geometry/surface-golden
```

**Read the diff before committing it.** The files are plain text and the numbers are physically
checkable, which is the point — for the nested spheres, subvolume sizes sum to exactly 1.0 (the unit
cube), and the smoothed sphere area of 2.100 is close to the analytic 4πr² = 2.011 while the
unsmoothed staircase value is 3.272. A change that moves a surface area away from its analytic value
is a bug report, not a golden update.

## What is captured

- what VCML's own `<SurfaceDescription>` carries: sample size, cutoff frequency, volume and membrane
regions with their sizes and adjacency;
- and what it does not: the region-label map, the mesh (node count, quantised coordinate digest,
polygon node-index digest, per-surface polygon counts and areas), and the surface classes.

Floating point: node coordinates are quantised to 1e-9 before hashing, so a last-ulp difference
cannot flip a golden while a real change still does — verified by perturbing the node origin by
~3e-9, which fails every fixture. Aggregate figures (bounding box, per-surface and total area) are
printed at full precision so a reviewer can see *how* a golden moved, not merely that it did.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
geometry analytic_sphere dimension=3
extent 1.000000000 1.000000000 1.000000000
origin 0.000000000 0.000000000 0.000000000
image none
subVolumes 2
subVolume inside(handle=0)
subVolume outside(handle=1)
sampleSize 24x24x24
cutoffFrequency 0.600000000
regionImage regions=2 dims=24x24x24
regionImage.pixelPartition sumOfRegions=13824 totalPixels=13824 complete=true
region index=0 pixelValue=1 numPixels=12448
region index=1 pixelValue=0 numPixels=1376
regionImage.encodedRegionIndexSHA e36b2ed0fa1e8f347e3b7e61
surfaceCollection surfaces=1 nodes=890
nodeBounds x=[0.195652174,0.804347826] y=[0.195652174,0.804347826] z=[0.195652174,0.804347826]
nodeCoordsSHA 80c718e6f08c532f929a1cca
surface[0] interiorRegion=0 exteriorRegion=1 polygons=888 area=1.678638941
polygonNodeIndicesSHA 60f0a5fd12528abc7e67a2c9
polygonVolumeNeighborsSHA c2439cb259bb025495adc0a2
totalArea 1.678638941
membraneEdgeNeighbors total=3552 SHA=572f8d13b9334e41e8133081
geometricRegions 3
surface membrane_outside0_inside1 size=1.678638941 adjacent=[inside1, outside0]
volume inside1 size=0.113092792 adjacent=[membrane_outside0_inside1]
volume outside0 size=0.886907208 adjacent=[membrane_outside0_inside1]
surfaceClasses 1
inside_outside_membrane adjacent=[inside, outside]
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
geometry stripes dimension=2
extent 1.000000000 1.000000000 1.000000000
origin 0.000000000 0.000000000 0.000000000
image 48x48x1 pixelClasses=2 pixelsSHA=ead7676c3957ff75a1cf4427
subVolumes 2
subVolume sv0(handle=0)
subVolume sv1(handle=1)
sampleSize 48x48x1
cutoffFrequency 0.600000000
regionImage regions=6 dims=48x48x1
regionImage.pixelPartition sumOfRegions=2304 totalPixels=2304 complete=true
region index=0 pixelValue=0 numPixels=384
region index=1 pixelValue=1 numPixels=384
region index=2 pixelValue=0 numPixels=384
region index=3 pixelValue=1 numPixels=384
region index=4 pixelValue=0 numPixels=384
region index=5 pixelValue=1 numPixels=384
regionImage.encodedRegionIndexSHA 6bf8a6c0043ad9fdc6314810
surfaceCollection surfaces=5 nodes=490
nodeBounds x=[0.159574468,0.840425532] y=[0.000000000,1.000000000] z=[0.000000000,1.000000000]
nodeCoordsSHA 707b60085919b98b4aa3010a
surface[0] interiorRegion=0 exteriorRegion=1 polygons=48 area=1.000000000
surface[1] interiorRegion=1 exteriorRegion=2 polygons=48 area=1.000000000
surface[2] interiorRegion=2 exteriorRegion=3 polygons=48 area=1.000000000
surface[3] interiorRegion=3 exteriorRegion=4 polygons=48 area=1.000000000
surface[4] interiorRegion=4 exteriorRegion=5 polygons=48 area=1.000000000
polygonNodeIndicesSHA 3b342984907dd091edeb25d6
polygonVolumeNeighborsSHA 3905179d58a75905667120d1
totalArea 5.000000000
membraneEdgeNeighbors total=960 SHA=220b176b1b8208b61eca1f48
geometricRegions 11
surface membrane_sv00_sv11 size=1.000000000 adjacent=[sv00, sv11]
surface membrane_sv02_sv13 size=1.000000000 adjacent=[sv02, sv13]
surface membrane_sv04_sv15 size=1.000000000 adjacent=[sv04, sv15]
surface membrane_sv11_sv02 size=1.000000000 adjacent=[sv02, sv11]
surface membrane_sv13_sv04 size=1.000000000 adjacent=[sv04, sv13]
volume sv00 size=0.159574468 adjacent=[membrane_sv00_sv11]
volume sv02 size=0.170212766 adjacent=[membrane_sv02_sv13, membrane_sv11_sv02]
volume sv04 size=0.170212766 adjacent=[membrane_sv04_sv15, membrane_sv13_sv04]
volume sv11 size=0.170212766 adjacent=[membrane_sv00_sv11, membrane_sv11_sv02]
volume sv13 size=0.170212766 adjacent=[membrane_sv02_sv13, membrane_sv13_sv04]
volume sv15 size=0.159574468 adjacent=[membrane_sv04_sv15]
surfaceClasses 1
sv0_sv1_membrane adjacent=[sv0, sv1]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
geometry getImageExample() dimension=2
extent 10.000000000 10.000000000 1.000000000
origin -5.000000000 -5.000000000 -5.000000000
image 100x100x1 pixelClasses=2 pixelsSHA=4ffef933700bbece710f0271
subVolumes 2
subVolume cytosol(handle=1)
subVolume ec(handle=0)
sampleSize 100x100x1
cutoffFrequency 0.300000000
regionImage regions=2 dims=100x100x1
regionImage.pixelPartition sumOfRegions=10000 totalPixels=10000 complete=true
region index=0 pixelValue=0 numPixels=5100
region index=1 pixelValue=1 numPixels=4900
regionImage.encodedRegionIndexSHA 318883260e11af650c232a68
surfaceCollection surfaces=1 nodes=202
nodeBounds x=[0.101010101,0.101010101] y=[-5.000000000,5.000000000] z=[-5.000000000,-4.000000000]
nodeCoordsSHA 9c58406fc98754b7d60eecef
surface[0] interiorRegion=0 exteriorRegion=1 polygons=100 area=10.000000000
polygonNodeIndicesSHA 3fd21f4c2560b06bfd6edae7
polygonVolumeNeighborsSHA ad0be0c96bddd90b79391a15
totalArea 10.000000000
membraneEdgeNeighbors total=400 SHA=91131fc70c55ba4304546308
geometricRegions 3
surface membrane_ec0_cytosol1 size=10.000000000 adjacent=[cytosol1, ec0]
volume cytosol1 size=48.989898990 adjacent=[membrane_ec0_cytosol1]
volume ec0 size=51.010101010 adjacent=[membrane_ec0_cytosol1]
surfaceClasses 1
cytosol_ec_membrane adjacent=[cytosol, ec]
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
geometry four_shells dimension=3
extent 1.000000000 1.000000000 1.000000000
origin 0.000000000 0.000000000 0.000000000
image 24x24x24 pixelClasses=4 pixelsSHA=1cd858149324189245432594
subVolumes 4
subVolume sv0(handle=0)
subVolume sv1(handle=1)
subVolume sv2(handle=2)
subVolume sv3(handle=3)
sampleSize 24x24x24
cutoffFrequency 0.600000000
regionImage regions=4 dims=24x24x24
regionImage.pixelPartition sumOfRegions=13824 totalPixels=13824 complete=true
region index=0 pixelValue=0 numPixels=10712
region index=1 pixelValue=1 numPixels=2200
region index=2 pixelValue=2 numPixels=776
region index=3 pixelValue=3 numPixels=136
regionImage.encodedRegionIndexSHA 78227dcd08be8851eac9bd1d
surfaceCollection surfaces=3 nodes=2406
nodeBounds x=[0.108695652,0.891304348] y=[0.108695652,0.891304348] z=[0.108695652,0.891304348]
nodeCoordsSHA 1c392601d61c1393e628cfcc
surface[0] interiorRegion=0 exteriorRegion=1 polygons=1536 area=2.903591682
surface[1] interiorRegion=1 exteriorRegion=2 polygons=672 area=1.270321361
surface[2] interiorRegion=2 exteriorRegion=3 polygons=192 area=0.362948960
polygonNodeIndicesSHA 076d7c92b816796302497b33
polygonVolumeNeighborsSHA e46494f5dac1c2e984cf9afd
totalArea 4.536862004
membraneEdgeNeighbors total=9600 SHA=b3c8ab3063190be41ee71349
geometricRegions 7
surface membrane_sv00_sv11 size=2.903591682 adjacent=[sv00, sv11]
surface membrane_sv11_sv22 size=1.270321361 adjacent=[sv11, sv22]
surface membrane_sv22_sv33 size=0.362948960 adjacent=[sv22, sv33]
volume sv00 size=0.744226186 adjacent=[membrane_sv00_sv11]
volume sv11 size=0.180816964 adjacent=[membrane_sv00_sv11, membrane_sv11_sv22]
volume sv22 size=0.063779075 adjacent=[membrane_sv11_sv22, membrane_sv22_sv33]
volume sv33 size=0.011177776 adjacent=[membrane_sv22_sv33]
surfaceClasses 3
sv0_sv1_membrane adjacent=[sv0, sv1]
sv1_sv2_membrane adjacent=[sv1, sv2]
sv2_sv3_membrane adjacent=[sv2, sv3]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
geometry nested_spheres dimension=3
extent 1.000000000 1.000000000 1.000000000
origin 0.000000000 0.000000000 0.000000000
image 32x32x32 pixelClasses=3 pixelsSHA=4274e9387b06b21d167545c7
subVolumes 3
subVolume sv0(handle=0)
subVolume sv1(handle=1)
subVolume sv2(handle=2)
sampleSize 32x32x32
cutoffFrequency 0.600000000
regionImage regions=3 dims=32x32x32
regionImage.pixelPartition sumOfRegions=32768 totalPixels=32768 complete=true
region index=0 pixelValue=0 numPixels=24024
region index=1 pixelValue=1 numPixels=7272
region index=2 pixelValue=2 numPixels=1472
regionImage.encodedRegionIndexSHA 5bc769104026854750567df3
surfaceCollection surfaces=2 nodes=4084
nodeBounds x=[0.080645161,0.919354839] y=[0.080645161,0.919354839] z=[0.080645161,0.919354839]
nodeCoordsSHA 805dc1773900a860552cadc9
surface[0] interiorRegion=0 exteriorRegion=1 polygons=3144 area=3.271592092
surface[1] interiorRegion=1 exteriorRegion=2 polygons=936 area=0.973985432
polygonNodeIndicesSHA dfe85130e97edec42004c6ef
polygonVolumeNeighborsSHA 0d026ac161c086ca1ee5a362
totalArea 4.245577523
membraneEdgeNeighbors total=16320 SHA=a47e4a57c3cd17224f6f71ca
geometricRegions 5
surface membrane_sv00_sv11 size=3.271592092 adjacent=[sv00, sv11]
surface membrane_sv11_sv22 size=0.973985432 adjacent=[sv11, sv22]
volume sv00 size=0.706488537 adjacent=[membrane_sv00_sv11]
volume sv11 size=0.244100567 adjacent=[membrane_sv00_sv11, membrane_sv11_sv22]
volume sv22 size=0.049410896 adjacent=[membrane_sv11_sv22]
surfaceClasses 2
sv0_sv1_membrane adjacent=[sv0, sv1]
sv1_sv2_membrane adjacent=[sv1, sv2]
Loading
Loading