Reproducing results obtained by Delgado Atencio, Jacques & Vázquez y Montiel 2011 in Python with MCML and CUDAMCML¶

Victor Lima
victorporto@ifsc.usp.br
victor.lima@ufscar.br
victorportog.github.io

Release date:
01 April 2025
Last modification:
01 April 2025

References:

[ASA09] Alerstam, Svensson & Stefan Andersson-Engels 2009.
CUDAMCML - User manual and implementation notes.
https://www.lth.se/fileadmin/atomfysik/Biophotonics/Software/CUDAMCML.pdf
https://www.researchgate.net/publication/229001993_User_manual_and_implementation_notes

[DJV11] Delgado Atencio, Jacques & Vázquez y Montiel 2011.
Monte Carlo Modeling of Light Propagation in Neonatal Skin.
https://doi.org/10.5772/15853

[B*17] Bauer, Pedersen, Hardeberg & Verdaasdonk 2017.
Skin color simulation - review and analysis of available Monte Carlo-based photon transport simulation models.
https://doi.org/10.2352/ISSN.2169-2629.2017.25.165

In [1]:
import os
import shutil
import time
import numpy as np
import matplotlib
from matplotlib import rcParams, style
import matplotlib.pyplot as plt
import pandas as pd
In [2]:
print('numpy version:', np.__version__)
print('matplotlib version:', matplotlib.__version__)
print('pandas version:', pd.__version__)
numpy version: 1.26.4
matplotlib version: 3.9.0
pandas version: 2.2.2
In [3]:
rcParams.update({'font.size': 12})
rcParams.update({'axes.labelsize': 12})
rcParams.update({'axes.titlesize': 12})
rcParams.update({'xtick.labelsize': 12})
rcParams.update({'ytick.labelsize': 12})
rcParams.update({'legend.fontsize': 10})
rcParams.update({'axes.grid': True}) 
plt.rcParams["figure.figsize"] = (6, 4)
In [4]:
import skinoptics
from skinoptics.absorption_coefficient import *
from skinoptics.scattering_coefficient import *
from skinoptics.anisotropy_factor import *
from skinoptics.refractive_index import *
from skinoptics.colors import *
In [5]:
print('pandas version:', skinoptics.__version__)
pandas version: 0.0.1

Absorption Coefficient¶

In [6]:
def mua_EP_Atencio(lambda0, fv_mel):
    return 10*fv_mel*mua_mel_Jacques(lambda0)

def mua_blo_Atencio(lambda0, S):
    return (S*mua_oxy_Prahl(lambda0, Cmass_oxy = 150, molar_mass_oxy = 64500)
            + (1-S)*mua_deo_Prahl(lambda0, Cmass_deo = 150, molar_mass_deo = 64500))

def mua_DE_Atencio(lambda0, fv_blo, S, fv_bil, C_bil):
    return 10*(fv_blo*mua_blo_Atencio(lambda0, S)
               + fv_bil*mua_bil_Li(lambda0, Cmass_bil = C_bil, molar_mass_bil = 585))
In [7]:
all_lambda = np.arange(400, 700+10, 10)
ones = np.ones(len(all_lambda))
color_cycle = ['k', 'r', 'g', 'b', 'c', 'm']
In [8]:
for i in range(3):
    plt.plot(all_lambda, mua_EP_Atencio(all_lambda, 0.02*i),
             color_cycle[i], marker = '.', lw = 2., label = 'fv_mel = {}%'.format(int(0.02*i*100)))
plt.xlabel('wavelength [nm]')
plt.ylabel('absorption coefficient [cm$^{-1}$]')
plt.title('epidermis')
plt.legend(loc = 'upper right')
plt.xlim(400, 700)
plt.ylim(0, 60)
plt.show()
In [9]:
for i in range(6):
    plt.plot(all_lambda, mua_DE_Atencio(all_lambda, 0.002, 0.7, 0.2, 0.05*i),
             color_cycle[i], marker = '.', label = 'C_bil = {:.2f} g/L'.format(int(0.05*i*100)/100))
