My end goal for the jacobian is to perform an inverse and reconstruct the HbO concentrations from a recording. Here is my current python implementation for reference too:
import json
import pmcx
import numpy as np
from Orbit_Utils import Load_Colin27_Mat_Volume, Load_NIfTI
import matplotlib.pyplot as plt
import os
from skimage.measure import block_reduce
def apply_basis(jacobian_matrix, original_shape, basis_size=(50,50,50)):
# Compute the downsampling factors for each dimension
downsample_factors = (
original_shape[0] // basis_size[0],
original_shape[1] // basis_size[1],
original_shape[2] // basis_size[2]
)
print(f"Downsampling factors: {downsample_factors}")
# Check validity of the downsampling factors
assert all(f >= 1 for f in downsample_factors), "Basis size larger than original!"
num_meas = jacobian_matrix.shape[0]
jacobian_basis = []
# Apply downsampling per measurement
for idx in range(num_meas):
vol = jacobian_matrix[idx].reshape(original_shape)
reduced_vol = block_reduce(vol, block_size=downsample_factors, func=np.mean)
jacobian_basis.append(reduced_vol.flatten())
jacobian_basis = np.array(jacobian_basis)
print(f"Original Jacobian shape: {jacobian_matrix.shape}")
print(f"Basis Jacobian shape: {jacobian_basis.shape}")
return jacobian_basis
if __name__ == '__main__':
# must set which layer grey matter jacobian is, different for different volumes
grey_matter_index = 2 # 2 for MNI
models = ["colin27", "MNI152", "ernie"]
use_basis = False # reduce jacobian size with basis
model_selection = models[1]
if model_selection == "colin27":
vol = Load_Colin27_Mat_Volume()
elif model_selection == "MNI152":
vol = Load_NIfTI('head_models/MNI152/final_tissues.nii.gz')
vol = np.squeeze(vol[0])
print(np.unique(vol))
file_path = "lumo_MNI152_850nm_config.json"
with open(file_path, "r") as f:
base_cfg = json.load(f)
#config file im loading in:
base_cfg = {
'nphoton': int(1e8),
'vol': vol,
# Optical properties: each row is [mua, mus, g, n], need to double check values, g = anistropy
'prop': [
[0.0, 0.0, 1.0, 1.0], # Free space / Background
[1.04, 60.8, 7, 1.37], # White Matter
[0.35, 7.1, 0.90, 1.37], # Gray Matter
[0.043, 0, 0.02, 1.33], # CSF (Using NaN for N/A)
[0.28, 16, 0.94, 1.43], # Bone (General)
[0.38, 18, 0.81, 1.37], # Scalp (Skin)
[0.075, 0.2, 0.90, 1.336], # Eyeball (using midpoints of ranges)
[0.125, 11.5, 0.90, 1.54], # Compact Bone (using midpoints of ranges)
[0.115, 5.5, 0.90, 1.50], # Spongy Bone (using midpoints of ranges)
[6.5, 3, 0.99, 1.40], # Blood (oxy) (using midpoints of ranges)
[4.5, 3, 0.99, 1.40], # Blood (deoxy) (using midpoints of ranges)
[0.30, 6.6, 0.90, 1.40] # Muscle
]
'srcpos': src_pos.tolist(),
'srcparam1': srcparam1,
'srcparam2':srcparam2,
'srcdir': src_dir,
#'srcpattern': # see cfg.srctype for details
#'omega': source modulation frequency (rad/s) for RF replay, 2*pi*f
'tstart': 0,
'tend': 5e-9, # end time of simulation
'tstep': 5e-9, # time step through simulation
'srcid': 0, # -1 mean each source separately simulated, 0 means altogether, number specifies single
# Default detectors (this will be replaced per setup below).
'detpos': det_pos.tolist(),
'issrcfrom0': 1, # 1-first voxel is [0 0 0], [0]- first voxel is [1 1 1]
'lambda': wavelength
}
# Convert lists back to numpy arrays
base_cfg['vol'] = vol
base_cfg['srcpos'] = np.array(base_cfg['srcpos'])
base_cfg['detpos'] = np.array(base_cfg['detpos'])
# Common simulation settings
base_cfg['autopilot'] = 1
base_cfg['issrcfrom0'] = 1
base_cfg['issavedet'] = 1
base_cfg['savedetflag'] = "dspmxvwi"
base_cfg['issaveseed'] = 1
# Get number of sources and detectors
n_src = base_cfg['srcpos'].shape[0] if len(base_cfg['srcpos'].shape) > 1 else 1
n_det = base_cfg['detpos'].shape[0] if len(base_cfg['detpos'].shape) > 1 else 1
print(f"Number of sources: {n_src}")
print(f"Number of detectors: {n_det}")
# Create a matrix to hold all Jacobians
jacobian_matrix = []
# Create a list to track the source-detector pairs
measurement_order = []
# Directory to save individual Jacobians
output_dir = "jacobian_results"
os.makedirs(output_dir, exist_ok=True)
none_detected = []
# Loop through each source-detector pair
for src_idx in range(n_src):
print(f"Processing source {src_idx+1}/{n_src}")
# Create configuration with only current source
src_cfg = base_cfg.copy()
if n_src > 1:
src_cfg['srcpos'] = base_cfg['srcpos'][src_idx:src_idx+1]
for det_idx in range(n_det):
print(f" Processing detector {det_idx+1}/{n_det}")
# Add this source-detector pair to our tracking list
measurement_order.append((src_idx, det_idx))
# Create configuration with only current detector
sd_cfg = src_cfg.copy()
if n_det > 1:
sd_cfg['detpos'] = base_cfg['detpos'][det_idx:det_idx+1]
# Run the forward simulation for this source-detector pair
res = pmcx.mcxlab(sd_cfg)
# Check if photons were detected
if 'detp' in res and 'seeds' in res:
detp = res['detp']
seeds = res['seeds']
# Set up Jacobian calculation
jac_cfg = sd_cfg.copy()
jac_cfg['seed'] = seeds
jac_cfg['outputtype'] = "jacobian"
jac_cfg['detphotons'] = detp["data"]
# Run the Jacobian calculation
jac_results = pmcx.mcxlab(jac_cfg)
if 'flux' in jac_results:
# Get the Jacobian for this SD pair
jac_volume = np.sum(jac_results['flux'], axis=3)
# Save the individual Jacobian volume
filename = f"{output_dir}/jacobian_src{src_idx}_det{det_idx}.npy"
np.save(filename, jac_volume)
# Reshape to a row vector
jac_row = jac_volume.flatten()
# Add to the Jacobian matrix
jacobian_matrix.append(jac_row)
print(f" Added Jacobian row, shape: {jac_row.shape}")
else:
print(f" No flux in Jacobian results for source {src_idx+1}, detector {det_idx+1}")
else:
none_detected.append((src_idx, det_idx))
print(f"------No photons detected for source {src_idx+1}, detector {det_idx+1} =( -------)")
print(f"None detected: {none_detected}")
print(f"Number of none detected: {len(none_detected)}")
# Convert to numpy array
jacobian_matrix = np.array(jacobian_matrix)
#measurement_order = np.array(measurement_order)
# Print final Jacobian shape
print(f"Final Jacobian matrix shape: {jacobian_matrix.shape}")
# Create a copy to avoid modifying the original
result = np.zeros_like(vol)
# Only keep the specified label
result[vol == grey_matter_index] = grey_matter_index
gm_mask_flat = vol.flatten().astype(bool)
jacobian_gm = jacobian_matrix[:, gm_mask_flat]
# Save the Jacobian matrix and measurement order
np.savez('jacobian_data.npz',
jacobian=jacobian_matrix)
np.savez('jacobian_gm.npz',
jacobian=jacobian_gm)
if use_basis:
basis_size = (50,50,50)
jacobian_basis = apply_basis(jacobian_matrix, vol.shape, basis_size=basis_size)
np.savez_compressed('jacobian_basis.npz',
jacobian=jacobian_basis,
measurement_order=measurement_order,
basis_size=basis_size)
np.save('jacobian_basis.npy', jacobian_basis)