Skip to content

Commit 1fa96c6

Browse files
Merge pull request #11 from virtualcell/AddedPythonInfix
Added python infix functionality
2 parents b108ccf + e1a4df5 commit 1fa96c6

12 files changed

Lines changed: 217 additions & 15 deletions

File tree

.github/workflows/on-release-main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
build:
1111
strategy:
1212
matrix:
13-
os: [macos-13, windows-latest, ubuntu-latest, macos-14]
13+
os: [macos-15-intel, windows-latest, ubuntu-latest, macos-15]
1414
fail-fast: false
1515
runs-on: ${{ matrix.os }}
1616
defaults:

build.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,37 @@ def main() -> None:
2121
libvcell_lib_dir.mkdir(parents=True, exist_ok=True)
2222

2323
# Build VCell Java project from submodule
24+
install_message_1: str = """
25+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
26+
* *
27+
* Building Original VCell... *
28+
* *
29+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
30+
""".strip()
31+
print(install_message_1)
2432
run_command("mvn --batch-mode clean install -DskipTests", cwd=vcell_submodule_dir)
2533

2634
# Build vcell-native as Java
35+
install_message_2: str = """
36+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
37+
* *
38+
* Building Lib VCell... *
39+
* *
40+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
41+
""".strip()
42+
print(install_message_2)
2743
run_command("mvn --batch-mode clean install", cwd=vcell_native_dir)
2844

2945
# Run with native-image-agent to record configuration for native-image
46+
install_message_3: str = """
47+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
48+
* *
49+
* Run with native-image-agent... *
50+
* *
51+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
52+
""".strip()
53+
print(install_message_3)
54+
run_command("echo $(which java) ", cwd=vcell_native_dir)
3055
run_command(
3156
"java -agentlib:native-image-agent=config-output-dir=target/recording "
3257
"-jar target/vcell-native-1.0-SNAPSHOT.jar "
@@ -39,6 +64,14 @@ def main() -> None:
3964
)
4065

4166
# Build vcell-native as native shared object library
67+
install_message_4: str = """
68+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
69+
* *
70+
* Rebuild as shared DLL... *
71+
* *
72+
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
73+
""".strip()
74+
print(install_message_4)
4275
run_command("mvn --batch-mode package -P shared-dll", cwd=vcell_native_dir)
4376

4477
# Copy the shared library to libvcell/lib

libvcell/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
from libvcell.model_utils import sbml_to_vcml, vcml_to_sbml, vcml_to_vcml
1+
from libvcell.model_utils import sbml_to_vcml, vcell_infix_to_python_infix, vcml_to_sbml, vcml_to_vcml
22
from libvcell.solver_utils import sbml_to_finite_volume_input, vcml_to_finite_volume_input
33

4-
__all__ = ["vcml_to_finite_volume_input", "sbml_to_finite_volume_input", "sbml_to_vcml", "vcml_to_sbml", "vcml_to_vcml"]
4+
__all__ = [
5+
"vcml_to_finite_volume_input",
6+
"sbml_to_finite_volume_input",
7+
"sbml_to_vcml",
8+
"vcml_to_sbml",
9+
"vcml_to_vcml",
10+
"vcell_infix_to_python_infix",
11+
]

libvcell/_internal/native_calls.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ class ReturnValue(BaseModel):
1212
message: str
1313

1414