for i in (1, 3, 5):
    plt.plot(all_lambda, 10*0.2*mua_bil_Li(all_lambda, 0.05*i),
             color_cycle[i], ls = ':', lw = 2., label = 'contribution due to bilirubin only')
plt.xlabel('wavelength [nm]')
plt.ylabel('absorption coefficient [cm$^{-1}$]')
plt.title('dermis (fv_blo = 0.2%, S = 70%, fv_bil = 20%)')
plt.legend(loc = 'upper right')
plt.xlim(400, 700)
plt.ylim(0, 14)
plt.show()
In [10]:
c = 0
for i in [0.001, 0.002, 0.01, 0.02]:
    plt.plot(all_lambda, mua_DE_Atencio(all_lambda, i, 0.7, 0.2, 0.1),
             color_cycle[c], marker = '.', lw = 2., label = 'fv_blo = {} %'.format(i*100))
    c += 1
plt.xlabel('wavelength [nm]')
plt.ylabel('absorption coefficient [cm$^{-1}$]')
plt.title('dermis (C_bil = 0.1 g/L, S = 70%, fv_bil = 20%)')
plt.legend(loc = 'upper right')
plt.xlim(400, 700)
plt.ylim(0, 60)
plt.show()

Scattering Coefficient¶

In [11]:
plt.plot(all_lambda, 10*rmus_Saidi(all_lambda, 6.8, 9.5E10), 
         color_cycle[0], label = 'epidermis & dermis')
plt.plot(all_lambda, 600*ones*(1-0.8), color_cycle[1], lw = 2., label = 'hypodermis')
plt.xlabel('wavelength [nm]')
plt.ylabel('reduced scattering coefficient [cm$^{-1}$]')
plt.legend(loc = 'upper right')
plt.xlim(400, 700)
plt.ylim(0, 160)
plt.show()
In [12]:
plt.plot(all_lambda, 10*rmus_Saidi(all_lambda, 6.8, 9.5E10)/(1 - g_vanGemert(all_lambda)),
         color_cycle[0], label = 'epidermis & dermis')
plt.plot(all_lambda, 600*ones, color_cycle[1], lw = 2., label = 'hypodermis')
plt.xlabel('wavelength [nm]')
plt.ylabel('scattering coefficient [cm$^{-1}$]')
plt.legend(loc = 'upper right')
plt.xlim(400, 700)
plt.ylim(100, 800)
plt.show()

Refractive Index¶

In [13]:
plt.plot(all_lambda, 1.37*ones, color_cycle[0], lw = 2., label = 'epidermis & dermis')
plt.plot(all_lambda, 1.44*ones, color_cycle[1], lw = 2., label = 'hypodermis')
plt.plot(all_lambda, 1.5*ones, color_cycle[2], lw = 2., label = 'skull')
plt.xlabel('wavelength [nm]')
plt.ylabel('refractive index [-]')
plt.legend(loc = 'upper right')
plt.xlim(400, 700)
plt.ylim(1.3, 1.6)
plt.show()

Anisotropy Factor¶

In [14]:
plt.plot(all_lambda, g_vanGemert(all_lambda), color_cycle[0], label = 'epidermis & dermis')
plt.plot(all_lambda, 0.8*ones, color_cycle[1], lw = 2., label = 'hypodermis')
plt.xlabel('wavelength [nm]')
plt.ylabel('anisotropy factor [-]')
plt.legend(loc = 'upper right')
plt.xlim(400, 700)
plt.show()

Simulations¶

In [15]:
all_Cbi = [0, 0.05, 0.1, 0.15, 0.20, 0.25]
simulator = 'CUDAMCML' # the user can choose one of the following... 'MCML' or 'CUDAMCML'
folder = r'C:\Users\Lili\Desktop\SkinOptics\simulators\{}'.format(simulator)

# folder2 address
folder2 = r'D:\Victor\2011DelgadoAtencio_{}'.format(simulator)
# create folder2 if it does not exist
if os.path.exists(folder2) == False:
    os.mkdir(folder2)
    
