pmcx 0.4.2__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.
- pmcx/__init__.py +69 -0
- pmcx/bench.py +46 -0
- pmcx/utils.py +683 -0
- pmcx-0.4.2.dist-info/METADATA +186 -0
- pmcx-0.4.2.dist-info/RECORD +7 -0
- pmcx-0.4.2.dist-info/WHEEL +5 -0
- pmcx-0.4.2.dist-info/top_level.txt +1 -0
pmcx/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Copyright (c) 2022-2023 Matin Raayai Ardakani <raayaiardakani.m at northeastern.edu>. All rights reserved.
|
|
2
|
+
# Copyright (c) 2022-2023 Qianqian Fang <q.fang at neu.edu>. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
"""PMCX - Python bindings for Monte Carlo eXtreme photon transport simulator
|
|
18
|
+
|
|
19
|
+
Example usage:
|
|
20
|
+
|
|
21
|
+
# To list available GPUs
|
|
22
|
+
import pmcx
|
|
23
|
+
pmcx.gpuinfo()
|
|
24
|
+
|
|
25
|
+
# To run a simulation
|
|
26
|
+
res = pmcx.run(nphoton=1000000, vol=np.ones([60, 60, 60], dtype='uint8'),
|
|
27
|
+
tstart=0, tend=5e-9, tstep=5e-9, srcpos=[30,30,0],
|
|
28
|
+
srcdir=[0,0,1], prop=np.array([[0, 0, 1, 1], [0.005, 1, 0.01, 1.37]]))
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
from _pmcx import gpuinfo, run, version
|
|
33
|
+
except ImportError: # pragma: no cover
|
|
34
|
+
print("the pmcx binary extension (_pmcx) is not compiled! please compile first")
|
|
35
|
+
|
|
36
|
+
from .utils import (
|
|
37
|
+
detweight,
|
|
38
|
+
cwdref,
|
|
39
|
+
meanpath,
|
|
40
|
+
meanscat,
|
|
41
|
+
dettpsf,
|
|
42
|
+
dettime,
|
|
43
|
+
tddiffusion,
|
|
44
|
+
getdistance,
|
|
45
|
+
detphoton,
|
|
46
|
+
mcxlab,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# from .files import loadmc2, loadmch, load, save
|
|
50
|
+
from .bench import bench
|
|
51
|
+
|
|
52
|
+
__version__ = "0.4.2"
|
|
53
|
+
|
|
54
|
+
__all__ = (
|
|
55
|
+
"gpuinfo",
|
|
56
|
+
"run",
|
|
57
|
+
"version",
|
|
58
|
+
"bench",
|
|
59
|
+
"detweight",
|
|
60
|
+
"cwdref",
|
|
61
|
+
"meanpath",
|
|
62
|
+
"meanscat",
|
|
63
|
+
"dettpsf",
|
|
64
|
+
"dettime",
|
|
65
|
+
"tddiffusion",
|
|
66
|
+
"getdistance",
|
|
67
|
+
"detphoton",
|
|
68
|
+
"mcxlab",
|
|
69
|
+
)
|
pmcx/bench.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Copyright (c) 2022-2023 Matin Raayai Ardakani <raayaiardakani.m at northeastern.edu>. All rights reserved.
|
|
2
|
+
# Copyright (c) 2022-2023 Qianqian Fang <q.fang at neu.edu>. All rights reserved.
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
"""Built-in benchmark configurations"""
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import copy
|
|
21
|
+
|
|
22
|
+
bench = {}
|
|
23
|
+
bench["cube60"] = {
|
|
24
|
+
"nphoton": 1000000,
|
|
25
|
+
"vol": np.ones([60, 60, 60], dtype="uint8"),
|
|
26
|
+
"tstart": 0,
|
|
27
|
+
"tend": 5e-9,
|
|
28
|
+
"tstep": 5e-9,
|
|
29
|
+
"srcpos": [29, 29, 0],
|
|
30
|
+
"srcdir": [0, 0, 1],
|
|
31
|
+
"prop": [[0, 0, 1, 1], [0.005, 1, 0.01, 1.37], [0.002, 5, 0.9, 1]],
|
|
32
|
+
"isreflect": 0,
|
|
33
|
+
"seed": 1648335518,
|
|
34
|
+
"session": "cube60",
|
|
35
|
+
"detpos": [[29, 19, 0, 1], [29, 39, 0, 1], [19, 29, 0, 1], [39, 29, 0, 1]],
|
|
36
|
+
"issrcfrom0": 1,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
bench["cube60b"] = copy.deepcopy(bench["cube60"])
|
|
40
|
+
bench["cube60b"]["isreflect"] = 1
|
|
41
|
+
|
|
42
|
+
bench["cube60planar"] = copy.deepcopy(bench["cube60b"])
|
|
43
|
+
bench["cube60planar"]["srctype"] = "planar"
|
|
44
|
+
bench["cube60planar"]["srcpos"] = [10, 10, -10]
|
|
45
|
+
bench["cube60planar"]["srcparam1"] = [40, 0, 0, 0]
|
|
46
|
+
bench["cube60planar"]["srcparam2"] = [0, 40, 0, 0]
|
pmcx/utils.py
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
# Copyright (c) 2023 Kuznetsov Ilya
|
|
2
|
+
# Copyright (c) 2023 Qianqian Fang (q.fang <at> neu.edu)
|
|
3
|
+
# Copyright (c) 2023 Fan-Yu (Ivy) Yen (yen.f at northeastern.edu)
|
|
4
|
+
#
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
"""Utilities for processing MCX input and output data"""
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
# sys.path.insert(0, '../../src/build/')
|
|
26
|
+
|
|
27
|
+
import pmcx
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def cwdref(detp, cfg):
|
|
31
|
+
"""
|
|
32
|
+
Compute CW diffuse reflectance from MC detected photon profiles.
|
|
33
|
+
|
|
34
|
+
(Python code was adapted from mcxcwdref.m MATLAB function, ported by Kuznetsov Ilya)
|
|
35
|
+
|
|
36
|
+
input:
|
|
37
|
+
detp: profiles of detected photons
|
|
38
|
+
cfg: a dictionary. Each element of cfg defines
|
|
39
|
+
the parameters associated with a MC simulation.
|
|
40
|
+
|
|
41
|
+
output:
|
|
42
|
+
dref: CW diffuse reflectance at detectors
|
|
43
|
+
|
|
44
|
+
this file is part of Monte Carlo eXtreme (MCX)
|
|
45
|
+
License: GPLv3, see https://mcx.space for details
|
|
46
|
+
see Yao2018
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
unitinmm = 1
|
|
50
|
+
if "unitinmm" in cfg:
|
|
51
|
+
unitinmm = cfg["unitinmm"]
|
|
52
|
+
|
|
53
|
+
det_weight = detweight(detp, cfg["prop"])
|
|
54
|
+
detnum = len(np.unique(detp["detid"]))
|
|
55
|
+
detweightsum = np.zeros(detnum)
|
|
56
|
+
|
|
57
|
+
for i in range(len(detp["detid"])):
|
|
58
|
+
index = int(detp["detid"][i]) - 1
|
|
59
|
+
detweightsum[index] += det_weight[i]
|
|
60
|
+
|
|
61
|
+
area = np.pi * (cfg["detpos"][:, 3] * unitinmm) ** 2
|
|
62
|
+
dref = detweightsum / area / cfg["nphoton"]
|
|
63
|
+
|
|
64
|
+
return dref
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------
|
|
68
|
+
def meanpath(detp, prop=None):
|
|
69
|
+
"""
|
|
70
|
+
Calculate the average pathlengths for each tissue type for a given source-detector pair
|
|
71
|
+
|
|
72
|
+
(Python code was adapted from mcxmeanpath.m MATLAB function, ported by Fan-Yu Yen)
|
|
73
|
+
|
|
74
|
+
input:
|
|
75
|
+
detp: the 2nd output from mcxlab. detp can be either a struct or an array (detp.data)
|
|
76
|
+
prop: optical property list, as defined in the cfg.prop field of mcxlab's input
|
|
77
|
+
|
|
78
|
+
output:
|
|
79
|
+
avgpath: the average pathlength for each tissue type
|
|
80
|
+
"""
|
|
81
|
+
if "unitinmm" in detp:
|
|
82
|
+
unitinmm = detp["unitinmm"]
|
|
83
|
+
else:
|
|
84
|
+
unitinmm = 1
|
|
85
|
+
|
|
86
|
+
if prop is None:
|
|
87
|
+
if "prop" in detp:
|
|
88
|
+
prop = detp["prop"]
|
|
89
|
+
else:
|
|
90
|
+
raise ValueError('Must provide input "prop"')
|
|
91
|
+
|
|
92
|
+
detw = detweight(detp, prop)
|
|
93
|
+
avgpath = np.sum(
|
|
94
|
+
detp["ppath"] * unitinmm * np.tile(detw, (detp["ppath"].shape[1], 1)).T, 0
|
|
95
|
+
) / np.sum(detw)
|
|
96
|
+
|
|
97
|
+
return avgpath
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def detweight(detp, prop=None, unitinmm=None):
|
|
101
|
+
"""
|
|
102
|
+
Recalculate the detected photon weight using partial path data and
|
|
103
|
+
optical properties (for perturbation Monte Carlo or detector readings)
|
|
104
|
+
|
|
105
|
+
(Python code was adapted from mcxdetweight.m MATLAB function, ported by Fan-Yu Yen)
|
|
106
|
+
|
|
107
|
+
input:
|
|
108
|
+
detp: the 2nd output from mcxlab. detp must be a dict.
|
|
109
|
+
prop: optical property list, as defined in the cfg.prop field of mcxlab's input.
|
|
110
|
+
unitinmm: voxel edge-length in mm, should use cfg.unitinmm used to generate detp;
|
|
111
|
+
if ignored, assume to be 1 (mm)
|
|
112
|
+
|
|
113
|
+
output:
|
|
114
|
+
detw: re-caculated detected photon weight based on the partial path data and optical property table
|
|
115
|
+
"""
|
|
116
|
+
if prop is None:
|
|
117
|
+
if "prop" in detp:
|
|
118
|
+
prop = detp["prop"]
|
|
119
|
+
else:
|
|
120
|
+
raise ValueError('Must provide input "prop"')
|
|
121
|
+
|
|
122
|
+
medianum = np.array(prop).shape[0]
|
|
123
|
+
if medianum <= 1:
|
|
124
|
+
raise ValueError("Empty property list")
|
|
125
|
+
|
|
126
|
+
if unitinmm is None:
|
|
127
|
+
if "unitinmm" in detp:
|
|
128
|
+
unitinmm = detp["unitinmm"]
|
|
129
|
+
else:
|
|
130
|
+
unitinmm = 1
|
|
131
|
+
|
|
132
|
+
if isinstance(detp, dict):
|
|
133
|
+
if "w0" not in detp:
|
|
134
|
+
detw = np.ones(detp["ppath"].shape[0])
|
|
135
|
+
else:
|
|
136
|
+
detw = detp["w0"]
|
|
137
|
+
|
|
138
|
+
for i in range(medianum - 1):
|
|
139
|
+
detw = detw * np.exp(-prop[i + 1, 0] * detp["ppath"][:, i] * unitinmm)
|
|
140
|
+
else:
|
|
141
|
+
raise ValueError('The first input must be a dict with a key named "ppath"')
|
|
142
|
+
|
|
143
|
+
return detw
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def meanscat(detp, prop):
|
|
147
|
+
"""
|
|
148
|
+
Calculate the average scattering event counts for each tissue type for a given source-detector pair
|
|
149
|
+
|
|
150
|
+
(Python code was adapted from mcxmeanscat.m MATLAB function, ported by Fan-Yu Yen)
|
|
151
|
+
|
|
152
|
+
input:
|
|
153
|
+
detp: the 2nd output from mcxlab. detp can be either a struct or an array (detp.data)
|
|
154
|
+
prop: optical property list, as defined in the cfg.prop field of mcxlab's input
|
|
155
|
+
|
|
156
|
+
output:
|
|
157
|
+
avgnscat: the average scattering event count for each tissue type
|
|
158
|
+
|
|
159
|
+
Python code was ported from mcxmeanpath.m MATLAB function by Fan-Yu Yen
|
|
160
|
+
"""
|
|
161
|
+
detw = detweight(detp, prop)
|
|
162
|
+
avgnscat = np.sum(
|
|
163
|
+
np.array(detp["nscat"], dtype=float) * detw[:, np.newaxis]
|
|
164
|
+
) / np.sum(detw)
|
|
165
|
+
|
|
166
|
+
return avgnscat
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def dettpsf(detp, detnum, prop, time):
|
|
170
|
+
"""
|
|
171
|
+
Calculate the temporal point spread function curve of a specified detector
|
|
172
|
+
given the partial path data, optical properties, and distribution of time bins
|
|
173
|
+
|
|
174
|
+
(Python code was adapted from mcxdettpsf.m MATLAB function, ported by Fan-Yu Yen)
|
|
175
|
+
|
|
176
|
+
input:
|
|
177
|
+
detp: the 2nd output from mcxlab. detp must be a struct with detid and ppath subfields
|
|
178
|
+
detnum: specified detector number
|
|
179
|
+
prop: optical property list, as defined in the cfg.prop field of mcxlab's input
|
|
180
|
+
time: distribution of time bins, a 1*3 vector [tstart tend tstep]
|
|
181
|
+
could be defined different from in the cfg of mcxlab's input
|
|
182
|
+
output:
|
|
183
|
+
tpsf: caculated temporal point spread function curve of the specified detector
|
|
184
|
+
"""
|
|
185
|
+
detp = detp.copy()
|
|
186
|
+
|
|
187
|
+
# select the photon data of the specified detector
|
|
188
|
+
detp["ppath"] = detp["ppath"][detp["detid"] == detnum, :]
|
|
189
|
+
detp["detid"] = detp["detid"][detp["detid"] == detnum]
|
|
190
|
+
|
|
191
|
+
# calculate the detected photon weight and arrival time
|
|
192
|
+
replayweight = detweight(detp, prop)
|
|
193
|
+
replaytime = dettime(detp, prop)
|
|
194
|
+
|
|
195
|
+
# define temporal point spread function vector
|
|
196
|
+
nTG = int(np.round((time[1] - time[0]) / time[2]))
|
|
197
|
+
tpsf = np.zeros((nTG, 1))
|
|
198
|
+
|
|
199
|
+
# calculate the time bin, make sure not to exceed the boundary
|
|
200
|
+
ntg = np.ceil((replaytime - time[0]) / time[2])
|
|
201
|
+
ntg[ntg < 1] = 1
|
|
202
|
+
ntg[ntg > nTG] = nTG
|
|
203
|
+
|
|
204
|
+
ntg = ntg.astype(int)
|
|
205
|
+
ntg = ntg[0]
|
|
206
|
+
|
|
207
|
+
# add each photon weight to corresponding time bin
|
|
208
|
+
for i in range(len(replayweight)):
|
|
209
|
+
tpsf[ntg[i] - 1] = tpsf[ntg[i] - 1] + replayweight[i]
|
|
210
|
+
|
|
211
|
+
return tpsf
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def dettime(detp, prop=None, unitinmm=None):
|
|
215
|
+
"""
|
|
216
|
+
Recalculate the detected photon time using partial path data and
|
|
217
|
+
optical properties (for perturbation Monte Carlo or detector readings)
|
|
218
|
+
|
|
219
|
+
(Python code was adapted from mcxdettime.m MATLAB function, ported by Fan-Yu Yen)
|
|
220
|
+
|
|
221
|
+
input:
|
|
222
|
+
detp: the 2nd output from mcxlab. detp must be a struct
|
|
223
|
+
prop: optical property list, as defined in the cfg.prop field of mcxlab's input
|
|
224
|
+
unitinmm: voxel edge-length in mm, should use cfg.unitinmm used to generate detp;
|
|
225
|
+
if ignored, assume to be 1 (mm)
|
|
226
|
+
|
|
227
|
+
output:
|
|
228
|
+
dett: re-caculated detected photon time based on the partial path data and optical property table
|
|
229
|
+
"""
|
|
230
|
+
R_C0 = 3.335640951981520e-12 # inverse of light speed in vacuum
|
|
231
|
+
if unitinmm is None:
|
|
232
|
+
if "unitinmm" in detp:
|
|
233
|
+
unitinmm = detp["unitinmm"]
|
|
234
|
+
else:
|
|
235
|
+
unitinmm = 1
|
|
236
|
+
|
|
237
|
+
if prop is None:
|
|
238
|
+
if "prop" in detp:
|
|
239
|
+
prop = detp["prop"]
|
|
240
|
+
else:
|
|
241
|
+
raise ValueError('must provide input "prop"')
|
|
242
|
+
|
|
243
|
+
medianum = prop.shape[0]
|
|
244
|
+
if medianum <= 1:
|
|
245
|
+
raise ValueError("empty property list")
|
|
246
|
+
|
|
247
|
+
if isinstance(detp, dict):
|
|
248
|
+
dett = np.zeros((1, detp["ppath"].shape[0]))
|
|
249
|
+
for i in range(medianum - 1):
|
|
250
|
+
dett = dett + prop[i + 1, 3] * detp["ppath"][:, i] * R_C0 * unitinmm
|
|
251
|
+
else:
|
|
252
|
+
raise ValueError(
|
|
253
|
+
'the first input must be a struct with a subfield named "ppath"'
|
|
254
|
+
)
|
|
255
|
+
return dett
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def tddiffusion(mua, musp, v, Reff, srcpos, detpos, t):
|
|
259
|
+
"""
|
|
260
|
+
Semi-infinite medium analytical solution to diffusion model
|
|
261
|
+
|
|
262
|
+
(Python code was adapted from tddiffusion.m MATLAB function, ported by Fan-Yu Yen)
|
|
263
|
+
|
|
264
|
+
input:
|
|
265
|
+
mua: the absorption coefficients in 1/mm
|
|
266
|
+
musp: the reduced scattering coefficients in 1/mm
|
|
267
|
+
v: the speed of light
|
|
268
|
+
Reff: the effective reflection coeff.
|
|
269
|
+
srcpos:array for the source positions (x,y,z)
|
|
270
|
+
detpos:array for the detector positions (x,y,z)
|
|
271
|
+
t: a list of time in s at which to evaluate the
|
|
272
|
+
analytical diffusion solution
|
|
273
|
+
|
|
274
|
+
output:
|
|
275
|
+
Phi: the output fluence for all time points
|
|
276
|
+
"""
|
|
277
|
+
srcpos = np.atleast_2d(srcpos)
|
|
278
|
+
detpos = np.atleast_2d(detpos)
|
|
279
|
+
|
|
280
|
+
D = 1 / (3 * (mua + musp))
|
|
281
|
+
zb = (1 + Reff) / (1 - Reff) * 2 * D
|
|
282
|
+
|
|
283
|
+
z0 = 1 / (musp + mua)
|
|
284
|
+
r = getdistance(
|
|
285
|
+
np.concatenate((srcpos[:, 0:2].flatten(), srcpos[:, 2].flatten() + z0)), detpos
|
|
286
|
+
)
|
|
287
|
+
r2 = getdistance(
|
|
288
|
+
np.concatenate(
|
|
289
|
+
(srcpos[:, 0:2].flatten(), srcpos[:, 2].flatten() - z0 - 2 * zb)
|
|
290
|
+
),
|
|
291
|
+
detpos,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
s = 4 * D * v * t
|
|
295
|
+
|
|
296
|
+
# unit of phi: 1/(mm^2*s)
|
|
297
|
+
Phi = (
|
|
298
|
+
v
|
|
299
|
+
/ ((s * np.pi) ** (3 / 2))
|
|
300
|
+
* np.exp(-mua * v * t)
|
|
301
|
+
* (np.exp(-(r**2) / s) - np.exp(-(r2**2) / s))
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
return Phi
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def getdistance(srcpos, detpos):
|
|
308
|
+
"""
|
|
309
|
+
Compute the source/detector separation from the positions
|
|
310
|
+
|
|
311
|
+
(Python code was adapted from getdistance.m MATLAB function, ported by Fan-Yu Yen)
|
|
312
|
+
|
|
313
|
+
input:
|
|
314
|
+
srcpos:array for the source positions (x,y,z)
|
|
315
|
+
detpos:array for the detector positions (x,y,z)
|
|
316
|
+
|
|
317
|
+
output:
|
|
318
|
+
separation: the distance matrix between all combinations
|
|
319
|
+
of sources and detectors. separation has the number
|
|
320
|
+
of source rows, and number of detector of columns.
|
|
321
|
+
"""
|
|
322
|
+
srcpos = np.atleast_2d(srcpos)
|
|
323
|
+
detpos = np.atleast_2d(detpos)
|
|
324
|
+
|
|
325
|
+
srcnum = len(srcpos[:, 0])
|
|
326
|
+
detnum = len(detpos[:, 0])
|
|
327
|
+
|
|
328
|
+
separation = np.zeros((detnum, srcnum))
|
|
329
|
+
|
|
330
|
+
for s in range(srcnum):
|
|
331
|
+
for r in range(detnum):
|
|
332
|
+
separation[r, s] = np.linalg.norm(srcpos[s, :] - detpos[r, :])
|
|
333
|
+
return separation
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def detphoton(detp, medianum, savedetflag, issaveref=None, srcnum=None):
|
|
337
|
+
"""
|
|
338
|
+
Separating combined detected photon data into easy-to-read structure based on
|
|
339
|
+
user-specified detected photon output format ("savedetflag")
|
|
340
|
+
|
|
341
|
+
(Python code was adapted from mcxdetphoton.m MATLAB function, ported by Fan-Yu Yen)
|
|
342
|
+
|
|
343
|
+
input:
|
|
344
|
+
detp: a 2-D array defining the combined detected photon data, usually
|
|
345
|
+
detp.data, where detp is the 2nd output from mcxlab
|
|
346
|
+
medianum: the total number of non-zero tissue types (row number of cfg.prop minus 1)
|
|
347
|
+
savedetflag: the cfg.savedetflag string, containing letters 'dspmxvwi' denoting different
|
|
348
|
+
output data fields, please see mcxlab's help
|
|
349
|
+
issaveref: the cfg.issaveref flag, 1 for saving diffuse reflectance, 0 not to save
|
|
350
|
+
srcnum: the cfg.srcnum flag, denoting the number of source patterns in the photon-sharing mode
|
|
351
|
+
|
|
352
|
+
output:
|
|
353
|
+
newdetp: re-organized detected photon data as a dict; the mapping of the fields are
|
|
354
|
+
newdetp['detid']: the ID(>0) of the detector that captures the photon (1)
|
|
355
|
+
newdetp['nscat']: cummulative scattering event counts in each medium (#medium)
|
|
356
|
+
newdetp['ppath']: cummulative path lengths in each medium, i.e. partial pathlength (#medium)
|
|
357
|
+
one need to multiply cfg.unitinmm with ppath to convert it to mm.
|
|
358
|
+
newdetp['mom']: cummulative cos_theta for momentum transfer in each medium (#medium)
|
|
359
|
+
newdetp['p'] or ['v']: exit position and direction, when cfg.issaveexit=1 (3)
|
|
360
|
+
newdetp['w0']: photon initial weight at launch time (3)
|
|
361
|
+
newdetp['s']: exit Stokes parameters for polarized photon (4)
|
|
362
|
+
newdetp['srcid']: the ID of the source when multiple sources are defined (1)
|
|
363
|
+
"""
|
|
364
|
+
newdetp = {}
|
|
365
|
+
c0 = 0
|
|
366
|
+
length = 0
|
|
367
|
+
|
|
368
|
+
if re.search("[dD]", savedetflag):
|
|
369
|
+
if issaveref is not None and issaveref > 1:
|
|
370
|
+
newdetp["w0"] = detp[0, :].transpose()
|
|
371
|
+
else:
|
|
372
|
+
newdetp["detid"] = detp[0, :].astype(int).transpose()
|
|
373
|
+
if np.any(newdetp["detid"] > 65535):
|
|
374
|
+
newdetp["srcid"] = np.right_shift(newdetp["detid"], 16, dtype=np.int32)
|
|
375
|
+
newdetp["detid"] = np.bitwise_and(
|
|
376
|
+
newdetp["detid"], 0xFFFF, dtype=np.int32
|
|
377
|
+
)
|
|
378
|
+
c0 = 1
|
|
379
|
+
|
|
380
|
+
length = medianum
|
|
381
|
+
if re.search("[sS]", savedetflag):
|
|
382
|
+
newdetp["nscat"] = detp[c0 : c0 + length, :].astype(int).transpose()
|
|
383
|
+
c0 = c0 + length
|
|
384
|
+
|
|
385
|
+
if re.search("[pP]", savedetflag):
|
|
386
|
+
newdetp["ppath"] = detp[c0 : c0 + length, :].transpose()
|
|
387
|
+
c0 = c0 + length
|
|
388
|
+
|
|
389
|
+
if re.search("[mM]", savedetflag):
|
|
390
|
+
newdetp["mom"] = detp[c0 : c0 + length, :].transpose()
|
|
391
|
+
c0 = c0 + length
|
|
392
|
+
|
|
393
|
+
length = 3
|
|
394
|
+
if re.search("[xX]", savedetflag):
|
|
395
|
+
newdetp["p"] = detp[c0 : c0 + length, :].transpose()
|
|
396
|
+
c0 = c0 + length
|
|
397
|
+
|
|
398
|
+
if re.search("[vV]", savedetflag):
|
|
399
|
+
newdetp["v"] = detp[c0 : c0 + length, :].transpose()
|
|
400
|
+
c0 = c0 + length
|
|
401
|
+
|
|
402
|
+
if re.search("[wW]", savedetflag):
|
|
403
|
+
length = 1
|
|
404
|
+
newdetp["w0"] = detp[c0 : c0 + length, :].transpose()
|
|
405
|
+
if srcnum is not None and srcnum > 1:
|
|
406
|
+
newdetp["w0"] = newdetp["w0"].view("uint32")
|
|
407
|
+
c0 = c0 + length
|
|
408
|
+
|
|
409
|
+
if re.search("[iI]", savedetflag):
|
|
410
|
+
length = 4
|
|
411
|
+
newdetp["s"] = detp[c0 : c0 + length, :].transpose()
|
|
412
|
+
c0 = c0 + length
|
|
413
|
+
|
|
414
|
+
return newdetp
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def mcxlab(*args):
|
|
418
|
+
"""
|
|
419
|
+
Python wrapper of mcxlab - please see the help information of mcxlab.m for details
|
|
420
|
+
|
|
421
|
+
(Python code was adapted from mcxlab.m MATLAB function, ported by Fan-Yu Yen)
|
|
422
|
+
|
|
423
|
+
Format:
|
|
424
|
+
res = mcxlab(cfg);
|
|
425
|
+
or
|
|
426
|
+
res = mcxlab(cfg, option);
|
|
427
|
+
|
|
428
|
+
Input:
|
|
429
|
+
cfg: a dictionary defining the parameters associated with a simulation.
|
|
430
|
+
if cfg='gpuinfo': return the supported GPUs and their parameters,
|
|
431
|
+
if cfg='version': return the version of MCXLAB as a string,
|
|
432
|
+
option: (optional), options is a string, specifying additional options
|
|
433
|
+
option='opencl': force using mcxcl.mex* instead of mcx.mex* on NVIDIA/AMD/Intel hardware
|
|
434
|
+
option='cuda': force using mcx.mex* instead of mcxcl.mex* on NVIDIA GPUs
|
|
435
|
+
|
|
436
|
+
if one defines USE_MCXCL=1 in as Python global variable, all following
|
|
437
|
+
pmcx.mcxlab calls will use _pmcxcl (OpenCL version of mcx); by setting option='cuda',
|
|
438
|
+
one can force pmcx.mcxlab to use _pmcx (CUDA version). Similarly, if
|
|
439
|
+
USE_MCXCL=0, all pmcx.mcxlab calls will use _pmcx by default, unless
|
|
440
|
+
one sets option='opencl'.
|
|
441
|
+
|
|
442
|
+
Output:
|
|
443
|
+
res: a dictionary containing the following subfields
|
|
444
|
+
res['flux'] is a 4D array with
|
|
445
|
+
dimensions specified by [size(vol) total-time-gates].
|
|
446
|
+
The content of the array is the normalized fluence at
|
|
447
|
+
each voxel of each time-gate.
|
|
448
|
+
|
|
449
|
+
when cfg.debuglevel contains 'T', fluence(i).data stores trajectory
|
|
450
|
+
output, see below
|
|
451
|
+
res['dref'] is a 4D array with the same dimension as fluence(i).data
|
|
452
|
+
if cfg.issaveref is set to 1, containing only non-zero values in the
|
|
453
|
+
layer of voxels immediately next to the non-zero voxels in cfg.vol,
|
|
454
|
+
storing the normalized total diffuse reflectance (summation of the weights
|
|
455
|
+
of all escaped photon to the background regardless of their direction);
|
|
456
|
+
it is an empty array [] when if cfg.issaveref is 0.
|
|
457
|
+
res['stat'] is a structure storing additional information, including
|
|
458
|
+
runtime: total simulation run-time in millisecond
|
|
459
|
+
nphoton: total simulated photon number
|
|
460
|
+
energytot: total initial weight/energy of all launched photons
|
|
461
|
+
energyabs: total absorbed weight/energy of all photons
|
|
462
|
+
normalizer: normalization factor
|
|
463
|
+
unitinmm: same as cfg.unitinmm, voxel edge-length in mm
|
|
464
|
+
|
|
465
|
+
res['detp']: res['detp'] is a directionary object including the following field
|
|
466
|
+
res['detp']['detid']: the ID(>0) of the detector that captures the photon
|
|
467
|
+
res['detp']['nscat']: cummulative scattering event counts in each medium
|
|
468
|
+
res['detp']['ppath']: cummulative path lengths in each medium (partial pathlength)
|
|
469
|
+
one need to multiply cfg.unitinmm with ppath to convert it to mm.
|
|
470
|
+
res['detp']['mom']: cummulative cos_theta for momentum transfer in each medium
|
|
471
|
+
res['detp']['p'] or ['v']: exit position and direction, when cfg.issaveexit=1
|
|
472
|
+
res['detp']['nscat']: photon initial weight at launch time
|
|
473
|
+
res['detp']['s']: exit Stokes parameters for polarized photon
|
|
474
|
+
res['detp']['prop']: optical properties, a copy of cfg.prop
|
|
475
|
+
res['detp']['data']: a concatenated and transposed array in the order of
|
|
476
|
+
[detid nscat ppath mom p v w0]'
|
|
477
|
+
|
|
478
|
+
if returned by pmcx.run, res['det'] is a 2D numpy array in a format as
|
|
479
|
+
res['detp']['data'] described above
|
|
480
|
+
|
|
481
|
+
res['vol']: (optional) a numpy array storing a preprocessed volume.
|
|
482
|
+
Each volume is a 3D int32 array.
|
|
483
|
+
res['seeds']: (optional), if give, mcxlab returns the seeds, in the form of
|
|
484
|
+
a byte array (uint8) for each detected photon. The column number
|
|
485
|
+
of seed equals that of res['detp'].
|
|
486
|
+
res['traj']: if given, mcxlab returns the trajectory data for
|
|
487
|
+
each simulated photon. The output has 6 rows, the meanings are
|
|
488
|
+
res['traj']['id']: 1: index of the photon packet
|
|
489
|
+
res['traj']['pos']: 2-4: x/y/z/ of each trajectory position
|
|
490
|
+
5: current photon packet weight
|
|
491
|
+
6: reserved
|
|
492
|
+
By default, mcxlab only records the first 1e7 positions along all
|
|
493
|
+
simulated photons; change cfg['maxjumpdebug'] to define a different limit.
|
|
494
|
+
"""
|
|
495
|
+
try:
|
|
496
|
+
defaultocl = eval("USE_MCXCL", globals())
|
|
497
|
+
except:
|
|
498
|
+
defaultocl = 0
|
|
499
|
+
|
|
500
|
+
useopencl = defaultocl
|
|
501
|
+
|
|
502
|
+
if len(args) == 1 and isinstance(args[0], str):
|
|
503
|
+
if args[0] == "gpuinfo":
|
|
504
|
+
varargout = pmcx.gpuinfo()
|
|
505
|
+
return varargout
|
|
506
|
+
elif args[0] == "version":
|
|
507
|
+
varargout = pmcx.version()
|
|
508
|
+
return varargout
|
|
509
|
+
|
|
510
|
+
if len(args) == 2 and isinstance(args[1], str):
|
|
511
|
+
if args[1] == "preview":
|
|
512
|
+
varargout = mcxpreview(args[0])
|
|
513
|
+
return varargout
|
|
514
|
+
elif args[1] == "opencl":
|
|
515
|
+
useopencl = 1
|
|
516
|
+
|
|
517
|
+
if isinstance(args[0], dict):
|
|
518
|
+
castlist = [
|
|
519
|
+
"srcpattern",
|
|
520
|
+
"srcpos",
|
|
521
|
+
"detpos",
|
|
522
|
+
"prop",
|
|
523
|
+
"workload",
|
|
524
|
+
"srcdir",
|
|
525
|
+
"srciquv",
|
|
526
|
+
]
|
|
527
|
+
for j in range(len(castlist)):
|
|
528
|
+
if castlist[j] in args[0]:
|
|
529
|
+
args[0][castlist[j]] = np.double(args[0][castlist[j]])
|
|
530
|
+
|
|
531
|
+
if "vol" in args[0] and np.ndim(args[0]["vol"]) == 4:
|
|
532
|
+
if (
|
|
533
|
+
isinstance(args[0]["vol"], np.float32)
|
|
534
|
+
or isinstance(args[0]["vol"], np.float64)
|
|
535
|
+
) and "unitinmm" in args[0]:
|
|
536
|
+
args[0]["vol"] = args[0]["vol"] * args[0]["unitinmm"]
|
|
537
|
+
|
|
538
|
+
if "tstart" not in args[0]:
|
|
539
|
+
args[0]["tstart"] = 0
|
|
540
|
+
|
|
541
|
+
if "tend" not in args[0]:
|
|
542
|
+
raise ValueError(
|
|
543
|
+
"you must define cfg.tend for the maximum time-of-flight of a photon in seconds"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
if "tstep" not in args[0]:
|
|
547
|
+
args[0]["tsetp"] = args[0]["tend"]
|
|
548
|
+
|
|
549
|
+
if "srcpos" not in args[0]:
|
|
550
|
+
raise ValueError(
|
|
551
|
+
"you must define cfg.srcpos to define the x/y/z position of the source in voxel unit"
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
if "detphotons" in args[0] and isinstance(args[0]["detphotons"], dict):
|
|
555
|
+
if "data" in args[0]["detphotons"]:
|
|
556
|
+
args[0]["detphotons"] = args[0]["dephotons"]["data"]
|
|
557
|
+
else:
|
|
558
|
+
fulldetdata = ["detid", "nscat", "ppath", "mom", "p", "v", "w0"]
|
|
559
|
+
detfields = [x in args[0]["detphotons"] for x in fulldetdata]
|
|
560
|
+
detdata = []
|
|
561
|
+
for j in range(len(detfields)):
|
|
562
|
+
if detfields[j]:
|
|
563
|
+
val = np.array(
|
|
564
|
+
args[0]["detphotons"][fulldetdata[j]], dtype=np.float32
|
|
565
|
+
)
|
|
566
|
+
detdata.append(
|
|
567
|
+
np.reshape(
|
|
568
|
+
val, np.shape(args[0]["detphotons"][fulldetdata[j]])
|
|
569
|
+
)
|
|
570
|
+
)
|
|
571
|
+
args[0]["detphotons"] = detdata.transpose()
|
|
572
|
+
args[0]["savedetflag"] = "dspmxvw"
|
|
573
|
+
args[0]["savedetflag"][detfields == 0] = []
|
|
574
|
+
|
|
575
|
+
if useopencl == 0:
|
|
576
|
+
varargout = pmcx.run(args[0])
|
|
577
|
+
else:
|
|
578
|
+
try:
|
|
579
|
+
import pmcxcl
|
|
580
|
+
|
|
581
|
+
varargout = pmcxcl.run(args[0])
|
|
582
|
+
except ImportError:
|
|
583
|
+
raise ImportError(
|
|
584
|
+
'To call OpenCL based MCX, one must first run "pip install pmcxcl" to install pmcxcl'
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
if len(args) == 0:
|
|
588
|
+
return
|
|
589
|
+
|
|
590
|
+
cfg = args[0]
|
|
591
|
+
|
|
592
|
+
if not isinstance(cfg, str):
|
|
593
|
+
if "srcnum" in cfg and cfg["srcnum"] > 1:
|
|
594
|
+
dim = varargout["flux"].shape
|
|
595
|
+
varargout["flux"] = varargout["flux"].reshape(
|
|
596
|
+
[cfg["srcnum"], int(dim[0] / cfg["srcnum"]), *dim[1:]]
|
|
597
|
+
)
|
|
598
|
+
varargout["flux"] = np.transpose(
|
|
599
|
+
varargout["flux"], [i for i in range(1, len(dim) + 1)] + [0]
|
|
600
|
+
)
|
|
601
|
+
if "dref" in varargout and varargout["dref"] is not None:
|
|
602
|
+
varargout["dref"] = varargout["dref"].reshape(
|
|
603
|
+
[cfg["srcnum"], int(dim[0] / cfg["srcnum"]), *dim[1:]]
|
|
604
|
+
)
|
|
605
|
+
varargout["dref"] = np.transpose(
|
|
606
|
+
varargout["dref"], [i for i in range(1, len(dim) + 1)] + [0]
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
if "detp" in varargout:
|
|
610
|
+
# for i in range(len(varargout.keys())): #for i in range(varargout[1]):
|
|
611
|
+
if ("savedetflag" not in cfg) or (
|
|
612
|
+
("savedetflag" in cfg) and (not cfg["savedetflag"])
|
|
613
|
+
):
|
|
614
|
+
cfg["savedetflag"] = "DP"
|
|
615
|
+
|
|
616
|
+
if "issaveexit" in cfg and cfg["issaveexit"]:
|
|
617
|
+
cfg["savedetflag"] += "XV"
|
|
618
|
+
|
|
619
|
+
if "ismomentum" in cfg and cfg["ismomentum"]:
|
|
620
|
+
cfg["savedetflag"] += "M"
|
|
621
|
+
|
|
622
|
+
if "polprop" in cfg and cfg["polprop"]:
|
|
623
|
+
cfg["savedetflag"] += "PVWI"
|
|
624
|
+
else:
|
|
625
|
+
cfg["savedetflag"] = (
|
|
626
|
+
cfg["savedetflag"].replace("I", "").replace("i", "")
|
|
627
|
+
) # cfg(i).savedetflag(regexp(cfg(i).savedetflag,'[Ii]'))=[];
|
|
628
|
+
|
|
629
|
+
if np.ndim(cfg["vol"]) == 4 and np.shape(cfg["vol"])[0] != 8:
|
|
630
|
+
cfg["savedetflag"] = ""
|
|
631
|
+
if (
|
|
632
|
+
isinstance(cfg["vol"], float) or isinstance(cfg["vol"], double)
|
|
633
|
+
) and "unitinmm" in cfg: # if((isa(cfg(i).vol,'single') || isa(cfg(i).vol,'double')) && isfield(cfg(i),'unitinmm'))
|
|
634
|
+
cfg["vol"] = cfg["vol"] * cfg["unitinmm"]
|
|
635
|
+
|
|
636
|
+
if "issaveexit" not in cfg or cfg["issaveexit"] != 2:
|
|
637
|
+
medianum = np.shape(cfg["prop"])[0] - 1
|
|
638
|
+
detp = varargout["detp"] # detp = varargout[1]['data']
|
|
639
|
+
# if not detp:
|
|
640
|
+
# continue
|
|
641
|
+
|
|
642
|
+
if "polprop" in cfg and cfg["polprop"]:
|
|
643
|
+
medianum = np.shape(cfg["polprop"])[0]
|
|
644
|
+
|
|
645
|
+
flags = [cfg["savedetflag"]]
|
|
646
|
+
|
|
647
|
+
if "issaveref" in cfg:
|
|
648
|
+
flags.append(cfg["issaveref"])
|
|
649
|
+
|
|
650
|
+
if "srcnum" in cfg:
|
|
651
|
+
flags.append(cfg["srcnum"])
|
|
652
|
+
|
|
653
|
+
newdetp = detphoton(
|
|
654
|
+
detp, medianum, *flags
|
|
655
|
+
) # newdetp=mcxdetphoton(detp,medianum,flags{:});
|
|
656
|
+
newdetp["prop"] = cfg["prop"]
|
|
657
|
+
|
|
658
|
+
if "polprop" in cfg and cfg["polprop"] and "prop" in varargout:
|
|
659
|
+
newdetp["prop"][1:] = np.transpose(varargout["prop"][:, 1:])
|
|
660
|
+
|
|
661
|
+
if "unitinmm" in cfg:
|
|
662
|
+
newdetp["unitinmm"] = cfg["unitinmm"]
|
|
663
|
+
|
|
664
|
+
newdetp["data"] = detp
|
|
665
|
+
newdetpstruct = newdetp
|
|
666
|
+
else:
|
|
667
|
+
newdetpstruct = varargout["detp"]
|
|
668
|
+
|
|
669
|
+
if "newdetpstruct" in locals():
|
|
670
|
+
varargout["detp"] = newdetpstruct # varargout = newdetpstruct
|
|
671
|
+
|
|
672
|
+
if "traj" in varargout:
|
|
673
|
+
data = varargout["traj"]
|
|
674
|
+
if len(data):
|
|
675
|
+
traj = {}
|
|
676
|
+
traj["pos"] = np.transpose(data[1:4, :])
|
|
677
|
+
traj["id"] = np.frombuffer(data[0, :].tobytes(), dtype=np.uint32)
|
|
678
|
+
traj["id"], idx = np.sort(traj["id"]), np.argsort(traj["id"])
|
|
679
|
+
traj["pos"] = traj["pos"][idx, :]
|
|
680
|
+
traj["data"] = np.vstack([np.single(traj["id"]), data[1:, idx]])
|
|
681
|
+
varargout["traj"] = traj
|
|
682
|
+
|
|
683
|
+
return varargout
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pmcx
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: Python bindings for Monte Carlo eXtreme photon transport simulator
|
|
5
|
+
Home-page: https://github.com/fangq/mcx
|
|
6
|
+
Download-URL: https://mcx.space
|
|
7
|
+
Author: Matin Raayai Ardakani, Qianqian Fang, Fan-Yu Yen
|
|
8
|
+
Author-email: raayaiardakani.m@northeastern.edu, q.fang@neu.edu, yen.f@northeastern.edu
|
|
9
|
+
Maintainer: Qianqian Fang
|
|
10
|
+
License: GPLv3+
|
|
11
|
+
Keywords: Monte Carlo simulation,Biophotonics,Ray-tracing,Rendering,GPU,Modeling,Biomedical Optics,Tissue Optics,Simulator,Optics,NVIDIA,CUDA
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Environment :: GPU :: NVIDIA CUDA
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.6
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Requires: numpy
|
|
24
|
+
Requires-Python: >=3.6
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
Dynamic: author
|
|
27
|
+
Dynamic: author-email
|
|
28
|
+
Dynamic: classifier
|
|
29
|
+
Dynamic: description
|
|
30
|
+
Dynamic: description-content-type
|
|
31
|
+
Dynamic: download-url
|
|
32
|
+
Dynamic: home-page
|
|
33
|
+
Dynamic: keywords
|
|
34
|
+
Dynamic: license
|
|
35
|
+
Dynamic: maintainer
|
|
36
|
+
Dynamic: requires
|
|
37
|
+
Dynamic: requires-python
|
|
38
|
+
Dynamic: summary
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+
# PMCX - Python bindings for Monte Carlo eXtreme photon transport simulator
|
|
43
|
+
|
|
44
|
+
- Copyright: (C) Matin Raayai Ardakani (2022-2023) <raayaiardakani.m at northeastern.edu>, Qianqian Fang (2019-2025) <q.fang at neu.edu>, Fan-Yu Yen (2023-2024) <yen.f at northeastern.edu>
|
|
45
|
+
- License: GNU Public License V3 or later
|
|
46
|
+
- Version: 0.4.2
|
|
47
|
+
- URL: https://pypi.org/project/pmcx/
|
|
48
|
+
- Github: https://github.com/fangq/mcx
|
|
49
|
+
|
|
50
|
+
\
|
|
51
|
+
\
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
This module provides a Python binding for Monte Carlo eXtreme (MCX).
|
|
55
|
+
For other binaries, including the standalone executable and the MATLAB bindings, see [our website](http://mcx.space).
|
|
56
|
+
|
|
57
|
+
Monte Carlo eXtreme (MCX) is a fast photon transport simulation software for 3D
|
|
58
|
+
heterogeneous turbid media. By taking advantage of the massively parallel
|
|
59
|
+
threads and extremely low memory latency in a modern graphics processing unit
|
|
60
|
+
(GPU), MCX is capable of performing Monte Carlo (MC) photon simulations at a
|
|
61
|
+
blazing speed, typically hundreds to a thousand times faster than a fully
|
|
62
|
+
optimized CPU-based MC implementation.
|
|
63
|
+
|
|
64
|
+
## How to Install
|
|
65
|
+
|
|
66
|
+
* PIP: ```pip install pmcx``` see https://pypi.org/project/pmcx/
|
|
67
|
+
|
|
68
|
+
## Runtime Dependencies
|
|
69
|
+
* **NVIDIA GPU Driver**: A CUDA-capable NVIDIA GPU and driver is required to run MCX. An up-to-date driver is recommended.
|
|
70
|
+
The binary wheel distributed over pip runs on NVIDIA drivers with CUDA 10.1 support on Windows, CUDA 9.2 support on Linux, and
|
|
71
|
+
CUDA 10.2 support on macOS, respectively. For more details on driver versions and their CUDA support, see the
|
|
72
|
+
[CUDA Release Notes](https://docs.nvidia.com/cuda/cuda-toolkit-release-notes/index.html).
|
|
73
|
+
To download the latest driver for your system, see the
|
|
74
|
+
[NVIDIA Driver Download Page](https://www.nvidia.com/download/index.aspx).
|
|
75
|
+
**You shouldn't need to have CUDA toolkit installed**. MCX is built with the static CUDA runtime library.
|
|
76
|
+
* **Python**: Python 3.6 and newer is required. **Python 2 is not supported**.
|
|
77
|
+
* **numpy**: Used to pass/receive volumetric information to/from pmcx. To install, use either conda or pip
|
|
78
|
+
package managers: `pip install numpy` or `conda install numpy`
|
|
79
|
+
* (optional) **jdata**: Only needed to read/write JNIfTI output files. To install, use pip: `pip install jdata`
|
|
80
|
+
on all operating systems; For Debian-based Linux distributions, you can also install to the system interpreter
|
|
81
|
+
using apt-get: `sudo apt-get install python3-jdata`. See https://pypi.org/project/jdata/ for more details.
|
|
82
|
+
* (optional) **bjdata**: Only needed to read/write BJData/UBJSON files. To install, run `pip install bjdata`
|
|
83
|
+
on all operating systems; For Debian-based Linux distributions, you can also install to the system interpreter
|
|
84
|
+
using apt-get: `sudo apt-get install python3-bjdata`. See https://pypi.org/project/bjdata/ for more details.
|
|
85
|
+
* (optional) **matplotlib**: For plotting the results. To install, run either `pip install matplotlib` or
|
|
86
|
+
`conda install matplotlib`
|
|
87
|
+
|
|
88
|
+
## Build Instructions
|
|
89
|
+
|
|
90
|
+
### Build Dependencies
|
|
91
|
+
* **Operating System**: Windows and Linux are fully supported; For building MCX on macOS, OSX 10.13 (High Sierra) and
|
|
92
|
+
older are highly recommended since 10.13 was the last version of macOS with NVIDIA CUDA support, and matching the CUDA
|
|
93
|
+
compiler version with the C/C++ compiler shipped with Xcode is easier. Newer macOS versions can be used for building MCX,
|
|
94
|
+
but need to have System Integrity Protection disabled prior to installing the CUDA toolkit due to the NVIDIA installer copying
|
|
95
|
+
its payload under the ```/Developer``` directory under root.
|
|
96
|
+
* **NVIDIA CUDA Toolkit**: CUDA 7.5 or newer is required. On macOS, 10.2 is the last available CUDA version.
|
|
97
|
+
For details on how to install CUDA, see the [CUDA Download Page](https://developer.nvidia.com/cuda-downloads).
|
|
98
|
+
The NVIDIA GPU driver of the target system must support the selected CUDA toolkit.
|
|
99
|
+
* **Python Interpreter**: Python 3.6 or above. The ```pip``` Python package manager and the ```wheel``` package (available
|
|
100
|
+
via ```pip```) are not required but recommended.
|
|
101
|
+
* **C/C++ Compiler**: CUDA Toolkit supports only the following compilers:
|
|
102
|
+
* GNU GCC for Linux-based distributions.
|
|
103
|
+
* Microsoft Visual Studio C/C++ Compiler for Windows.
|
|
104
|
+
* Apple Clang for macOS, available via Xcode. The last Xcode version supported by CUDA 10.2 is 10.3. If using an OSX
|
|
105
|
+
version higher than 10.15 it can be downloaded and installed from [Apple's Developer Website](https://developer.apple.com/download/)
|
|
106
|
+
with an Apple ID. After installation, select the proper Xcode version from the commandline, and set the ```SDKROOT```
|
|
107
|
+
environment variable:
|
|
108
|
+
```zsh
|
|
109
|
+
sudo xcode-select -s /Applications/Xcode_10.3.app/Contents/Developer/
|
|
110
|
+
export SDKROOT=/Applications/Xcode_10.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Refer to each OS's online documentations for more in-depth information on how to install these compilers.
|
|
114
|
+
Note that the version of the C/C++ compiler used must be supported by the CUDA toolkit version. If not, compilation
|
|
115
|
+
will fail with an error notifying you of this problem. See the [CUDA Installation Guides](https://developer.nvidia.com/cuda-toolkit-archive)
|
|
116
|
+
for more details.
|
|
117
|
+
* **OpenMP**: The installed C/C++ Compiler should have support for [OpenMP](https://www.openmp.org/).
|
|
118
|
+
GCC and Microsoft Visual Studio compiler support OpenMP out of the box. Apple Clang, however, requires manual
|
|
119
|
+
installation of OpenMP libraries for Apple Clang. The easiest way to do this is via the [Brew](https://brew.sh/) package
|
|
120
|
+
manager, preferably after selecting the correct Xcode version:
|
|
121
|
+
```zsh
|
|
122
|
+
brew install libomp
|
|
123
|
+
brew link --force libomp
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
* **CMake**: CMake version 3.15 and later is required. Refer to the [CMake website](https://cmake.org/download/) for more information on how to download.
|
|
127
|
+
CMake is also widely available on package managers across all operating systems.
|
|
128
|
+
Additionally, on Windows, make sure **Visual Studio's C++ CMake tools for Windows** is also installed by selecting its option
|
|
129
|
+
during installation.
|
|
130
|
+
|
|
131
|
+
### Build Steps
|
|
132
|
+
1. Ensure that ```cmake```, ```nvcc``` (NVIDIA CUDA Compiler) and the C/C++ compiler are all located over your ```PATH```.
|
|
133
|
+
This can be queried via ```echo $env:PATH``` on Windows or ```echo $PATH``` on Linux. If not, locate them and add their folder to the ```PATH```.
|
|
134
|
+
|
|
135
|
+
2. Clone the repository and switch to the ```pmcx/``` folder:
|
|
136
|
+
```bash
|
|
137
|
+
git clone --recursive https://github.com/fangq/mcx.git
|
|
138
|
+
cd mcx/pmcx
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
3. One can run `python3 setup.py install` or `python3 -m pip install .` to both locally build and install the module
|
|
142
|
+
|
|
143
|
+
4. If one only wants to locally build the module, one should run `python3 -m pip wheel .`
|
|
144
|
+
|
|
145
|
+
5. If the binary module is successfully built locally, you should see a binary wheel file `pmcx-X.X.X-cpXX-cpXX-*.whl`
|
|
146
|
+
stored inside the `mcx/pmcx` folder. You can install this wheel package using `python3 -m pip install --force-reinstall pmcx-*.whl`
|
|
147
|
+
to force installing this locally compiled `pmcx` module and overwrite any previously installed versions.
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
## How to use
|
|
151
|
+
|
|
152
|
+
The PMCX module is easy to use. You can use the `pmcx.gpuinfo()` function to first verify
|
|
153
|
+
if you have NVIDIA/CUDA compatible GPUs installed; if there are NVIDIA GPUs detected,
|
|
154
|
+
you can then call the `run()` function to launch a photon simulation.
|
|
155
|
+
|
|
156
|
+
A simulation can be defined conveniently in two approaches - a one-liner and a two-liner:
|
|
157
|
+
|
|
158
|
+
* For the one-liner, one simply pass on each MCX simulation setting as positional
|
|
159
|
+
argument. The supported setting names are compatible to nearly all the input fields
|
|
160
|
+
for the MATLAB version of MCX - [MCXLAB](https://github.com/fangq/mcx/blob/master/mcxlab/mcxlab.m))
|
|
161
|
+
|
|
162
|
+
```python3
|
|
163
|
+
import pmcx
|
|
164
|
+
import numpy as np
|
|
165
|
+
import matplotlib.pyplot as plt
|
|
166
|
+
|
|
167
|
+
res = pmcx.run(nphoton=1000000, vol=np.ones([60, 60, 60], dtype='uint8'), tstart=0, tend=5e-9,
|
|
168
|
+
tstep=5e-9, srcpos=[30,30,0], srcdir=[0,0,1], prop=np.array([[0, 0, 1, 1], [0.005, 1, 0.01, 1.37]]))
|
|
169
|
+
res['flux'].shape
|
|
170
|
+
|
|
171
|
+
plt.imshow(np.log10(res['flux'][30,:, :]))
|
|
172
|
+
plt.show()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
* Alternatively, one can also define a Python dict object containing each setting
|
|
176
|
+
as a key, and pass on the dict object to `pmcx.run()`, or preferably, `pmcx.mcxlab()`
|
|
177
|
+
|
|
178
|
+
```python3
|
|
179
|
+
import pmcx
|
|
180
|
+
import numpy as np
|
|
181
|
+
cfg = {'nphoton': 1000000, 'vol':np.ones([60,60,60],dtype='uint8'), 'tstart':0, 'tend':5e-9, 'tstep':5e-9,
|
|
182
|
+
'srcpos': [30,30,0], 'srcdir':[0,0,1], 'prop':[[0,0,1,1],[0.005,1,0.01,1.37]]}
|
|
183
|
+
res = pmcx.run(cfg) # pmcx.run returns detected photon data as a concatenated 2D array res['detp'], same for res['traj']
|
|
184
|
+
# or alternatively/preferably
|
|
185
|
+
res = pmcx.mcxlab(cfg) # pmcx.mcxlab calls pmcx.run, and postprocess res['detp'] and res['traj'] raw data into dict form
|
|
186
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
pmcx/__init__.py,sha256=2uQQ2eIXRBB8cKX8QQBbl4fdxR8OCnO47SdOZq6ZX8Y,1898
|
|
2
|
+
pmcx/bench.py,sha256=sFe2MprqqAx7HdUB_wgz8wMc6SsQcJrERrUe6fa5BdM,1691
|
|
3
|
+
pmcx/utils.py,sha256=qiarJdO-aVCHEvui7HAXnXvI0EUQYllocBF9A8dacpw,25781
|
|
4
|
+
pmcx-0.4.2.dist-info/METADATA,sha256=sdTGxQRM2p1H4xPZo0cFlZDTluSulI4SEX1ovqbqkFE,10460
|
|
5
|
+
pmcx-0.4.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
pmcx-0.4.2.dist-info/top_level.txt,sha256=Mzy152LlBPhQQ2nJ5TTpjuqQeZwfKz5gPXtj-YuU4yw,5
|
|
7
|
+
pmcx-0.4.2.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pmcx
|