Simulated Datasets with different source voltages and same filters

In this notebook, we provide an example about how to use xspec forward model to generate the simulated datset with 3 different source voltages and a same filter.

[1]:
# Basic Packages
import os
import numpy as np
import matplotlib.pyplot as plt
import svmbir
import h5py

from xspec.chem_consts import get_lin_att_c_vs_E
from xspec import calc_forward_matrix
from xspec._utils import Gen_Circle
import spekpy as sp  # Import SpekPy
from xspec.defs import Material
from xspec.chem_consts._periodictabledata import density
from xspec.models import *
[2]:
min_simkV = 30
max_simkV = 200

A. Calculate forward matrix

A detail explanation can be found in examples/notebooks/user_forward_projector.ipynb

[3]:
# Scanned cylinders
materials = ['V', 'Al', 'Ti', 'Mg']
mat_density = [density['%s' % formula] for formula in materials]
energies = np.linspace(1, max_simkV, max_simkV) # Define energies bins from 1 kV to 160 kV with step size 1 kV.
lac_vs_E_list = []

for i in range(len(materials)):
    formula = materials[i]
    den = mat_density[i]
    lac_vs_E_list.append(get_lin_att_c_vs_E(den, formula, energies))
[4]:
# FOV is about 10 mm * 10 mm
nchanl = 1024
rsize = 0.01  # mm

# 4 cylinders with 1mm radius are evenly distributed on a circle with 3mm radius.
Radius = [1 for _ in range(len(materials))]
arrange_with_radius = 3
centers = [[np.sin(rad_angle) * arrange_with_radius, np.cos(rad_angle) * arrange_with_radius]
               for rad_angle in np.linspace(-np.pi / 2, -np.pi / 2 + np.pi * 2, len(materials), endpoint=False)]

# Each mask represents a homogenous cylinder.
mask_list = []
for mat_id, mat in enumerate(materials):
    circle = Gen_Circle((nchanl, nchanl), (rsize, rsize))
    # Use np.newaxis to convert 2D array to 3D.
    mask_list.append(circle.generate_mask(Radius[mat_id], centers[mat_id])[np.newaxis])
[5]:
class fw_projector:
    """A class for forward projection using SVMBIR."""

    def __init__(self, angles, num_channels, delta_pixel=1):
        """
        Initializes the forward projector with specified geometric parameters.

        Parameters:
            angles (array): Array of projection angles.
            num_channels (int): Number of detector channels.
            delta_pixel (float, optional): Size of a pixel, defaults to 1.
        """
        self.angles = angles
        self.num_channels = num_channels
        self.delta_pixel = delta_pixel

    def forward(self, mask):
        """
        Computes the projection of a given mask.

        Parameters:
            mask (numpy.ndarray): 3D mask of the object to be projected.

        Returns:
            numpy.ndarray: The computed projection of the mask.
        """
        projections = svmbir.project(mask, self.angles, self.num_channels) * self.delta_pixel
        return projections

angles = np.linspace(-np.pi/2, np.pi, 40, endpoint=False)
projector = fw_projector(angles, num_channels=1024, delta_pixel=0.01)
[6]:
spec_F = calc_forward_matrix(mask_list, lac_vs_E_list, projector)
Found system matrix: /Users/damonli/.cache/svmbir/sysmatrix/fa02df7dd8cb1ddb5e4d.2Dsvmatrix
Found system matrix: /Users/damonli/.cache/svmbir/sysmatrix/fa02df7dd8cb1ddb5e4d.2Dsvmatrix
Found system matrix: /Users/damonli/.cache/svmbir/sysmatrix/fa02df7dd8cb1ddb5e4d.2Dsvmatrix
Found system matrix: /Users/damonli/.cache/svmbir/sysmatrix/fa02df7dd8cb1ddb5e4d.2Dsvmatrix

B.Generate ground truth X-ray spectral energy response

A detail explanation can be found in examples/notebooks/configure_spectral_models.ipynb