# folder4 address
folder4 = '{}\{}'.format(folder2, 'R_T_A')
# create folder4 if it does not exist
if os.path.exists(folder4) == False:
    os.mkdir(folder4) 
In [16]:
'''
This is a python version of the matlab script MCinput.m [DJV11]
'''

for l in range(6):
    Cbi = all_Cbi[l] # Bilirubin content in [g/L]
    
    # 1 SIMULATION PARAMETERS-I ---> The grid and number of photons
    Nf = 50000 # No. of photons
    dz = 20E-4 # dz, dr of the grid
    dr = 20E-4
    Nz = 62 # No. of dz, dr & da
    Nr = 200
    Na = 1

    # 1 SIMULATION PARAMETERS-II---> Geometrical and optical properties of layers
    nc = 3 # Number of layers
    nabove = 1.00 # Refractive index TOP medium
    n1 = 1.37
    d1 = 40E-4 # Layer 1 of 3
    n2 = 1.37
    d2 = 900E-4 # Layer 2 of 3
    n3 = 1.44
    d3 = 300E-4 # Layer 3 of 3
    nbelow = 1.5 # Refractive index BOTTOM medium
    fme = 2/100 # MELANIN content kept constant
    fbl = 0.2/100 # BLOOD content kept constant
    fbi = 20/100 # Partition number
    S = 70/100 # Oxygen saturation

    # 1 SIMULATION PARAMETERS-III---> Spectral data
    lambdai = int(400) # Spectral region and increment
    lambdaf = int(700)
    step = int(10)
    numruns = int(((lambdaf - lambdai)/step) + 1) # Number of runs
    all_lambda = np.arange(lambdai, lambdaf + step, step)  # Discret wavelengths

    # 2 OPTICAL PROPERTIES
    # ABSORPTION and SCATTERING in <<EPIDERMIS>>
    mua1 = mua_EP_Atencio(all_lambda, fv_mel = fme) # Epidermis absorption coefficient
    g = g_vanGemert(all_lambda) # Anisotropy factor
    miuspN = rmus_Saidi(all_lambda, 68, 9.5E11)
    mus1 = miuspN/(1-g) # Epidermis scattering coefficient
    # ABSORPTION and SCATTERING in <<DERMIS>>
    mua2 = mua_DE_Atencio(all_lambda, fv_blo = fbl, S = S, fv_bil = fbi, C_bil = Cbi)
    mus2 = mus1
    # ABSORPTION and SCATTERING in <<HYPODERMIS>>
    mua3 = 1
    mus3 = 600
    g3 = 0.8

    # 3 WRITE INPUT DATA FILES FOR EACH WAVELENGTH 
    # .mci file name
    mci_file = 'Cbi_{}.mci'.format(l)
    # .mci file address
    mci_file_address = '{}/{}'.format(folder, mci_file)
    
    # writting a run for each of the discret wavelengths in the .mci file
    fout = open(mci_file_address, 'w+')
    fout.write('####\n')
    fout.write('# Template of input files for Monte Carlo simulation (mcml).\n')
    fout.write('# Anything in a line after "#" is ignored as comments.\n')
    fout.write('# Space lines are also ignored.\n')
    fout.write('# Lengths are in cm, mua and mus are in 1/cm.\n')
    fout.write('####\n\n')
    fout.write('1.0\t\t\t\t\t\t# file version\n')
    fout.write('{}\t\t\t\t\t\t# number of runs\n\n'.format(numruns))
    for k in np.arange(int(numruns)):
        fout.write('### Specify data for run {}\n'.format(k))
        fout.write('{}.mco\t\tA\t\t\t\t# output filename, ASCII/Binary\n'.format(
            'Cbi_{}_{}'.format(l, all_lambda[k])))
        fout.write('{}\t\t\t\t\t\t# No. of photons\n'.format(Nf))
        fout.write('{}\t{}\t\t\t\t\t# dz, dr\n'.format(dz, dr))
        fout.write('{}\t{}\t{}\t\t\t\t# No. of dz, dr & da.\n\n'.format(Nz, Nr, Na))
        fout.write('{}\t\t\t\t\t\t# No. of layers\n'.format(nc))
        fout.write('# n\tmua\tmus\tg\td\t\t# One line for each layer\n')
        fout.write('{}\t\t\t\t\t\t# n for medium above.\n'.format(nabove))
        fout.write('{}\t{}\t{}\t{}\t{}\t\t# layer 0\n'.format(n1, mua1[k], mus1[k], g[k], d1))
        fout.write('{}\t{}\t{}\t{}\t{}\t\t# layer 1\n'.format(n2, mua2[k], mus2[k], g[k], d2))
        fout.write('{}\t{}\t{}\t{}\t{}\t\t# layer 2\n'.format(n3, mua3, mus3, g3, d3))
        fout.write('{}\t\t\t\t\t\t# n for medium below.\n\n\n'.format(nbelow))
    fout.close()
