pyvcell 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyvcell/__init__.py ADDED
File without changes
pyvcell/foo.py ADDED
@@ -0,0 +1,17 @@
1
+ def foo(bar: str) -> str:
2
+ """Summary line.
3
+
4
+ Extended description of function.
5
+
6
+ Args:
7
+ bar: Description of input argument.
8
+
9
+ Returns:
10
+ Description of return value
11
+ """
12
+
13
+ return bar
14
+
15
+
16
+ if __name__ == "__main__": # pragma: no cover
17
+ pass
File without changes
@@ -0,0 +1,34 @@
1
+ from pathlib import Path
2
+
3
+ import typer
4
+
5
+ from pyvcell.simdata.mesh import CartesianMesh
6
+ from pyvcell.simdata.simdata_models import DataFunctions, PdeDataSet
7
+ from pyvcell.simdata.zarr_writer import write_zarr
8
+
9
+ app = typer.Typer()
10
+
11
+
12
+ @app.command(name="vc_to_zarr", help="Convert a VCell FiniteVolume simulation dataset to Zarr")
13
+ def n5_to_zarr(
14
+ sim_data_dir: Path = typer.Argument(..., help="path to vcell dataset directory"),
15
+ sim_id: int = typer.Argument(..., help="simulation id (e.g. 946368938)"),
16
+ job_id: int = typer.Argument(..., help="job id (e.g. 0"),
17
+ zarr_path: Path = typer.Argument(..., help="path to zarr dataset to write to"),
18
+ ) -> None:
19
+ pde_dataset = PdeDataSet(base_dir=sim_data_dir, log_filename=f"SimID_{sim_id}_{job_id}_.log")
20
+ pde_dataset.read()
21
+ data_functions = DataFunctions(function_file=sim_data_dir / f"SimID_{sim_id}_{job_id}_.functions")
22
+ data_functions.read()
23
+ mesh = CartesianMesh(mesh_file=sim_data_dir / f"SimID_{sim_id}_{job_id}_.mesh")
24
+ mesh.read()
25
+
26
+ write_zarr(pde_dataset=pde_dataset, data_functions=data_functions, mesh=mesh, zarr_dir=zarr_path)
27
+
28
+
29
+ def main() -> None:
30
+ app()
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()
@@ -0,0 +1,224 @@
1
+ import zlib
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+
6
+ from pyvcell.simdata.vtk.vismesh import Box3D
7
+
8
+
9
+ class CartesianMesh:
10
+ """
11
+ reads the .mesh file and extracts the mesh information
12
+
13
+ Example .mesh file:
14
+ Version 1.2
15
+ CartesianMesh {
16
+ // X Y Z
17
+ Size 71 71 25
18
+ Extent 74.239999999999995 74.239999999999995 26
19
+ Origin 0 0 0
20
+
21
+ VolumeRegionsMapSubvolume {
22
+ 6
23
+ //VolRegID SubvolID Volume
24
+ 0 0 124767.54117864356 //ec
25
+ 1 1 14855.904388351477 //cytosol
26
+ 2 1 1.2185460680272107 //cytosol
27
+ 3 1 1.2185460680272107 //cytosol
28
+ 4 1 1.2185460680272107 //cytosol
29
+ 5 2 3673.9163951019395 //Nucleus
30
+ }
31
+
32
+ MembraneRegionsMapVolumeRegion {
33
+ 5
34
+ //MemRegID VolReg1 VolReg2 Surface
35
+ 0 1 0 4512.8782874369472
36
+ 1 2 0 1.7113582585034091
37
+ 2 3 0 1.7113582585033937
38
+ 3 4 0 1.711358258503394
39
+ 4 5 1 1306.5985272332098
40
+ }
41
+
42
+ VolumeElementsMapVolumeRegion {
43
+ 126025 Compressed
44
+ 789CEDDD8D72DBC81100612389DFFF9573572A5912B9BF2066A66176B32A57B12CE22B8022E5DD11
45
+ F5EB9799999999999999999999999999999999999999999999999999999999999999999999999999
46
+ ...
47
+ 3333338B8F3625C09A5BE069281EE2BC0BC543D530FA907034666666666666666666666666666666
48
+ 6666666666666666666666667F67FF07ABF56A9C
49
+ }
50
+
51
+ MembraneElements {
52
+ 7817
53
+ //Indx Vol1 Vol2 Conn0 Conn1 Conn2 Conn3 MemRegID
54
+ 0 6710 11751 5 507 493 1 0
55
+ 1 6711 11752 6 0 494 510 0
56
+ 2 6771 11812 10 524 503 3 0
57
+ 3 6772 11813 11 2 505 527 0
58
+ 4 6780 11821 16 533 508 5 0
59
+ ....
60
+ 7808 109155 104114 7807 7805 7792 7806 4
61
+ 7809 104179 104180 7810 7551 7798 7811 4
62
+ 7810 104251 104180 7812 7551 7809 -1 4
63
+ 7811 109221 104180 -1 7809 7799 7813 4
64
+ 7812 104252 104181 7815 7553 7810 -1 4
65
+ 7813 109222 104181 -1 7811 7800 7816 4
66
+ 7814 104183 104182 7815 7556 7802 7816 4
67
+ 7815 104253 104182 7814 7554 7812 -1 4
68
+ 7816 109223 104182 -1 7813 7801 7814 4
69
+ }
70
+ }
71
+ """
72
+
73
+ mesh_file: Path
74
+ size: list[int] # [x, y, z]
75
+ extent: list[float] # [x, y, z]
76
+ origin: list[float] # [x, y, z]
77
+ volume_regions: list[tuple[int, int, float, str]] # list of tuples (vol_reg_id, subvol_id, volume, domain_name)
78
+ membrane_regions: list[tuple[int, int, int, float]] # list of tuples (mem_reg_id, vol_reg1, vol_reg2, surface)
79
+
80
+ # membrane_element[m,:] = [idx, vol1, vol2, conn0, conn1, conn2, conn3, mem_reg_id]
81
+ membrane_elements: np.ndarray # shape (num_membrane_elements, 8)
82
+
83
+ # volume_region_map[m] = vol_reg_id
84
+ volume_region_map: np.ndarray # shape (size[0] * size[1] * size[2],)
85
+
86
+ def __init__(self, mesh_file: Path) -> None:
87
+ self.mesh_file = mesh_file
88
+ self.size = []
89
+ self.extent = []
90
+ self.origin = []
91
+ self.volume_regions = []
92
+ self.membrane_regions = []
93
+ # self.membrane_elements
94
+ self.volume_region_map = np.array([], dtype=np.uint8)
95
+
96
+ @property
97
+ def dimension(self) -> int:
98
+ if self.size[1] == 1 and self.size[2] == 1:
99
+ return 1
100
+ elif self.size[2] == 1:
101
+ return 2
102
+ else:
103
+ return 3
104
+
105
+ def read(self) -> None:
106
+ # read file as lines and parse
107
+ with self.mesh_file.open("r") as f:
108
+ # get line enumerator from f
109
+
110
+ iter_lines = iter(f.readlines())
111
+
112
+ if next(iter_lines) != "Version 1.2\n":
113
+ raise RuntimeError("Expected 'Version 1.2' at the beginning of the file")
114
+ if next(iter_lines) != "CartesianMesh {\n":
115
+ raise RuntimeError("Expected 'CartesianMesh {' after version")
116
+ if next(iter_lines) != "\t// X Y Z\n":
117
+ raise RuntimeError("Expected coordinate comment line")
118
+
119
+ size_line = next(iter_lines).split()
120
+ if size_line[0] == "Size":
121
+ self.size = [int(size_line[1]), int(size_line[2]), int(size_line[3])]
122
+
123
+ extent_line = next(iter_lines).split()
124
+ if extent_line[0] == "Extent":
125
+ self.extent = [float(extent_line[1]), float(extent_line[2]), float(extent_line[3])]
126
+
127
+ origin_line = next(iter_lines).split()
128
+ if origin_line[0] == "Origin":
129
+ self.origin = [float(origin_line[1]), float(origin_line[2]), float(origin_line[3])]
130
+
131
+ while next(iter_lines) != "\tVolumeRegionsMapSubvolume {\n":
132
+ pass
133
+ num_volume_regions = int(next(iter_lines))
134
+ _header_line = next(iter_lines)
135
+ self.volume_regions = []
136
+ for _i in range(num_volume_regions):
137
+ parts = next(iter_lines).split()
138
+ self.volume_regions.append((int(parts[0]), int(parts[1]), float(parts[2]), parts[3].strip("/")))
139
+
140
+ while next(iter_lines) != "\tMembraneRegionsMapVolumeRegion {\n":
141
+ pass
142
+ num_membrane_regions = int(next(iter_lines))
143
+ _header_line = next(iter_lines)
144
+ self.membrane_regions = []
145
+ for _i in range(num_membrane_regions):
146
+ parts = next(iter_lines).split()
147
+ self.membrane_regions.append((int(parts[0]), int(parts[1]), int(parts[2]), float(parts[3])))
148
+
149
+ while next(iter_lines) != "\tVolumeElementsMapVolumeRegion {\n":
150
+ pass
151
+ compressed_line = next(iter_lines).split()
152
+ num_volume_elements = int(compressed_line[0])
153
+ if compressed_line[1] != "Compressed":
154
+ raise ValueError("Expected 'Compressed' in VolumeElementsMapVolumeRegion")
155
+ # read HEX lines until "}" line, and concatenate into one string, then convert to bytes and decompress
156
+ hex_lines = []
157
+ while True:
158
+ line = next(iter_lines)
159
+ if line.strip() == "}":
160
+ break
161
+ hex_lines.append(line.strip())
162
+ hex_string: str = "".join(hex_lines).strip()
163
+ compressed_bytes = bytes.fromhex(hex_string)
164
+ uncompressed_bytes: bytes = zlib.decompress(compressed_bytes)
165
+ self.volume_region_map = np.frombuffer(uncompressed_bytes, dtype="<u2") # unsigned 2-byte integers
166
+ if self.volume_region_map.shape[0] != self.size[0] * self.size[1] * self.size[2]:
167
+ raise ValueError("Expected number of volume elements to match the size of volume region map")
168
+ if num_volume_elements != self.volume_region_map.shape[0]:
169
+ raise ValueError("Expected number of volume elements to match the size of volume region map")
170
+ if set(np.unique(self.volume_region_map)) != {v[0] for v in self.volume_regions}:
171
+ raise ValueError("Expected volume region map to have the same unique values as volume regions")
172
+
173
+ while next(iter_lines).strip() != "MembraneElements {":
174
+ pass
175
+ num_membrane_elements = int(next(iter_lines))
176
+ self.membrane_elements = np.zeros((num_membrane_elements, 8), dtype=np.int32)
177
+ _header_line = next(iter_lines)
178
+ mem_index = 0
179
+ while True:
180
+ line = next(iter_lines)
181
+ if line.strip() == "}":
182
+ break
183
+ parts = line.split()
184
+ idx = int(parts[0])
185
+ vol1 = int(parts[1])
186
+ vol2 = int(parts[2])
187
+ conn0 = int(parts[3])
188
+ conn1 = int(parts[4])
189
+ conn2 = int(parts[5])
190
+ conn3 = int(parts[6])
191
+ mem_reg_id = int(parts[7])
192
+ self.membrane_elements[mem_index, :] = [idx, vol1, vol2, conn0, conn1, conn2, conn3, mem_reg_id]
193
+ mem_index += 1
194
+ if self.membrane_elements.shape != (num_membrane_elements, 8):
195
+ raise RuntimeError("Expected membrane elements to have the correct shape")
196
+ if set(np.unique(self.membrane_elements[:, 7])) != {v[0] for v in self.membrane_regions}:
197
+ raise RuntimeError("Expected volume region ids in membrane elements to match volume regions")
198
+
199
+ def get_volume_element_box(self, i: int, j: int, k: int) -> Box3D:
200
+ x_lo = self.origin[0] + i * self.extent[0] / self.size[0]
201
+ y_lo = self.origin[1] + j * self.extent[1] / self.size[1]
202
+ z_lo = self.origin[2] + k * self.extent[2] / self.size[2]
203
+ x_hi = self.origin[0] + (i + 1) * self.extent[0] / self.size[0]
204
+ y_hi = self.origin[1] + (j + 1) * self.extent[1] / self.size[1]
205
+ z_hi = self.origin[2] + (k + 1) * self.extent[2] / self.size[2]
206
+ return Box3D(x_lo, y_lo, z_lo, x_hi, y_hi, z_hi)
207
+
208
+ def get_membrane_region_index(self, mem_element_index: int) -> int:
209
+ return int(self.membrane_elements[mem_element_index, 7])
210
+
211
+ def get_membrane_region_ids(self, volume_domain_name: str) -> set[int]:
212
+ return {
213
+ mem_reg_id
214
+ for mem_reg_id, vol_reg1, vol_reg2, surface in self.membrane_regions
215
+ if self.volume_regions[vol_reg1][3] == volume_domain_name
216
+ or self.volume_regions[vol_reg2][3] == volume_domain_name
217
+ }
218
+
219
+ def get_volume_region_ids(self, volume_domain_name: str) -> set[int]:
220
+ return {
221
+ vol_reg_id
222
+ for vol_reg_id, subvol_id, volume, domain_name in self.volume_regions
223
+ if domain_name == volume_domain_name
224
+ }
@@ -0,0 +1,191 @@
1
+ from enum import IntEnum
2
+ from pathlib import Path
3
+
4
+ import numpy as np
5
+ from h5py import Dataset, Group
6
+ from h5py import File as H5File
7
+
8
+
9
+ class StatisticType(IntEnum):
10
+ AVERAGE = 0
11
+ TOTAL = 1
12
+ MIN = 2
13
+ MAX = 3
14
+
15
+
16
+ class ImageMetadata:
17
+ name: str
18
+ group_path: str
19
+ extents: np.ndarray
20
+ origin: np.ndarray
21
+ shape: tuple[int, ...]
22
+
23
+ def __init__(self, name: str, group_path: str):
24
+ self.name = name
25
+ self.group_path = group_path
26
+
27
+ def get_dataset(self, hdf5_file: H5File, time_index: int) -> Dataset:
28
+ group_path_object = hdf5_file[self.group_path]
29
+ if not isinstance(group_path_object, Group):
30
+ raise TypeError(f"Expected a group at {self.group_path} but found {type(group_path_object)}")
31
+ image_group: Group = group_path_object
32
+ dataset_path_object = image_group[f"time{time_index:06d}"]
33
+ if not isinstance(dataset_path_object, Dataset):
34
+ raise TypeError(
35
+ f"Expected a dataset at {self.group_path}/time{time_index:06d} but found {type(dataset_path_object)}"
36
+ )
37
+ image_ds: Dataset = dataset_path_object
38
+ return image_ds
39
+
40
+ def read(self, f: H5File) -> None:
41
+ group_path_object = f[self.group_path]
42
+ if not isinstance(group_path_object, Group):
43
+ raise TypeError(f"Expected a group at {self.group_path} but found {type(group_path_object)}")
44
+ image_group: Group = group_path_object
45
+
46
+ # get attributes from the group
47
+ extents_list = []
48
+ origin_list = []
49
+ if "ExtentX" in image_group.attrs:
50
+ extents_list.append(image_group.attrs["ExtentX"])
51
+ if "ExtentY" in image_group.attrs:
52
+ extents_list.append(image_group.attrs["ExtentY"])
53
+ if "ExtentZ" in image_group.attrs:
54
+ extents_list.append(image_group.attrs["ExtentZ"])
55
+ if "OriginX" in image_group.attrs:
56
+ origin_list.append(image_group.attrs["OriginX"])
57
+ if "OriginY" in image_group.attrs:
58
+ origin_list.append(image_group.attrs["OriginY"])
59
+ if "OriginZ" in image_group.attrs:
60
+ origin_list.append(image_group.attrs["OriginZ"])
61
+ self.extents = np.array(extents_list)
62
+ self.origin = np.array(origin_list)
63
+ self.shape = self.get_dataset(f, 0).shape
64
+
65
+
66
+ class VariableInfo:
67
+ var_index: int
68
+ var_name: str # e.g. "C_cyt"
69
+ stat_channel: int
70
+ statistic_type: StatisticType # e.g. StatisticType.AVERAGE
71
+ stat_var_name: str # e.g. "C_cyt_average"
72
+ stat_var_unit: str # e.g. "uM"
73
+
74
+ def __init__(self, stat_var_name: str, stat_var_unit: str, stat_channel: int, var_index: int):
75
+ self.stat_var_name = stat_var_name
76
+ self.stat_var_unit = stat_var_unit
77
+ self.stat_channel = stat_channel
78
+ self.var_index = var_index
79
+ # stat_var_name is in the form of "C_cyt_average" so remove _average to get the variable name
80
+ stat_type_raw = stat_var_name.split("_")[-1]
81
+ self.statistic_type = StatisticType[stat_type_raw.upper()]
82
+ self.var_name = stat_var_name.replace("_" + stat_type_raw, "")
83
+
84
+
85
+ class PostProcessing:
86
+ postprocessing_hdf5_path: Path
87
+ times: np.ndarray
88
+ variables: list[VariableInfo]
89
+ statistics: np.ndarray # shape (times, vars, stats) where status is average=0, total=1, min=2, max=3
90
+ image_metadata: list[ImageMetadata]
91
+
92
+ def __init__(self, postprocessing_hdf5_path: Path):
93
+ self.postprocessing_hdf5_path = postprocessing_hdf5_path
94
+ self.variables = []
95
+ self.image_metadata = []
96
+
97
+ def read(self) -> None:
98
+ # read the file as hdf5
99
+ with H5File(name=self.postprocessing_hdf5_path, mode="r") as file: # type: ignore[call-arg]
100
+ # read dataset with path /PostProcessing/Times
101
+ postprocessing_times_object = file["/PostProcessing/Times"]
102
+ if not isinstance(postprocessing_times_object, Dataset):
103
+ raise TypeError(
104
+ f"Expected a dataset at /PostProcessing/Times but found {type(postprocessing_times_object)}"
105
+ )
106
+ times_ds: Dataset = postprocessing_times_object
107
+ # read array from times dataset into a ndarray
108
+ self.times = times_ds[()]
109
+
110
+ # read attributes from group /PostProcessing/VariableStatistics
111
+ # data is flat, so we can read the attributes directly, so name and units for each channel are separate
112
+ #
113
+ # key=comp_0_name, value=b'C_cyt_average'
114
+ # key=comp_0_unit, value=b'uM'
115
+ # key=comp_1_name, value=b'C_cyt_total'
116
+ # key=comp_1_unit, value=b'molecules'
117
+ # key=comp_2_name, value=b'C_cyt_min'
118
+ # key=comp_2_unit, value=b'uM'
119
+ # key=comp_3_name, value=b'C_cyt_max'
120
+ # key=comp_3_unit, value=b'uM'
121
+ #
122
+ var_stats_grp_object = file["/PostProcessing/VariableStatistics"]
123
+ if not isinstance(var_stats_grp_object, Group):
124
+ raise TypeError(
125
+ f"Expected a group at /PostProcessing/VariableStatistics but found {type(var_stats_grp_object)}"
126
+ )
127
+ var_stats_grp: Group = var_stats_grp_object
128
+ # gather stat_var_name and stat_var_unit for each channel into dictionaries by channel
129
+ var_name_by_channel: dict[int, str] = {}
130
+ var_unit_by_channel: dict[int, str] = {}
131
+ for k, v in var_stats_grp.attrs.items():
132
+ parts = k.split("_")
133
+ channel = int(parts[1])
134
+ if not isinstance(v, bytes):
135
+ raise TypeError(f"Expected a bytes object for attribute {k} but found {type(v)}")
136
+ value = v.decode("utf-8")
137
+ if parts[2] == "name":
138
+ var_name_by_channel[channel] = value
139
+ elif parts[2] == "unit":
140
+ var_unit_by_channel[channel] = value
141
+ # combine into a single list of VariableInfo objects, one for each channel
142
+ self.variables = [
143
+ VariableInfo(
144
+ stat_var_name=var_name_by_channel[i],
145
+ stat_var_unit=var_unit_by_channel[i],
146
+ stat_channel=i,
147
+ var_index=i // 4,
148
+ )
149
+ for i in range(len(var_name_by_channel))
150
+ ]
151
+
152
+ # within /PostProcessing/VariableStatistics, there are datasets for each time point
153
+ # PostProcessing/VariableStatistics
154
+ # PostProcessing/VariableStatistics/time000000
155
+ # PostProcessing/VariableStatistics/time000001
156
+ # PostProcessing/VariableStatistics/time000002
157
+ # PostProcessing/VariableStatistics/time000003
158
+ # PostProcessing/VariableStatistics/time000004
159
+
160
+ # we can read the data for each time point into a list of ndarrays
161
+ statistics_raw: np.ndarray = np.zeros((len(self.times), len(self.variables)))
162
+ for time_index in range(len(self.times)):
163
+ time_ds_object = var_stats_grp[f"time{time_index:06d}"]
164
+ if not isinstance(time_ds_object, Dataset):
165
+ raise TypeError(
166
+ f"Expected a dataset at /PostProcessing/VariableStatistics/time{time_index:06d} "
167
+ f"but found {type(time_ds_object)}"
168
+ )
169
+ time_ds: Dataset = time_ds_object
170
+ statistics_raw[time_index, :] = time_ds[()]
171
+
172
+ # reshape the statistics_raw into a 3D array (times, vars, stats)
173
+ self.statistics = statistics_raw.reshape((len(self.times), len(self.variables) // 4, 4))
174
+
175
+ # get list of child groups from /PostProcessing which are not Times or VariableStatistics
176
+ # e.g. /PostProcessing/fluor
177
+ postprocessing_dataset = file["/PostProcessing"]
178
+ if not isinstance(postprocessing_dataset, Group):
179
+ raise TypeError(f"Expected a group at /PostProcessing but found {type(postprocessing_dataset)}")
180
+ image_groups = [k for k in postprocessing_dataset if k not in ["Times", "VariableStatistics"]]
181
+
182
+ # for each image group, read the metadata to allow reading later
183
+ for image_group in image_groups:
184
+ metadata = ImageMetadata(group_path=f"/PostProcessing/{image_group}", name=image_group)
185
+ metadata.read(file)
186
+ self.image_metadata.append(metadata)
187
+
188
+ def read_image_data(self, image_metadata: ImageMetadata, time_index: int) -> np.ndarray:
189
+ with H5File(name=self.postprocessing_hdf5_path, mode="r") as file: # type: ignore[call-arg]
190
+ image_ds = image_metadata.get_dataset(hdf5_file=file, time_index=time_index)
191
+ return np.array(image_ds[()])