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
5 changes: 5 additions & 0 deletions .github/workflows/regression.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ jobs:
"SEDML_SBML_IT": {"count": 6, "module": CORE},
"SEDML_VCML_IT": {"count": 2, "module": CORE},
"SBML_IT": {"count": 1, "module": CORE},
# Geometry surface goldens over real corpus models. Not a model-suite
# sweep and not slow (~3 s); it lives here rather than in the fast lane
# because it depends on the VCML corpus resources and the natural way to
# extend it is to add more models. See the README beside the goldens.
"Geometry_IT": {"count": 1, "module": CORE},
"BSTS_IT": {"count": 1, "module": "vcell-cli"},
# Not a model-suite sweep: one Oracle container proving the database
# cleanup sweep's SQL is accepted by the database it actually runs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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.util.List;

/**
* The same golden comparison as {@link GeometrySurfaceRegressionTest}, over REAL stored models from
* the VCML test corpus rather than synthetic shapes.
*
* Separate class and separate group because these parse multi-megabyte documents and rebuild
* regions and surfaces over 0.5-4 MP images. They belong in {@code Geometry_IT}, which
* regression.yml runs, rather than in the fast lane on every push.
*
* They earn their place by being irregular in ways synthetic fixtures are not. The synthetic set is
* spheres, shells and stripes; a real segmentation has thin features, awkward aspect ratios and
* disconnected regions sharing a pixel value. {@code corpus_95707047_208x153x83} alone yields SIX
* regions from TWO pixel classes -- five separate cytosol bodies -- which no synthetic fixture here
* produces.
*
* Note these deliberately REBUILD surfaces rather than reading the stored {@code
* <SurfaceDescription>}; see {@code GeometrySurfaceGolden.fromCorpus}. Pinning the stored values
* would test the XML reader instead of surface generation.
*/
@Tag("Geometry_IT")
public class GeometrySurfaceCorpusRegressionTest {

@TestFactory
public List<DynamicTest> corpusSurfaceDescriptionsMatchTheDeployedImplementation() {
return GeometrySurfaceRegressionTest.testsFor(GeometrySurfaceGolden.corpusFixtures());
}

@Test
public void everyCorpusFixtureHasAGolden() {
GeometrySurfaceRegressionTest.assertEveryFixtureHasAGolden(GeometrySurfaceGolden.corpusFixtures());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
import cbit.vcell.geometry.surface.SurfaceCollection;
import cbit.vcell.geometry.surface.SurfaceGeometricRegion;
import cbit.vcell.geometry.surface.VolumeGeometricRegion;
import cbit.vcell.biomodel.BioModel;
import cbit.vcell.mapping.SimulationContext;
import cbit.vcell.parser.Expression;
import cbit.vcell.xml.XMLSource;
import cbit.vcell.xml.XmlHelper;
import org.vcell.sbml.VcmlTestSuiteFiles;
import org.vcell.util.Extent;
import org.vcell.util.ISize;
import org.vcell.util.Origin;
Expand Down Expand Up @@ -79,6 +84,54 @@ public static Map<String, GeometryFactory> fixtures() {
return map;
}

/**
* Real stored models from the VCML test corpus, kept separate from {@link #fixtures()} because
* they are far slower: each parses a multi-megabyte document and then builds regions and
* surfaces over a 0.5-4 MP image. They run in the {@code Geometry_IT} regression group, not in
* {@code Fast}.
*
* They are worth the time because synthetic fixtures are made of spheres, shells and stripes.
* Real segmentations are irregular, have thin features and awkward aspect ratios, and are the
* shapes that actually broke things. The selection spans 0.47-3.96 MP, 2D and 3D, cubic through
* to a 2151x504 slab.
*/
public static Map<String, GeometryFactory> corpusFixtures() {
Map<String, GeometryFactory> map = new LinkedHashMap<>();
map.put("corpus_209284198_600x300x22", () -> fromCorpus("biomodel_209284198.vcml"));
map.put("corpus_26454463_564x160x31", () -> fromCorpus("biomodel_26454463.vcml"));
map.put("corpus_95707047_208x153x83", () -> fromCorpus("biomodel_95707047.vcml"));
map.put("corpus_65311813_256x256x34", () -> fromCorpus("biomodel_65311813.vcml"));
map.put("corpus_12522025_2151x504_2d", () -> fromCorpus("biomodel_12522025_spatial.vcml"));
map.put("corpus_201022999_211x201x11", () -> fromCorpus("biomodel_201022999.vcml"));
return map;
}

/**
* The first spatial image geometry in a stored BioModel, with its surfaces REBUILT.
*
* Rebuilding matters. A stored document carries a {@code <SurfaceDescription>}, and XmlReader
* applies it, so {@code precomputeAll} skips {@code updateAll()} on parse and the geometry
* arrives with regions restored from the file rather than computed. Pinning that would test the
* XML reader, not surface generation. Calling {@code updateAll()} here forces a fresh
* RegionImage and SurfaceCollection, which is the thing under test.
*/
private static Geometry fromCorpus(String vcmlFile) throws Exception {
String vcml;
try (java.io.InputStream in = VcmlTestSuiteFiles.getVcmlTestCase(vcmlFile)) {
vcml = new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml));
for (SimulationContext simContext : bioModel.getSimulationContexts()) {
Geometry geometry = simContext.getGeometry();
if (geometry != null && geometry.getDimension() > 0
&& geometry.getGeometrySpec().getImage() != null) {
geometry.getGeometrySurfaceDescription().updateAll();
return geometry;
}
}
throw new IllegalStateException("no spatial image geometry in " + vcmlFile);
}

public interface GeometryFactory {
Geometry create() throws Exception;
}
Expand Down Expand Up @@ -454,17 +507,36 @@ public static Path goldenPath(String fixture) {
return goldenDir().resolve(fixture + ".txt");
}

/** Writes every fixture's description to the golden directory. */
public static void writeGoldens() throws Exception {
/**
* Writes every fixture's description to the golden directory.
*
* @param includeCorpus also regenerate the slow corpus goldens. Off by default when a single
* argument "fast" is given, so a quick iteration on the synthetic fixtures
* does not silently leave the corpus goldens stale or spend minutes
* rebuilding them.
*/
public static void writeGoldens(boolean includeCorpus) throws Exception {
Files.createDirectories(goldenDir());
for (Map.Entry<String, GeometryFactory> e : fixtures().entrySet()) {
writeSet(fixtures(), "fast");
if (includeCorpus) {
writeSet(corpusFixtures(), "corpus");
} else {
System.out.println("skipped corpus fixtures (pass 'all' to regenerate them)");
}
}

private static void writeSet(Map<String, GeometryFactory> set, String label) throws Exception {
for (Map.Entry<String, GeometryFactory> e : set.entrySet()) {
long t0 = System.currentTimeMillis();
String text = describe(e.getValue().create());
Files.writeString(goldenPath(e.getKey()), text, StandardCharsets.UTF_8);
System.out.printf("wrote %-38s %d bytes%n", e.getKey(), text.length());
System.out.printf("wrote [%s] %-34s %5d bytes %6d ms%n",
label, e.getKey(), text.length(), System.currentTimeMillis() - t0);
}
}

public static void main(String[] args) throws Exception {
writeGoldens();
boolean includeCorpus = args.length == 0 || !"fast".equalsIgnoreCase(args[0]);
writeGoldens(includeCorpus);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,17 @@
@Tag("Fast")
public class GeometrySurfaceRegressionTest {

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

@TestFactory
public List<DynamicTest> surfaceDescriptionsMatchTheDeployedImplementation() {
return testsFor(GeometrySurfaceGolden.fixtures());
}

/** Shared with {@link GeometrySurfaceCorpusRegressionTest}; the comparison is identical. */
static List<DynamicTest> testsFor(Map<String, GeometrySurfaceGolden.GeometryFactory> fixtures) {
List<DynamicTest> tests = new ArrayList<>();
for (Map.Entry<String, GeometrySurfaceGolden.GeometryFactory> entry
: GeometrySurfaceGolden.fixtures().entrySet()) {
for (Map.Entry<String, GeometrySurfaceGolden.GeometryFactory> entry : fixtures.entrySet()) {
String fixture = entry.getKey();
tests.add(DynamicTest.dynamicTest(fixture, () -> {
String expected = readGolden(fixture);
Expand All @@ -54,24 +58,28 @@ public List<DynamicTest> surfaceDescriptionsMatchTheDeployedImplementation() {
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() {
static void assertEveryFixtureHasAGolden(Map<String, GeometrySurfaceGolden.GeometryFactory> fixtures) {
List<String> missing = new ArrayList<>();
for (String fixture : GeometrySurfaceGolden.fixtures().keySet()) {
for (String fixture : 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");
assertFalse(fixtures.isEmpty(), "there must be fixtures to compare");
}

/**
* 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() {
assertEveryFixtureHasAGolden(GeometrySurfaceGolden.fixtures());
}

private static String readGolden(String fixture) throws Exception {
static String readGolden(String fixture) throws Exception {
try (InputStream in = GeometrySurfaceRegressionTest.class
.getResourceAsStream(RESOURCE_DIR + fixture + ".txt")) {
assertNotNull(in, "no golden for fixture '" + fixture + "'");
Expand All @@ -83,7 +91,7 @@ private static String readGolden(String fixture) throws Exception {
* 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) {
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ They exist because VCell's regression suites are math-generation centric and mos
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.

## Two sets

| prefix | fixtures | group | runtime |
|---|---|---|---|
| `image*` / `analytic*` | synthetic shapes — spheres, shells, stripes, an analytic subvolume | `Fast` | 0.42 s |
| `corpus_*` | real stored models from the VCML test corpus, 0.47–3.96 MP | `Geometry_IT` | 2.87 s |

The corpus set earns its place by being irregular in ways synthetic shapes are not: thin features,
awkward aspect ratios, and disconnected regions sharing a pixel value.
`corpus_95707047_208x153x83` yields **six regions from two pixel classes** — five separate cytosol
bodies — which nothing in the synthetic set produces.

They deliberately **rebuild** surfaces rather than reading the stored `<SurfaceDescription>` out of
the document. XmlReader applies that element on parse, so `precomputeAll` skips `updateAll()` and
the geometry arrives with regions restored from the file; pinning those would test the XML reader
rather than surface generation.

At 2.87 s the corpus set is not actually slow, and could live in `Fast` if you would rather it ran
on every push instead of only in the regression lane. It is separate mainly for headroom — it
depends on the corpus resources, and the natural way to extend it is to add more models.

## Where these came from, and why it matters

**Generated on `f35beaddcd`** — master as it stood before the #2026 / #2027 memory work. They record
Expand Down Expand Up @@ -61,8 +82,14 @@ An intentional improvement will fail these tests — that is correct, not a nuis
deliberately:

```bash
# both sets
mvn -q -pl vcell-core exec:java -Dexec.classpathScope=test \
-Dexec.mainClass=cbit.vcell.geometry.GeometrySurfaceGolden

# synthetic only, when iterating (leaves corpus goldens untouched)
mvn -q -pl vcell-core exec:java -Dexec.classpathScope=test \
-Dexec.mainClass=cbit.vcell.geometry.GeometrySurfaceGolden -Dexec.args=fast

git diff -- vcell-core/src/test/resources/cbit/vcell/geometry/surface-golden
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
geometry purk-neck3 dimension=2
extent 27.921000000 6.542000000 1.000000000
origin 0.000000000 0.000000000 0.000000000
image 2151x504x1 pixelClasses=2 pixelsSHA=a8d98c9564617179cf5ee751
subVolumes 2
subVolume cytosol(handle=1)
subVolume extracellular(handle=0)
sampleSize 208x48x1
cutoffFrequency 0.300000000
regionImage regions=3 dims=208x48x1
regionImage.pixelPartition sumOfRegions=9984 totalPixels=9984 complete=true
region index=0 pixelValue=0 numPixels=1969
region index=1 pixelValue=1 numPixels=5968
region index=2 pixelValue=0 numPixels=2047
regionImage.encodedRegionIndexSHA d406b42537b7931045e2d2c7
surfaceCollection surfaces=2 nodes=4166
nodeBounds x=[0.000000000,27.921000000] y=[0.078249924,6.325590769] z=[0.000000000,1.000000000]
nodeCoordsSHA b3737fb395aae69f47bc3e90
surface[0] interiorRegion=0 exteriorRegion=1 polygons=1036 area=102.049107892
surface[1] interiorRegion=1 exteriorRegion=2 polygons=1045 area=101.359268590
polygonNodeIndicesSHA a537270eb94537957e13088d
polygonVolumeNeighborsSHA 3cc17d70634baa3d999b2750
totalArea 203.408376482
membraneEdgeNeighbors total=8324 SHA=5d322119221f508ab3c78b2f
geometricRegions 5
surface membrane_cytosol1_extracellular2 size=101.359268590 adjacent=[cytosol1, extracellular2]
surface membrane_extracellular0_cytosol1 size=102.049107892 adjacent=[cytosol1, extracellular0]
volume cytosol1 size=111.681379805 adjacent=[membrane_cytosol1_extracellular2, membrane_extracellular0_cytosol1]
volume extracellular0 size=34.723831546 adjacent=[membrane_extracellular0_cytosol1]
volume extracellular2 size=36.253970649 adjacent=[membrane_cytosol1_extracellular2]
surfaceClasses 1
cytosol_extracellular_membrane adjacent=[cytosol, extracellular]
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
geometry geom_20190115_151208 dimension=3
extent 0.220000000 0.210000000 0.220000000
origin 0.000000000 0.000000000 0.000000000
image 211x201x11 pixelClasses=2 pixelsSHA=be54e3ae7a595731d630f355
subVolumes 2
subVolume MIM(handle=0)
subVolume Matrix(handle=1)
sampleSize 211x201x11
cutoffFrequency 0.300000000
regionImage regions=2 dims=211x201x11
regionImage.pixelPartition sumOfRegions=466521 totalPixels=466521 complete=true
region index=0 pixelValue=0 numPixels=258473
region index=1 pixelValue=1 numPixels=208048
regionImage.encodedRegionIndexSHA 9e39b8622c6c82f5fed7aa6c
surfaceCollection surfaces=1 nodes=160488
nodeBounds x=[0.018825469,0.199874502] y=[0.019426030,0.190573970] z=[0.011000000,0.209000000]
nodeCoordsSHA 25292c76b6267950cb3bc87a
surface[0] interiorRegion=0 exteriorRegion=1 polygons=160486 area=0.371065484
polygonNodeIndicesSHA 224d75136e1ca6645e26af80
polygonVolumeNeighborsSHA 147adf43e95c953596223842
totalArea 0.371065484
membraneEdgeNeighbors total=641944 SHA=49b8a287979823cb4889cb01
geometricRegions 3
surface membrane_MIM0_Matrix1 size=0.371065484 adjacent=[MIM0, Matrix1]
volume MIM0 size=0.005129238 adjacent=[membrane_MIM0_Matrix1]
volume Matrix1 size=0.005034762 adjacent=[membrane_MIM0_Matrix1]
surfaceClasses 1
MIM_Matrix_membrane adjacent=[MIM, Matrix]
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
geometry AliciaSpacial dimension=3
extent 30.000000000 15.000000000 11.000000000
origin 0.000000000 0.000000000 0.000000000
image 600x300x22 pixelClasses=3 pixelsSHA=dc7477deaa8eb18fc9810237
subVolumes 3
subVolume region0(handle=2)
subVolume region1(handle=0)
subVolume region2(handle=1)
sampleSize 89x44x32
cutoffFrequency 0.310000000
regionImage regions=3 dims=89x44x32
regionImage.pixelPartition sumOfRegions=125312 totalPixels=125312 complete=true
region index=0 pixelValue=2 numPixels=92423
region index=1 pixelValue=0 numPixels=27384
region index=2 pixelValue=1 numPixels=5505
regionImage.encodedRegionIndexSHA 80c635480536d32ad1a5246d
surfaceCollection surfaces=2 nodes=10866
nodeBounds x=[1.957006540,28.328177211] y=[1.920147696,13.010861278] z=[0.177419355,10.112903226]
nodeCoordsSHA 7efbd2d76f99c925445e0791
surface[0] interiorRegion=0 exteriorRegion=1 polygons=8564 area=753.088014302
surface[1] interiorRegion=1 exteriorRegion=2 polygons=2298 area=182.204229947
polygonNodeIndicesSHA a9ac9c28488a361805f793c3
polygonVolumeNeighborsSHA 21d7fa5cd23dd59837b7e652
totalArea 935.292244249
membraneEdgeNeighbors total=43448 SHA=520086290c17ce55723d7fd0
geometricRegions 5
surface membrane_region00_region11 size=753.088014302 adjacent=[region00, region11]
surface membrane_region11_region22 size=182.204229947 adjacent=[region11, region22]
volume region00 size=3562.148349587 adjacent=[membrane_region00_region11]
volume region11 size=1155.551387847 adjacent=[membrane_region00_region11, membrane_region11_region22]
volume region22 size=232.300262566 adjacent=[membrane_region11_region22]
surfaceClasses 2
region0_region1_membrane adjacent=[region0, region1]
region1_region2_membrane adjacent=[region1, region2]
Loading
Loading