In [17]:
t0 = time.time()

for l in range(6):
    # part in commom between folder3, .mci and .mco files names
    commom = 'Cbi_{}'.format(l)
    # .mci file name
    mci_file = commom + '.mci'.format(l)
    # .mci file address
    mci_file_address = '{}/{}'.format(folder, mci_file)
    
    # folder3 address
    folder3 = '{}\{}'.format(folder2, commom)
    # create folder3 if it does not exist
    if os.path.exists(folder3) == False:
        os.mkdir(folder3) 
    
    # changing directory to run the Monte Carlo simulator
    os.chdir(folder)
    # running simulator for the .mci file
    os.system('{} {}'.format(simulator, mci_file))
    # returning to previous directory
    os.chdir(r'C:\Users\Lili\Desktop\SkinOptics')
    
    # repeat process for each discret wavelength
    for k in range (lambdai, lambdaf + step, step):
        # .mco file name
        mco_file = '{}_{}.mco'.format(commom, k)
        # .mco file address
        mco_file_address = '{}\{}'.format(folder, mco_file)
        # move .mco file from folder1 to folder3
        shutil.move(mco_file_address, '{}\{}'.format(folder3, mco_file))
    # move .mci file from folder1 to folder2
    shutil.move(mci_file_address, '{}\{}'.format(folder2, mci_file))

print(time.time() - t0)
6.143056154251099

time needed to run all simulations with CUDAMCML (5E4 photons): approx. 6.14 seconds

time needed to run all simulations with MCML (5E4 photons): approx. 508.50 seconds

approx. 83-fold speed-up!

specifications of the machine:

  • Intel(R) Core(TM) i5-4460 CPU
  • NVIDIA GeForce GTX 1070 GPU
  • 16 GB DDR3 SRAM
  • ST1000DM003-1CH162 HDD
In [18]:
for l in range(6):
    
    # part in commom between folder3 and .mco files names
    commom = 'Cbi_{}'.format(l)
        
    # folder3 address
    folder3 = '{}\{}'.format(folder2, commom)
    # create folder3 if it does not exist
    if os.path.exists(folder3) == False:
        os.mkdir(folder3) 
    
    # .txt file name
    R_T_A_txt_file = 'R_T_A_{}.txt'.format(l)
    # .txt file address
    R_T_A_txt_file_address = '{}\{}'.format(folder4, R_T_A_txt_file)
    # open .txt file
    fout = open(R_T_A_txt_file_address, 'w+')
    # .txt file header
    fout.write('wavelength[nm] Rd[%] T[%] A0[%] A1[%] A2[%] A[%] Rs[%] Rs+Rd+A+T[%] \n')
    
    # repeat process for each discret wavelength
    for k in range (lambdai, lambdaf+step, step):
        
        # .mco file name
        mco_file = '{}_{}.mco'.format(commom, k)
        # .mco file address
        mco_file_address = '{}\{}'.format(folder3, mco_file)
        # open .mco file
        fin = open(mco_file_address, 'r')
        
        # get specular reflectance (Rs) from the .mco file
        for b in range(26):
            line = fin.readline()
        if line.split(' ')[0] == 'RAT': 
            line = fin.readline()
        Rs = float(line.split(' ')[0])*100
        # get diffuse reflectance (Rd) from the .mco file
        line = fin.readline()
        Rd = float(line.split(' ')[0])*100
        # get transmittance (T) from the .mco file
        for b in range(2):
            line = fin.readline()
        T = float(line.split(' ')[0])*100
        for b in range(2):
            line = fin.readline()
        # get absorption (A) from the .mco file
        A = np.zeros(3)
        for b in range(3):
            line = fin.readline()
            A[b] = float(line)*100
        
        # write wavelength, diffuse reflectance, transmittance and absorption
        # and absorption (for each layer and total) to the .txt file
        fout.write('{} {} {} {} {} {} {} {} {}\n'.format(k, Rd, T, A[0], A[1], A[2], A.sum(), Rs, Rd+T+A.sum()+Rs))
        
        # close .mco file
        fin.close()
        
    # close .txt file
    fout.close()

