From 84c71c5c3738bbc35111f1eb29c62f7a538f31f7 Mon Sep 17 00:00:00 2001 From: jcschaff Date: Fri, 4 Apr 2025 18:56:30 -0400 Subject: [PATCH 1/2] export SBML with compatable units, round-trip-validation is now optional --- libvcell/_internal/native_calls.py | 5 ++- libvcell/_internal/native_utils.py | 8 +++- libvcell/model_utils.py | 15 +++++-- tests/test_libvcell.py | 23 +++++++++- .../java/org/vcell/libvcell/Entrypoints.java | 6 ++- .../java/org/vcell/libvcell/MainRecorder.java | 2 +- .../java/org/vcell/libvcell/ModelUtils.java | 42 ++++++++++++++++--- .../vcell/libvcell/ModelEntrypointsTest.java | 23 ++++++++-- 8 files changed, 104 insertions(+), 20 deletions(-) diff --git a/libvcell/_internal/native_calls.py b/libvcell/_internal/native_calls.py index d7f81b0..180d8ac 100644 --- a/libvcell/_internal/native_calls.py +++ b/libvcell/_internal/native_calls.py @@ -59,7 +59,9 @@ def sbml_to_finite_volume_input(self, sbml_content: str, output_dir_path: Path) logging.exception("Error in sbml_to_finite_volume_input()", exc_info=e) raise - def vcml_to_sbml(self, vcml_content: str, application_name: str, sbml_file_path: Path) -> ReturnValue: + def vcml_to_sbml( + self, vcml_content: str, application_name: str, sbml_file_path: Path, round_trip_validation: bool + ) -> ReturnValue: try: with IsolateManager(self.lib) as isolate_thread: json_ptr: ctypes.c_char_p = self.lib.vcmlToSbml( @@ -67,6 +69,7 @@ def vcml_to_sbml(self, vcml_content: str, application_name: str, sbml_file_path: ctypes.c_char_p(vcml_content.encode("utf-8")), ctypes.c_char_p(application_name.encode("utf-8")), ctypes.c_char_p(str(sbml_file_path).encode("utf-8")), + ctypes.c_int(int(round_trip_validation)), ) value: bytes | None = ctypes.cast(json_ptr, ctypes.c_char_p).value diff --git a/libvcell/_internal/native_utils.py b/libvcell/_internal/native_utils.py index bac09d4..d4990a8 100644 --- a/libvcell/_internal/native_utils.py +++ b/libvcell/_internal/native_utils.py @@ -42,7 +42,13 @@ def _define_entry_points(self) -> None: self.lib.sbmlToVcml.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] self.lib.vcmlToSbml.restype = ctypes.c_char_p - self.lib.vcmlToSbml.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p] + self.lib.vcmlToSbml.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_int, + ] self.lib.vcmlToVcml.restype = ctypes.c_char_p self.lib.vcmlToVcml.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] diff --git a/libvcell/model_utils.py b/libvcell/model_utils.py index 65ac76f..dd31371 100644 --- a/libvcell/model_utils.py +++ b/libvcell/model_utils.py @@ -3,7 +3,9 @@ from libvcell._internal.native_calls import ReturnValue, VCellNativeCalls -def vcml_to_sbml(vcml_content: str, application_name: str, sbml_file_path: Path) -> tuple[bool, str]: +def vcml_to_sbml( + vcml_content: str, application_name: str, sbml_file_path: Path, round_trip_validation: bool +) -> tuple[bool, str]: """ Convert VCML content to SBML file @@ -16,7 +18,12 @@ def vcml_to_sbml(vcml_content: str, application_name: str, sbml_file_path: Path) tuple[bool, str]: A tuple containing the success status and a message """ native = VCellNativeCalls() - return_value: ReturnValue = native.vcml_to_sbml(vcml_content, application_name, sbml_file_path) + return_value: ReturnValue = native.vcml_to_sbml( + vcml_content=vcml_content, + application_name=application_name, + sbml_file_path=sbml_file_path, + round_trip_validation=round_trip_validation, + ) return return_value.success, return_value.message @@ -32,7 +39,7 @@ def sbml_to_vcml(sbml_content: str, vcml_file_path: Path) -> tuple[bool, str]: tuple[bool, str]: A tuple containing the success status and a message """ native = VCellNativeCalls() - return_value: ReturnValue = native.sbml_to_vcml(sbml_content, vcml_file_path) + return_value: ReturnValue = native.sbml_to_vcml(sbml_content=sbml_content, vcml_file_path=vcml_file_path) return return_value.success, return_value.message @@ -48,5 +55,5 @@ def vcml_to_vcml(vcml_content: str, vcml_file_path: Path) -> tuple[bool, str]: tuple[bool, str]: A tuple containing the success status and a message """ native = VCellNativeCalls() - return_value: ReturnValue = native.vcml_to_vcml(vcml_content, vcml_file_path) + return_value: ReturnValue = native.vcml_to_vcml(vcml_content=vcml_content, vcml_file_path=vcml_file_path) return return_value.success, return_value.message diff --git a/tests/test_libvcell.py b/tests/test_libvcell.py index 850d329..2497aee 100644 --- a/tests/test_libvcell.py +++ b/tests/test_libvcell.py @@ -33,13 +33,32 @@ def test_sbml_to_vcml(sbml_file_path: Path) -> None: assert msg == "Success" -def test_vcml_to_sbml(vcml_file_path: Path, vcml_app_name: str) -> None: +def test_vcml_to_sbml_with_validation(vcml_file_path: Path, vcml_app_name: str) -> None: vcml_content = vcml_file_path.read_text() with tempfile.TemporaryDirectory() as temp_dir: temp_output_dir = Path(temp_dir) sbml_file_path = temp_output_dir / "test.sbml" success, msg = vcml_to_sbml( - vcml_content=vcml_content, application_name=vcml_app_name, sbml_file_path=sbml_file_path + vcml_content=vcml_content, + application_name=vcml_app_name, + sbml_file_path=sbml_file_path, + round_trip_validation=True, + ) + assert sbml_file_path.exists() + assert success is True + assert msg == "Success" + + +def test_vcml_to_sbml_without_validation(vcml_file_path: Path, vcml_app_name: str) -> None: + vcml_content = vcml_file_path.read_text() + with tempfile.TemporaryDirectory() as temp_dir: + temp_output_dir = Path(temp_dir) + sbml_file_path = temp_output_dir / "test.sbml" + success, msg = vcml_to_sbml( + vcml_content=vcml_content, + application_name=vcml_app_name, + sbml_file_path=sbml_file_path, + round_trip_validation=False, ) assert sbml_file_path.exists() assert success is True diff --git a/vcell-native/src/main/java/org/vcell/libvcell/Entrypoints.java b/vcell-native/src/main/java/org/vcell/libvcell/Entrypoints.java index c32d971..1c94fb0 100644 --- a/vcell-native/src/main/java/org/vcell/libvcell/Entrypoints.java +++ b/vcell-native/src/main/java/org/vcell/libvcell/Entrypoints.java @@ -125,13 +125,15 @@ public static CCharPointer entrypoint_vcmlToSbml( IsolateThread ignoredThread, CCharPointer vcml_content, CCharPointer application_name, - CCharPointer sbml_file_path) { + CCharPointer sbml_file_path, + int roundTripValidation) { ReturnValue returnValue; try { String vcmlContentStr = CTypeConversion.toJavaString(vcml_content); String applicationName = CTypeConversion.toJavaString(application_name); Path sbmlFilePath = new File(CTypeConversion.toJavaString(sbml_file_path)).toPath(); - vcml_to_sbml(vcmlContentStr, applicationName, sbmlFilePath); + boolean bRoundTripValidation = CTypeConversion.toBoolean(roundTripValidation); + vcml_to_sbml(vcmlContentStr, applicationName, sbmlFilePath, bRoundTripValidation); returnValue = new ReturnValue(true, "Success"); }catch (Throwable t) { logger.error("Error translating vcml application to sbml", t); diff --git a/vcell-native/src/main/java/org/vcell/libvcell/MainRecorder.java b/vcell-native/src/main/java/org/vcell/libvcell/MainRecorder.java index 6fdd780..c6379d1 100644 --- a/vcell-native/src/main/java/org/vcell/libvcell/MainRecorder.java +++ b/vcell-native/src/main/java/org/vcell/libvcell/MainRecorder.java @@ -70,7 +70,7 @@ public static void main(String[] args) { // create a temporary file for the SBML output File temp_sbml_file = new File(output_dir, "temp.vcml"); - vcml_to_sbml(vcml_str, vcml_app_name, temp_sbml_file.toPath()); + vcml_to_sbml(vcml_str, vcml_app_name, temp_sbml_file.toPath(), true); // remove temporary file if (temp_sbml_file.exists()) { boolean deleted = temp_sbml_file.delete(); diff --git a/vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java b/vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java index 55d6ccc..942ae16 100644 --- a/vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java +++ b/vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java @@ -1,19 +1,25 @@ package org.vcell.libvcell; +import cbit.image.ImageException; import cbit.util.xml.VCLogger; import cbit.util.xml.VCLoggerException; import cbit.util.xml.XmlUtil; import cbit.vcell.biomodel.BioModel; +import cbit.vcell.biomodel.ModelUnitConverter; +import cbit.vcell.geometry.GeometryException; import cbit.vcell.geometry.GeometrySpec; import cbit.vcell.mapping.MappingException; import cbit.vcell.mapping.SimulationContext; import cbit.vcell.mongodb.VCMongoMessage; +import cbit.vcell.parser.ExpressionException; import cbit.vcell.xml.XMLSource; import cbit.vcell.xml.XmlHelper; import cbit.vcell.xml.XmlParseException; import org.vcell.sbml.SbmlException; +import org.vcell.sbml.vcell.SBMLAnnotationUtil; import org.vcell.sbml.vcell.SBMLExporter; import org.vcell.sbml.vcell.SBMLImporter; +import org.vcell.util.Pair; import javax.xml.stream.XMLStreamException; import java.io.ByteArrayInputStream; @@ -66,17 +72,43 @@ record LoggerMessage(VCLogger.Priority priority, VCLogger.ErrorType errorType, S } - public static void vcml_to_sbml(String vcml_content, String applicationName, Path sbmlPath) - throws XmlParseException, IOException, XMLStreamException, SbmlException, MappingException { + public static void vcml_to_sbml(String vcml_content, String applicationName, Path sbmlPath, boolean roundTripValidation) + throws XmlParseException, IOException, XMLStreamException, SbmlException, MappingException, ImageException, GeometryException, ExpressionException { GeometrySpec.avoidAWTImageCreation = true; VCMongoMessage.enabled = false; BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml_content)); bioModel.updateAll(false); - SimulationContext simContext = bioModel.getSimulationContext(applicationName); - boolean validateSBML = true; - SBMLExporter sbmlExporter = new SBMLExporter(simContext, 3, 1, validateSBML); + + if (applicationName == null || applicationName.isEmpty()) { + throw new RuntimeException("Error: Application name is null or empty"); + } + + if (bioModel.getSimulationContext(applicationName) == null) { + throw new RuntimeException("Error: Simulation context not found for application name: " + applicationName); + } + + // change the unit system to SBML preferred units if not already. + final BioModel sbmlPreferredUnitsBM; + if (!bioModel.getModel().getUnitSystem().compareEqual(ModelUnitConverter.createSbmlModelUnitSystem())) { + sbmlPreferredUnitsBM = ModelUnitConverter.createBioModelWithSBMLUnitSystem(bioModel); + if(sbmlPreferredUnitsBM == null) { + throw new RuntimeException("Unable to clone BioModel with SBML unit system"); + } + } else { + sbmlPreferredUnitsBM = bioModel; + } + + SimulationContext simContext = sbmlPreferredUnitsBM.getSimulationContext(applicationName); + + int sbml_level = 3; + int sbml_version = 1; + SBMLExporter sbmlExporter = new SBMLExporter(simContext, sbml_level, sbml_version, roundTripValidation); String sbml_string = sbmlExporter.getSBMLString(); + + // cleanup the string of all the "sameAs" statements + sbml_string = SBMLAnnotationUtil.postProcessCleanup(sbml_string); + XmlUtil.writeXMLStringToFile(sbml_string, sbmlPath.toFile().getAbsolutePath(), true); } diff --git a/vcell-native/src/test/java/org/vcell/libvcell/ModelEntrypointsTest.java b/vcell-native/src/test/java/org/vcell/libvcell/ModelEntrypointsTest.java index b6ae444..0743163 100644 --- a/vcell-native/src/test/java/org/vcell/libvcell/ModelEntrypointsTest.java +++ b/vcell-native/src/test/java/org/vcell/libvcell/ModelEntrypointsTest.java @@ -1,7 +1,10 @@ package org.vcell.libvcell; +import cbit.image.ImageException; import cbit.util.xml.VCLoggerException; +import cbit.vcell.geometry.GeometryException; import cbit.vcell.mapping.MappingException; +import cbit.vcell.parser.ExpressionException; import cbit.vcell.xml.XmlParseException; import org.junit.jupiter.api.Test; import org.vcell.sbml.SbmlException; @@ -26,13 +29,25 @@ public void test_sbml_to_vcml() throws MappingException, IOException, XmlParseEx } @Test - public void test_vcml_to_sbml() throws MappingException, IOException, XmlParseException, XMLStreamException, SbmlException { + public void test_vcml_to_sbml_with_round_trip() throws MappingException, IOException, XmlParseException, XMLStreamException, SbmlException, ImageException, GeometryException, ExpressionException { String vcmlContent = getFileContentsAsString("/TinySpatialProject_Application0.vcml"); File parent_dir = Files.createTempDirectory("vcmlToSbml").toFile(); - File sbml_temp_file = new File(parent_dir, "temp.sbml"); String applicationName = "unnamed_spatialGeom"; - vcml_to_sbml(vcmlContent, applicationName, sbml_temp_file.toPath()); - assert(sbml_temp_file.exists()); + + File sbml_temp_file_true = new File(parent_dir, "temp_true.sbml"); + vcml_to_sbml(vcmlContent, applicationName, sbml_temp_file_true.toPath(), true); + assert(sbml_temp_file_true.exists()); + } + + @Test + public void test_vcml_to_sbml_without_round_trip() throws MappingException, IOException, XmlParseException, XMLStreamException, SbmlException, ImageException, GeometryException, ExpressionException { + String vcmlContent = getFileContentsAsString("/TinySpatialProject_Application0.vcml"); + File parent_dir = Files.createTempDirectory("vcmlToSbml").toFile(); + String applicationName = "unnamed_spatialGeom"; + + File sbml_temp_file_false = new File(parent_dir, "temp_false.sbml"); + vcml_to_sbml(vcmlContent, applicationName, sbml_temp_file_false.toPath(), false); + assert(sbml_temp_file_false.exists()); } @Test From 30f4341c3da8086330ac7f497889dd8c33b015d6 Mon Sep 17 00:00:00 2001 From: jcschaff Date: Fri, 4 Apr 2025 19:12:25 -0400 Subject: [PATCH 2/2] bump version to 0.0.11 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e8b84ac..c9e0bcc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "libvcell" -version = "0.0.10" +version = "0.0.11" description = "This is a python package which wraps a subset of VCell Java code as a native python package." authors = ["Jim Schaff ", "Ezequiel Valencia "] repository = "https://github.com/virtualcell/libvcell"