Design¶
libvcell is a thin pure-Python layer over a GraalVM native-image shared library built from a subset of VCell's Java code. The Python side contains no compiled CPython extension; the native library is loaded and called via ctypes.
Two layers¶
Python layer (libvcell/)
__init__.py— public API surface.model_utils.py/solver_utils.py— thin wrappers that instantiateVCellNativeCallsand translate its structured results into friendly return values or exceptions._internal/native_utils.py— locates and loads the platform shared library (.so/.dylib/.dll) fromlibvcell/lib/, declares each entry point'sctypessignature, and providesIsolateManagerfor GraalVM isolate lifecycle._internal/native_calls.py— one method per native entry point; marshals arguments, manages the isolate, and parses the returned JSON document into a pydantic model.
Native/Java layer (vcell-native/)
Entrypoints.java—@CEntryPointmethods exported as C symbols. Each returns a JSON document as a C string (CCharPointer) describing success/failure.ModelUtils.java/SolverUtils.java— the actual logic, calling vcell-core fromvcell_submodule.MainRecorder.java— exercises each entry point undernative-image-agentso the build records the required reflection/resource config.
FFI conventions¶
- Every call returns a JSON string. A native entry point never returns a bare number/bool across the boundary; it returns a JSON document (via
createString, whose memory is tracked inEntrypoints.allocatedMemory). The Python side decodes the C string and validates it into a pydantic model (ReturnValue,EvalReturnValue, …). - Errors are data, not crashes. Entry points catch
Throwableand encode the failure into the JSON document (asuccess:falseflag plus a message and/or error type). The Python wrapper decides whether to return a status tuple or raise. - One isolate per call.
IsolateManagercreates a GraalVM isolate for the duration of a call and tears it down afterward. - New entry points are
hasattr-guarded innative_utils.pyso the package still imports against an older shared library that predates the symbol (the Python testsskipifon the same check).
Entry points¶
| Native symbol | Python API | Purpose |
|---|---|---|
vcmlToFiniteVolumeInput / sbmlToFiniteVolumeInput |
vcml_to_finite_volume_input / sbml_to_finite_volume_input |
write Finite Volume solver input |
vcmlToMovingBoundaryInput |
vcml_to_moving_boundary_input |
write Moving Boundary solver input (MovingBoundarySetup XML) |
vcmlToSbml / sbmlToVcml / vcmlToVcml |
vcml_to_sbml / sbml_to_vcml / vcml_to_vcml |
model format conversion |
vcellInfixToPythonInfix / vcellInfixToNumExprInfix |
vcell_infix_to_python_infix / vcell_infix_to_num_expr_infix |
translate VCell infix to other syntaxes |
evaluateExpression |
evaluate_expression |
evaluate a VCell infix expression to a float |
evaluate_expression¶
Evaluates a native-syntax VCell infix expression given a symbol table of values, returning a 64-bit float.
from libvcell import evaluate_expression, VCellExpressionError
evaluate_expression("a + b/c", {"a": 10.0, "b": 20.0, "c": 5.0}) # -> 14.0
evaluate_expression("2 + 3*sqrt(4)", {}) # -> 8.0
try:
evaluate_expression("1/c", {"c": 0.0})
except VCellExpressionError as e:
print(e.error_type) # "DivideByZeroException"
Semantics
- Any symbol referenced by the expression must be present in the symbol table; extra (unreferenced) symbols are permitted and ignored.
- The value is computed via vcell-core's
Expression: parse →bindExpression(new SimpleSymbolTable(names))→evaluateVector(values).SimpleSymbolTableprovides the lightweight "dummy" binding — no VCell model orMathDescriptionis required.
Native boundary
- Input: the infix string and the symbol table serialized as a JSON object of
{name: number}(json.dumpsof the dict; integers are accepted). - Output: a JSON document —
{"success": true, "value": <double>}on success, or{"success": false, "error_type": <exceptionClassName>, "message": <text>}on failure. This is parsed intoEvalReturnValue.
Error handling
native_calls.evaluate_expression(...)returns the rawEvalReturnValue(branch on.success).- The public
model_utils.evaluate_expression(...)returns thefloator raisesVCellExpressionError, which exposes.error_type(the originating Java exception's simple class name) and.message. Categories includeParseException(syntax),ExpressionBindingException(a referenced symbol was not supplied),DivideByZeroException,FunctionDomainException(e.g.sqrt(-1),log(0)), andIllegalArgumentException(malformed symbol-table JSON). - Non-finite results (
Infinity/NaN) cannot be represented in JSON and are surfaced as an error (error_type = "NonFiniteResultException").
Adding a new entry point¶
- Implement the logic in
ModelUtils.java/SolverUtils.java. - Add a
@CEntryPointmethod inEntrypoints.javathat returns a JSON document. - Exercise it in
MainRecorder.java(so native-image records its config). - Declare its
ctypessignature (hasattr-guarded) innative_utils.py. - Add a
VCellNativeCallsmethod returning a pydantic model innative_calls.py. - Add the friendly wrapper in
model_utils.py/solver_utils.pyand export it from__init__.py. - Add Java tests (JVM-level) and Python tests (
skipifon the new symbol until the native library is rebuilt).