Diffuse Reflectance¶

In [19]:
Rd_MCML, Rd_CUDAMCML = np.zeros([2, 6, len(all_lambda)])

for i in range(6):
    Rd_MCML[i,:] = np.array(pd.read_csv(r'D:\Victor\2011DelgadoAtencio_{}\R_T_A\R_T_A_{}.txt'.format('MCML', i), sep = ' '))[:,1]
    Rd_CUDAMCML[i,:] = np.array(pd.read_csv(r'D:\Victor\2011DelgadoAtencio_{}\R_T_A\R_T_A_{}.txt'.format('CUDAMCML', i), sep = ' '))[:,1]
In [20]:
i = 0
plt.scatter(all_lambda, Rd_MCML[i,:], color = color_cycle[i], marker = '.', label = 'MCML')
for i in range(6):
    plt.plot(all_lambda, Rd_CUDAMCML[i,:], color = color_cycle[i], lw = 2., label = 'CUDAMCML, C_bil = {:.2f} g/L'.format(all_Cbi[i]))
    plt.scatter(all_lambda, Rd_MCML[i,:], color = color_cycle[i], marker = '.')
plt.xlabel('wavelength [nm]')
plt.ylabel('diffuse reflectance [%]')
plt.title('(fv_mel = 2%, fv_blo = 0.2%, S = 70%, fv_bil = 20%)')
plt.legend(loc = 'lower right')
plt.xlim(400, 700)
plt.ylim(0, 70)
plt.show()
In [21]:
for i in range(6):
    plt.plot(all_lambda, np.abs(Rd_CUDAMCML[i,:] - Rd_MCML[i,:]),
             color = color_cycle[i], marker = '.', lw = 2., label = 'C_bil = {:.2f} g/L'.format(all_Cbi[i]))
plt.xlabel('wavelength [nm]')
plt.ylabel('absolute difference in Rd [%]')
plt.title('CUDAMCML & MCML')
plt.legend()
plt.xlim(400, 700)
plt.ylim(0, 2.5)
plt.show()

Extra: Transmittance¶

In [22]:
T_MCML, T_CUDAMCML = np.zeros([2, 6, len(all_lambda)])

for i in range(6):
    T_MCML[i,:] = np.array(pd.read_csv(r'D:\Victor\2011DelgadoAtencio_{}\R_T_A\R_T_A_{}.txt'.format('MCML', i), sep = ' '))[:,2]
    T_CUDAMCML[i,:] = np.array(pd.read_csv(r'D:\Victor\2011DelgadoAtencio_{}\R_T_A\R_T_A_{}.txt'.format('CUDAMCML', i), sep = ' '))[:,2]
In [23]:
i = 0
plt.scatter(all_lambda, T_MCML[i,:], color = color_cycle[i], marker = '.', label = 'MCML')
for i in range(6):
    plt.plot(all_lambda, T_CUDAMCML[i,:],
             color = color_cycle[i], lw = 2., label = 'CUDAMCML, C_bil = {:.2f} g/L'.format(all_Cbi[i]))
    plt.scatter(all_lambda, T_MCML[i,:], color = color_cycle[i], marker = '.')
