Skip to content

Commit bd31fb6

Browse files
authored
Merge pull request #8 from virtualcell/round-trip-validation
export SBML with compatable units and optional validation
2 parents 8df4045 + 30f4341 commit bd31fb6

File tree

9 files changed

+105
-21
lines changed

9 files changed

+105
-21
lines changed

libvcell/_internal/native_calls.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,17 @@ def sbml_to_finite_volume_input(self, sbml_content: str, output_dir_path: Path)
5959
logging.exception("Error in sbml_to_finite_volume_input()", exc_info=e)
6060
raise
6161

62-
def vcml_to_sbml(self, vcml_content: str, application_name: str, sbml_file_path: Path) -> ReturnValue:
62+
def vcml_to_sbml(
63+
self, vcml_content: str, application_name: str, sbml_file_path: Path, round_trip_validation: bool
64+
) -> ReturnValue:
6365
try:
6466
with IsolateManager(self.lib) as isolate_thread:
6567
json_ptr: ctypes.c_char_p = self.lib.vcmlToSbml(
6668
isolate_thread,
6769
ctypes.c_char_p(vcml_content.encode("utf-8")),
6870
ctypes.c_char_p(application_name.encode("utf-8")),
6971
ctypes.c_char_p(str(sbml_file_path).encode("utf-8")),
72+
ctypes.c_int(int(round_trip_validation)),
7073
)
7174

7275
value: bytes | None = ctypes.cast(json_ptr, ctypes.c_char_p).value

libvcell/_internal/native_utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,13 @@ def _define_entry_points(self) -> None:
4242
self.lib.sbmlToVcml.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
4343

4444
self.lib.vcmlToSbml.restype = ctypes.c_char_p
45-
self.lib.vcmlToSbml.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p]
45+
self.lib.vcmlToSbml.argtypes = [
46+
ctypes.c_void_p,
47+
ctypes.c_char_p,
48+
ctypes.c_char_p,
49+
ctypes.c_char_p,
50+
ctypes.c_int,
51+
]
4652

4753
self.lib.vcmlToVcml.restype = ctypes.c_char_p
4854
self.lib.vcmlToVcml.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]

libvcell/model_utils.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
from libvcell._internal.native_calls import ReturnValue, VCellNativeCalls
44

55

6-
def vcml_to_sbml(vcml_content: str, application_name: str, sbml_file_path: Path) -> tuple[bool, str]:
6+
def vcml_to_sbml(
7+
vcml_content: str, application_name: str, sbml_file_path: Path, round_trip_validation: bool
8+
) -> tuple[bool, str]:
79
"""
810
Convert VCML content to SBML file
911
@@ -16,7 +18,12 @@ def vcml_to_sbml(vcml_content: str, application_name: str, sbml_file_path: Path)
1618
tuple[bool, str]: A tuple containing the success status and a message
1719
"""
1820
native = VCellNativeCalls()
19-
return_value: ReturnValue = native.vcml_to_sbml(vcml_content, application_name, sbml_file_path)
21+
return_value: ReturnValue = native.vcml_to_sbml(
22+
vcml_content=vcml_content,
23+
application_name=application_name,
24+
sbml_file_path=sbml_file_path,
25+
round_trip_validation=round_trip_validation,
26+
)
2027
return return_value.success, return_value.message
2128

2229

@@ -32,7 +39,7 @@ def sbml_to_vcml(sbml_content: str, vcml_file_path: Path) -> tuple[bool, str]:
3239
tuple[bool, str]: A tuple containing the success status and a message
3340
"""
3441
native = VCellNativeCalls()
35-
return_value: ReturnValue = native.sbml_to_vcml(sbml_content, vcml_file_path)
42+
return_value: ReturnValue = native.sbml_to_vcml(sbml_content=sbml_content, vcml_file_path=vcml_file_path)
3643
return return_value.success, return_value.message
3744

3845

@@ -48,5 +55,5 @@ def vcml_to_vcml(vcml_content: str, vcml_file_path: Path) -> tuple[bool, str]:
4855
tuple[bool, str]: A tuple containing the success status and a message
4956
"""
5057
native = VCellNativeCalls()
51-
return_value: ReturnValue = native.vcml_to_vcml(vcml_content, vcml_file_path)
58+
return_value: ReturnValue = native.vcml_to_vcml(vcml_content=vcml_content, vcml_file_path=vcml_file_path)
5259
return return_value.success, return_value.message

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "libvcell"
7-
version = "0.0.10"
7+
version = "0.0.11"
88
description = "This is a python package which wraps a subset of VCell Java code as a native python package."
99
authors = ["Jim Schaff <[email protected]>", "Ezequiel Valencia <[email protected]>"]
1010
repository = "https://github.com/virtualcell/libvcell"

tests/test_libvcell.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,32 @@ def test_sbml_to_vcml(sbml_file_path: Path) -> None:
3333
assert msg == "Success"
3434

3535

