Skip to content

Walkthrough: skinvessel (voxel Monte Carlo)

This walkthrough runs the canonical skinvessel example bundled with MCX: Steven Jacques' mcxyz reference simulation of a layered skin model with a horizontal blood vessel embedded in the dermis. It is one of the standard test cases used throughout the MCX literature.

The same model ships in two equivalent forms:

  • mcx --bench skinvessel: fully embedded in the binary; runs with no external files.
  • $MCX_EXAMPLES/skinvessel/: the same configuration as JSON files you can copy, inspect, and modify.

The five steps below use the JSON form, because for a walkthrough you want to see what the model actually says. The --bench shortcut is the quickest way to confirm everything works and we use it in Step 1.

  1. Verify that mcx --bench skinvessel runs end to end.
  2. Copy and inspect the JSON form of the same model.
  3. Submit a longer production run as a SLURM job.
  4. Analyze the per-voxel fluence with mcxlab.
  5. (Optional) Inspect the model in MCXStudio inside a VNC session.

If you have not loaded the module before, start from the MCX overview and confirm that mcx -L sees a GPU. The rest of this page assumes you already have the suite working.

Step 1: Run the embedded benchmark

The quickest sanity check is a single command. From an interactive session on a GPU node:

srun --qos=regular --partition=general --gres=gpu:1 --cpus-per-task=4 --mem=16G --time=00:30:00 --pty bash

module load MCX/2025.10
mcx --bench skinvessel -G 1 -A
###############################################################################
#                      Monte Carlo eXtreme (MCX) -- CUDA                      #
###############################################################################
[banner omitted]
$Rev::4d6ca7$v2025.10$Date::2025-10-12 12:45:50 -04$ by $Author::Qianqian Fang$
- code name: [Kilo-Kelvin] compiled by nvcc [7.5] for CUDA-arch [350] on [Oct 12 2025]
- compiled with: RNG [xorshift128+] seed length [4]

GPU=1 (NVIDIA GeForce RTX 3090) threadph=2 extra=328256 np=1000000 nthread=335872 maxgate=1 repetition=1
initializing streams ...    init complete : 8 ms
launching MCX simulation for time window [0.00e+00ns 5.00e+01ns] ...
simulation run# 1 ...
kernel complete:      159 ms
retrieving fields ...     transfer complete:    184 ms
normalizing raw data ...    source 1, normalization factor alpha=0.040000
data normalization complete : 299 ms
saving data to file ...     compressing data [zlib] ...compression ratio: 85.8%    after encoding: 114.4%
saving data complete : 1602 ms

simulated 1000000 photons (1000000) with 335872 threads (repeat x1)
MCX simulation speed: 10989.01 photon/ms
total simulated energy: 1000000.00    absorbed: 39.79047%

A few things to read out of this:

  • MCX simulation speed: 10989 photon/ms is meaningfully slower than the cube60 benchmark (~31000 photon/ms on the same GPU). Skinvessel is heterogeneous with strongly absorbing media (epidermis melanin, blood), so the kernel does more work per photon.
  • absorbed: 39.79% is the characteristic skinvessel value at 1 million photons; it reflects the high absorption inside the vessel and the pigmented epidermis. Use it as a sanity check that the simulation actually saw the heterogeneous tissue.
  • saving data complete: 1602 ms dominates the wall time. The output file is the full 200×200×200×1 fluence (about 32 MB before zlib), which is why I/O takes longer than the kernel itself.

The run produces skinvessel.jnii in the current directory, containing the per-voxel fluence in JNIfTI format (see Step 3 for what that means).

Step 2: Copy and inspect the JSON form

The same model lives as JSON inputs under $MCX_EXAMPLES/skinvessel/. Copy the directory to a writable location:

cp -r $MCX_EXAMPLES/skinvessel ~/mcx_skinvessel
cd ~/mcx_skinvessel
ls
mcxyz_bench.json   README.txt   run_mcxyz_bench.bat   run_mcxyz_bench.sh   skinvessel.json

Two JSON files, with a small split that is worth understanding:

  • mcxyz_bench.json is the main MCX input: Session, Forward, Optode, and Domain. The last block contains a VolumeFile pointer.
  • skinvessel.json is the volume-shapes file that mcxyz_bench.json references. It declares the geometry (layers + vessel) that fills the 200×200×200 voxel grid.