[7]:
simkV_list = np.linspace(min_simkV, max_simkV, (max_simkV-min_simkV)//10+1, endpoint=True).astype('int')

takeoff_angle = 20
ref_takeoff_angle = 11
# Energy bins.
energies = np.linspace(1, max_simkV, max_simkV)

# Use Spekpy to generate a source spectra dictionary.
src_spec_list = []

print('\nRunning demo script (10 mAs, 100 cm)\n')
for simkV in simkV_list:
    s = sp.Spek(kvp=simkV + 1, th=ref_takeoff_angle, dk=1, mas=1, char=True)  # Create the spectrum model
    k, phi_k = s.get_spectrum(edges=True)  # Get arrays of energy & fluence spectrum
    phi_k = phi_k * ((rsize / 10) ** 2)
    src_spec = np.zeros((max_simkV))
    src_spec[:simkV] = phi_k[::2]
    src_spec_list.append(src_spec)

print('\nFinished!\n')

# A dictionary of source spectra with source voltage from 30 kV to 200 kV
src_spec_list = np.array(src_spec_list)

voltage_list = [80.0, 130.0, 180.0]  # kV
sources = [Reflection_Source(voltage=(voltage, None, None), takeoff_angle=(20, None, None), single_takeoff_angle=True) for
    voltage in voltage_list]
for src_i, source in enumerate(sources):
    source.set_src_spec_list(src_spec_list, simkV_list, ref_takeoff_angle)
    plt.plot(energies, source(energies), label='%d kV'%voltage_list[src_i])
plt.title('Ground Truth: 3 different Source spectra')
plt.xlabel('Energy  [keV]')
plt.legend()

Running demo script (10 mAs, 100 cm)

Finished!
[7]:
<matplotlib.legend.Legend at 0x157622ef0>
../../_images/examples_notebooks_simulated_data_3voltages_10_2.png
[8]:
psb_fltr_mat = [Material(formula='Al', density=2.702)]
filter_1 = Filter(psb_fltr_mat, thickness=(3, None, None))
plt.plot(energies, filter_1(energies), label='3mm Al')
plt.title('Ground Truth: Filter Responses')
plt.legend()
plt.xlabel('Energy  [keV]')
[8]:
Text(0.5, 0, 'Energy  [keV]')
../../_images/examples_notebooks_simulated_data_3voltages_11_1.png
[9]:
psb_scint_mat = [Material(formula='CsI', density=4.51)]
scintillator_1 = Scintillator(materials=psb_scint_mat[0:1], thickness=(0.33, None, None))
plt.plot(energies, scintillator_1(energies), label='0.33 mm CsI')
plt.title('Ground Truth: Scintillator Response.')
plt.legend()
plt.xlabel('Energy  [keV]')
[9]:
Text(0.5, 0, 'Energy  [keV]')
../../_images/examples_notebooks_simulated_data_3voltages_12_1.png
[10]:
gt_spec_list = [(source(energies) * filter_1(energies)  * scintillator_1(energies)).numpy() for source in sources]
for spec_i, gt_spec in enumerate(gt_spec_list):
    plt.plot(energies, gt_spec / np.trapz(gt_spec, energies), label='%d kV'%voltage_list[spec_i])
plt.legend()
plt.title('Ground Truth: Total X-ray Spectral Response')
plt.xlabel('Energy  [keV]')
[10]:
Text(0.5, 0, 'Energy  [keV]')
../../_images/examples_notebooks_simulated_data_3voltages_13_1.png

C. Generate simulated datasets with above 3 reponses.

[11]:
os.makedirs('../data/',exist_ok=True)
datasets = []
label_list = ['80 kV', '130 kV', '180 kV']

for case_i, gt_spec in zip(np.arange(len(gt_spec_list)), gt_spec_list):
    spec_F_train_list = []
    trans_list = []

    # Add poisson noise before reaching detector/scintillator.
    trans = np.trapz(spec_F * gt_spec, energies, axis=-1)
    trans_0 = np.trapz(gt_spec, energies, axis=-1)
    trans_noise = np.random.poisson(trans).astype(np.float64)
    trans_noise /= trans_0


     # Add poisson noise before reaching detector/scintillator.
    trans = np.trapz(spec_F * gt_spec, energies, axis=-1)
    trans_0 = np.trapz(gt_spec, energies, axis=-1)
    trans_noise = np.random.poisson(trans).astype(np.float64)
    trans_noise /= trans_0

    # Store noiseless transmission data and forward matrix.
    trans_list.append(trans_noise)
    spec_F_train = spec_F.reshape((-1, spec_F.shape[-1]))
    spec_F_train_list.append(spec_F_train)
    spec_F_train_list = np.array(spec_F_train_list)
    trans_list = np.array(trans_list)
    plt.plot(trans_list[0][16, 0], label=label_list[case_i])
    d = {
        'measurement': trans_list,
        'forward_mat': spec_F_train_list,
        'source': sources[case_i],
        'filter': filter_1,
        'scintillator': scintillator_1,
    }
    datasets.append(d)

with open('../data/sim_3v1f1s_dataset.npy', 'wb') as f:
    np.save(f, datasets,allow_pickle=True)
../../_images/examples_notebooks_simulated_data_3voltages_15_0.png
[11]: