From d0a6dad24ad9b7592d078ca1ca957bb1f6fe6969 Mon Sep 17 00:00:00 2001
From: Rahil Doshi <rahil.doshi@fau.de>
Date: Thu, 12 Sep 2024 14:31:06 +0200
Subject: [PATCH] Update the raytracer and add plots for better visualization

---
 src/ray_tracer/__init__.py        |  187 -----
 src/ray_tracer/raytracerMM.py     |  421 +++++++----
 src/ray_tracer/raytracer_plots.py | 1101 +++++++++++++++++++++++++++++
 3 files changed, 1378 insertions(+), 331 deletions(-)
 create mode 100644 src/ray_tracer/raytracer_plots.py

diff --git a/src/ray_tracer/__init__.py b/src/ray_tracer/__init__.py
index 57f07dd66..e69de29bb 100644
--- a/src/ray_tracer/__init__.py
+++ b/src/ray_tracer/__init__.py
@@ -1,187 +0,0 @@
-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/raytracerMM.py b/src/ray_tracer/raytracerMM.py
index eced7951d..7dcb79dfd 100644
--- a/src/ray_tracer/raytracerMM.py
+++ b/src/ray_tracer/raytracerMM.py
@@ -1,5 +1,6 @@
 import numpy as np
 import sympy as sp
+# import matplotlib.pyplot as plt
 from typing import Tuple
 from src.materials.alloys.Alloy import Alloy
 from src.materials.alloys import Ti6Al4V
@@ -22,25 +23,28 @@ print('Ti6Al4V.composition: ', Ti6Al4V.composition)
 # 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
+dx: float = 1000e-6  # Cell size in meters
+dy: float = 1000e-6  # Cell size in meters
+dz: float = 1000e-6  # Cell size in meters
+sigma: float = 3000e-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)
+# Define the work distance (height of the electron beam gun)
+w_d = 12e-3  # Work distance in meters (272 mm)
 
 # 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_min, x_max = -5000e-6, 5000e-6  # -500e-6, 500e-6
+y_min, y_max = -5000e-6, 5000e-6  # -250e-6, 250e-6
+z_min, z_max = 0, 4000e-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
+print(f"Domain dimensions: x={x_num}, y={y_num}, z={z_num}")
 domain = np.zeros((z_num, y_num, x_num))  # (z, y , x)
 print('domain shape: ', np.shape(domain))
 domain[0:2, :, :] = 1  # Solid cells
@@ -84,17 +88,19 @@ 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 = np.arange(-2, 3)
+y = np.arange(-2, 3)
 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
