Skip to content

Commit 0dae024

Browse files
authored
Merge pull request #18 from virtualcell/sysbio-course
prepare pyvcell for broader use
2 parents d6e9c33 + 97c50ec commit 0dae024

File tree

9 files changed

+228
-23
lines changed

9 files changed

+228
-23
lines changed

examples/scripts/fielddata_from_image_workflow.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,19 @@
44

55
import numpy as np
66

7-
from pyvcell.sim_results.var_types import NDArray3D
7+
from pyvcell._internal.simdata.mesh import CartesianMesh
8+
from pyvcell.sim_results.var_types import NDArray3D, NDArray4D
89
from pyvcell.vcml import VcmlReader
910
from pyvcell.vcml.field import Field
1011
from pyvcell.vcml.vcml_simulation import VcmlSpatialSimulation as Solver
1112

1213

13-
def create_sinusoid(shape: tuple[int, ...], freq: float) -> NDArray3D:
14-
indices = np.indices(shape)
14+
def create_sinusoid(
15+
coords: NDArray4D,
16+
freq: float,
17+
) -> NDArray3D:
1518
sinusoid: NDArray3D = (
16-
np.cos(freq * indices[0, :, :, :]) * np.sin(freq * indices[1, :, :, :]) * np.sin(freq * indices[2, :, :, :])
19+
np.cos(freq * coords[:, :, :, 0]) * np.sin(freq * coords[:, :, :, 1]) * np.sin(freq * coords[:, :, :, 2])
1720
)
1821
return sinusoid.astype(dtype=np.float64)
1922

@@ -47,8 +50,11 @@ def create_sinusoid(shape: tuple[int, ...], freq: float) -> NDArray3D:
4750
print(fields)
4851

4952
shape = fields[0].data_nD.shape
50-
fields[0].data_nD = np.multiply(create_sinusoid(shape=shape, freq=0.5), 8.0)
51-
fields[1].data_nD = np.multiply(create_sinusoid(shape=shape, freq=0.3), 4.0)
53+
coords_array = CartesianMesh.compute_coordinates(
54+
mesh_shape=(shape[0], shape[1], shape[2]), origin=app.geometry.origin, extent=app.geometry.extent
55+
)
56+
fields[0].data_nD = np.multiply(create_sinusoid(coords=coords_array, freq=0.5), 8.0)
57+
fields[1].data_nD = np.multiply(create_sinusoid(coords=coords_array, freq=0.3), 4.0)
5258

5359
# ---- add field data to the simulation
5460

examples/scripts/sysbio_class.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from pathlib import Path
2+
3+
import pyvcell.vcml as vcml
4+
from pyvcell.vcml.vcml_simulation import VcmlSpatialSimulation as VCSolver
5+
6+
WORKSPACE_DIR = Path(__file__).parent.resolve()
7+
8+
antimony_str = """
9+
// This is a simple example of a spatial model in VCell
10+
compartment extracellular;
11+
compartment cell;
12+
species A in cell;
13+
species B in cell;
14+
A -> B; k1*A - k2*B
15+
16+
k1 = 0.1; k2 = 0.2
17+
A = 10
18+
"""
19+
20+
biomodel = vcml.load_antimony_str(antimony_str)
21+
22+
# create a 3D geometry
23+
geo = vcml.Geometry(name="geo", origin=(0, 0, 0), extent=(10, 10, 10), dim=3)
24+
# cell = geo.add_sphere(name="cell", radius=4, center=(5,5,5))
25+
medium = geo.add_background(name="background")
26+
27+
# add an application to the biomodel
28+
app = biomodel.add_application("app1", geometry=geo)
29+
app.map_compartment(compartment=biomodel.model.get_compartment("cell"), domain=medium) # type: ignore[union-attr]
30+
app.species_mappings = [
31+
vcml.SpeciesMapping(species_name="A", init_conc="sin(x)"),
32+
vcml.SpeciesMapping(species_name="B", init_conc="cos(x+y+z)"),
33+
]
34+
sim = app.add_sim(name="sim1", duration=10.0, output_time_step=0.5, mesh_size=(50, 50, 50))
35+
results = VCSolver(bio_model=biomodel, out_dir=WORKSPACE_DIR / "sim1").run(simulation_name="sim1")
36+
results.plotter.plot_concentrations()

poetry.lock

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

pyproject.toml

Lines changed: 3 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.10"
3+
version = "0.1.11"
44
description = "This is the python wrapper for vcell modeling and simulation"
55
authors = ["Jim Schaff <[email protected]>"]
66
repository = "https://github.com/virtualcell/pyvcell"
@@ -32,13 +32,14 @@ matplotlib = "^3.10.0"
3232
lxml = "^5.3.1"
3333
imageio = "^2.37.0"
3434
tensorstore = "^0.1.72"
35-
libvcell = "^0.0.12"
35+
libvcell = "^0.0.13"
3636
trame = "^3.8.1"
3737
trame-vtk = "^2.8.15"
3838
trame-vuetify = "^2.8.1"
3939
pyvista = "^0.44.2"
4040
trame-server = "^3.4.0"
4141
sympy = "^1.13.1"
42+
antimony = "^2.15.0"
4243

