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.
- Verify that
mcx --bench skinvesselruns end to end. - Copy and inspect the JSON form of the same model.
- Submit a longer production run as a SLURM job.
- Analyze the per-voxel fluence with
mcxlab. - (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/msis meaningfully slower than thecube60benchmark (~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 msdominates 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.jsonis the main MCX input:Session,Forward,Optode, andDomain. The last block contains aVolumeFilepointer.skinvessel.jsonis the volume-shapes file thatmcxyz_bench.jsonreferences. It declares the geometry (layers + vessel) that fills the 200×200×200 voxel grid.
{
"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"
}
}
{
"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. TheIDis the basename of every output file, so this run will produceskinvessel.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 isParam1[0] = 60voxels.Domain: a 200×200×200 voxel grid withLengthUnit = 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 = 1places the origin at the corner of the voxel grid, matching theShapesindexing. The five rows ofMediaare[mua, mus, g, n]per medium tag (muaandmusin 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¶
#!/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:
% --- 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:
#!/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:

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:
-
Submit the VNC session on a GPU node. Save the snippet below as
vnc_mcx.slurmand submit withsbatch vnc_mcx.slurm. The first time you use VNC, set a password (/opt/TurboVNC/bin/vncpasswdfrom 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 -
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:
For just designing and editing a simulation,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 &mcxstudio &is enough. As soon as you open the 3D preview or invokemcxshow/mcxviewerfrom inside the GUI, software GLX kicks in and the windows become unusable (multi-second latency, blocky updates);vglrun -d eglroutes OpenGL calls to the GPU through EGL and they become smooth again. The same prefix applies tomcxshowandmcxviewerwhen launched standalone in step 5. -
Load the skinvessel project. From
File > Open Project, point at$MCXSTUDIO_DEMO/mcx_demo.mcxpfor a quick tour, or import~/mcx_skinvessel/mcxyz_bench.jsonfor the model used in this walkthrough. The Domain, Source, and Session tabs reflect the JSON blocks one to one. -
Run from the GUI. Press
Runto launchmcxagainst the local GPU in the VNC session. Outputs land in the configured output directory (Session > Output dir). -
Visualize.
vglrun -d egl mcxviewer skinvessel.jniiopens the 3D fluence in a volumetric OpenGL viewer;vglrun -d egl mcxshowdoes the same for the input volume.
See also¶
-
MCX overview
Module loading, GPU support, suite components, troubleshooting.
-
VNC remote desktop
Step-by-step instructions for the VNC session used by MCXStudio.
-
MCX wiki
Upstream documentation: JSON reference, source types, mcxlab API.