|
| 1 | +# Design |
| 2 | + |
| 3 | +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`. |
| 4 | + |
| 5 | +## Two layers |
| 6 | + |
| 7 | +**Python layer** (`libvcell/`) |
| 8 | + |
| 9 | +- `__init__.py` — public API surface. |
| 10 | +- `model_utils.py` / `solver_utils.py` — thin wrappers that instantiate `VCellNativeCalls` and translate its structured results into friendly return values or exceptions. |
| 11 | +- `_internal/native_utils.py` — locates and loads the platform shared library (`.so`/`.dylib`/`.dll`) from `libvcell/lib/`, declares each entry point's `ctypes` signature, and provides `IsolateManager` for GraalVM isolate lifecycle. |
| 12 | +- `_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. |
| 13 | + |
| 14 | +**Native/Java layer** (`vcell-native/`) |
| 15 | + |
| 16 | +- `Entrypoints.java` — `@CEntryPoint` methods exported as C symbols. Each returns a **JSON document** as a C string (`CCharPointer`) describing success/failure. |
| 17 | +- `ModelUtils.java` / `SolverUtils.java` — the actual logic, calling vcell-core from `vcell_submodule`. |
| 18 | +- `MainRecorder.java` — exercises each entry point under `native-image-agent` so the build records the required reflection/resource config. |
| 19 | + |
| 20 | +## FFI conventions |
| 21 | + |
| 22 | +- **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 in `Entrypoints.allocatedMemory`). The Python side decodes the C string and validates it into a pydantic model (`ReturnValue`, `EvalReturnValue`, …). |
| 23 | +- **Errors are data, not crashes.** Entry points catch `Throwable` and encode the failure into the JSON document (a `success:false` flag plus a message and/or error type). The Python wrapper decides whether to return a status tuple or raise. |
| 24 | +- **One isolate per call.** `IsolateManager` creates a GraalVM isolate for the duration of a call and tears it down afterward. |
| 25 | +- **New entry points are `hasattr`-guarded** in `native_utils.py` so the package still imports against an older shared library that predates the symbol (the Python tests `skipif` on the same check). |
| 26 | + |
| 27 | +## Entry points |
| 28 | + |
| 29 | +| Native symbol | Python API | Purpose | |
| 30 | +| ------------------------------------------------------ | --------------------------------------------------------------- | -------------------------------------------------------------- | |
| 31 | +| `vcmlToFiniteVolumeInput` / `sbmlToFiniteVolumeInput` | `vcml_to_finite_volume_input` / `sbml_to_finite_volume_input` | write Finite Volume solver input | |
| 32 | +| `vcmlToMovingBoundaryInput` | `vcml_to_moving_boundary_input` | write Moving Boundary solver input (`MovingBoundarySetup` XML) | |
| 33 | +| `vcmlToSbml` / `sbmlToVcml` / `vcmlToVcml` | `vcml_to_sbml` / `sbml_to_vcml` / `vcml_to_vcml` | model format conversion | |
| 34 | +| `vcellInfixToPythonInfix` / `vcellInfixToNumExprInfix` | `vcell_infix_to_python_infix` / `vcell_infix_to_num_expr_infix` | translate VCell infix to other syntaxes | |
| 35 | +| `evaluateExpression` | `evaluate_expression` | evaluate a VCell infix expression to a float | |
| 36 | + |
| 37 | +## `evaluate_expression` |
| 38 | + |
| 39 | +Evaluates a native-syntax VCell infix expression given a symbol table of values, returning a 64-bit float. |
| 40 | + |
| 41 | +```python |
| 42 | +from libvcell import evaluate_expression, VCellExpressionError |
| 43 | + |
| 44 | +evaluate_expression("a + b/c", {"a": 10.0, "b": 20.0, "c": 5.0}) # -> 14.0 |
| 45 | +evaluate_expression("2 + 3*sqrt(4)", {}) # -> 8.0 |
| 46 | + |
| 47 | +try: |
| 48 | + evaluate_expression("1/c", {"c": 0.0}) |
| 49 | +except VCellExpressionError as e: |
| 50 | + print(e.error_type) # "DivideByZeroException" |
| 51 | +``` |
| 52 | + |
| 53 | +**Semantics** |
| 54 | + |
| 55 | +- Any symbol referenced by the expression must be present in the symbol table; extra (unreferenced) symbols are permitted and ignored. |
| 56 | +- The value is computed via vcell-core's `Expression`: parse → `bindExpression(new SimpleSymbolTable(names))` → `evaluateVector(values)`. `SimpleSymbolTable` provides the lightweight "dummy" binding — no VCell model or `MathDescription` is required. |
| 57 | + |
| 58 | +**Native boundary** |
| 59 | + |
| 60 | +- Input: the infix string and the symbol table serialized as a JSON object of `{name: number}` (`json.dumps` of the dict; integers are accepted). |
| 61 | +- Output: a JSON document — `{"success": true, "value": <double>}` on success, or `{"success": false, "error_type": <exceptionClassName>, "message": <text>}` on failure. This is parsed into `EvalReturnValue`. |
| 62 | + |
| 63 | +**Error handling** |
| 64 | + |
| 65 | +- `native_calls.evaluate_expression(...)` returns the raw `EvalReturnValue` (branch on `.success`). |
| 66 | +- The public `model_utils.evaluate_expression(...)` returns the `float` or raises `VCellExpressionError`, which exposes `.error_type` (the originating Java exception's simple class name) and `.message`. Categories include `ParseException` (syntax), `ExpressionBindingException` (a referenced symbol was not supplied), `DivideByZeroException`, `FunctionDomainException` (e.g. `sqrt(-1)`, `log(0)`), and `IllegalArgumentException` (malformed symbol-table JSON). |
| 67 | +- Non-finite results (`Infinity`/`NaN`) cannot be represented in JSON and are surfaced as an error (`error_type = "NonFiniteResultException"`). |
| 68 | + |
| 69 | +## Adding a new entry point |
| 70 | + |
| 71 | +1. Implement the logic in `ModelUtils.java` / `SolverUtils.java`. |
| 72 | +2. Add a `@CEntryPoint` method in `Entrypoints.java` that returns a JSON document. |
| 73 | +3. Exercise it in `MainRecorder.java` (so native-image records its config). |
| 74 | +4. Declare its `ctypes` signature (`hasattr`-guarded) in `native_utils.py`. |
| 75 | +5. Add a `VCellNativeCalls` method returning a pydantic model in `native_calls.py`. |
| 76 | +6. Add the friendly wrapper in `model_utils.py` / `solver_utils.py` and export it from `__init__.py`. |
| 77 | +7. Add Java tests (JVM-level) and Python tests (`skipif` on the new symbol until the native library is rebuilt). |
0 commit comments