4344
[tool.poetry.group.dev.dependencies]
4445
pytest = "^7.2.0"

pyvcell/_internal/simdata/mesh.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from numpy._typing import NDArray
66

77
from pyvcell._internal.simdata.vtk.vismesh import Box3D
8+
from pyvcell.sim_results.var_types import NDArray4D
89

910

1011
class CartesianMesh:
@@ -103,6 +104,35 @@ def dimension(self) -> int:
103104
else:
104105
return 3
105106

107+
@property
108+
def coordinates(self) -> NDArray[np.float64]:
109+
"""
110+
Returns the coordinates of the mesh as a 3D array of shape (size[0], size[1], size[2], 3)
111+
"""
112+
return self.compute_coordinates(mesh_shape=self.size, origin=self.origin, extent=self.extent)
113+
114+
@staticmethod
115+
def compute_coordinates(
116+
mesh_shape: tuple[int, int, int] | list[int],
117+
origin: tuple[float, float, float] | list[float],
118+
extent: tuple[float, float, float] | list[float],
119+
) -> NDArray4D:
120+
"""
121+
Computes the coordinates of the mesh based on the mesh shape, origin, and extent.
122+
Returns a 3D array of shape (size[0], size[1], size[2], 3)
123+
"""
124+
coords = np.empty((*mesh_shape, 3), dtype=np.float64)
125+
coords[..., 0] = origin[0] + np.arange(mesh_shape[0])[:, None, None] * extent[0] / (mesh_shape[0] - 1)
126+
if mesh_shape[1] > 1:
127+
coords[..., 1] = origin[1] + np.arange(mesh_shape[1])[None, :, None] * extent[1] / (mesh_shape[1] - 1)
128+
else:
129+
coords[..., 1] = origin[1]
130+
if mesh_shape[2] > 1:
131+
coords[..., 2] = origin[2] + np.arange(mesh_shape[2])[None, None, :] * extent[2] / (mesh_shape[2] - 1)
132+
else:
133+
coords[..., 2] = origin[2]
134+
return coords
135+
106136
def read(self) -> None:
107137
# read file as lines and parse
108138
with self.mesh_file.open("r") as f:

pyvcell/sim_results/var_types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
NDArray1D: TypeAlias = np.ndarray[tuple[int], np.dtype[np.float64]]
66
NDArray2D: TypeAlias = np.ndarray[tuple[int, int], np.dtype[np.float64]]
77
NDArray3D: TypeAlias = np.ndarray[tuple[int, int, int], np.dtype[np.float64]]
8+
NDArray4D: TypeAlias = np.ndarray[tuple[int, int, int, int], np.dtype[np.float64]]
89
NDArrayND: TypeAlias = np.ndarray[tuple[int, ...], np.dtype[np.float64]]
910

1011
NDArray1Du8: TypeAlias = np.ndarray[tuple[int], np.dtype[np.uint8]]

pyvcell/vcml/__init__.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import logging
2+
import tempfile
3+
from os import PathLike
4+
from pathlib import Path
5+
16
from pyvcell.vcml.models import (
27
Application,
38
Biomodel,
@@ -50,3 +55,71 @@
5055
"Application",
5156
"Simulation",
5257
]
58+
59+
60+
def load_antimony_str(antimony_str: str) -> Biomodel:
61+
import antimony # type: ignore[import-untyped]
62+
63+
antimony_success = antimony.loadAntimonyString(antimony_str)
64+
if antimony_success != -1:
65+
sbml_str = antimony.getSBMLString()
66+
sbml_str = sbml_str.replace("sboTerm", "metaid")
67+
logging.info(f"Hack - introduced a metaid in place of sboTerm to SBML string:\n{sbml_str}")
68+
return load_sbml_str(sbml_str)
69+
else:
70+
raise ValueError("Error loading model:", antimony.getLastError())
71+
72+
73+
def load_antimony_file(antimony_file: PathLike[str]) -> Biomodel:
74+
import antimony # ignore
75+
76+
antimony_success = antimony.loadAntimonyFile(antimony_file)
77+
if antimony_success != -1:
78+
sbml_str = antimony.getSBMLString()
79+
return load_sbml_str(sbml_str)
80+
else:
81+
raise ValueError("Error loading model:", antimony.getLastError())
82+
83+
84+
def load_sbml_file(sbml_file: PathLike[str]) -> Biomodel:
85+
import libvcell
86+
87+
with tempfile.TemporaryDirectory() as tempdir:
88+
with open(sbml_file) as f:
89+
sbml_str = f.read()
90+
vcml_path = Path(tempdir) / "model.vcml"
91+
vc_success, vc_errmsg = libvcell.sbml_to_vcml(sbml_content=sbml_str, vcml_file_path=vcml_path)
92+
if vc_success:
93+
return VcmlReader.biomodel_from_file(vcml_path=vcml_path)
94+
else:
95+
raise ValueError("Error loading model:", vc_errmsg)
96+
97+
98+
def load_sbml_str(sbml_str: str) -> Biomodel:
99+
import libvcell
100+
101+
with tempfile.TemporaryDirectory() as tempdir:
102+
vcml_path = Path(tempdir) / "model.vcml"
103+
vc_success, vc_errmsg = libvcell.sbml_to_vcml(sbml_content=sbml_str, vcml_file_path=vcml_path)
104+
if vc_success:
105+
return VcmlReader.biomodel_from_file(vcml_path=vcml_path)
106+
else:
107+
raise ValueError("Error loading model:", vc_errmsg)
108+
109+
110+
def write_vcml_file(bio_model: Biomodel, vcml_file: PathLike[str]) -> None:
111+
vcml_document = VCMLDocument(biomodel=bio_model)
112+
VcmlWriter.write_to_file(vcml_document=vcml_document, file_path=vcml_file)
113+
114+
115+
def to_vcml_str(bio_model: Biomodel) -> str:
116+
vcml_document = VCMLDocument(biomodel=bio_model)
117+
vcml_str: str = VcmlWriter().write_vcml(document=vcml_document)
118+
return vcml_str
119+
120+
121+
def refresh_biomodel(bio_model: Biomodel) -> Biomodel:
122+
with tempfile.TemporaryDirectory() as tempdir:
123+
vcml_path = Path(tempdir) / "model.vcml"
124+
write_vcml_file(bio_model=bio_model, vcml_file=vcml_path)
125+
return VcmlReader.biomodel_from_file(vcml_path=vcml_path)

