Skip to content

Commit 12262d0

Browse files
Merge pull request #33 from virtualcell/add_proper_expression_handling
Extend support for more features of VCell expressions
2 parents b90cb33 + 6cd4af0 commit 12262d0

File tree

18 files changed

+3938
-2955
lines changed

18 files changed

+3938
-2955
lines changed

.github/actions/setup-poetry-env/action.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ runs:
1717

1818
- name: Install Poetry
1919
env:
20-
POETRY_VERSION: "1.7.1"
20+
POETRY_VERSION: "1.8.5"
2121
run: curl -sSL https://install.python-poetry.org | python - -y
2222
shell: bash
2323

@@ -32,11 +32,12 @@ runs:
3232
- name: Load cached venv
3333
id: cached-poetry-dependencies
3434
uses: actions/cache@v4
35+
if: github.run_attempt == 1
3536
with:
3637
path: .venv
3738
key: venv-${{ runner.os }}-${{ inputs.python-version }}-${{ hashFiles('poetry.lock') }}
3839

3940
- name: Install dependencies
40-
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
41+
if: github.run_attempt > 1 || steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
4142
run: poetry install --no-interaction
4243
shell: bash

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,4 @@ examples/test_output/SimID_*
182182
examples/notebooks/workspace/
183183

184184
examples/scripts/workspace/
185+
workspace

examples/notebooks/sbml_workflow.ipynb

Lines changed: 203 additions & 184 deletions
Large diffs are not rendered by default.

examples/scripts/_internal_data_demo.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494
# Set labels for axes
9595
ax.set_xlabel("X")
9696
ax.set_ylabel("Y")
97-
ax.set_zlabel("Z") # type: ignore[attr-defined]
97+
ax.set_zlabel("Z")
9898

9999
# Show the plot
100100
plt.show()
@@ -138,7 +138,7 @@
138138
# plot each series on a different plot arranged in a 2x2 grid with a legend from series_legends
139139
fig, ax = plt.subplots(2, 2, figsize=(10, 10))
140140
for i, series_array in enumerate(series_arrays):
141-
axis = ax[int(i / 2), i % 2] # type: ignore[index]
141+
axis = ax[int(i / 2), i % 2]
142142
axis.plot(times, series_array[:, 0], label="mean")
143143
axis.fill_between(times, series_array[:, 1], series_array[:, 2], alpha=0.2)
144144
axis.set_title(series_legend[i])

examples/scripts/_internal_n5_download_demo.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import matplotlib.pyplot as plt
2-
from tensorstore._tensorstore import TensorStore # type: ignore[import-untyped]
2+
import tensorstore._tensorstore as ts # type: ignore[import-not-found]
33

44
from pyvcell._internal.simdata.n5_data import vcell_n5_datastore
55
from pyvcell.sim_results.var_types import NDArray2D
66

77
url = "https://vcell-dev.cam.uchc.edu/n5Data/ACowan/4b5ac930c40d5ba.n5"
88
dataset_name = "4248805214"
9-
data_store: TensorStore = vcell_n5_datastore(base_url=url, dataset_name=dataset_name)
9+
data_store: ts.TensorStore = vcell_n5_datastore(base_url=url, dataset_name=dataset_name)
1010
print(f"shape = {data_store.shape}")
1111

1212
np_data: NDArray2D = data_store[:, :, 0, 0, 0].read().result()

poetry.lock

Lines changed: 3478 additions & 2750 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pyvcell"
3-
version = "0.1.19"
3+
version = "0.1.20"
44
description = "This is the python wrapper for vcell modeling and simulation"
55
authors = ["Jim Schaff <schaff@uchc.edu>"]
66
repository = "https://github.com/virtualcell/pyvcell"
@@ -32,7 +32,7 @@ matplotlib = "^3.10.0"
3232
lxml = "^5.3.1"
3333
imageio = "^2.37.0"
3434
tensorstore = "^0.1.72"
35-
libvcell = "^0.0.13"
35+
libvcell = "^0.0.15"
3636
trame = "^3.8.1"
3737
trame-vtk = "^2.8.15"
3838
trame-vuetify = "^2.8.1"
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import tensorstore as ts # type: ignore[import-untyped]
2-
from tensorstore._tensorstore import TensorStore # type: ignore[import-untyped]
1+
import tensorstore as ts
32

43

5-
def vcell_n5_datastore(base_url: str, dataset_name: str) -> TensorStore:
4+
def vcell_n5_datastore(base_url: str, dataset_name: str) -> ts._tensorstore.TensorStore:
65
spec = {"driver": "n5", "kvstore": {"driver": "http", "base_url": base_url}, "path": dataset_name}
76
return ts.open(spec, read=True).result()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from libvcell import vcell_infix_to_num_expr_infix
2+
3+
4+
def get_numexpr_expression(vcell_expression: str) -> str:
5+
# First, get the string from vcell
6+
translation_result: bool
7+
result_message: str
8+
num_expr_expression: str
9+
translation_result, result_message, num_expr_expression = vcell_infix_to_num_expr_infix(vcell_expression)
10+
if not translation_result:
11+
raise ValueError(
12+
result_message if result_message is not None else "Unable to convert expression: " + vcell_expression
13+
)
14+
# NumExpr has a restricted set of what's allowed, in order to protect `eval`
15+
sanitized_expression: str = (
16+
num_expr_expression.lstrip(" ")
17+
.rstrip(" ")
18+
.replace(" and ", " & ")
19+
.replace(" or ", " | ")
20+
.replace(" not ", " ~")
21+
.replace("math.", "")
22+
)
23+
return sanitized_expression

pyvcell/_internal/simdata/simdata_models.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import numpy as np
1010
from numpy._typing import NDArray
1111

12+
from pyvcell._internal.simdata.python_infix import get_numexpr_expression
1213
from pyvcell.sim_results.var_types import NDArray1D
1314

1415
PYTHON_ENDIANNESS: Literal["little", "big"] = "big"
@@ -350,23 +351,23 @@ def get_data(self, variable: VariableInfo | str, time: float) -> NDArray1D:
350351
class NamedFunction:
351352
name: str
352353
vcell_expression: str
353-
python_expression: str
354+
num_expr_expression: str
354355
variables: list[str]
355356
variable_type: VariableType
356357

357358
def __init__(self, name: str, vcell_expression: str, variable_type: VariableType) -> None:
358359
self.name = name
359360
self.vcell_expression = vcell_expression
360-
self.python_expression = vcell_expression.replace("^", "**").lstrip(" ").rstrip(" ")
361+
self.num_expr_expression = get_numexpr_expression(self.vcell_expression)
361362
self.variable_type = variable_type
362363

363364
# Parse the python expression into an AST and extract all Name nodes (which represent variables)
364-
tree = ast.parse(self.python_expression)
365+
tree = ast.parse(self.num_expr_expression)
365366
self.variables = [node.id for node in ast.walk(tree) if isinstance(node, ast.Name)]
366367

367368
def evaluate(self, variable_bindings: dict[str, NDArray[np.float64]]) -> NDArray[np.float64]:
368369
ne.set_num_threads(1)
369-
expression = self.python_expression
370+
expression = self.num_expr_expression
370371
result = ne.evaluate(expression, local_dict=variable_bindings)
371372
if not isinstance(result, np.ndarray):
372373
raise TypeError(f"Expression {expression} did not evaluate to a numpy array")

0 commit comments

Comments
 (0)