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.src/main/native-image/jsbml/reachability-metadata.json— checked-in reflection config, merged with the recording: every jsbml tree node's methods and fields, because jsbml'sAbstractTreeNode.toString()reflects over them while formatting debug messages, on paths no recording reaches. Generated byscripts/JsbmlReflection.java; regenerate when the jsbml version changes.
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 |
vcmlRegenerateMath |
vcml_regenerate_math |
regenerate each application's math with VCell's forced upgrade |
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 |
vcml_regenerate_math¶
Regenerates the math of every application of a VCML document as VCell does when it upgrades an old document
(SimulationContext.updateAll(true)), and writes the result. A non-spatial application that gives no absolute
structure size gets the sizes VCell's StructureSizeSolver computes from its surface-to-volume ratios and volume
fractions, with the model's first structure anchored at 1; the sizes are symbolic over the ratios, which stay in the
math as constants. vcml_to_vcml (BioModel.updateAll(false)) generates the same math but leaves such an
application's structure mappings without sizes. Each application is regenerated on its own: the result,
MathRegeneration, lists the applications regenerated and, for each one whose math generation failed, the reason
(that application keeps the math it was stored with). Its use: comparing another math generator against the math
current VCell generates, not the math a document was last saved with.
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).