36-
def test_vcml_to_sbml(vcml_file_path: Path, vcml_app_name: str) -> None:
36+
def test_vcml_to_sbml_with_validation(vcml_file_path: Path, vcml_app_name: str) -> None:
3737
vcml_content = vcml_file_path.read_text()
3838
with tempfile.TemporaryDirectory() as temp_dir:
3939
temp_output_dir = Path(temp_dir)
4040
sbml_file_path = temp_output_dir / "test.sbml"
4141
success, msg = vcml_to_sbml(
42-
vcml_content=vcml_content, application_name=vcml_app_name, sbml_file_path=sbml_file_path
42+
vcml_content=vcml_content,
43+
application_name=vcml_app_name,
44+
sbml_file_path=sbml_file_path,
45+
round_trip_validation=True,
46+
)
47+
assert sbml_file_path.exists()
48+
assert success is True
49+
assert msg == "Success"
50+
51+
52+
def test_vcml_to_sbml_without_validation(vcml_file_path: Path, vcml_app_name: str) -> None:
53+
vcml_content = vcml_file_path.read_text()
54+
with tempfile.TemporaryDirectory() as temp_dir:
55+
temp_output_dir = Path(temp_dir)
56+
sbml_file_path = temp_output_dir / "test.sbml"
57+
success, msg = vcml_to_sbml(
58+
vcml_content=vcml_content,
59+
application_name=vcml_app_name,
60+
sbml_file_path=sbml_file_path,
61+
round_trip_validation=False,
4362
)
4463
assert sbml_file_path.exists()
4564
assert success is True