plt.xlabel('wavelength [nm]')
plt.ylabel('transmittance [%]')
plt.title('(fv_mel = 2%, f_blo = 0.2%, S = 70%, fv_bil = 20%)')
plt.legend(loc = 'upper left')
plt.xlim(400, 700)
plt.ylim(0, 50)
plt.show()
In [24]:
for i in range(6):
    plt.plot(all_lambda, np.abs(T_CUDAMCML[i,:] - T_MCML[i,:]),
                color = color_cycle[i], marker = '.', lw = 2., label = 'C_bil = {:.2f} g/L'.format(all_Cbi[i]))
plt.xlabel('wavelength [nm]')
plt.ylabel('absolute difference in T [%]')
plt.title('CUDAMCML & MCML')
plt.legend()
plt.xlim(400, 700)
plt.ylim(0, 2.5)
plt.show()

Extra: Checking if R+T+A = 100%¶

In [25]:
RTA_sum_MCML, RTA_sum_CUDAMCML = np.zeros([2, 6, len(all_lambda)])

for i in range(6):
    RTA_sum_MCML[i,:] = np.array(pd.read_csv(r'D:\Victor\2011DelgadoAtencio_{}\R_T_A\R_T_A_{}.txt'.format('MCML', i), sep = ' '))[:,8]
    RTA_sum_CUDAMCML[i,:] = np.array(pd.read_csv(r'D:\Victor\2011DelgadoAtencio_{}\R_T_A\R_T_A_{}.txt'.format('CUDAMCML', i), sep = ' '))[:,8]
In [26]:
i = 0
plt.scatter(all_lambda, RTA_sum_MCML[i,:],
            color = color_cycle[i], marker = '.', label = 'MCML')
for i in range(6):
    plt.plot(all_lambda, RTA_sum_CUDAMCML[i,:],
             color = color_cycle[i], lw = 2., label = 'CUDAMCML, C_bil = {:.2f} g/L'.format(all_Cbi[i]))
    plt.scatter(all_lambda, RTA_sum_MCML[i,:],
                color = color_cycle[i], marker = '.')
plt.ylabel('R+T+A [%]')
plt.title('(fv_mel = 2%, f_blo = 0.2%, S = 70%, fv_bil = 20%)')
plt.xlim(400, 700)
plt.ylim(98.9, 100.1)
plt.legend(loc = 'lower right')
plt.show()
In [27]:
pd.DataFrame(np.array([[RTA_sum_MCML.min(), RTA_sum_MCML.max()],
                       [RTA_sum_CUDAMCML.min(), RTA_sum_CUDAMCML.max()]]),
             index = ['R+T+A (MCML) [%]', 'R+T+A (CUDAMCML) [%]'],
             columns = ['min', 'max'])
Out[27]:
min max
R+T+A (MCML) [%] 99.99129 100.008290
R+T+A (CUDAMCML) [%] 99.50065 99.924765

Extra: sRGB color space¶

In [28]:
R, G, B = np.zeros([3, 6])

for i in range(6):
    R[i], G[i], B[i] = sRGB_from_spectrum(all_lambda, Rd_CUDAMCML[i,:])
RGB = np.vstack((R, G, B)).T
In [29]:
rcParams.update({'axes.grid': False})
for i in range(6):
    plt.gca().add_patch(plt.Rectangle((i, 0.), 1., 1., 
                                      fc = RGB[i], 
                                      ec = (0, 0, 0)))
plt.xticks([0.5, 1.5, 2.5, 3.5, 4.5, 5.5], all_Cbi)
plt.yticks([0], '')
plt.xlabel('bilirubin concentration [g/L]')
plt.axis('scaled')
plt.axis([0, 6, 0, 1])
plt.show()
rcParams.update({'axes.grid': True})

Color scale very similar to Figure 6 from the work by Bauer et al. 2017 [B*17].