scripts/run_notebook.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ cd ${ROOT_DIR} || (echo "Failed to cd into ${ROOT_DIR}" && exit 1)
88

99
python -m venv .venv_jupyter
1010
source .venv_jupyter/bin/activate
11-
pip install jupyter ipython matplotlib tensorstore
11+
pip install jupyter ipython
1212
poetry install
1313
jupyter notebook

tests/_internal/simdata/fv_mesh_mapping_test.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from pathlib import Path
22

3+
import numpy as np
4+
35
from pyvcell._internal.simdata.mesh import CartesianMesh
46
from pyvcell._internal.simdata.vtk.fv_mesh_mapping import from_mesh3d_membrane, from_mesh3d_volume
57
from pyvcell._internal.simdata.vtk.vtkmesh_fv import write_finite_volume_smoothed_vtk_grid_and_index_data
@@ -46,3 +48,45 @@ def test_mesh_parse(temp_sim_946368938_path: Path) -> None:
4648
"cytosol.json",
4749
"SimID_946368938_0_.functions",
4850
}
51+
52+
53+
def test_mesh_coords(temp_sim_946368938_path: Path) -> None:
54+
input_filenames = {"SimID_946368938_0_.mesh", "SimID_946368938_0_.functions"}
55+
# remove all files not in input_filenames
56+
for p in temp_sim_946368938_path.iterdir():
57+
if p.name not in input_filenames:
58+
p.unlink()
59+
60+
filenames = {p.name for p in temp_sim_946368938_path.iterdir()}
61+
assert filenames == input_filenames
62+
63+
mesh = CartesianMesh(mesh_file=temp_sim_946368938_path / "SimID_946368938_0_.mesh")
64+
mesh.read()
65+
66+
assert mesh.size == [71, 71, 25]
67+
assert mesh.dimension == 3
68+
69+
# method 1 - using mesh property
70+
coords_1 = mesh.coordinates
71+
assert coords_1.shape == (71, 71, 25, 3)
72+
assert np.all(coords_1[0, 0, 0, :] == tuple(mesh.origin))
73+
assert np.all(
74+
coords_1[-1, -1, -1, :]
75+
== (mesh.origin[0] + mesh.extent[0], mesh.origin[1] + mesh.extent[1], mesh.origin[2] + mesh.extent[2])
76+
)
77+
78+
# method 2 - using static method
79+
coords = CartesianMesh.compute_coordinates(
80+
(mesh.size[0], mesh.size[1], mesh.size[2]),
81+
(mesh.origin[0], mesh.origin[1], mesh.origin[2]),
82+
(mesh.extent[0], mesh.extent[1], mesh.extent[2]),
83+
)
84+
assert coords.shape == (71, 71, 25, 3)
85+
assert np.all(coords[0, 0, 0, :] == tuple(mesh.origin))
86+
assert np.all(
87+
coords[-1, -1, -1, :]
88+
== (mesh.origin[0] + mesh.extent[0], mesh.origin[1] + mesh.extent[1], mesh.origin[2] + mesh.extent[2])
89+
)
90+
91+
# check that the two methods give the same result
92+
assert np.all(coords == coords_1)

0 commit comments

Comments
 (0)