vcell-native/src/main/java/org/vcell/libvcell/Entrypoints.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,15 @@ public static CCharPointer entrypoint_vcmlToSbml(
125125
IsolateThread ignoredThread,
126126
CCharPointer vcml_content,
127127
CCharPointer application_name,
128-
CCharPointer sbml_file_path) {
128+
CCharPointer sbml_file_path,
129+
int roundTripValidation) {
129130
ReturnValue returnValue;
130131
try {
131132
String vcmlContentStr = CTypeConversion.toJavaString(vcml_content);
132133
String applicationName = CTypeConversion.toJavaString(application_name);
133134
Path sbmlFilePath = new File(CTypeConversion.toJavaString(sbml_file_path)).toPath();
134-
vcml_to_sbml(vcmlContentStr, applicationName, sbmlFilePath);
135+
boolean bRoundTripValidation = CTypeConversion.toBoolean(roundTripValidation);
136+
vcml_to_sbml(vcmlContentStr, applicationName, sbmlFilePath, bRoundTripValidation);
135137
returnValue = new ReturnValue(true, "Success");
136138
}catch (Throwable t) {
137139
logger.error("Error translating vcml application to sbml", t);

vcell-native/src/main/java/org/vcell/libvcell/MainRecorder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ public static void main(String[] args) {
7070

7171
// create a temporary file for the SBML output
7272
File temp_sbml_file = new File(output_dir, "temp.vcml");
73-
vcml_to_sbml(vcml_str, vcml_app_name, temp_sbml_file.toPath());
73+
vcml_to_sbml(vcml_str, vcml_app_name, temp_sbml_file.toPath(), true);
7474
// remove temporary file
7575
if (temp_sbml_file.exists()) {
7676
boolean deleted = temp_sbml_file.delete();

vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,25 @@
11
package org.vcell.libvcell;
22

3+
import cbit.image.ImageException;
34
import cbit.util.xml.VCLogger;
45
import cbit.util.xml.VCLoggerException;
56
import cbit.util.xml.XmlUtil;
67
import cbit.vcell.biomodel.BioModel;
8+
import cbit.vcell.biomodel.ModelUnitConverter;
9+
import cbit.vcell.geometry.GeometryException;
710
import cbit.vcell.geometry.GeometrySpec;
811
import cbit.vcell.mapping.MappingException;
912
import cbit.vcell.mapping.SimulationContext;
1013
import cbit.vcell.mongodb.VCMongoMessage;
14+
import cbit.vcell.parser.ExpressionException;
1115
import cbit.vcell.xml.XMLSource;
1216
import cbit.vcell.xml.XmlHelper;
1317
import cbit.vcell.xml.XmlParseException;
1418
import org.vcell.sbml.SbmlException;
19+
import org.vcell.sbml.vcell.SBMLAnnotationUtil;
1520
import org.vcell.sbml.vcell.SBMLExporter;
1621
import org.vcell.sbml.vcell.SBMLImporter;
22+
import org.vcell.util.Pair;
1723

1824
import javax.xml.stream.XMLStreamException;
1925
import java.io.ByteArrayInputStream;
@@ -66,17 +72,43 @@ record LoggerMessage(VCLogger.Priority priority, VCLogger.ErrorType errorType, S
6672
}
6773

6874

69-
public static void vcml_to_sbml(String vcml_content, String applicationName, Path sbmlPath)
70-
throws XmlParseException, IOException, XMLStreamException, SbmlException, MappingException {
75+
public static void vcml_to_sbml(String vcml_content, String applicationName, Path sbmlPath, boolean roundTripValidation)
76+
throws XmlParseException, IOException, XMLStreamException, SbmlException, MappingException, ImageException, GeometryException, ExpressionException {
7177
GeometrySpec.avoidAWTImageCreation = true;
7278
VCMongoMessage.enabled = false;
7379

7480
BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml_content));
7581
bioModel.updateAll(false);
76-
SimulationContext simContext = bioModel.getSimulationContext(applicationName);
77-
boolean validateSBML = true;
78-
SBMLExporter sbmlExporter = new SBMLExporter(simContext, 3, 1, validateSBML);
82+
83+
if (applicationName == null || applicationName.isEmpty()) {
84+
throw new RuntimeException("Error: Application name is null or empty");
85+
}
86+
87+
if (bioModel.getSimulationContext(applicationName) == null) {
88+
throw new RuntimeException("Error: Simulation context not found for application name: " + applicationName);
89+
}
90+
91+
// change the unit system to SBML preferred units if not already.
92+
final BioModel sbmlPreferredUnitsBM;
93+
if (!bioModel.getModel().getUnitSystem().compareEqual(ModelUnitConverter.createSbmlModelUnitSystem())) {
94+
sbmlPreferredUnitsBM = ModelUnitConverter.createBioModelWithSBMLUnitSystem(bioModel);
95+
if(sbmlPreferredUnitsBM == null) {
96+
throw new RuntimeException("Unable to clone BioModel with SBML unit system");
97+
}
98+
} else {
99+
sbmlPreferredUnitsBM = bioModel;
100+
}
101+
102+
SimulationContext simContext = sbmlPreferredUnitsBM.getSimulationContext(applicationName);
103+
104+
int sbml_level = 3;
105+
int sbml_version = 1;
106+
SBMLExporter sbmlExporter = new SBMLExporter(simContext, sbml_level, sbml_version, roundTripValidation);
79107
String sbml_string = sbmlExporter.getSBMLString();
108+
109+
// cleanup the string of all the "sameAs" statements
110+
sbml_string = SBMLAnnotationUtil.postProcessCleanup(sbml_string);
111+
80112
XmlUtil.writeXMLStringToFile(sbml_string, sbmlPath.toFile().getAbsolutePath(), true);
81113
}
82114

vcell-native/src/test/java/org/vcell/libvcell/ModelEntrypointsTest.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package org.vcell.libvcell;
22

3+
import cbit.image.ImageException;
34
import cbit.util.xml.VCLoggerException;
5+
import cbit.vcell.geometry.GeometryException;
46
import cbit.vcell.mapping.MappingException;
7+
import cbit.vcell.parser.ExpressionException;
58
import cbit.vcell.xml.XmlParseException;
69
import org.junit.jupiter.api.Test;
710
import org.vcell.sbml.SbmlException;
@@ -26,13 +29,25 @@ public void test_sbml_to_vcml() throws MappingException, IOException, XmlParseEx
2629
}
2730

2831
@Test
29-
public void test_vcml_to_sbml() throws MappingException, IOException, XmlParseException, XMLStreamException, SbmlException {
32+
public void test_vcml_to_sbml_with_round_trip() throws MappingException, IOException, XmlParseException, XMLStreamException, SbmlException, ImageException, GeometryException, ExpressionException {
3033
String vcmlContent = getFileContentsAsString("/TinySpatialProject_Application0.vcml");
3134
File parent_dir = Files.createTempDirectory("vcmlToSbml").toFile();
32-
File sbml_temp_file = new File(parent_dir, "temp.sbml");
3335
String applicationName = "unnamed_spatialGeom";
34-
vcml_to_sbml(vcmlContent, applicationName, sbml_temp_file.toPath());
35-
assert(sbml_temp_file.exists());
36+
37+
File sbml_temp_file_true = new File(parent_dir, "temp_true.sbml");
38+
vcml_to_sbml(vcmlContent, applicationName, sbml_temp_file_true.toPath(), true);
39+
assert(sbml_temp_file_true.exists());
40+
}
41+
42+
@Test
43+
public void test_vcml_to_sbml_without_round_trip() throws MappingException, IOException, XmlParseException, XMLStreamException, SbmlException, ImageException, GeometryException, ExpressionException {
44+
String vcmlContent = getFileContentsAsString("/TinySpatialProject_Application0.vcml");
45+
File parent_dir = Files.createTempDirectory("vcmlToSbml").toFile();
46+
String applicationName = "unnamed_spatialGeom";
47+
48+
File sbml_temp_file_false = new File(parent_dir, "temp_false.sbml");
49+
vcml_to_sbml(vcmlContent, applicationName, sbml_temp_file_false.toPath(), false);
50+
assert(sbml_temp_file_false.exists());
3651
}
3752

3853
@Test

0 commit comments

Comments
 (0)