15+
class MutableString:
16+
def __init__(self, value: str):
17+
self.value: str = value
18+
19+
1520
class VCellNativeCalls:
1621
def __init__(self) -> None:
1722
self.loader = VCellNativeLibraryLoader()
@@ -122,3 +127,41 @@ def vcml_to_vcml(self, vcml_content: str, vcml_file_path: Path) -> ReturnValue:
122127
except Exception as e:
123128
logging.exception("Error in vcml_to_vcml()", exc_info=e)
124129
raise
130+
131+
def vcell_infix_to_python_infix(
132+
self, vcell_infix: str, target_python_infix: MutableString, buffer_size: int | None = None
133+
) -> ReturnValue:
134+
try:
135+
needed_buffer_size = int(1.5 * len(vcell_infix)) if buffer_size is None else buffer_size
136+
buff = ctypes.create_string_buffer(needed_buffer_size)
137+
with IsolateManager(self.lib) as isolate_thread:
138+
json_ptr = self.lib.vcellInfixToPythonInfix(
139+
isolate_thread, ctypes.c_char_p(vcell_infix.encode("utf-8")), buff, needed_buffer_size
140+
)
141+
value: bytes | None = ctypes.cast(json_ptr, ctypes.c_char_p).value
142+
if value is None:
143+
logging.error("Failed to regenerate vcml")
144+
return ReturnValue(success=False, message="Failed to generate python infix")
145+
json_str = value.decode("utf-8")
146+
if "not enough room, need: `" in json_str:
147+
if buffer_size is not None:
148+
logging.error("Failed to identify correct buffer size reported by previous error")
149+
return ReturnValue(
150+
success=False, message="Failed to identify correct buffer size reported by previous error"
151+
)
152+
# get the size from the error
153+
index = json_str.find("not enough room, need: `") + len("not enough room, need: `")
154+
end_index = json_str.find("`", index)
155+
size_as_string: str = json_str[index:end_index]
156+
if not size_as_string.isnumeric():
157+
logging.error("Buffer size reported by previous error is not an integer!")
158+
return ReturnValue(
159+
success=False, message="Buffer size reported by previous error is not an integer!"
160+
)
161+
return self.vcell_infix_to_python_infix(vcell_infix, target_python_infix, int(size_as_string))
162+
# self.lib.freeString(json_ptr)
163+
target_python_infix.value = buff.value.decode("utf-8")
164+
return ReturnValue.model_validate_json(json_data=json_str)
165+
except Exception as e:
166+
logging.exception("Error in vcell_infix_to_python_infix()", exc_info=e)
167+
raise

libvcell/_internal/native_utils.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ def _define_entry_points(self) -> None:
5353
self.lib.vcmlToVcml.restype = ctypes.c_char_p
5454
self.lib.vcmlToVcml.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p]
5555

56+
self.lib.vcellInfixToPythonInfix.restype = ctypes.c_char_p
57+
self.lib.vcellInfixToPythonInfix.argtypes = [
58+
ctypes.c_void_p,
59+
ctypes.c_char_p,
60+
ctypes.c_char_p,
61+
ctypes.c_longlong,
62+
]
63+
5664
self.lib.freeString.restype = None
5765
self.lib.freeString.argtypes = [ctypes.c_char_p]
5866

libvcell/model_utils.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from pathlib import Path
22

3-
from libvcell._internal.native_calls import ReturnValue, VCellNativeCalls
3+
from libvcell._internal.native_calls import MutableString, ReturnValue, VCellNativeCalls
44

55

