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
2 changes: 1 addition & 1 deletion .github/workflows/on-release-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
33 changes: 33 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand All @@ -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
Expand Down
11 changes: 9 additions & 2 deletions libvcell/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
43 changes: 43 additions & 0 deletions libvcell/_internal/native_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
8 changes: 8 additions & 0 deletions libvcell/_internal/native_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
18 changes: 17 additions & 1 deletion libvcell/model_utils.py
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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
2 changes: 0 additions & 2 deletions scripts/local_build_native.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
24 changes: 23 additions & 1 deletion tests/test_libvcell.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
45 changes: 42 additions & 3 deletions vcell-native/src/main/java/org/vcell/libvcell/Entrypoints.java
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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);
}

}
14 changes: 11 additions & 3 deletions vcell-native/src/main/java/org/vcell/libvcell/ModelUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<LoggerMessage> messages = new ArrayList<>();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -116,13 +117,20 @@ 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);
// write the BioModel to a VCML file
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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("<sbml xmlns=\"http://www.sbml.org/sbml")) {
throw new IllegalArgumentException("expecting VCML content, not SBML");
Expand Down Expand Up @@ -135,8 +135,8 @@ private static FieldDataIdentifierSpec[] getFieldDataIdentifierSpecs(Simulation

public static void sbmlToFiniteVolumeInput(String sbml_content, File outputDir) throws MappingException, PropertyVetoException, SolverException, ExpressionException, VCLoggerException {
GeometrySpec.avoidAWTImageCreation = true;
XmlHelper.cloneUsingXML = true;
VCMongoMessage.enabled = false;
XmlHelper.cloneUsingXML = true;

SBMLExporter.MemoryVCLogger vcl = new SBMLExporter.MemoryVCLogger();
boolean bValidateSBML = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,32 @@ public void test_vcml_to_vcml() throws MappingException, IOException, XmlParseEx
assert(vcml_temp_file.exists());
}

@Test
public void test_get_python_infix(){
String vcellInfix = "id_1 * csc(id_0 ^ 2.2)";
String pythonInfix;
try {
pythonInfix = get_python_infix(vcellInfix);
} catch (ExpressionException e) {
System.err.println("get_python_infix exception: " + e.getMessage());
assert(false);
return;
}
String expected = "(id_1 * (1.0/math.sin(((id_0)**(2.2)))))";
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);
}

}