├── Grids.py ├── Layer.py ├── Photon.py ├── README.md ├── main.py ├── output.py ├── paras.py ├── phiz_n_1000000_ri_1.00.npy ├── phiz_n_1000000_ri_1.37.npy ├── phiz_n_100000_ri_1.00.npy ├── phiz_n_100000_ri_1.37.npy ├── phiz_n_10000_ri_1.00.npy ├── phiz_n_10000_ri_1.37.npy ├── phiz_n_1000_ri_1.00.npy ├── phiz_n_1000_ri_1.37.npy ├── plot.py └── run.py /Grids.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | class Grids: 4 | 5 | def __init__(self, nr, delta_r, nz, delta_z): 6 | self.a_rz = np.zeros([nr, nz]) 7 | self.a_xz = np.zeros([nz, 2*nr-1]) 8 | self.delta_r = delta_r 9 | self.delta_z = delta_z 10 | self.nr = nr 11 | self.nz = nz 12 | self.rr = (nr-1)*delta_r 13 | self.zz = (nz-1)*delta_z 14 | 15 | def update_a(self, ph, dw): 16 | r = np.sqrt(ph.x**2 + ph.y**2) 17 | z = ph.z 18 | ir = int(r/self.delta_r) 19 | iz = int(z/self.delta_z) 20 | if ir >= self.nr-1 or ph.scatters == 0: 21 | ir = -1 22 | if iz >= self.nz-1: 23 | iz = -1 24 | self.a_rz[ir, iz] += dw 25 | if abs(ph.y < 2*self.delta_r): 26 | ix = int(ph.x/self.delta_r) + int(self.nr) 27 | if ix >= 2*self.nr-1: 28 | ix = -1 29 | elif ix < 0: 30 | ix = 0 31 | self.a_xz[iz, ix] += dw 32 | 33 | def get_az(self, n_photon): 34 | a_z = np.sum(self.a_rz, axis=0) 35 | a_z /= (self.delta_z*n_photon) 36 | return a_z 37 | 38 | def get_phiz(self, ly_ls, n_photon): 39 | zt = self.zz 40 | layer = 1 41 | a_z = self.get_az(n_photon) 42 | phi_z = np.zeros(self.nz) 43 | if len(ly_ls) == 3: 44 | this_ly = ly_ls[1] 45 | phi_z = a_z / this_ly.mu_a 46 | else: 47 | while zt > 0: 48 | this_ly = ly_ls[layer] 49 | thickness = this_ly.z1 - this_ly.z0 50 | iz0 = int(this_ly.z0/self.delta_z) 51 | if zt > thickness: 52 | iz1 = int(this_ly.z1/self.delta_z) 53 | else: 54 | iz1 = -1 55 | 56 | phi_z[iz0:iz1] = a_z[iz0:iz1]/this_ly.mu_a 57 | layer += 1 58 | zt -= thickness 59 | return phi_z 60 | 61 | def get_phixz(self, ly_ls): 62 | zt = self.zz 63 | layer = 1 64 | phi_xz = np.zeros(self.a_xz.shape) 65 | if len(ly_ls) == 3: 66 | this_ly = ly_ls[1] 67 | phi_xz = self.a_xz / this_ly.mu_a 68 | else: 69 | while zt > 0: 70 | this_ly = ly_ls[layer] 71 | thickness = this_ly.z1 - this_ly.z0 72 | iz0 = int(this_ly.z0/self.delta_z) 73 | # if this is not the final layer covered by scoring grid 74 | if zt > thickness: 75 | iz1 = int(this_ly.z1/self.delta_z) 76 | # if this is the final layer covered by scoring grid 77 | else: 78 | iz1 = -1 79 | phi_xz[iz0:iz1] = self.a_xz[iz0:iz1]/this_ly.mu_a 80 | layer += 1 81 | zt -= thickness 82 | return phi_xz 83 | 84 | def get_rcoords(self): 85 | i = np.arange(self.nr) 86 | r_coords = ((i+0.5)+1/(12*(i+0.5))) * self.delta_r 87 | return r_coords 88 | 89 | def get_xcoords(self): 90 | i = np.arange(2*self.nr-1) 91 | i = i + int(self.nr) 92 | x_coords = ((i+0.5)+1/(12*(i+0.5))) * self.delta_r 93 | return x_coords 94 | 95 | def get_zcoords(self): 96 | i = np.arange(self.nz) 97 | z_coords = (i+0.5) * self.delta_z 98 | return z_coords 99 | 100 | 101 | -------------------------------------------------------------------------------- /Layer.py: -------------------------------------------------------------------------------- 1 | class Layer(): 2 | 3 | def __init__(self, z0, z1, mu_a, mu_s, g, n, clear, ambient): 4 | self.z0 = z0 5 | self.z1 = z1 6 | self.mu_a = mu_a 7 | self.mu_s = mu_s 8 | self.mu_t = mu_a + mu_s 9 | self.g = g 10 | self.n = n 11 | self.clear = clear 12 | self.ambient = ambient 13 | -------------------------------------------------------------------------------- /Photon.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | class Photon(): 5 | def __init__(self, x, y, ly_ls): 6 | # initialize photon 7 | self.x = x 8 | self.y = y 9 | self.z = 0 10 | self.ux = 0 11 | self.uy = 0 12 | self.uz = 1 13 | self.dead = False 14 | self.s = 0 15 | self.scatters = 0 16 | self.layer = 1 17 | # in case that first layer is clear, do reflection 18 | amb_ly = ly_ls[0] 19 | this_ly = ly_ls[1] 20 | bot_ly = ly_ls[2] 21 | n1 = amb_ly.n 22 | n2 = this_ly.n 23 | n3 = bot_ly.n 24 | if this_ly.clear: 25 | r1 = ((n1 - n2) / (n1 + n2)) ** 2 26 | r2 = ((n3 - n2) / (n3 + n2)) ** 2 27 | r_sp = r1 + (1 - r1) ** 2 * r2 / (1 - r1 * r2) 28 | self.w = 1 - r_sp 29 | else: 30 | if n1 != n2: 31 | r_sp = ((n1 - n2) / (n1 + n2)) ** 2 32 | self.w = 1 - r_sp 33 | else: 34 | self.w = 1 35 | 36 | def get_s(self): 37 | # get random s 38 | xi = np.random.rand() 39 | self.s = -np.log(xi) 40 | 41 | def absorb(self, ly_ls): 42 | # deduce weight due to absorption 43 | this_ly = ly_ls[self.layer] 44 | mu_ratio = this_ly.mu_a / this_ly.mu_t 45 | delta_w = self.w * mu_ratio 46 | self.w -= delta_w 47 | return delta_w 48 | 49 | def scatter(self, ly_ls): 50 | # update direction cosines upon scattering 51 | xi = np.random.rand() 52 | this_ly = ly_ls[self.layer] 53 | g = this_ly.g 54 | if g != 0: 55 | cosine = 1 + g ** 2 - ((1 - g ** 2) / (1 - g + 2 * g * xi)) ** 2 56 | cosine /= (2 * g) 57 | else: 58 | cosine = 2 * xi - 1 59 | theta = np.arccos(cosine) 60 | xi = np.random.rand() 61 | phi = 2 * np.pi * xi 62 | mux = self.ux 63 | muy = self.uy 64 | muz = self.uz 65 | if np.abs(muz) < 0.99999: 66 | self.ux = np.sin(theta) * (mux * muz * np.cos(phi) - muy * np.sin(phi)) 67 | self.ux = self.ux / np.sqrt(1 - muz ** 2) + mux * cosine 68 | self.uy = np.sin(theta) * (muy * muz * np.cos(phi) + mux * np.sin(phi)) 69 | self.uy = self.uy / np.sqrt(1 - muz ** 2) + muy * cosine 70 | self.uz = -np.sin(theta) * np.cos(phi) * np.sqrt(1 - muz ** 2) + muz * cosine 71 | else: 72 | self.ux = np.sin(theta) * np.cos(phi) 73 | self.uy = np.sin(theta) * np.sin(phi) 74 | self.uz = np.sign(muz) * cosine 75 | self.scatters += 1 76 | 77 | def move(self, ly_ls): 78 | # judge if the photon will hit boundary then move it 79 | this_ly = ly_ls[self.layer] 80 | mu_t = this_ly.mu_t 81 | z_bound = [this_ly.z0, this_ly.z1] 82 | if self.uz < 0: 83 | db = (z_bound[0] - self.z) / self.uz 84 | elif self.uz == 0: 85 | db = np.inf 86 | else: 87 | db = (z_bound[1] - self.z) / self.uz 88 | if db * mu_t <= self.s: 89 | # hit boundary 90 | self.x += self.ux * db 91 | self.y += self.uy * db 92 | self.z += self.uz * db 93 | self.s -= db * mu_t 94 | else: 95 | # does not hit boundary 96 | self.x += self.ux * self.s / mu_t 97 | self.y += self.uy * self.s / mu_t 98 | self.z += self.uz * self.s / mu_t 99 | self.s = 0 100 | 101 | def reflect_transmit(self, ly_ls): 102 | # do reflection or transmission 103 | this_ly = ly_ls[self.layer] 104 | a_i = np.arccos(np.abs(self.uz)) 105 | n_i = this_ly.n 106 | if self.uz < 0: 107 | next_ly = ly_ls[self.layer - 1] 108 | direct = -1 109 | else: 110 | next_ly = ly_ls[self.layer + 1] 111 | direct = 1 112 | n_t = next_ly.n 113 | if n_i > n_t and a_i > np.arcsin(n_t / n_i): 114 | a_t = np.pi / 2 115 | r = 1 116 | else: 117 | a_t = np.arcsin(n_i * np.sin(a_i) / n_t) 118 | r = (np.sin(a_i - a_t) / np.sin(a_i + a_t)) ** 2 + (np.tan(a_i - a_t) / np.tan(a_i + a_t)) ** 2 119 | r *= 0.5 120 | xi = np.random.rand() 121 | if xi <= r: 122 | # internally reflected 123 | self.uz = -self.uz 124 | else: 125 | # transmitted 126 | if direct == -1: 127 | self.layer -= self.layer 128 | else: 129 | self.layer += self.layer 130 | if self.layer == 0 or self.layer == len(ly_ls) - 1: 131 | self.dead = True 132 | else: 133 | self.ux = self.ux * n_i / n_t 134 | self.uy = self.uy * n_i / n_t 135 | self.uz = np.sign(self.uz) * np.cos(a_t) 136 | 137 | def terminate(self, m): 138 | xi = np.random.rand() 139 | if xi <= 1 / m: 140 | self.w = m * self.w 141 | else: 142 | self.w = 0 143 | self.dead = True 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Monte Carlo simulation program for optical microscopy. Based on Lihong Wang, Steven L. Jacques, Liqiong Zheng, MCML—Monte Carlo modeling of light transport in multi-layered tissues, Computer Methods and Programs in Biomedicine, Volume 47, Issue 2, 1995, Pages 131-146, ISSN 0169-2607, http://dx.doi.org/10.1016/0169-2607(95)01640-F. 2 | 3 | Yes these are exactly the same stuff as another of my repos called `opt_microscopy_mc` except that it is in Python which is an unfortunate and miserable practice for me as I found my Matlab codes took 5 days to run on my Mac while the only high performance server I can access without elaborating on a proposal does not have Matlab installed and only a Python interpreter is available! 4 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | from Photon import Photon 5 | from Grids import Grids 6 | from Layer import Layer 7 | from run import * 8 | from output import * 9 | from paras import * 10 | import time 11 | 12 | # set recording grids 13 | out_grids = Grids(n_r, delta_r, n_z, delta_z) 14 | 15 | # perform MC simulation 16 | run_mc(n_photon, ly_ls, out_grids, w_thresh, m) 17 | 18 | # output 19 | # get_z_fluence(out_grids) 20 | get_xz_fluence(out_grids) -------------------------------------------------------------------------------- /output.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from paras import * 3 | import numpy as np 4 | import matplotlib.pyplot as plt 5 | 6 | 7 | def get_z_fluence(out_grids): 8 | 9 | # post-processing 10 | phi_z = out_grids.get_phiz(ly_ls, n_photon) 11 | z_coords = out_grids.get_zcoords() 12 | fname = 'phiz_n_{:d}_ri_{:.2f}'.format(n_photon, ly2.n) 13 | np.save(fname, phi_z) 14 | 15 | # plot 16 | fig = plt.figure() 17 | plt.semilogy(z_coords[:-1], phi_z[:-1]) 18 | plt.xlabel('z [cm]') 19 | plt.ylabel('Fluence') 20 | plt.title('Fluence vs. depth (N = {:d}, n = {:.2f})'.format(n_photon, ly2.n)) 21 | fname = 'n_{:d}_ri_{:.2f}.png'.format(n_photon, ly2.n) 22 | plt.savefig(fname) 23 | 24 | 25 | def get_xz_fluence(out_grids): 26 | 27 | phi_xz = out_grids.get_phixz(ly_ls) 28 | z_coords = out_grids.get_zcoords() 29 | 30 | plt.figure() 31 | plt.imshow(phi_xz[:-1, 1:-1]) 32 | fname = 'xz_abs.png'.format(n_photon, ly2.n) 33 | plt.savefig(fname) 34 | 35 | -------------------------------------------------------------------------------- /paras.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from Grids import Grids 3 | from Layer import Layer 4 | from run import * 5 | 6 | # set parameters 7 | ly1 = Layer(z0=-1, z1=0, mu_a=0, mu_s=0, g=1, n=1, clear=True, ambient=True) 8 | ly2 = Layer(z0=0, z1=np.inf, mu_a=0.1, mu_s=100, g=0.9, n=1.37, clear=False, ambient=False) 9 | ly3 = Layer(z0=np.inf, z1=np.inf, mu_a=0, mu_s=0, g=1, n=1, clear=True, ambient=True) 10 | ly_ls = [ly1, ly2, ly3] 11 | n_photon = 1000 12 | w_thresh = 0.0001 13 | m = 10 14 | n_r = 500 # number of grid pixels along r 15 | n_z = 200 # number of grid pixels along z 16 | delta_r = 0.005 17 | delta_z = 0.005 18 | 19 | # set recording grids 20 | out_grids = Grids(n_r, delta_r, n_z, delta_z) -------------------------------------------------------------------------------- /phiz_n_1000000_ri_1.00.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdw771/opt_microscopy_mc_py/5a58948133b1c6c1c0c23e28e0fba36c4fbef9c5/phiz_n_1000000_ri_1.00.npy -------------------------------------------------------------------------------- /phiz_n_1000000_ri_1.37.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdw771/opt_microscopy_mc_py/5a58948133b1c6c1c0c23e28e0fba36c4fbef9c5/phiz_n_1000000_ri_1.37.npy -------------------------------------------------------------------------------- /phiz_n_100000_ri_1.00.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdw771/opt_microscopy_mc_py/5a58948133b1c6c1c0c23e28e0fba36c4fbef9c5/phiz_n_100000_ri_1.00.npy -------------------------------------------------------------------------------- /phiz_n_100000_ri_1.37.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdw771/opt_microscopy_mc_py/5a58948133b1c6c1c0c23e28e0fba36c4fbef9c5/phiz_n_100000_ri_1.37.npy -------------------------------------------------------------------------------- /phiz_n_10000_ri_1.00.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdw771/opt_microscopy_mc_py/5a58948133b1c6c1c0c23e28e0fba36c4fbef9c5/phiz_n_10000_ri_1.00.npy -------------------------------------------------------------------------------- /phiz_n_10000_ri_1.37.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdw771/opt_microscopy_mc_py/5a58948133b1c6c1c0c23e28e0fba36c4fbef9c5/phiz_n_10000_ri_1.37.npy -------------------------------------------------------------------------------- /phiz_n_1000_ri_1.00.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdw771/opt_microscopy_mc_py/5a58948133b1c6c1c0c23e28e0fba36c4fbef9c5/phiz_n_1000_ri_1.00.npy -------------------------------------------------------------------------------- /phiz_n_1000_ri_1.37.npy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdw771/opt_microscopy_mc_py/5a58948133b1c6c1c0c23e28e0fba36c4fbef9c5/phiz_n_1000_ri_1.37.npy -------------------------------------------------------------------------------- /plot.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from Grids import * 4 | 5 | 6 | n_r = 500 # number of grid pixels along r 7 | n_z = 200 # number of grid pixels along z 8 | delta_r = 0.005 9 | delta_z = 0.005 10 | 11 | out_grids = Grids(n_r, delta_r, n_z, delta_z) 12 | 13 | for n in [1000, 10000, 100000, 1000000]: 14 | fig = plt.figure() 15 | n100 = np.load('phiz_n_{:d}_ri_1.00.npy'.format(n)) 16 | n137 = np.load('phiz_n_{:d}_ri_1.37.npy'.format(n)) 17 | z = out_grids.get_zcoords() 18 | plt.semilogy(z[:-1], n100[:-1], z[:-1], n137[:-1]) 19 | plt.ylim([1e-1, 1e1]) 20 | plt.legend([r'$n_{rel}$ = 1.00', r'$n_{rel}$ = 1.37']) 21 | plt.xlabel('z [cm]') 22 | plt.ylabel('Fluence') 23 | plt.title('Number of photon packets = {:d}'.format(n)) 24 | plt.savefig('comb_n_{:d}'.format(n)) 25 | 26 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import numpy as np 3 | import time 4 | from Photon import * 5 | 6 | 7 | def run_mc(n_photon, ly_ls, out_grids, w_thresh, m): 8 | 9 | # get random seed 10 | cpu_time = time.time() * 10000 % 10000 11 | np.random.seed(seed=int(cpu_time)) 12 | 13 | # start loop 14 | t0 = time.time() 15 | for i_photon in range(n_photon): 16 | 17 | print('\r{:d}/{:d}'.format(i_photon+1, n_photon), end='') 18 | 19 | # initialize 20 | ph = Photon(0, 0, ly_ls) 21 | 22 | while not ph.dead: 23 | 24 | # set new s if s = 0 25 | if ph.s == 0: 26 | ph.get_s() 27 | 28 | # check boundary and move 29 | ph.move(ly_ls) 30 | 31 | # if no booundary hit, absorb and scatter 32 | if ph.s == 0: 33 | dw = ph.absorb(ly_ls) 34 | out_grids.update_a(ph, dw) 35 | ph.scatter(ly_ls) 36 | # if boundary hit, transmit or reflect 37 | else: 38 | ph.reflect_transmit(ly_ls) 39 | 40 | # if photon is alive but low in weight, run roulette 41 | if not ph.dead: 42 | if ph.w < w_thresh: 43 | ph.terminate(m) 44 | 45 | t = time.time() - t0 46 | print('\nMC Done in {:.2f} s ({:.2f} hours). '.format(t, t/3600)) --------------------------------------------------------------------------------