66
def vcml_to_sbml(
@@ -57,3 +57,19 @@ def vcml_to_vcml(vcml_content: str, vcml_file_path: Path) -> tuple[bool, str]:
5757
native = VCellNativeCalls()
5858
return_value: ReturnValue = native.vcml_to_vcml(vcml_content=vcml_content, vcml_file_path=vcml_file_path)
5959
return return_value.success, return_value.message
60+
61+
62+
def vcell_infix_to_python_infix(vcell_infix: str) -> tuple[bool, str, str]:
63+
"""
64+
Converts an infix string version of a VCell Native Expression, and converts it to a Python compatible version
65+
66+
Args:
67+
vcell_infix (str): the infix to convert
68+
69+
Returns:
70+
tuple[bool, str, str]: A tuple containing the success status, a message, and the converted infix
71+
"""
72+
native = VCellNativeCalls()
73+
target_python_infix = MutableString("")
74+
return_value: ReturnValue = native.vcell_infix_to_python_infix(vcell_infix, target_python_infix)
75+
return return_value.success, return_value.message, target_python_infix.value

scripts/local_build_native.sh

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ echo "ROOT_DIR: $ROOT_DIR"
1111
cd "$ROOT_DIR"/vcell_submodule || ( echo "'vcell' directory not found" && exit 1 )
1212
mvn clean install -DskipTests
1313

14-
export JAVA_HOME=$(jenv javahome)
15-
1614
# test if JAVA_HOME is set
1715
if [ -z "$JAVA_HOME" ]; then
1816
echo "JAVA_HOME is not set"

tests/test_libvcell.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import tempfile
22
from pathlib import Path
33

4-
from libvcell import sbml_to_finite_volume_input, sbml_to_vcml, vcml_to_finite_volume_input, vcml_to_sbml, vcml_to_vcml
4+
from libvcell import (
5+
sbml_to_finite_volume_input,
6+
sbml_to_vcml,
7+
vcell_infix_to_python_infix,
8+
vcml_to_finite_volume_input,
9+
vcml_to_sbml,
10+
vcml_to_vcml,
11+
)
512

613

714
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:
7481
assert vcml_file_path.exists()
7582
assert success is True
7683
assert msg == "Success"
84+
85+
86+
def test_vcell_infix_to_python_infix() -> None:
87+
vcell_infix = "id_1 * csc(id_0 ^ 2.2)"
88+
success, msg, value = vcell_infix_to_python_infix(vcell_infix)
89+
expectedResult = "(id_1 * (1.0/math.sin(((id_0)**(2.2)))))"
90+
assert success is True
91+
assert msg == "Success"
92+
assert value == expectedResult
93+
94+
95+
def test_bad_vcell_infix() -> None:
96+
vcellInfix = "id_1 / + / /- cos(/ / /) id_2"
97+
success, msg, value = vcell_infix_to_python_infix(vcellInfix)
98+
assert success is False

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

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
package org.vcell.libvcell;
22

3+
import cbit.vcell.parser.Expression;
34
import org.apache.logging.log4j.LogManager;
45
import org.apache.logging.log4j.Logger;
56
import org.graalvm.nativeimage.IsolateThread;
67
import org.graalvm.nativeimage.c.function.CEntryPoint;
78
import org.graalvm.nativeimage.c.type.CCharPointer;
9+
import org.graalvm.nativeimage.c.type.CIntPointer;
810
import org.graalvm.nativeimage.c.type.CTypeConversion;
11+
import org.graalvm.word.UnsignedWord;
12+
import org.graalvm.word.WordFactory;
913
import org.json.simple.JSONValue;
1014

1115
import java.io.File;
1216
import java.nio.file.Path;
1317
import java.util.concurrent.ConcurrentHashMap;
1418

19+
import static org.vcell.libvcell.ModelUtils.*;
1520
import static org.vcell.libvcell.SolverUtils.sbmlToFiniteVolumeInput;
1621
import static org.vcell.libvcell.SolverUtils.vcmlToFiniteVolumeInput;
17-
import static org.vcell.libvcell.ModelUtils.vcml_to_sbml;
18-
import static org.vcell.libvcell.ModelUtils.sbml_to_vcml;
19-
import static org.vcell.libvcell.ModelUtils.vcml_to_vcml;
2022

2123

2224
public class Entrypoints {
@@ -201,4 +203,41 @@ public static CCharPointer entrypoint_vcmlToVcml(
201203
return createString(json);
202204
}
203205

206+
@CEntryPoint(
207+
name = "vcellInfixToPythonInfix",
208+
documentation = """
209+
converts a vcell infix into a python-safe version"""
210+
)
211+
public static CCharPointer entrypoint_vcellInfixToPythonInfix(
212+
IsolateThread ignoredThread,
213+
CCharPointer vcellInfixPtr,
214+
CCharPointer targetBufferForPythonInfix,
215+
long sizeOfBuffer
216+
){
217+
System.err.println("Entrypoint_vcellInfixToPythonInfix");
218+
ReturnValue returnValue;
219+
try {
220+
String vcellInfix = CTypeConversion.toJavaString(vcellInfixPtr);
221+
String pythonInfix = get_python_infix(vcellInfix);
222+
if (pythonInfix.length() >= sizeOfBuffer){
223+
// not enough room
224+
returnValue = new ReturnValue(false, "not enough room, need: `" + pythonInfix.length() + 1 + "`");
225+
} else {
226+
CTypeConversion.toCString(
227+
pythonInfix,
228+
targetBufferForPythonInfix,
229+
WordFactory.unsigned(pythonInfix.length() + 1)
230+
);
231+
returnValue = new ReturnValue(true, "Success");
232+
}
233+
} catch (Throwable t) {
234+
logger.error("Error translating vcell infix to python infix", t);
235+
returnValue = new ReturnValue(false, t.getMessage());
236+
}
237+
// return result as a json string
238+
String json = returnValue.toJson();
239+
logger.info("Returning from vcellInfixToPythonInfix: " + json);
240+
return createString(json);
241+
}
242+
204243
}

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import cbit.vcell.mapping.MappingException;
1212
import cbit.vcell.mapping.SimulationContext;
1313
import cbit.vcell.mongodb.VCMongoMessage;
14+
import cbit.vcell.parser.Expression;
1415
import cbit.vcell.parser.ExpressionException;
1516
import cbit.vcell.xml.XMLSource;
1617
import cbit.vcell.xml.XmlHelper;
@@ -34,8 +35,8 @@ public static void sbml_to_vcml(String sbml_content, Path vcmlPath)
3435
throws VCLoggerException, XmlParseException, IOException, MappingException {
3536

3637
GeometrySpec.avoidAWTImageCreation = true;
38+
XmlHelper.cloneUsingXML = true;
3739
VCMongoMessage.enabled = false;
38-
XmlHelper.cloneUsingXML = true;
3940

4041
record LoggerMessage(VCLogger.Priority priority, VCLogger.ErrorType errorType, String message) {};
4142
final ArrayList<LoggerMessage> messages = new ArrayList<>();
@@ -76,8 +77,8 @@ record LoggerMessage(VCLogger.Priority priority, VCLogger.ErrorType errorType, S
7677
public static void vcml_to_sbml(String vcml_content, String applicationName, Path sbmlPath, boolean roundTripValidation)
7778
throws XmlParseException, IOException, XMLStreamException, SbmlException, MappingException, ImageException, GeometryException, ExpressionException {
7879
GeometrySpec.avoidAWTImageCreation = true;
80+
XmlHelper.cloneUsingXML = true;
7981
VCMongoMessage.enabled = false;
80-
XmlHelper.cloneUsingXML = true;
8182

8283
BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml_content));
8384
bioModel.updateAll(false);
@@ -116,13 +117,20 @@ public static void vcml_to_sbml(String vcml_content, String applicationName, Pat
116117

117118
public static void vcml_to_vcml(String vcml_content, Path vcmlPath) throws XmlParseException, IOException, MappingException {
118119
GeometrySpec.avoidAWTImageCreation = true;
120+
XmlHelper.cloneUsingXML = true;
119121
VCMongoMessage.enabled = false;
120-
XmlHelper.cloneUsingXML = true;
121122

122123
BioModel bioModel = XmlHelper.XMLToBioModel(new XMLSource(vcml_content));
123124
bioModel.updateAll(false);
124125
// write the BioModel to a VCML file
125126
String vcml_str = XmlHelper.bioModelToXML(bioModel);
126127
XmlUtil.writeXMLStringToFile(vcml_str, vcmlPath.toFile().getAbsolutePath(), true);
127128
}
129+
130+
public static String get_python_infix(String vcellInfix) throws ExpressionException {
131+
GeometrySpec.avoidAWTImageCreation = true;
132+
XmlHelper.cloneUsingXML = true;
133+
VCMongoMessage.enabled = false;
134+
return new Expression(vcellInfix).infix_Python();
135+
}
128136
}

0 commit comments

Comments
 (0)