From 5b8b0c7bb5c6a91cbf6357ffaa92c360431c76dc Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Fri, 27 Feb 2026 15:45:45 -0500 Subject: [PATCH 1/4] Added python infix support --- build.py | 33 ++++++++++++++ libvcell/__init__.py | 11 ++++- libvcell/_internal/native_calls.py | 43 ++++++++++++++++++ libvcell/_internal/native_utils.py | 8 ++++ libvcell/model_utils.py | 18 +++++++- scripts/local_build_native.sh | 2 - tests/test_libvcell.py | 18 +++++++- .../java/org/vcell/libvcell/Entrypoints.java | 45 +++++++++++++++++-- .../java/org/vcell/libvcell/ModelUtils.java | 10 +++-- .../java/org/vcell/libvcell/SolverUtils.java | 2 - .../vcell/libvcell/ModelEntrypointsTest.java | 15 +++++++ 11 files changed, 191 insertions(+), 14 deletions(-) diff --git a/build.py b/build.py index a2a2e21..afe1bfa 100644 --- a/build.py +++ b/build.py @@ -21,12 +21,37 @@ def main() -> None: libvcell_lib_dir.mkdir(parents=True, exist_ok=True) # Build VCell Java project from submodule + install_message_1: str = """ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + * * + * Building Original VCell... * + * * +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + """.strip() + print(install_message_1) run_command("mvn --batch-mode clean install -DskipTests", cwd=vcell_submodule_dir) # Build vcell-native as Java + install_message_2: str = """ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + * * + * Building Lib VCell... * + * * +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + """.strip() + print(install_message_2) run_command("mvn --batch-mode clean install", cwd=vcell_native_dir) # Run with native-image-agent to record configuration for native-image + install_message_3: str = """ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + * * + * Run with native-image-agent... * + * * +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + """.strip() + print(install_message_3) + run_command("echo $(which java) ", cwd=vcell_native_dir) run_command( "java -agentlib:native-image-agent=config-output-dir=target/recording " "-jar target/vcell-native-1.0-SNAPSHOT.jar " @@ -39,6 +64,14 @@ def main() -> None: ) # Build vcell-native as native shared object library + install_message_4: str = """ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + * * + * Rebuild as shared DLL... * + * * +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + """.strip() + print(install_message_4) run_command("mvn --batch-mode package -P shared-dll", cwd=vcell_native_dir) # Copy the shared library to libvcell/lib diff --git a/libvcell/__init__.py b/libvcell/__init__.py index 820bdb8..819f52f 100644 --- a/libvcell/__init__.py +++ b/libvcell/__init__.py @@ -1,4 +1,11 @@ -from libvcell.model_utils import sbml_to_vcml, vcml_to_sbml, vcml_to_vcml +from libvcell.model_utils import sbml_to_vcml, vcell_infix_to_python_infix, vcml_to_sbml, vcml_to_vcml from libvcell.solver_utils import sbml_to_finite_volume_input, vcml_to_finite_volume_input -__all__ = ["vcml_to_finite_volume_input", "sbml_to_finite_volume_input", "sbml_to_vcml", "vcml_to_sbml", "vcml_to_vcml"] +__all__ = [ + "vcml_to_finite_volume_input", + "sbml_to_finite_volume_input", + "sbml_to_vcml", + "vcml_to_sbml", + "vcml_to_vcml", + "vcell_infix_to_python_infix", +] diff --git a/libvcell/_internal/native_calls.py b/libvcell/_internal/native_calls.py index 180d8ac..1007364 100644 --- a/libvcell/_internal/native_calls.py +++ b/libvcell/_internal/native_calls.py @@ -12,6 +12,11 @@ class ReturnValue(BaseModel): message: str +class MutableString: + def __init__(self, value: str): + self.value: str = value + + class VCellNativeCalls: def __init__(self) -> None: self.loader = VCellNativeLibraryLoader() @@ -122,3 +127,41 @@ def vcml_to_vcml(self, vcml_content: str, vcml_file_path: Path) -> ReturnValue: except Exception as e: logging.exception("Error in vcml_to_vcml()", exc_info=e) raise + + def vcell_infix_to_python_infix( + self, vcell_infix: str, target_python_infix: MutableString, buffer_size: int | None = None + ) -> ReturnValue: + try: + needed_buffer_size = int(1.5 * len(vcell_infix)) if buffer_size is None else buffer_size + buff = ctypes.create_string_buffer(needed_buffer_size) + with IsolateManager(self.lib) as isolate_thread: + json_ptr = self.lib.vcellInfixToPythonInfix( + isolate_thread, ctypes.c_char_p(vcell_infix.encode("utf-8")), buff, needed_buffer_size + ) + value: bytes | None = ctypes.cast(json_ptr, ctypes.c_char_p).value + if value is None: + logging.error("Failed to regenerate vcml") + return ReturnValue(success=False, message="Failed to generate python infix") + json_str = value.decode("utf-8") + if "not enough room, need: `" in json_str: + if buffer_size is not None: + logging.error("Failed to identify correct buffer size reported by previous error") + return ReturnValue( + success=False, message="Failed to identify correct buffer size reported by previous error" + ) + # get the size from the error + index = json_str.find("not enough room, need: `") + len("not enough room, need: `") + end_index = json_str.find("`", index) + size_as_string: str = json_str[index:end_index] + if not size_as_string.isnumeric(): + logging.error("Buffer size reported by previous error is not an integer!") + return ReturnValue( + success=False, message="Buffer size reported by previous error is not an integer!" + ) + return self.vcell_infix_to_python_infix(vcell_infix, target_python_infix, int(size_as_string)) + # self.lib.freeString(json_ptr) + target_python_infix.value = buff.value.decode("utf-8") + return ReturnValue.model_validate_json(json_data=json_str) + except Exception as e: + logging.exception("Error in vcell_infix_to_python_infix()", exc_info=e) + raise diff --git a/libvcell/_internal/native_utils.py b/libvcell/_internal/native_utils.py index d4990a8..3208b66 100644 --- a/libvcell/_internal/native_utils.py +++ b/libvcell/_internal/native_utils.py @@ -53,6 +53,14 @@ def _define_entry_points(self) -> None: self.lib.vcmlToVcml.restype = ctypes.c_char_p self.lib.vcmlToVcml.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p] + self.lib.vcellInfixToPythonInfix.restype = ctypes.c_char_p + self.lib.vcellInfixToPythonInfix.argtypes = [ + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_char_p, + ctypes.c_longlong, + ] + self.lib.freeString.restype = None self.lib.freeString.argtypes = [ctypes.c_char_p] diff --git a/libvcell/model_utils.py b/libvcell/model_utils.py index dd31371..ef61e7e 100644 --- a/libvcell/model_utils.py +++ b/libvcell/model_utils.py @@ -1,6 +1,6 @@ from pathlib import Path -from libvcell._internal.native_calls import ReturnValue, VCellNativeCalls +from libvcell._internal.native_calls import MutableString, ReturnValue, VCellNativeCalls def vcml_to_sbml( @@ -57,3 +57,19 @@ def vcml_to_vcml(vcml_content: str, vcml_file_path: Path) -> tuple[bool, str]: native = VCellNativeCalls() return_value: ReturnValue = native.vcml_to_vcml(vcml_content=vcml_content, vcml_file_path=vcml_file_path) return return_value.success, return_value.message + + +def vcell_infix_to_python_infix(vcell_infix: str) -> tuple[bool, str, str]: + """ + Converts an infix string version of a VCell Native Expression, and converts it to a Python compatible version + + Args: + vcell_infix (str): the infix to convert + + Returns: + tuple[bool, str, str]: A tuple containing the success status, a message, and the converted infix + """ + native = VCellNativeCalls() + target_python_infix = MutableString("") + return_value: ReturnValue = native.vcell_infix_to_python_infix(vcell_infix, target_python_infix) + return return_value.success, return_value.message, target_python_infix.value diff --git a/scripts/local_build_native.sh b/scripts/local_build_native.sh index 5c582ce..9d6175d 100755 --- a/scripts/local_build_native.sh +++ b/scripts/local_build_native.sh @@ -11,8 +11,6 @@ echo "ROOT_DIR: $ROOT_DIR" cd "$ROOT_DIR"/vcell_submodule || ( echo "'vcell' directory not found" && exit 1 ) mvn clean install -DskipTests -export JAVA_HOME=$(jenv javahome) - # test if JAVA_HOME is set if [ -z "$JAVA_HOME" ]; then echo "JAVA_HOME is not set" diff --git a/tests/test_libvcell.py b/tests/test_libvcell.py index 2497aee..904c9b2 100644 --- a/tests/test_libvcell.py +++ b/tests/test_libvcell.py @@ -1,7 +1,14 @@ import tempfile from pathlib import Path -from libvcell import sbml_to_finite_volume_input, sbml_to_vcml, vcml_to_finite_volume_input, vcml_to_sbml, vcml_to_vcml +from libvcell import ( + sbml_to_finite_volume_input, + sbml_to_vcml, + vcell_infix_to_python_infix, + vcml_to_finite_volume_input, + vcml_to_sbml, + vcml_to_vcml, +) def test_vcml_to_finite_volume_input(temp_output_dir: Path, vcml_file_path: Path, vcml_sim_name: str) -> None: @@ -74,3 +81,12 @@ def test_vcml_to_vcml(vcml_file_path: Path) -> None: assert vcml_file_path.exists() assert success is True assert msg == "Success" + + +def test_vcell_infix_to_python_infix() -> None: + vcell_infix = "id_1 * csc(id_0 ^ 2.2)" + success, msg, value = vcell_infix_to_python_infix(vcell_infix) + expectedResult = "(id_1 * (1.0/math.sin(((id_0)**(2.2)))))" + assert success is True + assert msg == "Success" + assert value == expectedResult 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 1c94fb0..a344909 100644 --- a/vcell-native/src/main/java/org/vcell/libvcell/Entrypoints.java +++ b/vcell-native/src/main/java/org/vcell/libvcell/Entrypoints.java @@ -1,22 +1,24 @@ package org.vcell.libvcell; +import cbit.vcell.parser.Expression; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.c.function.CEntryPoint; import org.graalvm.nativeimage.c.type.CCharPointer; +import org.graalvm.nativeimage.c.type.CIntPointer; import org.graalvm.nativeimage.c.type.CTypeConversion; +import org.graalvm.word.UnsignedWord; +import org.graalvm.word.WordFactory; import org.json.simple.JSONValue; import java.io.File; import java.nio.file.Path; import java.util.concurrent.ConcurrentHashMap; +import static org.vcell.libvcell.ModelUtils.*; import static org.vcell.libvcell.SolverUtils.sbmlToFiniteVolumeInput; import static org.vcell.libvcell.SolverUtils.vcmlToFiniteVolumeInput; -import static org.vcell.libvcell.ModelUtils.vcml_to_sbml; -import static org.vcell.libvcell.ModelUtils.sbml_to_vcml; -import static org.vcell.libvcell.ModelUtils.vcml_to_vcml; public class Entrypoints { @@ -201,4 +203,41 @@ public static CCharPointer entrypoint_vcmlToVcml( return createString(json); } + @CEntryPoint( + name = "vcellInfixToPythonInfix", + documentation = """ + converts a vcell infix into a python-safe version""" + ) + public static CCharPointer entrypoint_vcellInfixToPythonInfix( + IsolateThread ignoredThread, + CCharPointer vcellInfixPtr, + CCharPointer targetBufferForPythonInfix, + long sizeOfBuffer + ){ + System.err.println("Entrypoint_vcellInfixToPythonInfix"); + ReturnValue returnValue; + try { + String vcellInfix = CTypeConversion.toJavaString(vcellInfixPtr); + String pythonInfix = get_python_infix(vcellInfix); + if (pythonInfix.length() >= sizeOfBuffer){ + // not enough room + returnValue = new ReturnValue(false, "not enough room, need: `" + pythonInfix.length() + 1 + "`"); + } else { + CTypeConversion.toCString( + pythonInfix, + targetBufferForPythonInfix, + WordFactory.unsigned(pythonInfix.length() + 1) + ); + returnValue = new ReturnValue(true, "Success"); + } + } catch (Throwable t) { + logger.error("Error translating vcell infix to python infix", t); + returnValue = new ReturnValue(false, t.getMessage()); + } + // return result as a json string + String json = returnValue.toJson(); + logger.info("Returning from vcellToVcml: " + json); + return createString(json); + } + } 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 fb79f14..d664ddf 100644 --- a/vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java +++ b/vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java @@ -11,6 +11,7 @@ import cbit.vcell.mapping.MappingException; import cbit.vcell.mapping.SimulationContext; import cbit.vcell.mongodb.VCMongoMessage; +import cbit.vcell.parser.Expression; import cbit.vcell.parser.ExpressionException; import cbit.vcell.xml.XMLSource; import cbit.vcell.xml.XmlHelper; @@ -35,7 +36,6 @@ public static void sbml_to_vcml(String sbml_content, Path vcmlPath) GeometrySpec.avoidAWTImageCreation = true; VCMongoMessage.enabled = false; - XmlHelper.cloneUsingXML = true; record LoggerMessage(VCLogger.Priority priority, VCLogger.ErrorType errorType, String message) {}; final ArrayList messages = new ArrayList<>(); @@ -77,7 +77,6 @@ public static void vcml_to_sbml(String vcml_content, String applicationName, Pat throws XmlParseException, IOException, XMLStreamException, SbmlException, MappingException, ImageException, GeometryException, ExpressionException { GeometrySpec.avoidAWTImageCreation = true; VCMongoMessage.enabled = false; - XmlHelper.cloneUsingXML = true; BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml_content)); bioModel.updateAll(false); @@ -117,7 +116,6 @@ public static void vcml_to_sbml(String vcml_content, String applicationName, Pat public static void vcml_to_vcml(String vcml_content, Path vcmlPath) throws XmlParseException, IOException, MappingException { GeometrySpec.avoidAWTImageCreation = true; VCMongoMessage.enabled = false; - XmlHelper.cloneUsingXML = true; BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml_content)); bioModel.updateAll(false); @@ -125,4 +123,10 @@ public static void vcml_to_vcml(String vcml_content, Path vcmlPath) throws XmlPa String vcml_str = XmlHelper.bioModelToXML(bioModel); XmlUtil.writeXMLStringToFile(vcml_str, vcmlPath.toFile().getAbsolutePath(), true); } + + public static String get_python_infix(String vcellInfix) throws ExpressionException { + GeometrySpec.avoidAWTImageCreation = true; + VCMongoMessage.enabled = false; + return new Expression(vcellInfix).infix_Python(); + } } diff --git a/vcell-native/src/main/java/org/vcell/libvcell/SolverUtils.java b/vcell-native/src/main/java/org/vcell/libvcell/SolverUtils.java index b141127..66cc79c 100644 --- a/vcell-native/src/main/java/org/vcell/libvcell/SolverUtils.java +++ b/vcell-native/src/main/java/org/vcell/libvcell/SolverUtils.java @@ -41,7 +41,6 @@ public class SolverUtils { public static void vcmlToFiniteVolumeInput(String vcml_content, String simulation_name, File parentDir, File outputDir) throws XmlParseException, MappingException, SolverException, ExpressionException, MathException { GeometrySpec.avoidAWTImageCreation = true; VCMongoMessage.enabled = false; - XmlHelper.cloneUsingXML = true; if (vcml_content.substring(0, 300).contains(" Date: Fri, 27 Feb 2026 16:06:47 -0500 Subject: [PATCH 2/4] more build config updates --- .github/workflows/on-release-main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/on-release-main.yml b/.github/workflows/on-release-main.yml index bdd2c13..fbcd676 100644 --- a/.github/workflows/on-release-main.yml +++ b/.github/workflows/on-release-main.yml @@ -10,7 +10,7 @@ jobs: build: strategy: matrix: - os: [macos-13, windows-latest, ubuntu-latest, macos-14] + os: [macos-15-intel, windows-latest, ubuntu-latest, macos-15] fail-fast: false runs-on: ${{ matrix.os }} defaults: From a1f1f586dca3fc5b5c253a5dd144c3ecc5be18f9 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Tue, 3 Mar 2026 12:08:45 -0500 Subject: [PATCH 3/4] Logging correction and fail-testing --- tests/test_libvcell.py | 6 ++++++ .../main/java/org/vcell/libvcell/Entrypoints.java | 2 +- .../org/vcell/libvcell/ModelEntrypointsTest.java | 13 +++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/test_libvcell.py b/tests/test_libvcell.py index 904c9b2..cd381bd 100644 --- a/tests/test_libvcell.py +++ b/tests/test_libvcell.py @@ -90,3 +90,9 @@ def test_vcell_infix_to_python_infix() -> None: assert success is True assert msg == "Success" assert value == expectedResult + + +def test_bad_vcell_infix() -> None: + vcellInfix = "id_1 / + / /- cos(/ / /) id_2" + success, msg, value = vcell_infix_to_python_infix(vcellInfix) + assert success is False 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 a344909..eeb11a0 100644 --- a/vcell-native/src/main/java/org/vcell/libvcell/Entrypoints.java +++ b/vcell-native/src/main/java/org/vcell/libvcell/Entrypoints.java @@ -236,7 +236,7 @@ public static CCharPointer entrypoint_vcellInfixToPythonInfix( } // return result as a json string String json = returnValue.toJson(); - logger.info("Returning from vcellToVcml: " + json); + logger.info("Returning from vcellInfixToPythonInfix: " + json); return createString(json); } 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 e960fad..2cc0614 100644 --- a/vcell-native/src/test/java/org/vcell/libvcell/ModelEntrypointsTest.java +++ b/vcell-native/src/test/java/org/vcell/libvcell/ModelEntrypointsTest.java @@ -74,4 +74,17 @@ public void test_get_python_infix(){ assert expected.equals(pythonInfix); } + @Test + public void test_bad_python_infix_attempt(){ + String vcellInfix = "id_1 / + / /- cos(/ / /) id_2"; + String pythonInfix; + try { + pythonInfix = get_python_infix(vcellInfix); + } catch (ExpressionException e) { + return; // this is what we'd expect + } + System.err.println("test_bad_python_infix_attempt did not throw an exception"); + assert(false); + } + } From e1a4df530cbcbf8a3a8e5218f6fdcafbfdf0c188 Mon Sep 17 00:00:00 2001 From: Logan Drescher Date: Tue, 3 Mar 2026 12:22:02 -0500 Subject: [PATCH 4/4] Restoring cloning variable setting --- vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java | 4 ++++ .../src/main/java/org/vcell/libvcell/SolverUtils.java | 2 ++ 2 files changed, 6 insertions(+) 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 d664ddf..a1a960b 100644 --- a/vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java +++ b/vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java @@ -35,6 +35,7 @@ public static void sbml_to_vcml(String sbml_content, Path vcmlPath) throws VCLoggerException, XmlParseException, IOException, MappingException { GeometrySpec.avoidAWTImageCreation = true; + XmlHelper.cloneUsingXML = true; VCMongoMessage.enabled = false; record LoggerMessage(VCLogger.Priority priority, VCLogger.ErrorType errorType, String message) {}; @@ -76,6 +77,7 @@ record LoggerMessage(VCLogger.Priority priority, VCLogger.ErrorType errorType, S 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; + XmlHelper.cloneUsingXML = true; VCMongoMessage.enabled = false; BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml_content)); @@ -115,6 +117,7 @@ public static void vcml_to_sbml(String vcml_content, String applicationName, Pat public static void vcml_to_vcml(String vcml_content, Path vcmlPath) throws XmlParseException, IOException, MappingException { GeometrySpec.avoidAWTImageCreation = true; + XmlHelper.cloneUsingXML = true; VCMongoMessage.enabled = false; BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml_content)); @@ -126,6 +129,7 @@ public static void vcml_to_vcml(String vcml_content, Path vcmlPath) throws XmlPa public static String get_python_infix(String vcellInfix) throws ExpressionException { GeometrySpec.avoidAWTImageCreation = true; + XmlHelper.cloneUsingXML = true; VCMongoMessage.enabled = false; return new Expression(vcellInfix).infix_Python(); } diff --git a/vcell-native/src/main/java/org/vcell/libvcell/SolverUtils.java b/vcell-native/src/main/java/org/vcell/libvcell/SolverUtils.java index 66cc79c..f8e7018 100644 --- a/vcell-native/src/main/java/org/vcell/libvcell/SolverUtils.java +++ b/vcell-native/src/main/java/org/vcell/libvcell/SolverUtils.java @@ -40,6 +40,7 @@ public class SolverUtils { public static void vcmlToFiniteVolumeInput(String vcml_content, String simulation_name, File parentDir, File outputDir) throws XmlParseException, MappingException, SolverException, ExpressionException, MathException { GeometrySpec.avoidAWTImageCreation = true; + XmlHelper.cloneUsingXML = true; VCMongoMessage.enabled = false; if (vcml_content.substring(0, 300).contains("