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 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
+ ![](https://mcx.space/img/mcx_wiki_banner.png)
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
+ ![Linux Python Module](https://github.com/fangq/mcx/actions/workflows/build_linux_manywheel.yml/badge.svg)\
51
+ ![MacOS Python Module](https://github.com/fangq/mcx/actions/workflows/build_macos_wheel.yml/badge.svg)\
52
+ ![Windows Python Module](https://github.com/fangq/mcx/actions/workflows/build_windows_wheel.yml/badge.svg)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pmcx