+print('beam_profile: ', 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_height = 12e-3  # Detector height in meters (272 mm)
+detector_spacing = 5e-3  # Detector spacing in meters (127 mm)
+detector_diameter = 5e-3  # Detector diameter in meters (50 mm)
+
 detector_positions = np.array([
     [detector_spacing, 0, detector_height],  # Right detector
     [-detector_spacing, 0, detector_height],  # Left detector
@@ -103,6 +109,33 @@ detector_positions = np.array([
 ])
 
 
+
+
+# Define the origin coordinates
+origin_x = (x_num - 1) // 2  # Assuming x_num = 101, origin_x = 50
+print('origin_x: ',origin_x)
+origin_y = (y_num - 1) // 2  # Assuming y_num = 51, origin_y = 25
+print('origin_y: ',origin_y)
+origin_z = 2  # Assuming the origin is at z=2 (interface)
+
+origin_point = np.array([origin_x * dx, origin_y * dy, origin_z * dz])
+# origin_point = np.array([0, 0, 0])
+print('origin_x * dx: ', origin_x * dx)
+print("origin_point: ", origin_point)
+
+# Loop over detector positions
+for i in range(len(detector_positions)):
+    for j in range(i+1, len(detector_positions)):
+        dist = np.linalg.norm(detector_positions[i] - detector_positions[j])
+        print(f"Distance between detectors {i+1} and {j+1}: {dist}")
+
+    # Calculate distance from detector to origin
+    dist_to_origin = np.linalg.norm(detector_positions[i] - origin_point)
+    print(f"Distance from detector {i+1} to origin ({origin_x}, {origin_y}, {origin_z}): {dist_to_origin}")
+
+
+
+
 # Define detector vertices for solid angle calculation
 def get_detector_vertices(detector_position: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
     """
@@ -115,11 +148,13 @@ def get_detector_vertices(detector_position: np.ndarray) -> Tuple[np.ndarray, np
     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
+    side_length = np.sqrt(3) * (detector_diameter / 2)
+    height = np.sqrt(3) * (side_length / 2)
 
     # Determine the orientation based on the detector position
-    if detector_position[0] > 0:  # Right detector
+    if np.allclose(detector_position[:2], [0, 0]):
+        raise ValueError("Detector position cannot be at the origin (0, 0, 0)")
+    elif 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])
@@ -131,10 +166,20 @@ def get_detector_vertices(detector_position: np.ndarray) -> Tuple[np.ndarray, np
         _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
+    elif detector_position[1] < 0:  # 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])
+    else:
+        raise ValueError("Invalid detector position. Must be in one of the four expected positions.")
+
+    print(f"Detector {det_idx+1} vertices:")
+    print(f"v1: {_v1}")
+    print(f"v2: {_v2}")
+    print(f"v3: {_v3}")
+    _normal = np.cross(_v2 - _v1, _v3 - _v1)
+    _normal /= np.linalg.norm(_normal)
+    print(f"Detector {det_idx+1} normal vector: {_normal}")
 
     return _v1, _v2, _v3
 
@@ -251,139 +296,227 @@ def g_BSE(_theta: float, _alpha: float, _beta: float) -> float:
     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)
+# Calculate the beam direction vector p_E
+def calculate_p_E(_beam_x, _beam_y):
+    # Calculate the vector from the beam gun to the beam location
+    beam_vector = np.array([_beam_x * dx, _beam_y * dy, -w_d])
+    # Normalize the beam vector
+    _p_E = beam_vector / np.linalg.norm(beam_vector)
+    return _p_E
+
+
+# Calculate phi as the angle between p_E and the negative z-axis
+def calculate_phi(_p_E):
+    z_axis = np.array([0, 0, -1])
+    dot_product = np.dot(_p_E, z_axis)
+    _phi_rad = np.arccos(np.clip(dot_product, -1.0, 1.0))
+    return _phi_rad
 
-# 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('x_num: ', x_num)
 print('y_num: ', y_num)
-beam_loc = ((x_num+1) // 2, (y_num+1) // 2)  # (x, y)
+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
+domain_center = ((x_num-1)/2, (y_num-1)/2)
+print(f"Domain center: {domain_center}")
+print(f"Is beam centered: {beam_loc == domain_center}")'''
+
+
+# Initialize arrays to store angles
+phi_angles = []
+theta_local_angles = []
+alpha_angles = []
+beta_angles = []
+
+# Initialize arrays to store electron counts
+electrons_per_detector_per_step = np.zeros((x_num, y_num, len(detector_positions)))
+total_electrons_per_step = np.zeros((x_num, y_num))
+
+# Calculate the center indices
+x_center = (x_num - 1) // 2  # 5
+y_center = (y_num - 1) // 2  # 5
+print('x_center, y_center: ', x_center, y_center)
+
+# Main simulation loop to iterate over the simulation domain
+# for beam_x in range(x_num//4, (x_num*3)//4+2):
+for beam_x in range(x_center, x_center + 1):
+    for beam_y in range(y_center, y_center + 1):
+        beam_loc = (beam_x, beam_y)
+        print(f"Beam location: {beam_loc}")
+
+        # Calculate the beam direction vector p_E
+        p_E = calculate_p_E(beam_x - x_center, beam_y - y_center)
+        print('p_E: ', p_E)
+        # dx = (beam_x - x_center) * dx
+        # dy = (beam_y - y_center) * dy
+
+        # Calculate phi as the angle between p_E and the negative z-axis
+        # phi_rad = np.arctan2(np.sqrt(dx**2 + dy**2), w_d)
+        phi_rad = calculate_phi(p_E)
+        phi_angles.append(phi_rad)  # Store phi angles
+        print(f"phi_rad: {phi_rad}")
+
+        # p_E = np.array([np.sin(phi_rad), 0, -np.cos(phi_rad)])
+        # print('p_E: ', p_E)
+
+        # Reset counters for this beam location
+        total_electrons: int = 0
+        electrons_per_detector = np.zeros(len(detector_positions))
+
+        # 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
+                        if not np.allclose(np.linalg.norm(n_S_local), 1.0):
+                            raise ValueError("n_S_local is not normalized.")
 
                         # 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"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 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)
+                            )
+                        theta_local_angles.append(theta_local)  # Store theta_local angles
+
+                        scattering_point = np.array([
+                            x_min + domain_x * dx,
+                            y_min + domain_y * dy,
+                            z_min + interface_layer * dz
+                        ])
+                        print('scattering_point: ', scattering_point)
+
+                        for det_idx, det_pos in enumerate(detector_positions):
+                            print(f"Detector {det_idx+1} position: {det_pos}")
+                            det_idx: int
+                            det_pos: np.ndarray[int]
+
+                            # Calculate the vector from the scattering point to the detector
+                            det_vec = det_pos - scattering_point
+                            det_vec /= np.linalg.norm(det_vec)
+                            if not np.allclose(np.linalg.norm(det_vec), 1.0):
+                                raise ValueError("det_vec is not normalized.")
+
+                            # Calculate the distance from the scattering point to the detector
+                            distance = np.linalg.norm(det_pos - scattering_point)
+                            print(f"Distance to detector {det_idx + 1} ({domain_x}, {domain_y}): {distance:.9f} m")
+
+                            # Debugging print statements
+                            print(f"Detector {det_idx+1} position: {det_pos}")
+                            print(f"Detector Position Type: {type(det_pos)}")
+                            print(f"Scattering Point: {scattering_point}")
+                            print(f"Detector Vector (normalized): {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))
+                            alpha_angles.append(alpha)  # Store alpha angles
+
+                            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))
+                            beta_angles.append(beta)  # Store beta angles
+
+                            # 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)
+                            normal = np.cross(v2 - v1, v3 - v1)
+                            normal /= np.linalg.norm(normal)
+                            print(f"Detector {det_idx+1}:")
+                            print(f"  Vertices: {v1}, {v2}, {v3}")
+                            print(f"  Normal vector: {normal}")
+                            # print("Type of vertices:", type(v1), type(v2), type(v3))
+                            dOmega = compute_solid_angle(v1 - scattering_point,
+                                                         v2 - scattering_point,
+                                                         v3 - scattering_point)
+                            print(f"Detector {det_idx+1} solid angle: {dOmega}")
+
+                            # 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}")'''
+                            # 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}")
                         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
 
+        # Store results for this beam location
+        electrons_per_detector_per_step[beam_x, beam_y] = electrons_per_detector
+        total_electrons_per_step[beam_x, beam_y] = total_electrons
+
+        print(f"Electrons per detector at this step: {electrons_per_detector}")
+        print(f"Total electrons at this step: {total_electrons}")
+
+
+# Compute final results
+total_electrons_all_steps = np.sum(total_electrons_per_step)
+total_electrons_per_detector = np.sum(electrons_per_detector_per_step, axis=(0, 1))
+
+# print(f"Beam location: {beam_loc}")
 
-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}")
+print(f"Total electrons hitting all detectors: {total_electrons_all_steps}")
+for i, electrons in enumerate(total_electrons_per_detector):
+    print(f"Total electrons hitting detector {i + 1}: {electrons}")
diff --git a/src/ray_tracer/raytracer_plots.py b/src/ray_tracer/raytracer_plots.py
new file mode 100644
index 000000000..53d639b7d
--- /dev/null
+++ b/src/ray_tracer/raytracer_plots.py
@@ -0,0 +1,1101 @@
+import numpy as np
+import sympy as sp
+import matplotlib.pyplot as plt
+import matplotlib.colors as colors
+from typing import Tuple
+from src.materials.alloys.Alloy import Alloy
+from src.materials.alloys import Ti6Al4V
+
+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)
+print('A: ', A)
+dx: float = 1000e-6  # Cell size in meters
+dy: float = 1000e-6  # Cell size in meters
+dz: float = 1000e-6  # Cell size in meters
+sigma: float = 3000e-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)
+# Define the work distance (height of the electron beam gun)
+# w_d = 12e-3  # Work distance in meters (272 mm)
+
+# Domain setup (Section 2.1.1)
+# Define the 3D domain for the simulation
+x_min, x_max = -40000e-6, 40000e-6  # -40 mm to 40 mm
+y_min, y_max = -40000e-6, 40000e-6  # -40 mm to 40 mm
+z_min, z_max = -2000e-6, 2000e-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
+print(f"Domain dimensions: x={x_num}, y={y_num}, z={z_num}")
+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(-2, 3)
+y = np.arange(-2, 3)
+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
+print('beam_profile: ', beam_profile)
+
+# Detector setup (Section 2.1.1)
+# Define positions of the detectors
+detector_height = 272e-3  # Detector height in meters (272 mm)
+detector_spacing = 127e-3  # Detector spacing in meters (127 mm)
+detector_diameter = 50e-3  # Detector diameter in meters (50 mm)
+
+# Define detector positions relative to the origin
+detector_positions = np.array([
+    [detector_spacing, 0, detector_height],
+    [-detector_spacing, 0, detector_height],
+    [0, detector_spacing, detector_height],
+    [0, -detector_spacing, detector_height]
+])
+'''detector_positions = np.array([
+    [x_max, y_max / 2, detector_height],  # Right detector
+    [0, y_max / 2, detector_height],  # Left detector
+    [x_max / 2, y_max, detector_height],  # Back detector
+    [x_max / 2, 0, detector_height]  # Front detector
+])'''
+print('detector_positions:', detector_positions)
+
+# Define the origin coordinates
+'''origin_x = (x_num - 1) // 2  # Assuming x_num = 101, origin_x = 50
+print('origin_x: ', origin_x)
+origin_y = (y_num - 1) // 2  # Assuming y_num = 51, origin_y = 25
+print('origin_y: ', origin_y)
+origin_z = (z_num - 1) // 2  # Assuming the origin is at z=2 (interface)
+
+origin_point = np.array([origin_x * dx, origin_y * dy, origin_z * dz])
+# origin_point = np.array([0, 0, 0])
+print('origin_x * dx: ', origin_x * dx)
+print("origin_point: ", origin_point)
+
+# Loop over detector positions
+for i in range(len(detector_positions)):
+    for j in range(i + 1, len(detector_positions)):
+        dist = np.linalg.norm(detector_positions[i] - detector_positions[j])
+        print(f"Distance between detectors {i + 1} and {j + 1}: {dist}")
+
+    # Calculate distance from detector to origin
+    dist_to_origin = np.linalg.norm(detector_positions[i] - origin_point)
+    print(f"Distance from detector {i + 1} to origin ({origin_x}, {origin_y}, {origin_z}): {dist_to_origin}")'''
+
+
+# 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) * (side_length / 2)
+
+    # Determine the orientation based on the detector position
+    if np.allclose(detector_position[:2], [0, 0]):
+        raise ValueError("Detector position cannot be at the origin (0, 0, 0)")
+    elif 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])
+    elif detector_position[1] < 0:  # 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])
+    else:
+        raise ValueError("Invalid detector position. Must be in one of the four expected positions.")
+
+    '''print(f"Detector {det_idx+1} vertices:")
+    print(f"v1: {_v1}")
+    print(f"v2: {_v2}")
+    print(f"v3: {_v3}")
+    _normal = np.cross(_v2 - _v1, _v3 - _v1)
+    _normal /= np.linalg.norm(_normal)
+    print(f"Detector {det_idx+1} normal vector: {_normal}")'''
+
+    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
+
+
+# Calculate the beam direction vector p_E
+def calculate_p_E(_beam_x, _beam_y):
+    # Calculate the vector from the beam gun to the beam location
+    beam_vector = np.array([_beam_x * dx, _beam_y * dy, -detector_height])
+    # Normalize the beam vector
+    _p_E = beam_vector / np.linalg.norm(beam_vector)
+    return _p_E
+
+
+# Calculate phi as the angle between p_E and the negative z-axis
+def calculate_phi(_p_E):
+    z_axis = np.array([0, 0, -1])
+    dot_product = np.dot(_p_E, z_axis)
+    _phi_rad = np.arccos(np.clip(dot_product, -1.0, 1.0))
+    return _phi_rad
+
+
+def set_beam_movement(_x_center, _y_center, _x_offset, _y_offset, _movement_type):
+    if _movement_type == 'X':
+        if _x_offset >= 0:
+            _beam_x_start = _x_center + _x_offset
+            _beam_x_end = _x_center - _x_offset - 1
+        else:
+            _beam_x_start = _x_center + _x_offset
+            _beam_x_end = _x_center - _x_offset + 1
+        _beam_y_start = _y_center + _y_offset
+        _beam_y_end = _y_center + _y_offset + 1
+    elif _movement_type == 'Y':
+        _beam_x_start = _x_center + _x_offset
+        _beam_x_end = _x_center + _x_offset + 1
+        if _y_offset >= 0:
+            _beam_y_start = _y_center + _y_offset
+            _beam_y_end = _y_center - _y_offset - 1
+        else:
+            _beam_y_start = _y_center + _y_offset
+            _beam_y_end = _y_center - _y_offset + 1
+    elif _movement_type == 'XY':
+        if _x_offset >= 0:
+            _beam_x_start = _x_center + _x_offset
+            _beam_x_end = _x_center - _x_offset - 1
+        else:
+            _beam_x_start = _x_center + _x_offset
+            _beam_x_end = _x_center - _x_offset + 1
+        if _y_offset >= 0:
+            _beam_y_start = _y_center + _y_offset
+            _beam_y_end = _y_center - _y_offset - 1
+        else:
+            _beam_y_start = _y_center + _y_offset
+            _beam_y_end = _y_center - _y_offset + 1
+    else:
+        raise ValueError("movement_type must be 'X' or 'Y' or 'XY'")
+
+    return _beam_x_start, _beam_x_end, _beam_y_start, _beam_y_end
+
+
+
+'''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)
+domain_center = ((x_num-1)/2, (y_num-1)/2)
+print(f"Domain center: {domain_center}")
+print(f"Is beam centered: {beam_loc == domain_center}")'''
+
+
+# Initialize arrays to store electron counts
+electrons_per_detector_per_step = np.zeros((x_num, y_num, len(detector_positions)))
+total_electrons_per_step = np.zeros((x_num, y_num))
+
+# Additional arrays to capture detailed information
+electrons_per_detector_per_beam_profile = np.zeros(
+    (beam_profile.shape[0], beam_profile.shape[1], len(detector_positions)))
+total_electrons_per_beam_profile = np.zeros((beam_profile.shape[0], beam_profile.shape[1]))
+
+# Calculate the center indices
+x_center = (x_num - 1) // 2
+y_center = (y_num - 1) // 2
+
+x_offset: int = -5
+y_offset: int = 20
+movement_type: str = 'X'
+
+# Beam X-Movement
+# beam_x_start, beam_x_end = x_center - x_offset, x_center + x_offset + 1
+# beam_y_start, beam_y_end = y_center + y_offset, y_center + y_offset + 1
+
+# Beam Y-Movement
+# beam_x_start, beam_x_end = x_center + x_offset, x_center + x_offset + 1
+# beam_y_start, beam_y_end = y_center - y_offset, y_center + y_offset + 1
+
+# For X or Y Movement
+beam_x_start, beam_x_end, beam_y_start, beam_y_end = set_beam_movement(
+    x_center, y_center, x_offset, y_offset, movement_type)
+
+# Determine the step size based on the direction of movement
+step_x = 1 if beam_x_start <= beam_x_end else -1
+step_y = 1 if beam_y_start <= beam_y_end else -1
+
+print(beam_x_start, beam_x_end, beam_y_start, beam_y_end, step_x, step_y)
+
+# exit()
+
+# Main simulation loop to iterate over the simulation domain
+for beam_x in range(beam_x_start, beam_x_end, step_x):
+    for beam_y in range(beam_y_start, beam_y_end, step_y):
+        beam_loc = (beam_x, beam_y)
+        print(f"Beam location: {beam_loc}")
+
+        # Calculate the beam direction vector p_E
+        p_E = calculate_p_E(beam_x - x_center, beam_y - y_center)
+        # print('p_E: ', p_E)
+
+        # Calculate phi as the angle between p_E and the negative z-axis
+        # phi_rad = np.arctan2(np.sqrt(dx**2 + dy**2), w_d)
+        phi_rad = calculate_phi(p_E)
+        # print(f"phi_rad: {phi_rad}")
+
+        # Reset counters for this beam location
+        total_electrons: int = 0
+        electrons_per_detector = np.zeros(len(detector_positions))
+
+        # 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
+                        if not np.allclose(np.linalg.norm(n_S_local), 1.0):
+                            raise ValueError("n_S_local is not normalized.")
+
+                        # 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 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)
+                            )
+
+                        scattering_point = np.array([
+                            x_min + domain_x * dx,
+                            y_min + domain_y * dy,
+                            z_min + interface_layer * dz
+                        ])
+                        print('scattering_point: ', scattering_point)
+
+                        for det_idx, det_pos in enumerate(detector_positions):
+                            print(f"Detector {det_idx + 1} position: {det_pos}")
+                            det_idx: int
+                            det_pos: np.ndarray[float]
+
+                            # Calculate the vector from the scattering point to the detector
+                            det_vec = det_pos - scattering_point
+                            det_vec /= np.linalg.norm(det_vec)
+                            if not np.allclose(np.linalg.norm(det_vec), 1.0):
+                                raise ValueError("det_vec is not normalized.")
+
+                            # Calculate the distance from the scattering point to the detector
+                            distance = np.linalg.norm(det_pos - scattering_point)
+                            print(f"Distance to detector {det_idx + 1} ({domain_x}, {domain_y}): {distance:.9f} m")
+
+                            # Debugging print statements
+                            '''print(f"Detector {det_idx+1} position: {det_pos}")
+                            print(f"Detector Position Type: {type(det_pos)}")
+                            print(f"Scattering Point: {scattering_point}")
+                            print(f"Detector Vector (normalized): {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)
+                            normal = np.cross(v2 - v1, v3 - v1)
+                            normal /= np.linalg.norm(normal)
+                            '''print(f"Detector {det_idx+1}:")
+                            print(f"  Vertices: {v1}, {v2}, {v3}")
+                            print(f"  Normal vector: {normal}")'''
+
+                            dOmega = compute_solid_angle(v1 - scattering_point, v2 - scattering_point,
+                                                         v3 - scattering_point)
+                            print(f"Detector {det_idx + 1} solid angle: {dOmega}")
+
+                            # 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 for this beam profile point
+                            electrons_per_detector_per_beam_profile[profile_x, profile_y, det_idx] += N_1_d
+
+                            # Accumulate electrons per detector for this domain point
+                            electrons_per_detector_per_step[beam_x, beam_y, det_idx] += N_1_d
+
+                            # Add to total electrons for this beam profile point
+                            total_electrons_per_beam_profile[profile_x, profile_y] += N_1_d
+
+                            # Add to total electrons for this domain point
+                            total_electrons_per_step[beam_x, beam_y] += N_1_d
+
+                            # Debugging print statements
+                            print(f"Before addition: electrons_per_detector[{det_idx + 1}]: {electrons_per_detector[det_idx]}")
+                            print(f"N_0: {N_0}, N_1: {N_1}, g_bse_value: {g_bse_value}, dOmega: {dOmega}")
+                            print(f"N_1_d: {N_1_d}")
+
+                            # Accumulate electrons per detector
+                            electrons_per_detector[det_idx] += N_1_d
+
+                            # Add to total electrons
+                            total_electrons += N_1_d
+
+                            print(f"After addition: electrons_per_detector[{det_idx + 1}]: {electrons_per_detector[det_idx]}")
+
+                # Print detailed information for this beam profile point
+                print(f"Electrons per detector at this beam profile point ({profile_x}, {profile_y}): {electrons_per_detector_per_beam_profile[profile_x, profile_y]}")
+                print(f"Total electrons at this beam profile point ({profile_x}, {profile_y}): {total_electrons_per_beam_profile[profile_x, profile_y]}")
+
+        # Print detailed information for this domain point after iterating over the beam profile
+        print(f"Electrons per detector at this domain point ({beam_x}, {beam_y}): {electrons_per_detector_per_step[beam_x, beam_y]}")
+        print(f"Total electrons at this domain point ({beam_x}, {beam_y}): {total_electrons_per_step[beam_x, beam_y]}")
+
+# Compute final results
+total_electrons_all_steps = np.sum(total_electrons_per_step)
+total_electrons_per_detector = np.sum(electrons_per_detector_per_step, axis=(0, 1))
+
+# Print final results
+print(f"Total electrons hitting all detectors: {total_electrons_all_steps}")
+for i, electrons in enumerate(total_electrons_per_detector):
+    print(f"Total electrons hitting detector {i + 1}: {electrons}")
+
+for beam_x in range(beam_x_start, beam_x_end, step_x):
+    for beam_y in range(beam_y_start, beam_y_end, step_y):
+        print(f"Electrons per detector at this domain point ({beam_x}, {beam_y}): {electrons_per_detector_per_step[beam_x, beam_y]}")
+
+
+
+
+# Plot results
+# Define the domain points
+domain_points_x = np.arange(beam_x_start, beam_x_end, step_x)
+domain_points_y = np.arange(beam_y_start, beam_y_end, step_y)
+
+# Extract electron counts for each detector based on movement type
+if movement_type == 'X':
+    detector_1_electrons_x = electrons_per_detector_per_step[beam_x_start:beam_x_end:step_x, y_center+y_offset, 0].flatten()
+    detector_2_electrons_x = electrons_per_detector_per_step[beam_x_start:beam_x_end:step_x, y_center+y_offset, 1].flatten()
+    detector_3_electrons_x = electrons_per_detector_per_step[beam_x_start:beam_x_end:step_x, y_center+y_offset, 2].flatten()
+    detector_4_electrons_x = electrons_per_detector_per_step[beam_x_start:beam_x_end:step_x, y_center+y_offset, 3].flatten()
+
+    # Plot for detectors 1 and 2
+    plt.figure(figsize=(10, 6))
+    plt.plot(domain_points_x, detector_1_electrons_x, label='Detector 1', marker='o')
+    plt.plot(domain_points_x, detector_2_electrons_x, label='Detector 2', marker='s')
+    # Add annotations
+    for x, y in zip(domain_points_x, detector_1_electrons_x):
+        plt.annotate(f'{y:.2e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+    for x, y in zip(domain_points_x, detector_2_electrons_x):
+        plt.annotate(f'{y:.2e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+    plt.title("Electrons Captured by Detectors 1 and 2 (X-axis Movement)")
+    plt.xlabel("Domain Point (X-axis)")
+    plt.ylabel("Electrons Captured")
+    plt.legend()
+    plt.grid(True)
+    plt.tight_layout()
+    plt.savefig("detectors_1_2_x_axis.png", dpi=300)
+    plt.close()
+
+    # Plot for detectors 3 and 4
+    plt.figure(figsize=(10, 6))
+    plt.plot(domain_points_x, detector_3_electrons_x, label='Detector 3', marker='o')
+    plt.plot(domain_points_x, detector_4_electrons_x, label='Detector 4', marker='s')
+    # Add annotations
+    for x, y in zip(domain_points_x, detector_3_electrons_x):
+        plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+    for x, y in zip(domain_points_x, detector_4_electrons_x):
+        plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+    plt.title("Electrons Captured by Detectors 3 and 4 (X-axis Movement)")
+    plt.xlabel("Domain Point (X-axis)")
+    plt.ylabel("Electrons Captured")
+    plt.legend()
+    plt.grid(True)
+    plt.tight_layout()
+    plt.savefig("detectors_3_4_x_axis.png", dpi=300)
+    plt.close()
+
+    # Normalize the data by subtracting the minimum value
+    detector_3_normalized = detector_3_electrons_x - np.min(detector_3_electrons_x)
+    detector_4_normalized = detector_4_electrons_x - np.min(detector_4_electrons_x)
+
+    plt.figure(figsize=(12, 8))  # Increase figure size for better visibility
+
+    plt.plot(domain_points_x, detector_3_normalized, label='Detector 3', marker='o')
+    plt.plot(domain_points_x, detector_4_normalized, label='Detector 4', marker='s')
+
+    # Add annotations for Detector 3
+    for x, y in zip(domain_points_x, detector_3_normalized):
+        plt.annotate(f'{y:.2f}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+    # Add annotations for Detector 4
+    for x, y in zip(domain_points_x, detector_4_normalized):
+        plt.annotate(f'{y:.2f}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+    plt.title("Normalized Electrons Captured by Detectors 3 and 4 (X-axis Movement)")
+    plt.xlabel("Domain Point (X-axis)")
+    plt.ylabel("Electrons Captured (Normalized)")
+    plt.legend()
+    plt.grid(True)
+    plt.tight_layout()
+    plt.savefig("detectors_3_4_x_axis_normalized.png", dpi=300)
+    plt.close()
+
+elif movement_type == 'Y':
+    detector_1_electrons_y = electrons_per_detector_per_step[x_center+x_offset, beam_y_start:beam_y_end:step_y, 0].flatten()
+    detector_2_electrons_y = electrons_per_detector_per_step[x_center+x_offset, beam_y_start:beam_y_end:step_y, 1].flatten()
+    detector_3_electrons_y = electrons_per_detector_per_step[x_center+x_offset, beam_y_start:beam_y_end:step_y, 2].flatten()
+    detector_4_electrons_y = electrons_per_detector_per_step[x_center+x_offset, beam_y_start:beam_y_end:step_y, 3].flatten()
+
+    # Plot for detectors 1 and 2
+    plt.figure(figsize=(10, 6))
+    plt.plot(domain_points_y, detector_1_electrons_y, label='Detector 1', marker='o')
+    plt.plot(domain_points_y, detector_2_electrons_y, label='Detector 2', marker='s')
+    # Add annotations
+    for x, y in zip(domain_points_y, detector_1_electrons_y):
+        plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+    for x, y in zip(domain_points_y, detector_2_electrons_y):
+        plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+    plt.title("Electrons Captured by Detectors 1 and 2 (Y-axis Movement)")
+    plt.xlabel("Domain Point (Y-axis)")
+    plt.ylabel("Electrons Captured")
+    plt.legend()
+    plt.grid(True)
+    plt.tight_layout()
+    plt.savefig("detectors_1_2_y_axis.png", dpi=300)
+    plt.close()
+
+    # Normalize the data by subtracting the minimum value
+    detector_1_normalized = detector_1_electrons_y - np.min(detector_1_electrons_y)
+    detector_2_normalized = detector_2_electrons_y - np.min(detector_2_electrons_y)
+
+    plt.figure(figsize=(12, 8))  # Increase figure size for better visibility
+
+    plt.plot(domain_points_y, detector_1_normalized, label='Detector 1', marker='o')
+    plt.plot(domain_points_y, detector_2_normalized, label='Detector 2', marker='s')
+
+    # Add annotations for Detector 1
+    for x, y in zip(domain_points_y, detector_1_normalized):
+        plt.annotate(f'{y:.2f}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+    # Add annotations for Detector 2
+    for x, y in zip(domain_points_y, detector_2_normalized):
+        plt.annotate(f'{y:.2f}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+    plt.title("Normalized Electrons Captured by Detectors 1 and 2 (Y-axis Movement)")
+    plt.xlabel("Domain Point (Y-axis)")
+    plt.ylabel("Electrons Captured (Normalized)")
+    plt.legend()
+    plt.grid(True)
+    plt.tight_layout()
+    plt.savefig("detectors_1_2_y_axis_normalized.png", dpi=300)
+    plt.close()
+
+    # Plot for detectors 3 and 4
+    plt.figure(figsize=(10, 6))
+    plt.plot(domain_points_y, detector_3_electrons_y, label='Detector 3', marker='o')
+    plt.plot(domain_points_y, detector_4_electrons_y, label='Detector 4', marker='s')
+    # Add annotations
+    for x, y in zip(domain_points_y, detector_3_electrons_y):
+        plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+    for x, y in zip(domain_points_y, detector_4_electrons_y):
+        plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+    plt.title("Electrons Captured by Detectors 3 and 4 (Y-axis Movement)")
+    plt.xlabel("Domain Point (Y-axis)")
+    plt.ylabel("Electrons Captured")
+    plt.legend()
+    plt.grid(True)
+    plt.tight_layout()
+    plt.savefig("detectors_3_4_y_axis.png", dpi=300)
+    plt.close()
+
+elif movement_type == 'XY':
+    # Extract 2D data for XY movement
+    detector_1_electrons_xy = electrons_per_detector_per_step[beam_x_start:beam_x_end:step_x, beam_y_start:beam_y_end:step_y, 0]
+    detector_2_electrons_xy = electrons_per_detector_per_step[beam_x_start:beam_x_end:step_x, beam_y_start:beam_y_end:step_y, 1]
+    detector_3_electrons_xy = electrons_per_detector_per_step[beam_x_start:beam_x_end:step_x, beam_y_start:beam_y_end:step_y, 2]
+    detector_4_electrons_xy = electrons_per_detector_per_step[beam_x_start:beam_x_end:step_x, beam_y_start:beam_y_end:step_y, 3]
+
+    # Create meshgrid for x and y coordinates
+    X, Y = np.meshgrid(np.arange(beam_x_start, beam_x_end, step_x), np.arange(beam_y_start, beam_y_end, step_y))
+
+    # Plot for detectors 1 and 2 (XY movement)
+    '''plt.figure(figsize=(12, 8))
+    plt.pcolormesh(X, Y, detector_1_electrons_xy.T, shading='auto', cmap='viridis')
+    plt.colorbar(label='Electrons Captured (Detector 1)')
+    plt.title("Electrons Captured by Detector 1 (XY Movement)")
+    plt.xlabel("Domain Point (X-axis)")
+    plt.ylabel("Domain Point (Y-axis)")
+    plt.tight_layout()
+    plt.savefig("detector_1_xy_movement.png", dpi=300)
+    plt.close()
+
+    plt.figure(figsize=(12, 8))
+    plt.pcolormesh(X, Y, detector_2_electrons_xy.T, shading='auto', cmap='viridis')
+    plt.colorbar(label='Electrons Captured (Detector 2)')
+    plt.title("Electrons Captured by Detector 2 (XY Movement)")
+    plt.xlabel("Domain Point (X-axis)")
+    plt.ylabel("Domain Point (Y-axis)")
+    plt.tight_layout()
+    plt.savefig("detector_2_xy_movement.png", dpi=300)
+    plt.close()
+
+    # Plot for detectors 3 and 4 (XY movement)
+    plt.figure(figsize=(12, 8))
+    plt.pcolormesh(X, Y, detector_3_electrons_xy.T, shading='auto', cmap='viridis')
+    plt.colorbar(label='Electrons Captured (Detector 3)')
+    plt.title("Electrons Captured by Detector 3 (XY Movement)")
+    plt.xlabel("Domain Point (X-axis)")
+    plt.ylabel("Domain Point (Y-axis)")
+    plt.tight_layout()
+    plt.savefig("detector_3_xy_movement.png", dpi=300)
+    plt.close()
+
+    plt.figure(figsize=(12, 8))
+    plt.pcolormesh(X, Y, detector_4_electrons_xy.T, shading='auto', cmap='viridis')
+    plt.colorbar(label='Electrons Captured (Detector 4)')
+    plt.title("Electrons Captured by Detector 4 (XY Movement)")
+    plt.xlabel("Domain Point (X-axis)")
+    plt.ylabel("Domain Point (Y-axis)")
+    plt.tight_layout()
+    plt.savefig("detector_4_xy_movement.png", dpi=300)
+    plt.close()'''
+
+
+
+
+
+    # Plot for each detector
+    '''fig, axs = plt.subplots(2, 2, figsize=(12, 10))
+
+    # Detector 1
+    vmin = np.min(detector_1_electrons_xy)
+    vmax = np.max(detector_1_electrons_xy)
+    axs[0, 0].pcolormesh(X, Y, detector_1_electrons_xy, shading='auto', cmap='viridis', vmin=vmin, vmax=vmax, norm=None)
+    axs[0, 0].set_title("Electrons Captured by Detector 1")
+    axs[0, 0].set_xlabel("Domain Point (X-axis)")
+    axs[0, 0].set_ylabel("Domain Point (Y-axis)")
+    # plt.colorbar(label='Electrons Captured')
+    axs[0, 0].set_aspect('equal')
+
+    # Detector 2
+    axs[0, 1].pcolormesh(X, Y, detector_2_electrons_xy, shading='auto', cmap='viridis')
+    axs[0, 1].set_title("Electrons Captured by Detector 2")
+    axs[0, 1].set_xlabel("Domain Point (X-axis)")
+    axs[0, 1].set_ylabel("Domain Point (Y-axis)")
+    axs[0, 1].set_aspect('equal')
+
+    # Detector 3
+    axs[1, 0].pcolormesh(X, Y, detector_3_electrons_xy, shading='auto', cmap='viridis')
+    axs[1, 0].set_title("Electrons Captured by Detector 3")
+    axs[1, 0].set_xlabel("Domain Point (X-axis)")
+    axs[1, 0].set_ylabel("Domain Point (Y-axis)")
+    axs[1, 0].set_aspect('equal')
+
+    # Detector 4
+    axs[1, 1].pcolormesh(X, Y, detector_4_electrons_xy, shading='auto', cmap='viridis')
+    axs[1, 1].set_title("Electrons Captured by Detector 4")
+    axs[1, 1].set_xlabel("Domain Point (X-axis)")
+    axs[1, 1].set_ylabel("Domain Point (Y-axis)")
+    axs[1, 1].set_aspect('equal')
+
+    # Add colorbars
+    for ax in axs.flat:
+        fig.colorbar(plt.cm.ScalarMappable(cmap='viridis'), ax=ax, shrink=0.6)
+
+    # Layout so plots do not overlap
+    fig.tight_layout()
+
+    # Save the figure
+    plt.savefig("electron_heatmaps.png", dpi=300)
+    plt.close()'''
+
+
+
+
+    # Create a figure with multiple subplots
+    fig, axs = plt.subplots(2, 2, figsize=(16, 16))
+
+    # Plot for each detector
+    for i in range(4):
+        row = i // 2
+        col = i % 2
+        im = axs[row, col].pcolormesh(X, Y, electrons_per_detector_per_step[beam_x_start:beam_x_end:step_x, beam_y_start:beam_y_end:step_y, i].T, shading='auto', cmap='viridis', norm=colors.LogNorm())
+        axs[row, col].set_title(f"Electrons Captured by Detector {i+1}")
+        axs[row, col].set_xlabel("Domain Point (X-axis)")
+        axs[row, col].set_ylabel("Domain Point (Y-axis)")
+        fig.colorbar(im, ax=axs[row, col])
+
+    # Adjust layout and save
+    plt.tight_layout()
+    plt.savefig("_all_detectors_xy_movement_.png", dpi=300)
+    plt.close()
+
+    # Now let's create individual plots for each detector
+    for i in range(4):
+        plt.figure(figsize=(10, 8))
+        im = plt.pcolormesh(X, Y, electrons_per_detector_per_step[beam_x_start:beam_x_end:step_x, beam_y_start:beam_y_end:step_y, i].T, shading='auto', cmap='viridis', norm=colors.LogNorm())
+        plt.colorbar(im)
+        plt.title(f"Electrons Captured by Detector {i+1}")
+        plt.xlabel("Domain Point (X-axis)")
+        plt.ylabel("Domain Point (Y-axis)")
+        plt.savefig(f"_detector_{i+1}_xy_movement_.png", dpi=300)
+        plt.close()
+
+
+
+
+    detector_1_electrons_xy = electrons_per_detector_per_step[beam_x_start:beam_x_end, beam_y_start:beam_y_end, 0]
+    print(detector_1_electrons_xy)
+
+    # Create the plot
+    plt.figure(figsize=(10, 8))
+    im = plt.pcolormesh(X, Y, detector_1_electrons_xy, shading='auto', cmap='viridis', norm=colors.LogNorm())
+    plt.colorbar(im, label='Electrons Captured')
+    plt.title("Electrons Captured by Detector 1 (XY Movement)")
+    plt.xlabel("Domain Point (X-axis)")
+    plt.ylabel("Domain Point (Y-axis)")
+
+    # Set the correct extent for the plot
+    # plt.xlim(beam_x_start, beam_x_end - 1)
+    # plt.ylim(beam_y_start, beam_y_end - 1)
+
+    # Adjust color scale to highlight variations
+    '''vmin = np.min(detector_1_electrons_xy)
+    vmax = np.max(detector_1_electrons_xy)
+    plt.clim(float(vmin), float(vmax))'''
+
+    # Add text annotations for each cell
+    for i in range(detector_1_electrons_xy.shape[0]):
+        for j in range(detector_1_electrons_xy.shape[1]):
+            plt.text(beam_x_start + i, beam_y_start + j, f'{detector_1_electrons_xy[i, j]:.2f}',
+                     ha='center', va='center', fontsize=6, color='white')
+
+    plt.tight_layout()
+    plt.savefig("detector_1_xy_movement_corrected.png", dpi=300)
+    plt.close()
+
+    print(f"Min: {np.min(detector_1_electrons_xy)}, Max: {np.max(detector_1_electrons_xy)}")
+
+# --------------------------------------------------
+'''
+# Plot for detectors 1 and 2 (X-axis movement)
+plt.figure(figsize=(10, 6))
+plt.plot(domain_points_x, detector_1_electrons_x, label='Detector 1', marker='o')
+plt.plot(domain_points_x, detector_2_electrons_x, label='Detector 2', marker='s')
+# Add annotations for Detector 1
+for x, y in zip(domain_points_x, detector_1_electrons_x):
+    plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+# Add annotations for Detector 2
+for x, y in zip(domain_points_x, detector_2_electrons_x):
+    plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+plt.title("Electrons Captured by Detectors 1 and 2 (X-axis Movement)")
+plt.xlabel("Domain Point (X-axis)")
+plt.ylabel("Electrons Captured")
+plt.legend()
+plt.grid(True)
+plt.tight_layout()
+plt.savefig("detectors_1_2_x_axis.png", dpi=300)
+plt.close()
+
+# --------------------------------------------------
+
+# Plot for detectors 3 and 4 (X-axis movement)
+plt.figure(figsize=(10, 6))
+plt.plot(domain_points_x, detector_3_electrons_x, label='Detector 3', marker='o')
+plt.plot(domain_points_x, detector_4_electrons_x, label='Detector 4', marker='s')
+
+# Add annotations for Detector 3
+for x, y in zip(domain_points_x, detector_3_electrons_x):
+    plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+# Add annotations for Detector 4
+for x, y in zip(domain_points_x, detector_4_electrons_x):
+    plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+# Calculate y-axis limits
+y_min = min(min(detector_3_electrons_x), min(detector_4_electrons_x))
+y_max = max(max(detector_3_electrons_x), max(detector_4_electrons_x))
+y_range = y_max - y_min
+
+# Set y-axis limits to focus on the region of interest
+plt.ylim(y_min - 0.1*y_range, y_max + 0.1*y_range)
+
+# Use scientific notation for y-axis
+plt.ticklabel_format(axis='y', style='sci', scilimits=(0,0))
+
+plt.title("Electrons Captured by Detectors 3 and 4 (X-axis Movement)")
+plt.xlabel("Domain Point (X-axis)")
+plt.ylabel("Electrons Captured")
+plt.legend()
+plt.grid(True)
+plt.tight_layout()
+plt.savefig("detectors_3_4_x_axis.png", dpi=300)
+plt.close()
+
+
+# Normalize the data by subtracting the minimum value
+detector_3_normalized = detector_3_electrons_x - np.min(detector_3_electrons_x)
+detector_4_normalized = detector_4_electrons_x - np.min(detector_4_electrons_x)
+
+plt.figure(figsize=(12, 8))  # Increase figure size for better visibility
+
+plt.plot(domain_points_x, detector_3_normalized, label='Detector 3', marker='o')
+plt.plot(domain_points_x, detector_4_normalized, label='Detector 4', marker='s')
+
+# Add annotations for Detector 3
+for x, y in zip(domain_points_x, detector_3_normalized):
+    plt.annotate(f'{y:.2f}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+# Add annotations for Detector 4
+for x, y in zip(domain_points_x, detector_4_normalized):
+    plt.annotate(f'{y:.2f}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+plt.title("Normalized Electrons Captured by Detectors 3 and 4 (X-axis Movement)")
+plt.xlabel("Domain Point (X-axis)")
+plt.ylabel("Electrons Captured (Normalized)")
+plt.legend()
+plt.grid(True)
+plt.tight_layout()
+plt.savefig("detectors_3_4_x_axis_normalized.png", dpi=300)
+plt.close()
+
+# --------------------------------------------------
+
+# Plot for detectors 1 and 2 (Y-axis movement)
+plt.figure(figsize=(10, 6))
+plt.plot(domain_points_y, detector_1_electrons_y, label='Detector 1', marker='o')
+plt.plot(domain_points_y, detector_2_electrons_y, label='Detector 2', marker='s')
+
+# Add annotations for Detector 1
+for x, y in zip(domain_points_y, detector_1_electrons_y):
+    plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+# Add annotations for Detector 2
+for x, y in zip(domain_points_y, detector_2_electrons_y):
+    plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+plt.title("Electrons Captured by Detectors 1 and 2 (Y-axis Movement)")
+plt.xlabel("Domain Point (Y-axis)")
+plt.ylabel("Electrons Captured")
+plt.legend()
+plt.grid(True)
+plt.tight_layout()
+plt.savefig("detectors_1_2_y_axis.png", dpi=300)
+plt.close()
+
+
+# Normalize the data by subtracting the minimum value
+detector_1_normalized = detector_1_electrons_y - np.min(detector_1_electrons_y)
+detector_2_normalized = detector_2_electrons_y - np.min(detector_2_electrons_y)
+
+plt.figure(figsize=(12, 8))  # Increase figure size for better visibility
+
+plt.plot(domain_points_y, detector_1_normalized, label='Detector 1', marker='o')
+plt.plot(domain_points_y, detector_2_normalized, label='Detector 2', marker='s')
+
+# Add annotations for Detector 1
+for x, y in zip(domain_points_y, detector_1_normalized):
+    plt.annotate(f'{y:.2f}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+# Add annotations for Detector 2
+for x, y in zip(domain_points_y, detector_2_normalized):
+    plt.annotate(f'{y:.2f}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+plt.title("Normalized Electrons Captured by Detectors 1 and 2 (Y-axis Movement)")
+plt.xlabel("Domain Point (Y-axis)")
+plt.ylabel("Electrons Captured (Normalized)")
+plt.legend()
+plt.grid(True)
+plt.tight_layout()
+plt.savefig("detectors_1_2_y_axis_normalized.png", dpi=300)
+plt.close()
+
+# --------------------------------------------------
+
+# Plot for detectors 3 and 4 (Y-axis movement)
+plt.figure(figsize=(10, 6))
+plt.plot(domain_points_y, detector_3_electrons_y, label='Detector 3', marker='o')
+plt.plot(domain_points_y, detector_4_electrons_y, label='Detector 4', marker='s')
+
+# Add annotations for Detector 3
+for x, y in zip(domain_points_y, detector_3_electrons_y):
+    plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+# Add annotations for Detector 4
+for x, y in zip(domain_points_y, detector_4_electrons_y):
+    plt.annotate(f'{y:.8e}', (x, y), textcoords="offset points", xytext=(5,-15), ha='center', fontsize=8, rotation=45)
+
+plt.title("Electrons Captured by Detectors 3 and 4 (Y-axis Movement)")
+plt.xlabel("Domain Point (Y-axis)")
+plt.ylabel("Electrons Captured")
+plt.legend()
+plt.grid(True)
+plt.tight_layout()
+plt.savefig("detectors_3_4_y_axis.png", dpi=300)
+plt.close()
+'''
+# --------------------------------------------------
+
+'''
+# Create meshgrid for x and y coordinates
+X, Y = np.meshgrid(np.arange(beam_x_start, beam_x_end), np.arange(beam_y_start, beam_y_end))
+
+# Extract electron counts for each detector
+detector_1_electrons = electrons_per_detector_per_step[beam_x_start:beam_x_end, beam_y_start:beam_y_end, 0]
+detector_2_electrons = electrons_per_detector_per_step[beam_x_start:beam_x_end, beam_y_start:beam_y_end, 1]
+detector_3_electrons = electrons_per_detector_per_step[beam_x_start:beam_x_end, beam_y_start:beam_y_end, 2]
+detector_4_electrons = electrons_per_detector_per_step[beam_x_start:beam_x_end, beam_y_start:beam_y_end, 3]
+
+# Plot for detectors 1 and 2
+plt.figure(figsize=(12, 8))
+plt.pcolormesh(X, Y, detector_1_electrons, shading='auto', cmap='viridis')
+plt.colorbar(label='Electrons Captured (Detector 1)')
+plt.title("Electrons Captured by Detector 1")
+plt.xlabel("Domain Point (X-axis)")
+plt.ylabel("Domain Point (Y-axis)")
+plt.tight_layout()
+plt.savefig("detector_1_heatmap.png", dpi=300)
+plt.close()
+
+plt.figure(figsize=(12, 8))
+plt.pcolormesh(X, Y, detector_2_electrons, shading='auto', cmap='viridis')
+plt.colorbar(label='Electrons Captured (Detector 2)')
+plt.title("Electrons Captured by Detector 2")
+plt.xlabel("Domain Point (X-axis)")
+plt.ylabel("Domain Point (Y-axis)")
+plt.tight_layout()
+plt.savefig("detector_2_heatmap.png", dpi=300)
+plt.close()
+
+# Plot for detectors 3 and 4
+plt.figure(figsize=(12, 8))
+plt.pcolormesh(X, Y, detector_3_electrons, shading='auto', cmap='viridis')
+plt.colorbar(label='Electrons Captured (Detector 3)')
+plt.title("Electrons Captured by Detector 3")
+plt.xlabel("Domain Point (X-axis)")
+plt.ylabel("Domain Point (Y-axis)")
+plt.tight_layout()
+plt.savefig("detector_3_heatmap.png", dpi=300)
+plt.close()
+
+plt.figure(figsize=(12, 8))
+plt.pcolormesh(X, Y, detector_4_electrons, shading='auto', cmap='viridis')
+plt.colorbar(label='Electrons Captured (Detector 4)')
+plt.title("Electrons Captured by Detector 4")
+plt.xlabel("Domain Point (X-axis)")
+plt.ylabel("Domain Point (Y-axis)")
+plt.tight_layout()
+plt.savefig("detector_4_heatmap.png", dpi=300)
+plt.close()
+'''
-- 
GitLab