mcxyz_bench.json
{
    "Session": {
        "ID": "skinvessel",
        "DoMismatch": 1,
        "DoAutoThread": 1,
        "Photons": 10000000
    },
    "Forward": {
        "T0": 0,
        "T1": 5e-08,
        "Dt": 5e-08
    },
    "Optode": {
        "Source": {
            "Pos": [100, 100, 20],
            "Dir": [0, 0, 1],
            "Param1": [60, 0, 0, 0],
            "Type": "disk"
        }
    },
    "Domain": {
        "OriginType": 1,
        "LengthUnit": 0.005,
        "Media": [
            {"mua": 1e-05,        "mus": 0,           "g": 1,    "n": 1.37},
            {"mua": 3.564e-05,    "mus": 1,           "g": 1,    "n": 1.37},
            {"mua": 23.05426549,  "mus": 9.398496241, "g": 0.9,  "n": 1.37},
            {"mua": 0.04584957865,"mus": 35.65405549, "g": 0.9,  "n": 1.37},
            {"mua": 1.657237447,  "mus": 37.59398496, "g": 0.9,  "n": 1.37}
        ],
        "Dim": [200, 200, 200],
        "VolumeFile": "skinvessel.json"
    }
}
skinvessel.json (volume shapes)
{
    "Shapes": [
        {"ZLayers": [[1, 20, 1], [21, 32, 4], [33, 200, 3]]},
        {"Cylinder": {"Tag": 2, "C0": [0, 100.5, 100.5], "C1": [200, 100.5, 100.5], "R": 20}}
    ]
}

Block by block:

  • Session: 1×10⁷ photons (more than the 10⁶ default of --bench), refractive-index mismatch enabled, auto thread selection. The ID is the basename of every output file, so this run will produce skinvessel.jnii.
  • Forward: T0 = 0, T1 = Dt = 5×10⁻⁸ defines a single 50 ns time gate. The simulation accumulates into one CW fluence frame.
  • Optode.Source: a disk source at voxel position [100, 100, 20] (top of the epidermis, see Shapes below), pointing down into the tissue. The disk radius is Param1[0] = 60 voxels.
  • Domain: a 200×200×200 voxel grid with LengthUnit = 0.005 (voxel edge size, in MCX length units; the mcxyz convention is centimetres, so this is 50 μm per voxel and the cube is 1 cm on a side). OriginType = 1 places the origin at the corner of the voxel grid, matching the Shapes indexing. The five rows of Media are [mua, mus, g, n] per medium tag (mua and mus in cm⁻¹, consistent with the cm length unit); row 0 is the implicit ambient.

The optical-property table reveals the implied wavelength: mua = 23 cm⁻¹ for blood and 1.66 cm⁻¹ for the pigmented epidermis are characteristic of red-visible illumination (around 570-600 nm), not NIR. This is the wavelength the original mcxyz example targeted, chosen so that hemoglobin absorption produces a clear vessel shadow. - Shapes (from skinvessel.json): first fills three Z layers with tags 1, 4, 3 respectively; then overwrites a horizontal cylinder along the X axis with tag 2.

The resulting geometry, in physical units:

Z range (voxels) Z range (depth) Tag Tissue
1 - 20 0 - 1.0 mm 1 Water / coupling layer above the skin
21 - 32 1.0 - 1.6 mm 4 Epidermis (pigmented, high mua)
33 - 200 1.6 - 10.0 mm 3 Dermis
inside cylinder centered at 5 mm 2 Blood vessel, radius 1 mm, running ‖ X

The disk source sits at the boundary between the water layer and the epidermis, illuminating a 3 mm spot vertically into the tissue. The vessel runs parallel to the X axis at the dermis mid-depth.

For the historical background of these specific optical-property values (wavelength, source paper), open README.txt in the example directory.

Step 3: Submit the simulation as a batch job

mcx.slurm
#!/bin/bash
#SBATCH --qos=regular
#SBATCH --partition=general
#SBATCH --job-name=skinvessel
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=4
#SBATCH --gres=gpu:1
#SBATCH --mem=16G
#SBATCH --time=00:30:00
#SBATCH --output=%x.%j.out
#SBATCH --error=%x.%j.err

module load MCX/2025.10

# -f : input file
# -G : GPU index (1-based)
# -A : autopilot (let mcx pick thread/block size)
mcx -f mcxyz_bench.json -G 1 -A

Submit with sbatch mcx.slurm from the directory holding both JSON files (so MCX resolves the VolumeFile relative path correctly). 10×10⁶ photons take a few seconds of GPU time on RTX 3090; A100 nodes are faster. The job log on RTX 3090 looks like this:

