diff --git a/src/ray_tracer/__init__.py b/src/ray_tracer/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..57f07dd6671c593d9a4d36ea57839ff187b0a416
--- /dev/null
+++ b/src/ray_tracer/__init__.py
@@ -0,0 +1,187 @@
+import numpy as np
+
+# Constants and parameters
+pi = np.pi
+e = 1.60217662e-19  # Elementary charge
+atomic_number = 21.5
+
+# User-defined parameters
+beam_current = 3e-3  # Beam current in Amperes
+dwell_time = 0.4e-6  # Dwell time in seconds
+transimpedance = 1e5  # Transimpedance in Volts per Ampere
+
+# Geometry
+build_surface_width = 80e-3  # Build surface width in meters
+num_scan_lines = 1500  # Number of scan lines
+pixel_size = 53.3e-6  # Pixel size in meters
+detector_height = 272e-3  # Detector height in meters
+detector_spacing = 127e-3  # Detector spacing in meters
+
+# Scattering function parameters
+theta = np.deg2rad(0)
+k = np.deg2rad(theta / 13)  # Scattering lobe width parameter
+
+# Define domain size
+dx = 5e-6  # Cell size in meters
+dy = dx
+dz = dx
+x_min, x_max = -250e-6, 250e-6
+y_min, y_max = -250e-6, 250e-6
+z_min, z_max = 0, 20e-6
+x_num = int((x_max - x_min) / dx) + 1
+y_num = int((y_max - y_min) / dy) + 1
+z_num = int((z_max - z_min) / dz) + 1
+
+# Initialize the domain array
+domain = np.zeros((z_num, y_num, x_num))
+domain[:, :, 0:2] = 1  # Solid cells
+domain[:, :, 2] = 0.5  # Interface layer
+
+# Calculate gradients to determine surface normals
+fill_level = domain
+grad_x = np.gradient(fill_level, axis=2)
+grad_y = np.gradient(fill_level, axis=1)
+grad_z = np.gradient(fill_level, axis=0)
+n_s = np.stack((grad_x, grad_y, grad_z), axis=-1)
+norm = np.linalg.norm(n_s, axis=-1)
+
+interface_mask = (fill_level > 0) & (fill_level < 1)
+nonzero_mask = norm > 0
+valid_mask = interface_mask & nonzero_mask
+n_s[valid_mask] /= norm[valid_mask][..., np.newaxis]
+n_s[fill_level == 1] = np.array([0, 0, -1])
+n_s[fill_level == 0] = np.array([0, 0, 1])
+
+# Beam profile calculation
+sigma = 300e-6 / (2 * dx)
+x = np.arange(-30, 31)
+y = np.arange(-30, 31)
+x, y = np.meshgrid(x, y)
+beam_profile = np.exp(-(x ** 2 + y ** 2) / (2 * sigma ** 2))
+beam_profile /= np.sum(beam_profile)
+
+# Extend beam_profile to 3D to match the domain shape
+beam_profile_3d = np.tile(beam_profile[:, :, np.newaxis], (1, 1, z_num))
+
+# Extend beam profile to 3D to match domain shape
+'''beam_profile_3d = np.zeros((z_num, y_num, x_num))
+beam_profile_3d[z_num // 2 - beam_profile.shape[0] // 2:z_num // 2 + beam_profile.shape[0] // 2,
+y_num // 2 - beam_profile.shape[1] // 2:y_num // 2 + beam_profile.shape[1] // 2,
+x_num // 2] = beam_profile'''
+
+# Beam direction
+beam_dir = np.array([np.sin(theta), 0, -np.cos(theta)])
+
+# Define detector
+detector_distance = int(120e-3 / dx)
+detector_height_cells = int(300e-3 / dx)
+detector_width = 10
+detector_vertices = np.array([
+    [detector_distance, -detector_width / 2, detector_height_cells],
+    [detector_distance, detector_width / 2, detector_height_cells],
+    [detector_distance, 0, 0]
+])
+
+# Initialize arrays
+beam_interactions = np.zeros_like(domain)
+scattered_electrons = np.zeros((num_scan_lines, int(build_surface_width / pixel_size)))
+
+
+def compute_reflection_vector(beam_dir, surface_normal):
+    beam_dir = beam_dir / np.linalg.norm(beam_dir)
+    surface_normal = surface_normal / np.linalg.norm(surface_normal)
+    p_reflected = beam_dir - 2 * np.dot(beam_dir, surface_normal) * surface_normal
+    return p_reflected
+
+
+def bse_coefficient_0(A):
+    return -0.0254 + 0.016 * A - 1.86e-4 * A ** 2 + 8.3e-7 * A ** 3
+
+
+def bse_coefficient(theta, A):
+    B = 0.89
+    if theta == 0:
+        return bse_coefficient_0(A)
+    return B * (bse_coefficient_0(A) / B) ** np.cos(theta)
+
+
+def scattering_function_correction(theta):
+    return 1.01 - 4.06 * theta + 1.36e-5 * theta ** 2 - 1.71 * theta ** 3
+
+
+def scattering_function(theta, alpha, beta, A):
+    g_BSE = (bse_coefficient_0(A) / pi) * np.cos(alpha) + \
+            (((bse_coefficient(theta, A) - bse_coefficient_0(A)) / scattering_function_correction(theta)) * \
+             ((k + 1) / (2 * pi))) * np.cos(beta) ** k
+    return g_BSE
+
+
+# Iterate over the beam profile
+for profile_x in range(beam_profile.shape[0]):
+    for profile_y in range(beam_profile.shape[1]):
+        for profile_z in range(z_num):
+            domain_x = x_num // 2 - beam_profile.shape[0] // 2 + profile_x
+            domain_y = y_num // 2 - beam_profile.shape[1] // 2 + profile_y
+            if (0 <= domain_x < x_num) and (0 <= domain_y < y_num):
+                if beam_profile_3d[profile_x, profile_y, profile_z] > 0.001:
+                    beam_interactions[profile_z, domain_y, domain_x] += beam_profile_3d[profile_x, profile_y, profile_z]
+
+                    cell_center = np.array([domain_x, domain_y, profile_z]) * dx
+                    v_1 = detector_vertices[0] - cell_center
+                    v_2 = detector_vertices[1] - cell_center
+                    v_3 = detector_vertices[2] - cell_center
+
+                    omega = np.linalg.norm(np.cross(v_1, v_2)) + \
+                            np.linalg.norm(np.cross(v_2, v_3)) + \
+                            np.linalg.norm(np.cross(v_3, v_1))
+
+                    norm_surface_normal = n_s[profile_z, domain_y, domain_x]
+                    if norm_surface_normal.shape[0] == 5:
+                        norm_surface_normal = norm_surface_normal[0]
+
+                    alpha = np.arccos(np.dot(norm_surface_normal, beam_dir) /
+                                      (np.linalg.norm(norm_surface_normal) * np.linalg.norm(beam_dir)))
+                    p_reflected = compute_reflection_vector(beam_dir, norm_surface_normal)
+                    beta = np.arccos(np.dot(p_reflected, norm_surface_normal) /
+                                     (np.linalg.norm(p_reflected) * np.linalg.norm(norm_surface_normal)))
+
+                    g_value = scattering_function(theta, alpha, beta, atomic_number)
+                    N1_d = beam_interactions[profile_z, domain_y, domain_x] * g_value * omega
+                    scattered_electrons[:, domain_x] += N1_d
+
+
+# Ray tracing function
+def ray_tracing(build_surface, detectors, alpha, beta):
+    electrons_per_detector = np.zeros_like(detectors)
+    for i in range(build_surface.shape[0]):
+        for j in range(build_surface.shape[1]):
+            scattering_value = scattering_function(theta, alpha, beta, atomic_number)
+            bse_value = bse_coefficient(theta, atomic_number)
+            correction_value = scattering_function_correction(theta)
+            interaction = scattering_value * bse_value * correction_value * np.random.random()
+            for d in range(detectors.shape[0]):
+                electrons_per_detector[d, i, j] += interaction
+    return electrons_per_detector, scattering_value, bse_value, correction_value
+
+
+# Define parameters based on the paper
+alpha = 0.5  # Example value for alpha (related to Lambert reflection)
+beta = 0.5  # Example value for beta (related to the scattering lobe)
+
+# Initialize build surface and detectors
+build_surface = np.zeros((num_scan_lines, int(build_surface_width / pixel_size)))
+detectors = np.zeros((4, num_scan_lines, int(build_surface_width / pixel_size)))
+
+# Perform ray tracing
+electrons_per_detector, scattering_value, bse_value, correction_value = ray_tracing(build_surface, detectors, alpha,
+                                                                                    beta)
+
+# Convert detected electrons to voltage
+voltage_per_detector = electrons_per_detector * e * transimpedance
+
+# Log the results
+print(f"Scattering Value: {scattering_value}")
+print(f"BSE Coefficient: {bse_value}")
+print(f"Correction Value: {correction_value}")
+print(f"Electrons per detector:\n{electrons_per_detector}")
+print(f"Voltage per detector:\n{voltage_per_detector}")
diff --git a/src/ray_tracer/raytracer.py b/src/ray_tracer/raytracer.py
new file mode 100644
index 0000000000000000000000000000000000000000..014b2832f098808b3b8c939cd30b784de8be7f7e
--- /dev/null
+++ b/src/ray_tracer/raytracer.py
@@ -0,0 +1,575 @@
+import numpy as np
+import sympy as sp
+from typing import Tuple, List
+from src.materials.alloys.Alloy import Alloy
+from src.materials.alloys import Ti6Al4V
+# from src.materials.alloys.SS316L import SS316L
+
+T = sp.Symbol('T')
+
+# Constants
+pi: float = np.pi  # Mathematical constant π
+e: float = 1.60217662e-19  # Elementary charge in Coulombs
+beam_current: float = 3e-3  # Beam current in Amperes
+dwell_time: float = 0.4e-6  # Dwell time in seconds
+# A: float = 21.5  # Approximation for Ti6Al4V
+Ti6Al4V = Ti6Al4V.create_Ti6Al4V(T)
+A: float = Ti6Al4V.atomic_number
+print('Ti6Al4V.elements: ', Ti6Al4V.elements)
+print('Ti6Al4V.composition: ', Ti6Al4V.composition)
+# SS316L = SS316L.create_SS316L(T)
+# A: float = SS316L.atomic_number
+# print('SS316L.elements: ', SS316L.elements)
+# print('SS316L.composition: ', SS316L.composition)
+print('A: ', A)
+dx: float = 10e-6  # Cell size in meters
+dy: float = 10e-6  # Cell size in meters
+dz: float = 5e-6  # Cell size in meters
+sigma: float = 300e-6 / (2 * dx)  # Beam profile sigma
+threshold: float = 0.0001  # Beam profile threshold
+# The voltage at detector d can be computed using the transimpedance T (in units of V /A ) of a virtual amplifier in the context of the model
+transimpedance: float = 1e5  # Transimpedance in Volts per Ampere
+eV_threshold = 50  # Threshold energy in eV
+threshold_energy = eV_threshold * e  # Threshold energy in joules
+print('threshold_energy: ', threshold_energy)
+
+# Domain setup (Section 2.1.1)
+# Define the 3D domain for the simulation
+x_min, x_max = -500e-6, 500e-6
+y_min, y_max = -250e-6, 250e-6
+z_min, z_max = 0, 20e-6
+x_num = int((x_max - x_min) / dx) + 1
+y_num = int((y_max - y_min) / dy) + 1
+z_num = int((z_max - z_min) / dz) + 1
+domain = np.zeros((z_num, y_num, x_num))  # (z, y , x)
+print('domain shape: ', np.shape(domain))
+domain[0:2, :, :] = 1  # Solid cells
+domain[2, :, :] = 0.5  # Interface layer
+
+
+def compute_surface_normals(_domain: np.ndarray) -> Tuple[np.ndarray, Tuple[np.ndarray, np.ndarray, np.ndarray], int]:
+    """
+    Compute surface normals from the domain.
+
+    Args:
+        _domain (ndarray): The 3D domain array.
+
+    Returns:
+        tuple: A tuple containing the surface normals array and the gradients (grad_x, grad_y, grad_z).
+    """
+    _grad_x = np.gradient(_domain, axis=2)  # x-direction
+    _grad_y = np.gradient(_domain, axis=1)  # y-direction
+    _grad_z = np.gradient(_domain, axis=0)  # z-direction
+    _n_S = np.stack((_grad_x, _grad_y, _grad_z), axis=-1)  # (z, y, x, 3)
+    norm = np.linalg.norm(_n_S, axis=-1)  # (z, y, x)
+    interface_mask = (_domain > 0) & (_domain < 1)
+    nonzero_mask = norm > 0
+    valid_mask = interface_mask & nonzero_mask
+
+    _n_S[valid_mask] /= norm[valid_mask][..., np.newaxis]
+    _n_S[valid_mask] *= -1  # Invert the surface normals to point towards the detectors
+
+    if not np.allclose(np.linalg.norm(_n_S[valid_mask], axis=-1), 1.0, atol=1e-8):
+        raise ValueError("n_S is not normalized.")
+
+    _interface_layer = np.argmax(np.any(interface_mask, axis=(1, 2)))
+
+    return _n_S, (_grad_x, _grad_y, _grad_z), _interface_layer
+
+
+# Compute surface normals (Section 2.1.1)
+n_S, (grad_x, grad_y, grad_z), interface_layer = compute_surface_normals(domain)
+print('np.shape(n_S): ', np.shape(n_S))  # (z, y, x, 3)
+print('interface_layer: ', interface_layer)
+
+# Beam profile (Section 2.1)
+# Define the Gaussian beam profile
+x = np.arange(-30, 31)
+y = np.arange(-30, 31)
+X, Y = np.meshgrid(x, y)
+beam_profile = np.exp(-(X ** 2 + Y ** 2) / (2 * sigma ** 2))
+beam_profile /= np.sum(beam_profile)  # Normalize beam profile
+
+# Detector setup (Section 2.1.1)
+# Define positions of the detectors
+detector_height = 272e-3  # Detector height in meters
+detector_spacing = 127e-3  # Detector spacing in meters
+detector_diameter = 50e-3  # Detector diameter in meters
+detector_positions = np.array([
+    [detector_spacing, 0, detector_height],  # Right detector
+    [-detector_spacing, 0, detector_height],  # Left detector
+    [0, detector_spacing, detector_height],  # Back detector
+    [0, -detector_spacing, detector_height]  # Front detector
+])
+
+
+# Define detector vertices for solid angle calculation
+def get_detector_vertices(detector_position: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
+    """
+    Get the vertices of an equilateral triangle detector surface.
+
+    Parameters:
+        detector_position (array-like): The position of the center of the detector.
+
+    Returns:
+        tuple: The vertices of the detector surface.
+    """
+    print('detector_positions: ', detector_position)
+    print('detector_position type: ', type(detector_position))
+    # Calculate the side length of the detector
+    side_length = np.sqrt(3) * detector_diameter / 2
+    height = (np.sqrt(3) / 2) * side_length
+
+    _v1 = detector_position + np.array([0, 2 / 3 * height, 0])
+    _v2 = detector_position + np.array([-0.5 * side_length, -1 / 3 * height, 0])
+    _v3 = detector_position + np.array([0.5 * side_length, -1 / 3 * height, 0])
+
+    # print("Vertices:", v1, v2, v3)
+    # print("Type of vertices:", type(v1), type(v2), type(v3))
+
+    return _v1, _v2, _v3
+
+
+# Compute solid angle using Equation A1 (Appendix A)
+def compute_solid_angle(vector1: np.ndarray, vector2: np.ndarray, vector3: np.ndarray) -> float:
+    """
+    Compute the solid angle of a 3D triangle.
+
+    Args:
+        vector1: The first vector of the triangle.
+        vector2: The second vector of the triangle.
+        vector3: The third vector of the triangle.
+
+    Returns:
+        The solid angle of the triangle.
+    """
+    # Input validation
+    if not isinstance(vector1, np.ndarray) or not isinstance(vector2, np.ndarray) or not isinstance(vector3, np.ndarray):
+        raise ValueError("Inputs must be numpy arrays")
+    if vector1.shape != (3,) or vector2.shape != (3,) or vector3.shape != (3,):
+        raise ValueError("Inputs must be 3D vectors")
+    if not np.isfinite(vector1).all() or not np.isfinite(vector2).all() or not np.isfinite(vector3).all():
+        raise ValueError("Inputs must be finite")
+
+    cross_product = np.cross(vector2, vector3)
+    # Check if the cross product is nearly zero
+    if np.linalg.norm(cross_product) < 1e-8:
+        raise ValueError("Vectors are nearly coplanar")
+
+    # Compute solid angle
+    try:
+        # Compute the numerator of the solid angle formula
+        numerator: float = np.dot(vector1, cross_product)
+        # Check if the numerator is nearly zero
+        if np.abs(numerator) < 1e-8:
+            raise ValueError("Vectors are nearly parallel")
+
+        # Compute the denominator of the solid angle formula
+        denominator: float = (
+                np.linalg.norm(vector1) * np.linalg.norm(vector2) * np.linalg.norm(vector3)
+                + np.dot(vector1, vector2) * np.linalg.norm(vector3)
+                + np.dot(vector1, vector3) * np.linalg.norm(vector2)
+                + np.dot(vector2, vector3) * np.linalg.norm(vector1)
+        )
+
+        # Compute the solid angle in radians
+        solid_angle = 2 * np.arctan2(numerator, denominator)
+        # print("Solid angle:", solid_angle)
+        return solid_angle
+    except ZeroDivisionError:
+        raise ValueError("Error computing solid angle: division by zero")
+    except np.linalg.LinAlgError:
+        raise ValueError("Error computing solid angle: linear algebra error")
+
+
+# BSE coefficient functions (Equations 11 and 10, Section 2.1.2)
+def eta_0(_A: float) -> float:
+    """Equation 11 from the paper"""
+    _eta_0: float = -0.0254 + 0.016 * _A - 1.86e-4 * _A ** 2 + 8.3e-7 * _A ** 3
+    # print('eta_0: ', _eta_0)
+    return _eta_0
+
+
+# BSE Coefficient for single atomic targets, found to hold in the range of 10–100 keV
+def eta(theta: float, _A: float) -> float:
+    """Equation 10 from the paper"""
+    B: float = 0.89
+    _eta: float = B * (eta_0(_A) / B) ** np.cos(theta)
+    # print('eta: ', _eta)
+    return _eta
+
+
+# Compute the weighted average BSE coefficient for the alloy
+def compute_weighted_eta(theta: float, alloy: Alloy) -> float:
+    """Equation 12 from the paper"""
+    weighted_eta: float = 0
+    for element, weight_fraction in zip(alloy.elements, alloy.composition):
+        # print('element: ', element)
+        # print('weight_fraction: ', weight_fraction)
+        _A: float = element.atomic_number
+        # print('A: ', _A)
+        eta_i: float = eta(theta, _A)
+        # print('eta_i: ', eta_i)
+        weighted_eta += weight_fraction * eta_i
+        # print('weighted_eta: ', weighted_eta)
+    return weighted_eta
+
+
+# Correction function C(theta) (Equation 14, Section 2.1.3)
+def C(theta: float) -> float:
+    """Equation 14 from the paper"""
+    # theta = np.rad2deg(theta) # Ensure angle is in degrees for C function (?)
+    _C: float = 1.01 - 4.06 * theta + 1.36e-5 * theta ** 2 - 1.71e-6 * theta ** 3
+    # print('C: ', _C)
+    # Check if C is negative and raise an exception
+    if _C < 0:
+        raise ValueError(f"C function returned a negative value for theta: {theta}. "
+                         f"Please check the input and the C function implementation.")
+    return _C
+
+
+# Scattering function g_BSE (Equation 13, Section 2.1.3)
+def g_BSE(_theta: float, _alpha: float, _beta: float) -> float:
+    """Equation 13 from the paper"""
+    # k: float = np.rad2deg(_theta) / 13  # Correct implementation as per the paper
+    diffusive_part: float = (eta_0(A) / pi) * np.cos(_alpha)
+    # reflective_part: float = ((eta(_theta, A) - eta_0(A)) / C(_theta)) * ((k + 1) / (2 * pi)) * (np.cos(_beta) ** k)
+
+    # Compute a weighted average BSE coefficient for the alloy
+    # reflective_part: float = ((compute_weighted_eta(_theta, Ti6Al4V) - eta_0(A)) / C(_theta)) * ((k + 1) / (2 * pi)) * (np.cos(_beta) ** k)
+
+    # print('g_BSE: ', diffusive_part + reflective_part)
+    return diffusive_part #+ reflective_part
+
+
+# Beam direction
+phi: float = 0.0  # Angle in degrees (for phi > ~14.25°, C(theta) becomes negative)
+phi_rad: float = np.deg2rad(phi)  # Convert to radians
+p_E = np.array([np.sin(phi_rad), 0, -np.cos(phi_rad)])  # Beam direction vector
+p_E /= np.linalg.norm(p_E)
+
+# Initialize total electrons counter and electrons per detector array
+total_electrons: int = 0
+electrons_per_detector = np.zeros(len(detector_positions))
+beam_loc = (x_num // 2, y_num // 2)  # (x, y)
+print('beam_loc: ', beam_loc)
+
+# Iterate over the beam profile
+for profile_x in range(beam_profile.shape[0]):
+    for profile_y in range(beam_profile.shape[1]):
+        # print(f"beam_profile.shape ({profile_x}, {profile_y}):")
+        domain_x = beam_loc[0] - beam_profile.shape[0] // 2 + profile_x
+        domain_y = beam_loc[1] - beam_profile.shape[1] // 2 + profile_y
+        # print(f"Domain ({domain_x}, {domain_y}):")
+        if (0 <= domain_x < x_num) and (0 <= domain_y < y_num):
+            if beam_profile[profile_x, profile_y] > threshold:
+                n_S_local = n_S[interface_layer, domain_y, domain_x]  # Interface layer
+
+                # Debugging print statements
+                print(f"grad_x: {grad_x[2, domain_y, domain_x]}")
+                print(f"grad_y: {grad_y[2, domain_y, domain_x]}")
+                print(f"grad_z: {grad_z[2, domain_y, domain_x]}")
+                print(f"n_S_local: {n_S_local}")
+
+                if not np.allclose(np.linalg.norm(n_S_local), 1.0):
+                    raise ValueError("n_S_local is not normalized.")
+
+                if np.allclose(n_S_local, np.array([0, 0, 1])) or np.allclose(n_S_local, np.array([0, 0, -1])):
+                    print('hellooo')
+                    theta_local: float = phi_rad
+                else:
+                    theta_local: float = np.arccos(
+                        np.clip(np.dot(p_E, n_S_local) / (np.linalg.norm(p_E) * np.linalg.norm(n_S_local)),
+                                -1.0, 1.0)
+                    )
+
+                for det_idx, det_pos in enumerate(detector_positions):
+                    det_idx: int
+                    det_pos: np.ndarray[int]
+
+                    det_vec = det_pos - np.array([domain_x * dx, domain_y * dx, 0])
+                    det_vec /= np.linalg.norm(det_vec)
+
+                    # Debugging print statements
+                    print(f"Detector Position: {det_pos}")
+                    print(f"Detector Position Type: {type(det_pos)}")
+                    print(f"Domain Point: {np.array([domain_x * dx, domain_y * dx, 0])}")
+                    print(f"Detector Vector: {det_vec}")
+                    print(f"Dot product: {np.dot(det_vec, n_S_local)}")
+
+                    alpha = np.arccos(np.clip(np.dot(det_vec, n_S_local), -1.0, 1.0))
+
+                    p_E_reflected = p_E - 2 * np.dot(p_E, n_S_local) * n_S_local
+                    p_E_reflected /= np.linalg.norm(p_E_reflected)
+
+                    beta = np.arccos(np.clip(np.dot(p_E_reflected, det_vec), -1.0, 1.0))
+
+                    # Apply energy threshold
+                    # electron_energy = N_0 * e * g_bse_value
+                    # print('electron_energy: ', electron_energy)
+
+                    # Calculate the initial energy of the primary electrons (E_0)
+                    V_acc = 60e3  # Accelerating voltage in Volts
+                    E_0 = V_acc * e  # Initial energy of primary electrons in joules
+
+                    # Calculate the energy of the backscatter electrons
+                    # E_BSE = E_0 * eta(theta_local, A)
+                    # Compute a weighted average BSE coefficient for the alloy
+                    E_BSE = E_0 * compute_weighted_eta(theta_local, Ti6Al4V)
+                    print('E_BSE: ', E_BSE)
+
+                    # Bifurcation logic for PE and SE
+                    if E_BSE > threshold_energy:
+                        # Backscatter electrons (BSE)
+                        # Compute scattering function g_bse_value
+                        g_bse_value = g_BSE(theta_local, alpha, beta)
+
+                        # Ensure g_bse_value is non-negative
+                        if g_bse_value < 0:
+                            raise ValueError("g_bse_value is negative, check calculations.")
+
+                        # Compute solid angle (Equation A1)
+                        v1, v2, v3 = get_detector_vertices(det_pos)
+                        print("Vertices:", v1, v2, v3)
+                        print("Type of vertices:", type(v1), type(v2), type(v3))
+                        dOmega = compute_solid_angle(v1, v2, v3)
+
+                        # Compute number of electrons collected by the detector (Equation A2)
+                        # The beam ray stays on each grid point ij for the dwell time t_d and distributes N_0 PE into it
+                        N_0 = beam_current * dwell_time / e  # Equation 3, Section 2.1.1
+
+                        # There are N1 electrons in the chamber after the 1st scattering event
+                        # N_1 = N_0 * eta(theta_local, A)  # Equation 4, Section 2.1.1
+                        # Compute a weighted average BSE coefficient for the alloy
+                        N_1 = N_0 * compute_weighted_eta(theta_local, Ti6Al4V)  # Equation 4, Section 2.1.1
+
+                        # The number of electrons N_1_d collected by a general detector d
+                        N_1_d = N_1 * np.sum(g_bse_value * dOmega)  # Equation 5, Section 2.1.1
+
+                        # Accumulate electrons per detector
+                        electrons_per_detector[det_idx] += N_1_d
+
+                        # Add to total electrons
+                        total_electrons += N_1_d
+
+                        # Debugging print statements
+                        print(f"Domain ({domain_x}, {domain_y}):")
+                        print(f"n_S_local: {n_S_local}")
+                        print(f"theta_local: {np.rad2deg(theta_local)}, "
+                              f"alpha: {np.rad2deg(alpha)}, "
+                              f"beta: {np.rad2deg(beta)}")
+                        print(f"det_vec: {det_vec}")
+                        print(f"p_E_reflected: {p_E_reflected}")
+                        print(f"g_bse_value: {g_bse_value}, N_0: {N_0}, N_1: {N_1}, N_1_d: {N_1_d}")
+                        print(f"total_electrons: {total_electrons}")
+                    else:
+                        # Secondary electrons (SE)
+                        # Note: This part is currently neglected in the model
+                        # but can be implemented if necessary
+                        continue  # Skip to the next iteration if energy is below threshold
+
+# Initialize build surface and detectors (Section 2.2)
+num_scan_lines = 1500  # y (rows)
+build_surface_width = 80e-3  # x (columns)
+pixel_size = 53.3e-6
+build_surface = np.zeros((num_scan_lines, int(build_surface_width / pixel_size)))  # (y, x) -> (rows, columns)
+print('build_surface: ', np.shape(build_surface))  # (1500, 1500)
+detectors = np.zeros((4, num_scan_lines, int(build_surface_width / pixel_size)))  # (4, y, x)
+
+
+# Ray tracing function
+def ray_tracing(_build_surface, _detectors, _interface_layer):
+    _electrons_per_detector_ray_tracing = np.zeros_like(_detectors)
+    # Iterate over the scan lines and simulate ray interactions
+    for _j in range(_build_surface.shape[0]):  # j -> y
+        for _i in range(_build_surface.shape[1]):  # i -> x
+            # Calculate the ray direction using detector vectors
+            for _det_idx, _det_pos in enumerate(detector_positions):
+                if _j < n_S.shape[1] and _i < n_S.shape[2]:  # Ensure indices are within bounds
+                    _n_S_local = n_S[_interface_layer, _j, _i, :]  # Ensure correct surface normal is used
+
+                    if not np.allclose(np.linalg.norm(_n_S_local), 1.0):
+                        raise ValueError("n_S_local is not normalized.")
+
+                    if np.allclose(_n_S_local, np.array([-0, -0, 1])):
+                        _theta_local = phi_rad
+                    else:
+                        _theta_local = np.arccos(
+                            np.clip(np.dot(_n_S_local, p_E) / (np.linalg.norm(_n_S_local) * np.linalg.norm(p_E)),
+                                    -1.0, 1.0))
+
+                    _det_vec = _det_pos - np.array([_i * pixel_size, _j * pixel_size, 0])
+                    _det_vec /= np.linalg.norm(_det_vec)
+
+                    _alpha = np.arccos(np.clip(np.dot(_det_vec, _n_S_local), -1.0, 1.0))
+
+                    _p_E_reflected = p_E - 2 * np.dot(p_E, _n_S_local) * _n_S_local
+                    _p_E_reflected /= np.linalg.norm(_p_E_reflected)
+
+                    _beta = np.arccos(np.clip(np.dot(_p_E_reflected, _det_vec), -1.0, 1.0))
+
+                    # Compute scattering function
+                    scattering_value = g_BSE(_theta_local, _alpha, _beta)
+                    # Apply the BSE coefficient
+                    # bse_value = eta(_theta_local, A)
+                    # Compute a weighted average BSE coefficient for the alloy
+                    bse_value = compute_weighted_eta(_theta_local, Ti6Al4V)
+                    # Apply scattering function correction
+                    correction_value = C(_theta_local)
+                    interaction = scattering_value * bse_value * correction_value
+                    _electrons_per_detector_ray_tracing[_det_idx, _j, _i] += interaction
+
+                    # print(electrons_per_detector)
+                    # print(electrons_per_detector.shape)
+
+                    # Debugging print statements
+                    print(f"Ray Tracing - Scan line ({_i}, {_j}):")
+                    print(f"n_S_local: {_n_S_local}")
+                    print(f"theta_local: {np.rad2deg(_theta_local)}, "
+                          f"alpha: {np.rad2deg(_alpha)}, "
+                          f"beta: {np.rad2deg(_beta)}")
+                    print(f"det_vec: {_det_vec}")
+                    print(f"p_E_reflected: {_p_E_reflected}")
+                    print(f"scattering_value: {scattering_value}, "
+                          f"bse_value: {bse_value}, "
+                          f"correction_value: {correction_value}")
+                    print(f"interaction: {interaction}")
+
+    return _electrons_per_detector_ray_tracing, np.sum(_electrons_per_detector_ray_tracing)
+
+
+# Perform ray tracing
+electrons_per_detector_ray_tracing, total_electrons_ray_tracing = ray_tracing(build_surface, detectors, interface_layer)
+
+# Convert detected electrons to voltage using the transimpedance amplifier
+voltage_per_detector = electrons_per_detector * e * transimpedance
+
+# Print results
+print(f"Electrons per detector (ray_tracing):\n {electrons_per_detector_ray_tracing}")
+for i, electrons in enumerate(electrons_per_detector_ray_tracing):
+    print(f"Electrons hitting detector {i + 1} (ray_tracing): {np.sum(electrons)}")
+print(f"Total electrons hitting all detectors (ray_tracing): {total_electrons_ray_tracing}")
+# print(f"Voltage per detector (ray tracing):\n {voltage_per_detector}")
+print(f"Electrons per detector: {electrons_per_detector}")
+for i, electrons in enumerate(electrons_per_detector):
+    print(f"Electrons hitting detector {i + 1}: {np.sum(electrons)}")
+print(f"Total electrons hitting all detectors: {total_electrons}")
+
+
+# Test functions
+def test_eta_0() -> None:
+    assert np.isclose(eta_0(21.5), 0.24087)
+
+
+def test_eta() -> None:
+    theta: float = np.deg2rad(10)
+    assert np.isclose(eta(theta, A), 0.24570)
+
+
+def test_C() -> None:
+    assert np.isclose(C(0), 1.01)
+    theta: float = np.deg2rad(10)
+    assert np.isclose(C(theta), 0.30139672887864666)
+
+
+def test_g_BSE() -> None:
+    _theta: float = np.deg2rad(10)
+    _alpha: float = np.deg2rad(25)
+    _beta: float = np.deg2rad(25)
+    assert np.isclose(g_BSE(_theta, _alpha, _beta), 0.07367186921200412)
+
+
+def test_compute_solid_angle() -> None:
+    _v1: np.ndarray = np.array([1, 0, 0])
+    _v2: np.ndarray = np.array([0, 1, 0])
+    _v3: np.ndarray = np.array([0, 0, 1])
+    solid_angle: float = compute_solid_angle(_v1, _v2, _v3)
+    assert np.isclose(solid_angle, np.pi / 2)
+
+
+def test_incident_angles() -> None:
+    incident_angles: List[float] = [0, 2, 4, 6, 8, 10]  # in degrees
+    for _theta in incident_angles:
+        _theta_rad: float = np.deg2rad(_theta)
+        _p_E: np.ndarray = np.array([np.sin(_theta_rad), 0, -np.cos(_theta_rad)])
+        _p_E /= np.linalg.norm(_p_E)
+
+        _eta_val: float = eta(_theta_rad, A)
+        _C_val: float = C(_theta_rad)
+        # print('C_val: ', C_val)
+
+        for _alpha in np.linspace(0, np.pi / 4, 5):
+            for _beta in np.linspace(0, np.pi / 4, 5):
+                _g_BSE_val: float = g_BSE(_theta_rad, _alpha, _beta)
+                assert 0 < _eta_val < 1
+                assert 0 < _C_val <= 1.01
+                assert _g_BSE_val >= 0
+
+
+def test_vector_calculations() -> None:
+    # Test reflection
+    _p_E = np.array([0, 0, -1])
+    _n_S = np.array([0, 1, 0])
+    dot_product = np.dot(_p_E, _n_S)
+    reflection_vector = _p_E - 2 * dot_product * _n_S
+    _p_E_reflected = reflection_vector / np.linalg.norm(reflection_vector)
+
+    # Check if the reflected vector is orthogonal to the surface normal
+    assert np.isclose(np.dot(_p_E_reflected, _n_S), 0, atol=1e-8)
+    # Check if the magnitude of the reflected vector is correct
+    assert np.isclose(np.linalg.norm(_p_E_reflected), 1, atol=1e-8)
+
+    # Test detector vectors
+    detector_pos = np.array([1, 1, 1], dtype=np.float64)
+    domain_point = np.array([0, 0, 0], dtype=np.float64)
+    detector_vector = detector_pos - domain_point
+    detector_vector = detector_vector.astype(np.float64)  # Ensure floating-point type
+    detector_vector /= np.linalg.norm(detector_vector)
+    assert np.allclose(detector_vector, [0.57735027, 0.57735027, 0.57735027])
+
+    # Test angles between vectors
+    vec1 = np.array([1, 0, 0], dtype=np.float64)
+    vec2 = np.array([1, 1, 0], dtype=np.float64)  # Ensure floating-point type
+    vec2 /= np.linalg.norm(vec2)
+    angle = np.arccos(np.dot(vec1, vec2))
+    assert np.isclose(np.rad2deg(angle), 45)
+
+
+def test_compute_surface_normals() -> None:
+    _domain = np.zeros((5, 10, 10))
+    _domain[0:2, :, :] = 1
+    _domain[2, :, :] = 0.5
+    _n_S, _, _ = compute_surface_normals(_domain)
+
+    # Check normalization for valid mask
+    '''valid_mask = (domain > 0) & (domain < 1)
+    nonzero_mask = np.linalg.norm(n_S, axis=-1) > 0
+    valid_mask = valid_mask & nonzero_mask'''
+
+    # Check if the surface normals are normalized
+    for _i in range(_n_S.shape[1]):
+        for _j in range(_n_S.shape[2]):
+            # if valid_mask[2, _i, _j]:
+            assert np.isclose(np.linalg.norm(_n_S[2, _i, _j]), 1.0, atol=1e-8)
+
+            # Check if the normal is close to [0, 0, 1] or [0, 0, -1]
+            assert (np.allclose(_n_S[2, _i, _j], [0, 0, 1], atol=1e-8) or
+                    np.allclose(_n_S[2, _i, _j], [0, 0, -1], atol=1e-8))
+
+
+def main() -> None:
+    test_eta_0()
+    test_eta()
+    test_C()
+    test_g_BSE()
+    test_compute_solid_angle()
+    test_incident_angles()
+    test_vector_calculations()
+    test_compute_surface_normals()
+    print("All tests passed!")
+
+
+if __name__ == "__main__":
+    weighted_eta_value = compute_weighted_eta(np.deg2rad(10), Ti6Al4V)
+    print("Weighted eta value:", weighted_eta_value)
+    # main()
diff --git a/src/ray_tracer/raytracerMM.py b/src/ray_tracer/raytracerMM.py
new file mode 100644
index 0000000000000000000000000000000000000000..eced7951d93ffb0b0a2a297e76eb12349fe17570
--- /dev/null
+++ b/src/ray_tracer/raytracerMM.py
@@ -0,0 +1,389 @@
+import numpy as np
+import sympy as sp
+from typing import Tuple
+from src.materials.alloys.Alloy import Alloy
+from src.materials.alloys import Ti6Al4V
+# from src.materials.alloys.SS316L import SS316L
+
+T = sp.Symbol('T')
+
+# Constants
+pi: float = np.pi  # Mathematical constant π
+e: float = 1.60217662e-19  # Elementary charge in Coulombs
+beam_current: float = 3e-3  # Beam current in Amperes
+dwell_time: float = 0.4e-6  # Dwell time in seconds
+# A: float = 21.5  # Approximation for Ti6Al4V
+Ti6Al4V = Ti6Al4V.create_Ti6Al4V(T)
+A: float = Ti6Al4V.atomic_number
+print('Ti6Al4V.elements: ', Ti6Al4V.elements)
+print('Ti6Al4V.composition: ', Ti6Al4V.composition)
+# SS316L = SS316L.create_SS316L(T)
+# A: float = SS316L.atomic_number
+# print('SS316L.elements: ', SS316L.elements)
+# print('SS316L.composition: ', SS316L.composition)
+print('A: ', A)
+dx: float = 10e-6  # Cell size in meters
+dy: float = 10e-6  # Cell size in meters
+dz: float = 5e-6  # Cell size in meters
+sigma: float = 300e-6 / (2 * dx)  # Beam profile sigma
+threshold: float = 0.0001  # Beam profile threshold
+# The voltage at detector d can be computed using the transimpedance T (in units of V /A ) of a virtual amplifier in the context of the model
+transimpedance: float = 1e5  # Transimpedance in Volts per Ampere
+eV_threshold = 50  # Threshold energy in eV
+threshold_energy = eV_threshold * e  # Threshold energy in joules
+print('threshold_energy: ', threshold_energy)
+
+# Domain setup (Section 2.1.1)
+# Define the 3D domain for the simulation
+x_min, x_max = -250e-6, 250e-6
+y_min, y_max = -250e-6, 250e-6
+z_min, z_max = 0, 20e-6
+x_num = int((x_max - x_min) / dx) + 1
+y_num = int((y_max - y_min) / dy) + 1
+z_num = int((z_max - z_min) / dz) + 1
+domain = np.zeros((z_num, y_num, x_num))  # (z, y , x)
+print('domain shape: ', np.shape(domain))
+domain[0:2, :, :] = 1  # Solid cells
+domain[2, :, :] = 0.5  # Interface layer
+
+
+def compute_surface_normals(_domain: np.ndarray) -> Tuple[np.ndarray, Tuple[np.ndarray, np.ndarray, np.ndarray], int]:
+    """
+    Compute surface normals from the domain.
+
+    Args:
+        _domain (ndarray): The 3D domain array.
+
+    Returns:
+        tuple: A tuple containing the surface normals array and the gradients (grad_x, grad_y, grad_z).
+    """
+    _grad_x = np.gradient(_domain, axis=2)  # x-direction
+    _grad_y = np.gradient(_domain, axis=1)  # y-direction
+    _grad_z = np.gradient(_domain, axis=0)  # z-direction
+    _n_S = np.stack((_grad_x, _grad_y, _grad_z), axis=-1)  # (z, y, x, 3)
+    norm = np.linalg.norm(_n_S, axis=-1)  # (z, y, x)
+    interface_mask = (_domain > 0) & (_domain < 1)
+    nonzero_mask = norm > 0
+    valid_mask = interface_mask & nonzero_mask
+
+    _n_S[valid_mask] /= norm[valid_mask][..., np.newaxis]
+    _n_S[valid_mask] *= -1  # Invert the surface normals to point towards the detectors
+
+    if not np.allclose(np.linalg.norm(_n_S[valid_mask], axis=-1), 1.0, atol=1e-8):
+        raise ValueError("n_S is not normalized.")
+
+    _interface_layer = np.argmax(np.any(interface_mask, axis=(1, 2)))
+
+    return _n_S, (_grad_x, _grad_y, _grad_z), _interface_layer
+
+
+# Compute surface normals (Section 2.1.1)
+n_S, (grad_x, grad_y, grad_z), interface_layer = compute_surface_normals(domain)
+print('np.shape(n_S): ', np.shape(n_S))  # (z, y, x, 3)
+print('interface_layer: ', interface_layer)
+
+# Beam profile (Section 2.1)
+# Define the Gaussian beam profile
+x = np.arange(-30, 31)
+y = np.arange(-30, 31)
+X, Y = np.meshgrid(x, y)
+beam_profile = np.exp(-(X ** 2 + Y ** 2) / (2 * sigma ** 2))
+beam_profile /= np.sum(beam_profile)  # Normalize beam profile
+
+# Detector setup (Section 2.1.1)
+# Define positions of the detectors
+detector_height = 272e-3  # Detector height in meters
+detector_spacing = 127e-3  # Detector spacing in meters
+detector_diameter = 50e-3  # Detector diameter in meters
+detector_positions = np.array([
+    [detector_spacing, 0, detector_height],  # Right detector
+    [-detector_spacing, 0, detector_height],  # Left detector
+    [0, detector_spacing, detector_height],  # Back detector
+    [0, -detector_spacing, detector_height]  # Front detector
+])
+
+
+# Define detector vertices for solid angle calculation
+def get_detector_vertices(detector_position: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
+    """
+    Get the vertices of an equilateral triangle detector surface oriented symmetrically.
+
+    Parameters:
+    detector_position (array-like): The position of the center of the detector.
+
+    Returns:
+    tuple: The vertices of the detector surface.
+    """
+    # Calculate the side length of the detector
+    side_length = np.sqrt(3) * detector_diameter / 2
+    height = (np.sqrt(3) / 2) * side_length
+
+    # Determine the orientation based on the detector position
+    if detector_position[0] > 0:  # Right detector
+        _v1 = detector_position + np.array([2/3*height, 0, 0])
+        _v2 = detector_position + np.array([-height/3, side_length/2, 0])
+        _v3 = detector_position + np.array([-height/3, -side_length/2, 0])
+    elif detector_position[0] < 0:  # Left detector
+        _v1 = detector_position + np.array([-2/3*height, 0, 0])
+        _v2 = detector_position + np.array([height/3, -side_length/2, 0])
+        _v3 = detector_position + np.array([height/3, side_length/2, 0])
+    elif detector_position[1] > 0:  # Back detector
+        _v1 = detector_position + np.array([0, 2/3*height, 0])
+        _v2 = detector_position + np.array([-side_length/2, -height/3, 0])
+        _v3 = detector_position + np.array([side_length/2, -height/3, 0])
+    else:  # Front detector
+        _v1 = detector_position + np.array([0, -2/3*height, 0])
+        _v2 = detector_position + np.array([side_length/2, height/3, 0])
+        _v3 = detector_position + np.array([-side_length/2, height/3, 0])
+
+    return _v1, _v2, _v3
+
+
+# Compute solid angle using Equation A1 (Appendix A)
+def compute_solid_angle(vector1: np.ndarray, vector2: np.ndarray, vector3: np.ndarray) -> float:
+    """
+    Compute the solid angle of a 3D triangle.
+
+    Args:
+        vector1: The first vector of the triangle.
+        vector2: The second vector of the triangle.
+        vector3: The third vector of the triangle.
+
+    Returns:
+        The solid angle of the triangle.
+    """
+    # Input validation
+    if not isinstance(vector1, np.ndarray) or not isinstance(vector2, np.ndarray) or not isinstance(vector3, np.ndarray):
+        raise ValueError("Inputs must be numpy arrays")
+    if vector1.shape != (3,) or vector2.shape != (3,) or vector3.shape != (3,):
+        raise ValueError("Inputs must be 3D vectors")
+    if not np.isfinite(vector1).all() or not np.isfinite(vector2).all() or not np.isfinite(vector3).all():
+        raise ValueError("Inputs must be finite")
+
+    cross_product = np.cross(vector2, vector3)
+    # Check if the cross product is nearly zero
+    if np.linalg.norm(cross_product) < 1e-8:
+        raise ValueError("Vectors are nearly coplanar")
+
+    # Compute solid angle
+    try:
+        # Compute the numerator of the solid angle formula
+        numerator: float = np.dot(vector1, cross_product)
+        # Check if the numerator is nearly zero
+        if np.abs(numerator) < 1e-8:
+            raise ValueError("Vectors are nearly parallel")
+
+        # Compute the denominator of the solid angle formula
+        denominator: float = (
+                np.linalg.norm(vector1) * np.linalg.norm(vector2) * np.linalg.norm(vector3)
+                + np.dot(vector1, vector2) * np.linalg.norm(vector3)
+                + np.dot(vector1, vector3) * np.linalg.norm(vector2)
+                + np.dot(vector2, vector3) * np.linalg.norm(vector1)
+        )
+
+        # Compute the solid angle in radians
+        solid_angle = 2 * np.arctan2(numerator, denominator)
+        # print("Solid angle:", solid_angle)
+        return solid_angle
+    except ZeroDivisionError:
+        raise ValueError("Error computing solid angle: division by zero")
+    except np.linalg.LinAlgError:
+        raise ValueError("Error computing solid angle: linear algebra error")
+
+
+# BSE coefficient functions (Equations 11 and 10, Section 2.1.2)
+def eta_0(_A: float) -> float:
+    """Equation 11 from the paper"""
+    _eta_0: float = -0.0254 + 0.016 * _A - 1.86e-4 * _A ** 2 + 8.3e-7 * _A ** 3
+    # print('eta_0: ', _eta_0)
+    return _eta_0
+
+
+# BSE Coefficient for single atomic targets, found to hold in the range of 10–100 keV
+def eta(theta: float, _A: float) -> float:
+    """Equation 10 from the paper"""
+    B: float = 0.89
+    _eta: float = B * (eta_0(_A) / B) ** np.cos(theta)
+    # print('eta: ', _eta)
+    return _eta
+
+
+# Compute the weighted average BSE coefficient for the alloy
+def compute_weighted_eta(theta: float, alloy: Alloy) -> float:
+    """Equation 12 from the paper"""
+    weighted_eta: float = 0
+    for element, weight_fraction in zip(alloy.elements, alloy.composition):
+        # print('element: ', element)
+        # print('weight_fraction: ', weight_fraction)
+        _A: float = element.atomic_number
+        # print('A: ', _A)
+        eta_i: float = eta(theta, _A)
+        # print('eta_i: ', eta_i)
+        weighted_eta += weight_fraction * eta_i
+        # print('weighted_eta: ', weighted_eta)
+    return weighted_eta
+
+
+# Correction function C(theta) (Equation 14, Section 2.1.3)
+def C(theta: float) -> float:
+    """Equation 14 from the paper"""
+    theta = np.rad2deg(theta) # Ensure angle is in degrees for C function
+    _C: float = 1.01 - 4.06 * theta + 1.36e-5 * theta ** 2 - 1.71e-6 * theta ** 3
+    # print('C: ', _C)
+    # Check if C is negative and raise an exception
+    if _C < 0:
+        raise ValueError(f"C function returned a negative value for theta: {theta}. "
+                         f"Please check the input and the C function implementation.")
+    return _C
+
+
+# Scattering function g_BSE (Equation 13, Section 2.1.3)
+def g_BSE(_theta: float, _alpha: float, _beta: float) -> float:
+    """Equation 13 from the paper"""
+    # k: float = np.rad2deg(_theta) / 13  # Correct implementation as per the paper
+    diffusive_part: float = (eta_0(A) / pi) * np.cos(_alpha)
+    # reflective_part: float = ((eta(_theta, A) - eta_0(A)) / C(_theta)) * ((k + 1) / (2 * pi)) * (np.cos(_beta) ** k)
+
+    # Compute a weighted average BSE coefficient for the alloy
+    # reflective_part: float = ((compute_weighted_eta(_theta, Ti6Al4V) - eta_0(A)) / C(_theta)) * ((k + 1) / (2 * pi)) * (np.cos(_beta) ** k)
+
+    # print('g_BSE: ', diffusive_part + reflective_part)
+    return diffusive_part #+ reflective_part
+
+
+# Beam direction
+phi: float = 0.0  # Angle in degrees (for phi > ~14.25°, C(theta) becomes negative)
+phi_rad: float = np.deg2rad(phi)  # Convert to radians
+p_E = np.array([np.sin(phi_rad), 0, -np.cos(phi_rad)])  # Beam direction vector
+p_E /= np.linalg.norm(p_E)
+
+# Initialize total electrons counter and electrons per detector array
+total_electrons: int = 0
+electrons_per_detector = np.zeros(len(detector_positions))
+print('x_num: ', x_num)
+print('y_num: ', y_num)
+beam_loc = ((x_num+1) // 2, (y_num+1) // 2)  # (x, y)
+print('beam_loc: ', beam_loc)
+
+# loop over beam_loc from x_num // 4 to (x_num*3)//4
+#    compute phi depending on beam origin and beam_loc
+
+# Iterate over the beam profile
+for profile_x in range(beam_profile.shape[0]):
+    for profile_y in range(beam_profile.shape[1]):
+        # print(f"beam_profile.shape ({profile_x}, {profile_y}):")
+        domain_x = beam_loc[0] - beam_profile.shape[0] // 2 + profile_x
+        domain_y = beam_loc[1] - beam_profile.shape[1] // 2 + profile_y
+        # print(f"Domain ({domain_x}, {domain_y}):")
+        if (0 <= domain_x < x_num) and (0 <= domain_y < y_num):
+            if beam_profile[profile_x, profile_y] > threshold:
+                n_S_local = n_S[interface_layer, domain_y, domain_x]  # Interface layer
+
+                # Debugging print statements
+                print(f"grad_x: {grad_x[2, domain_y, domain_x]}")
+                print(f"grad_y: {grad_y[2, domain_y, domain_x]}")
+                print(f"grad_z: {grad_z[2, domain_y, domain_x]}")
+                print(f"n_S_local: {n_S_local}")
+
+                if not np.allclose(np.linalg.norm(n_S_local), 1.0):
+                    raise ValueError("n_S_local is not normalized.")
+
+                if np.allclose(n_S_local, np.array([0, 0, 1])) or np.allclose(n_S_local, np.array([0, 0, -1])):
+                    print('hellooo')
+                    theta_local: float = phi_rad
+                else:
+                    theta_local: float = np.arccos(
+                        np.clip(np.dot(p_E, n_S_local) / (np.linalg.norm(p_E) * np.linalg.norm(n_S_local)),
+                                -1.0, 1.0)
+                    )
+
+                for det_idx, det_pos in enumerate(detector_positions):
+                    det_idx: int
+                    det_pos: np.ndarray[int]
+
+                    det_vec = det_pos - np.array([domain_x * dx, domain_y * dx, 0])
+                    det_vec /= np.linalg.norm(det_vec)
+
+                    # Debugging print statements
+                    print(f"Detector Position: {det_pos}")
+                    print(f"Detector Position Type: {type(det_pos)}")
+                    print(f"Domain Point: {np.array([domain_x * dx, domain_y * dx, 0])}")
+                    print(f"Detector Vector: {det_vec}")
+                    print(f"Dot product: {np.dot(det_vec, n_S_local)}")
+
+                    alpha = np.arccos(np.clip(np.dot(det_vec, n_S_local), -1.0, 1.0))
+
+                    p_E_reflected = p_E - 2 * np.dot(p_E, n_S_local) * n_S_local
+                    p_E_reflected /= np.linalg.norm(p_E_reflected)
+
+                    beta = np.arccos(np.clip(np.dot(p_E_reflected, det_vec), -1.0, 1.0))
+
+                    # Apply energy threshold
+                    # electron_energy = N_0 * e * g_bse_value
+                    # print('electron_energy: ', electron_energy)
+
+                    # Calculate the initial energy of the primary electrons (E_0)
+                    V_acc = 60e3  # Accelerating voltage in Volts
+                    E_0 = V_acc * e  # Initial energy of primary electrons in joules
+
+                    # Calculate the energy of the backscatter electrons
+                    # E_BSE = E_0 * eta(theta_local, A)
+                    # Compute a weighted average BSE coefficient for the alloy
+                    E_BSE = E_0 * compute_weighted_eta(theta_local, Ti6Al4V)
+                    print('E_BSE: ', E_BSE)
+
+                    # Bifurcation logic for PE and SE
+                    if E_BSE > threshold_energy:
+                        # Backscatter electrons (BSE)
+                        # Compute scattering function g_bse_value
+                        g_bse_value = g_BSE(theta_local, alpha, beta)
+
+                        # Ensure g_bse_value is non-negative
+                        if g_bse_value < 0:
+                            raise ValueError("g_bse_value is negative, check calculations.")
+
+                        # Compute solid angle (Equation A1)
+                        v1, v2, v3 = get_detector_vertices(det_pos)
+                        print("Vertices:", v1, v2, v3)
+                        # print("Type of vertices:", type(v1), type(v2), type(v3))
+                        dOmega = compute_solid_angle(v1, v2, v3)
+
+                        # Compute number of electrons collected by the detector (Equation A2)
+                        # The beam ray stays on each grid point ij for the dwell time t_d and distributes N_0 PE into it
+                        N_0 = beam_current * dwell_time / e  # Equation 3, Section 2.1.1
+
+                        # There are N1 electrons in the chamber after the 1st scattering event
+                        # N_1 = N_0 * eta(theta_local, A)  # Equation 4, Section 2.1.1
+                        # Compute a weighted average BSE coefficient for the alloy
+                        N_1 = N_0 * compute_weighted_eta(theta_local, Ti6Al4V)  # Equation 4, Section 2.1.1
+
+                        # The number of electrons N_1_d collected by a general detector d
+                        N_1_d = N_1 * np.sum(g_bse_value * dOmega)  # Equation 5, Section 2.1.1
+
+                        # Accumulate electrons per detector
+                        electrons_per_detector[det_idx] += N_1_d
+
+                        # Add to total electrons
+                        total_electrons += N_1_d
+
+                        # Debugging print statements
+                        print(f"Domain ({domain_x}, {domain_y}):")
+                        print(f"n_S_local: {n_S_local}")
+                        print(f"theta_local: {np.rad2deg(theta_local)}, "
+                              f"alpha: {np.rad2deg(alpha)}, "
+                              f"beta: {np.rad2deg(beta)}")
+                        print(f"det_vec: {det_vec}")
+                        print(f"p_E_reflected: {p_E_reflected}")
+                        print(f"g_bse_value: {g_bse_value}, N_0: {N_0}, N_1: {N_1}, N_1_d: {N_1_d}")
+                        print(f"total_electrons: {total_electrons}")
+                    else:
+                        # Secondary electrons (SE)
+                        # Note: This part is currently neglected in the model
+                        # but can be implemented if necessary
+                        continue  # Skip to the next iteration if energy is below threshold
+
+
+print(f"Electrons per detector: {electrons_per_detector}")
+for i, electrons in enumerate(electrons_per_detector):
+    print(f"Electrons hitting detector {i + 1}: {np.sum(electrons)}")
+print(f"Total electrons hitting all detectors: {total_electrons}")