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: 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..cd381bd 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,18 @@ 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 + + +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 1c94fb0..eeb11a0 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 vcellInfixToPythonInfix: " + 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..a1a960b 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; @@ -34,8 +35,8 @@ 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; - XmlHelper.cloneUsingXML = true; record LoggerMessage(VCLogger.Priority priority, VCLogger.ErrorType errorType, String message) {}; final ArrayList messages = new ArrayList<>(); @@ -76,8 +77,8 @@ 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; - XmlHelper.cloneUsingXML = true; BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml_content)); bioModel.updateAll(false); @@ -116,8 +117,8 @@ 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; - XmlHelper.cloneUsingXML = true; BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml_content)); bioModel.updateAll(false); @@ -125,4 +126,11 @@ 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; + 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 b141127..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,8 +40,8 @@ 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; - XmlHelper.cloneUsingXML = true; if (vcml_content.substring(0, 300).contains("