[banner omitted]
GPU=1 (NVIDIA GeForce RTX 3090) threadph=29 extra=259712 np=10000000 nthread=335872 maxgate=1 repetition=1
initializing streams ...    init complete : 8 ms
launching MCX simulation for time window [0.00e+00ns 5.00e+01ns] ...
simulation run# 1 ...
kernel complete:      1042 ms
retrieving fields ...     transfer complete:    1067 ms
normalizing raw data ...    source 1, normalization factor alpha=80000.000000
data normalization complete : 1183 ms
saving data to file ...     compressing data [zlib] ...compression ratio: 88.2%    after encoding: 117.6%
saving data complete : 2507 ms

simulated 10000000 photons (10000000) with 335872 threads (repeat x1)
MCX simulation speed: 10266.94 photon/ms
total simulated energy: 10000000.00    absorbed: 39.75513%

The run produces one file next to the input:

File Content
skinvessel.jnii Per-voxel fluence in JNIfTI format (zlib-compressed).

JNIfTI is the JSON-based NIfTI container used across the NeuroJSON ecosystem and is the default MCX output since v2020. The on-disk size for this run is ~37 MB (200×200×200 float32 ≈ 32 MB raw, plus JSON header and minor zlib overhead). Older raw .mc2 output is still available with -F mc2, and other formats (nii, mat, bnii, tx3) with the corresponding -F value.

Where to write outputs

For parameter sweeps that produce many full-resolution outputs, write under /lscratch/$USER/$SLURM_JOB_ID/ and copy back only the ones you keep at the end. See Local scratch for I/O-intensive jobs.

Step 4: Read and analyze the output

The JNIfTI loader (loadjnifti) ships with the bundled iso2mesh toolbox; MATLABPATH is already configured by the module, so it is callable without addpath:

analyze.m
% --- Load the JNIfTI fluence file ---
dat  = loadjnifti('skinvessel.jnii');
flux = dat.NIFTIData;     % size [200 200 200 1], single precision

% --- Cross-section through the vessel (y = 100, vessel mid-plane) ---
fig = figure('Visible', 'off');
imagesc(log10(squeeze(flux(:, 100, :, 1)) + 1e-12)');
axis image; colorbar;
xlabel('x (voxel)'); ylabel('z (voxel, depth)');
set(gca, 'YDir', 'reverse');
title('log_{10} fluence on the y = 100 slice');
saveas(fig, 'fluence_y100.png');

% --- Quick summary ---
fprintf('Fluence range: [%.3e, %.3e]\n', min(flux(:)), max(flux(:)));
fprintf('Total energy:  %.3e\n', sum(flux(:)));

loadjnifti returns a struct with NIFTIHeader (acquisition and dimensional metadata, mirroring the NIfTI-1 header) and NIFTIData (the actual array). For this run NIFTIData is a 200×200×200×1 single-precision array; the trailing singleton dimension is the single time gate from the Forward block.

Run it as a CPU-only SLURM job (no GPU needed for post-processing). MATLAB in -batch mode cannot open windows, so the script writes the figure straight to disk as fluence_y100.png:

analyze.slurm
#!/bin/bash
#SBATCH --qos=regular
#SBATCH --partition=general
#SBATCH --job-name=analyze
#SBATCH --nodes=1
#SBATCH --ntasks=1
#SBATCH --cpus-per-task=4
#SBATCH --mem=16G
#SBATCH --time=00:15:00
#SBATCH --output=%x.%j.out
#SBATCH --error=%x.%j.err

module load MATLAB/2024b-IKUR
module load MCX/2025.10

matlab -nodisplay -nosplash -batch "run('analyze.m')"

Submit with sbatch analyze.slurm from the directory containing skinvessel.jnii and analyze.m. The job writes fluence_y100.png to the same directory and prints the two fprintf lines to stdout:

Fluence range: [3.258e+03, 1.170e+08]
Total energy:  1.152e+14

The resulting cross-section looks like this:

log10 fluence on the y = 100 slice of the skinvessel model

Three things to read out of the plot, all interpretable in physical units (50 μm per voxel, so the 200×200 slice is 10 mm × 10 mm):

  • The bright yellow band at the top (z ≈ 20-40, x ≈ 40-160) is the 3 mm-radius disk source illuminating the epidermis. The disk footprint matches x ∈ [40, 160]; the brightest band ends around z = 40, i.e. roughly 1 mm below the surface, after photons have crossed the pigmented epidermis (tag 4, mua = 1.66 cm⁻¹).
  • The smooth green-to-blue gradient between z ≈ 40 and z ≈ 100 is diffusion through the dermis (tag 3, mua = 0.046 cm⁻¹, mus = 35.6 cm⁻¹): high scattering, low absorption, the regime where the modified Beer-Lambert law applies and where source-detector geometries probe the tissue.
  • The slightly darker horizontal band centred at z ≈ 100, x ≈ 100 is the blood vessel (tag 2, mua = 23 cm⁻¹, about 500× the dermis). Its cross-section at the y = 100 slice extends from z ≈ 80 to z ≈ 120 (the vessel radius is 20 voxels = 1 mm). Photons that cross the vessel are absorbed almost immediately, so the dimmer region below it is the absorption shadow that diffuse-optics instruments measure indirectly through changes in detected photon weights. It is only visible near x = 100 because that is where the source-illuminated photon flux is high enough for the vessel-dermis contrast to rise above the diffusion floor.

Interactive alternative

From inside the VNC session used for MCXStudio (Step 5), you can also start MATLAB normally (matlab &), paste the script into the editor, and run it line by line. Remove the 'Visible', 'off' argument and the saveas call so the figure pops on screen as you iterate.

Submit it as a CPU-only SLURM job (no GPU needed for post-processing) or run interactively from MATLAB inside the same VNC desktop used for MCXStudio. The figure shows the fluence distribution on a vertical slice through the centre of the vessel: a bright halo at the top under the source, a sharply absorbing band at the epidermis, and a localized dark shadow centred at depth z ≈ 100 where the blood vessel sits.

That shadow is the perturbation that diffuse-optics instruments measure indirectly through changes in detected photon weights. It is also the result you would compare against an MMC tetrahedral simulation of the same model, to see whether the voxel discretisation smears the vessel boundary.

Step 5: Inspect the model in MCXStudio

MCXStudio loads the same JSON files into a tabbed GUI that mirrors the Session, Domain, Optode, and Forward blocks. You can change any field, re-run against the local GPU, and inspect the resulting fluence with mcxviewer without ever leaving the desktop. The Lazarus/GTK2 toolkit and OpenGL viewers need a real graphical session; the supported path on our clusters is a VNC remote desktop on a GPU node.

The procedure:

  1. Submit the VNC session on a GPU node. Save the snippet below as vnc_mcx.slurm and submit with sbatch vnc_mcx.slurm. The first time you use VNC, set a password (/opt/TurboVNC/bin/vncpasswd from an interactive session); the VNC guide walks through password setup and the SSH tunnel step by step.

    vnc_mcx.slurm
    #!/bin/bash
    #SBATCH --qos=regular
    #SBATCH --partition=general
    #SBATCH --job-name=vnc_mcx
    #SBATCH --nodes=1
    #SBATCH --ntasks=1
    #SBATCH --cpus-per-task=4
    #SBATCH --gres=gpu:1
    #SBATCH --mem=16G
    #SBATCH --time=04:00:00
    #SBATCH --output=%x.%j.out
    #SBATCH --error=%x.%j.err
    
    module load DIPC-Utils
    
    vncserver-start
    
  2. Open the VNC desktop with your local viewer following the connection info in the job log, then open a terminal inside the desktop and load the module. You can launch MCXStudio two ways:

    module load MCX/2025.10
    
    # Plain launch: the Lazarus/GTK2 GUI (menus, tabs, project editor) works fine.
    mcxstudio &
    
    # vglrun wrapper: also accelerates the embedded 3D preview through hardware EGL.
    vglrun -d egl mcxstudio &
    
    For just designing and editing a simulation, mcxstudio & is enough. As soon as you open the 3D preview or invoke mcxshow / mcxviewer from inside the GUI, software GLX kicks in and the windows become unusable (multi-second latency, blocky updates); vglrun -d egl routes OpenGL calls to the GPU through EGL and they become smooth again. The same prefix applies to mcxshow and mcxviewer when launched standalone in step 5.

  3. Load the skinvessel project. From File > Open Project, point at $MCXSTUDIO_DEMO/mcx_demo.mcxp for a quick tour, or import ~/mcx_skinvessel/mcxyz_bench.json for the model used in this walkthrough. The Domain, Source, and Session tabs reflect the JSON blocks one to one.

  4. Run from the GUI. Press Run to launch mcx against the local GPU in the VNC session. Outputs land in the configured output directory (Session > Output dir).

  5. Visualize. vglrun -d egl mcxviewer skinvessel.jnii opens the 3D fluence in a volumetric OpenGL viewer; vglrun -d egl mcxshow does the same for the input volume.

See also

  • MCX overview


    Module loading, GPU support, suite components, troubleshooting.

    MCX overview

  • VNC remote desktop


    Step-by-step instructions for the VNC session used by MCXStudio.

    VNC remote desktop

  • MCX wiki


    Upstream documentation: JSON reference, source types, mcxlab API.

    MCX wiki