#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Created on Wed May 20 13:32:14 2026 @author: jamesszalkie """ import numpy as np import pandas as pd from scipy.interpolate import interp1d import uproot import pycatima as catima from scipy.integrate import cumulative_trapezoid import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import cmd import shlex import textwrap import readline import atexit import os import periodictable as pt import re from matplotlib.colors import PowerNorm #Run program from terminal or IDE, and prompts will provide user steps #histfile = os.path.expanduser("~/.uproot_shell_history") #try: # readline.read_history_file(histfile) #except FileNotFoundError: # pass #atexit.register(readline.write_history_file, histfile) #data = [z, mass_u, maximum MeV, name] alpha_data = [2, 4.0026, 40, "alpha"] proton_data = [1, 1.0078, 20, "proton"] deuteron_data = [1, 2.014102, 30, "deuteron"] dA = 3.2 #cm dC = 4.2 #cm dsx3 = 8.8 #cm particles = { "alpha": alpha_data, "proton": proton_data, "deuteron": deuteron_data } interp_cache = {} def make_E_vs_x(z, mass_u, emax_mev, label, npoints, P_TORR, TEMP_K): # GAS SETUP R = 8.3144 P_TORR = 400 TEMP_K = 293.15 R = 8.3144 # Gas density p_pa = P_TORR * 133.322 molar_density = p_pa / (R * TEMP_K) m_he = 4.0026 m_c = 12.0000 m_o = 15.9949 m_mix_avg = (0.96 * m_he) + (0.04 * (m_c + 2*m_o)) rho_g_cm3 = (molar_density * m_mix_avg) / 1e6 print(f"Gas density = {rho_g_cm3:.6e} g/cm^3") # MATERIAL material_def = [ (m_he, 2, 0.96), (m_c, 6, 0.04), (m_o, 8, 0.08) ] gas_mix = catima.Material(material_def) gas_mix.density(rho_g_cm3) projectile = catima.Projectile(mass_u, z) # Energy grid E = np.linspace(0.01, emax_mev, npoints) # Stopping power array S_mass = np.zeros_like(E) for i, energy in enumerate(E): projectile.T(energy / mass_u) # MeV / (g/cm^2) S_mass[i] = catima.dedx(projectile, gas_mix) # Convert to MeV/cm S_linear = S_mass * rho_g_cm3 # Sort descending energy sort_idx = np.argsort(E)[::-1] E = E[sort_idx] S_linear = S_linear[sort_idx] # Integrate dx/dE = 1/S(E) invS = 1.0 / S_linear x = cumulative_trapezoid( invS, E, initial=0 ) x = -x # Output table output = pd.DataFrame({ "Distance_cm": x, "Energy_MeV": E }) outfile = f"E_vs_x_{label}.dat" output.to_csv( outfile, sep='\t', index=False ) print(f"Saved: {outfile}") return x, E #Generate energy loss tables from file def load_table(filename): """ Load table with columns: x(cm) E(MeV) Returns: x_array, E_array """ data = pd.read_csv( filename, sep=r'\s+', comment="#", header=None, skiprows=1 ) x = data.iloc[:, 0].to_numpy() E = data.iloc[:, 1].to_numpy() return x, E def get_interpolators(particle): if particle in interp_cache: return interp_cache[particle] filename = f"E_vs_x_{particle}.dat" x, E = load_table(filename) E_of_x = interp1d( x, E, bounds_error=False, fill_value=0.0 ) x_of_E = interp1d( E[::-1], x[::-1], bounds_error=False, fill_value="extrapolate" ) interp_cache[particle] = (E_of_x, x_of_E) return E_of_x, x_of_E def energy_loss(particle, Ei, dl): E_of_x, x_of_E = get_interpolators(particle) xi = x_of_E(Ei) xf = xi + dl Ef = E_of_x(xf) return np.maximum(Ef, 0.0) #def energy_loss_angular(particle, Ei, theta): def energy_reconstruction(particle, Ef, dl): E_of_x, x_of_E = get_interpolators(particle) xf = x_of_E(Ef) xi = xf - dl Ei = E_of_x(xi) return np.maximum(Ei, 0.0) def energy_distance(particle, Ei, Ef): _, x_of_E = get_interpolators(particle) xi = x_of_E(Ei) xf = x_of_E(Ef) return np.abs(xf - xi) def resolve_particle(name): name = name.lower().strip().rstrip("s") if name in particles: return particles[name] match = re.match(r"([a-zA-Z]+)[-\s]?(\d+)", name) if match: element_symbol = match.group(1).capitalize() A = int(match.group(2)) try: element = pt.elements.symbol(element_symbol) isotope = element[A] # <-- THIS is the correct way return ( isotope.number, # Z isotope.mass, # mass in u 30.0, f"{element_symbol}-{A}" ) except Exception: raise ValueError(f"Unknown isotope: {name}") try: elem = pt.elements.symbol(name.capitalize()) return elem.number, elem.mass, 30.0, name except Exception: raise ValueError(f"Unknown particle/isotope: {name}") def update_plot_data(name, values): for i, (existing_name, _) in enumerate(plot_data): if existing_name == name: plot_data[i] = (name, values) return plot_data.append((name, values)) def process_file(filename): tree = uproot.open(filename)["tree"] branches = ["Tb", "thetab", "sx3Z"] data = tree.arrays(branches, library="np") Ei = data["Tb"] theta = np.radians(data["thetab"]) sx3Z = data["sx3Z"] mask = np.sin(theta) != 0 Ei = Ei[mask] theta = theta[mask] sx3Z = sx3Z[mask] sin_theta = np.sin(theta) radii = np.array([3.2, 4.2, 6.6]) dA = radii[0] / sin_theta dC = radii[1] / sin_theta dsx3 = radii[2] / sin_theta # Determine particle from filename lower = filename.lower() if "proton" in lower: particle = "proton" elif "alpha" in lower: particle = "alpha" else: particle = "proton" print(f"Computing energies for {particle}...") EA = energy_loss(particle, Ei, dA) EC = energy_loss(particle, Ei, dC) Esx3 = energy_loss(particle, Ei, dsx3) Elost = Ei - Esx3 Eprop = EA - EC return { "particle": particle, "Ei": Ei, "sx3Z": sx3Z, "EA": EA, "EC": EC, "Esx3": Esx3, "Elost": Elost, "Eprop": Eprop } class MyInteractiveApp(cmd.Cmd): def __init__(self): super().__init__() # Initial value set when the script starts self.buckets = 500 self.T = 293.15 self.P = 400 self.temp_particle = [0, 0.0, 0.0, ""] self.rootFile = "SimAnasen1.root" self.file = None self.initialize_file() self.tree = None print("-" * 30) print("INTERACTIVE SHELL STARTED") self.print_params() print("Type 'help' for commands.") print("Type 'exit' to end program") print("-" * 30) def print_params(self): """Helper method to display current state""" print(f"Current Parameters: T={self.T} K, P={self.P} Torr") def initialize_file(self): """Load in default root file for anasen""" try: self.file = uproot.open(f"../Armory/{self.rootFile}") except: self.file = None print("\nATTENTION: Root file not found, continue without uproot functions or uproot file manually") #intro = "Interactive Shell Started. Type 'help' to see commands." prompt = ">> " def default(self, line): # Check if the command starts with our multi-word phrase if line.startswith("make table "): # Extract everything after "make table " args = line[len("make table "):].strip() self.do_make_table(args) elif line.startswith("set t") or line.startswith("Set T") or line.startswith("set T") or line.startswith("Set t"): args = line[len("set t "):].strip() self.do_set_T(args) elif line.startswith("set p") or line.startswith("Set P") or line.startswith("set P") or line.startswith("Set p"): args = line[len("set p "):].strip() self.do_set_P(args) elif line.startswith("set buckets") or line.startswith("Set Buckets") or line.startswith("set Buckets") or line.startswith("Set buckets"): args = line[len("set buckets "):].strip() self.do_set_Buckets(args) elif line.startswith("energy loss") or line.startswith("Energy Loss") or line.startswith("Energy loss"): args = line[len("energy loss "):].strip() self.do_energy_loss(args) elif line.startswith("energy reconstruction") or line.startswith("Energy Reconstruction") or line.startswith("Energy reconstruction"): args = line[len("energy reconstruction "):].strip() self.do_energy_reconstruction(args) elif line.startswith("energy distance") or line.startswith("Energy Distance") or line.startswith("Energy distance"): args = line[len("energy distance "):].strip() self.do_energy_distance(args) else: print(f"*** Unknown syntax: {line}") def do_T(self, arg): """Print value of T""" print(self.T) def do_Buckets(self, arg): """print number of histogram buckets and points""" print(self.buckets) def do_P(self, arg): """Print value of P (pressure)""" print(self.P) def do_make_particle(self, arg): """Enter parameters for new particle to be given to catyma system Ex: >> make_particle """ args = shlex.split(arg) z = int(args[0]) mass_u = float(args[1]) emax_mev = float(args[2]) particle = args[3] self.temp_particle = [z, mass_u, emax_mev, particle] print(f"New particle '{particle}': z = {z}, mass_u = {mass_u}, emax_mev = {emax_mev}") def do_set_T(self, arg): """Changes the value of T. Usage: set_t 300""" try: self.T = float(arg) print(f"T has been updated to {self.T}") except ValueError: print("Please enter a valid number for T.") def do_set_P(self, arg): """Changes the value of P in Torr. Usage: set_ 400""" try: self.P = float(arg) print(f"P has been updated to {self.P}") except ValueError: print("Please enter a valid number for P.") def do_set_Buckets(self, arg): """Changes the value of buckets""" try: self.buckets = int(arg) print(f"Number of buckets has been updated to {self.buckets}") except ValueError: print("Please enter a valid number of buckets.") def do_exit(self, arg): """Exits the application.""" print("Closing application...") return True # Returning True stops the cmdloop() def do_make_table(self, arg): """Create E vs X tables for particle, or isotopes Ex: >> make table proton Ex: >> make table Co60 Ex: >> make table N17 """ try: args = shlex.split(arg) if not args: print("Please enter desired reaction particle") return name = args[0] if len(args) > 1: emax_mev = float(args[1]) else: emax_mev = None z, mass_u, default_emax, label = resolve_particle(name) if emax_mev is None: emax_mev = default_emax x, E = make_E_vs_x( z, mass_u, emax_mev, label, self.buckets, self.P, self.T ) plt.figure(figsize=(8,6)) plt.plot(x, E) plt.xlabel("Distance (cm)") plt.ylabel("Energy (MeV)") plt.title(f"Energy Loss Curve {label.capitalize()}") plt.grid(True) textstr = f"T = {self.T:.2f} K\nP = {self.P:.2f} Torr" plt.gca().text( 0.02, 0.02, textstr, transform=plt.gca().transAxes, fontsize=10, verticalalignment='bottom', bbox=dict(boxstyle="round", facecolor="white", alpha=0.7) ) plt.tight_layout() filename = f"Energy_Loss_Curve_{label}.png" plt.savefig(filename, dpi=300, bbox_inches="tight") plt.close() print(f"Saved plot: {filename}") except Exception as e: print(f"Error in make_table: {e}") def load_table(self, arg): args = shlex.split(arg) particle = args[0] filename = f"E_vs_x_{particle}.dat" load_table(filename) print(f"Loaded table {filename}") def do_energy_loss(self, arg): """Find a final energy given an initial energy and distance travelled Ex: >> energy loss """ args = shlex.split(arg) try: particle = args[0] Ei = float(args[1]) dl = float(args[2]) Ei_offset = Ei * 1.1 table_specs = f"{particle} {Ei_offset}" try: self.do_make_table(table_specs) Ef = energy_loss(particle, Ei, dl) print(f"\nFinal energy: {Ef:.6f} MeV\n") except: return except IndexError: print("Please input particle, initial energy, and distance travelled") def do_energy_reconstruction(self, arg): """Find a vertex energy given an final energy and distance travelled Ex: >> energy reconstruction """ args = shlex.split(arg) try: particle = args[0] Ef = float(args[1]) dl = float(args[2]) try: Ei = energy_reconstruction(particle, Ef, dl) if Ei > 0: print(f"\nInitial energy: {Ei:.6f} MeV\n") else: print("Error: remake table with larger value, fallen off map") except: print("Particle energy table not made yet, please do so using 'make table'") except IndexError: print("Please input particle, final energy from detector, and distance travelled") def do_energy_distance(self, arg): """Find a distance travelled given an initial and final energy Ex: >> energy distance """ args = shlex.split(arg) try: particle = args[0] Ei = float(args[1]) Ef = float(args[2]) dE = Ei - Ef try: dl = energy_distance(particle, Ei, Ef) if dl > 0: print(f"\nChange in energy: {dE:.6f} MeV") print(f"Distance travelled: {dl:.6f} cm\n") else: print("Error: remake table with larger value, fallen off map") except: print("Particle energy table not made yet, please do so using 'make table'") except IndexError: print("Please input particle, final energy from detector, and distance travelled") def do_uproot_file(self, arg): """Open a specific root file for inspection""" args = shlex.split(arg) if len(args) > 0: filename = args[0] else: filename = self.rootFile try: print(f"Opening {filename}") # Try Armory path first try: self.file = uproot.open(f"../Armory/{filename}") except FileNotFoundError: self.file = uproot.open(filename) print("File loaded successfully.") print("Keys:", self.file.keys()) except Exception as e: print("Error opening file:", e) def do_print_file(self, arg): """Print contents of ROOT file""" args = shlex.split(arg) file = self.file if "keys" in args or len(args) == 0: print("Keys in file: ", file.keys()) if "class_names" in args or len(args) == 0: print("Class names: ", file.classnames()) def do_set_tree(self, arg): if self.file is None: print("No ROOT file loaded.") return keys = self.file.keys() print("Available trees:", keys) treeName = arg if len(arg) > 0 else "tree" try: # uproot automatically resolves ";1" self.tree = self.file[treeName] print(f"Tree loaded: {self.tree}") print("Branches:", self.tree.keys()) except Exception as e: print("Error loading tree:", treeName) print("Exception:", e) def run_command_line(self): import readline import atexit import os import awkward as ak histfile = os.path.expanduser("~/.uproot_shell_history") try: readline.read_history_file(histfile) except FileNotFoundError: pass readline.set_history_length(1000) readline.parse_and_bind("tab: complete") atexit.register(readline.write_history_file, histfile) print("Custom Python CMD (type 'exit' to stop)") local_vars = { "file": self.file, "uproot": uproot, "np": np, #"ak": ak, "plt": plt, "self": self } startup_script = textwrap.dedent(""" tree = file["tree"] """) exec(startup_script, globals(), local_vars) while True: try: entry = input(">>> ").strip() if entry.lower() in ["exit", "quit"]: break if not entry: continue try: result = eval(entry, globals(), local_vars) if result is not None: print(result) except SyntaxError: exec(entry, globals(), local_vars) if "tree" in local_vars: self.tree = local_vars["tree"] except OverflowError(): print("Arrays too large, causing crash") except Exception as e: print(f"Error: {e}") self.shell_vars = local_vars def do_uproot(self, arg): """Start up an in-program command line to use root tools with python, look up 'uproot' for more details""" self.run_command_line() def do_energy_analysis(self, arg): args = shlex.split(arg) try: particle = args[0] except IndexError: print("Please indicate reactant for analysis") return try: max_events = int(args[1]) except IndexError: max_events = None if self.tree is None: self.do_set_tree("") print(f"Using TTree: {self.tree}") branches = [ "Tb", "thetab", "sx3Z" ] if max_events: n_events = max_events else: n_events = self.tree.num_entries print(f"Loading {n_events} events...") data = self.tree.arrays( branches, library="np", entry_stop=max_events ) global Ei Ei = data["Tb"] global sx3Z sx3Z = data["sx3Z"] theta = np.radians(data["thetab"]) # Remove theta = 0 events mask = np.sin(theta) != 0 Ei = Ei[mask] theta = theta[mask] sin_theta = np.sin(theta) radii = np.array([3.2, 4.2, 6.6]) dA = radii[0] / sin_theta dC = radii[1] / sin_theta dsx3 = radii[2] / sin_theta print("Calculating energy losses...") global EA EA = energy_loss(particle, Ei, dA) global EC EC = energy_loss(particle, Ei, dC) global Esx3 Esx3 = energy_loss(particle, Ei, dsx3) global Eprop Eprop = EA - EC global Elost Elost = Ei - Esx3 print("Analysis complete") print(f"Processed events: {len(Ei)}") print(f"Anode average energy: {np.mean(EA):.3f} MeV") print(f"Cathode average energy: {np.mean(EC):.3f} MeV") print(f"sx3 average energy: {np.mean(Esx3):.3f} MeV") print(f"Average total energy loss to sx3: {np.mean(Elost):.3f} MeV") print(f"Maximum total energy loss to sx3: {np.max(Elost):.3f} MeV") print(f"Minimum total energy loss to sx3: {np.min(Elost):.3f} MeV") print(f"Proportion counter average energy difference: {np.mean(Eprop):.3f} MeV") print(f"Maximum proportion counter energy difference: {np.max(Eprop):.3f} MeV") print(f"Minimum proportion counter energy difference: {np.min(Eprop):.3f} MeV") output_filename = "energy_analysis.root" print(f"Writing new tree to {output_filename}") # Load ALL original branches all_data = self.tree.arrays( library="np", entry_stop=max_events ) # Create full-length arrays initialized to NaN n_total = len(data["Tb"]) EA_full = np.full(n_total, np.nan) EC_full = np.full(n_total, np.nan) Esx3_full = np.full(n_total, np.nan) Eprop_full = np.full(n_total, np.nan) Elost_full = np.full(n_total, np.nan) # Put values back into valid entries EA_full[mask] = EA EC_full[mask] = EC Esx3_full[mask] = Esx3 Eprop_full[mask] = Eprop Elost_full[mask] = Elost # Add new branches all_data["EA"] = EA_full all_data["EC"] = EC_full all_data["Esx3"] = Esx3_full all_data["Eprop"] = Eprop_full all_data["Elost"] = Elost_full # Write new ROOT file as a classic TTree with uproot.recreate(output_filename) as fout: branch_types = { name: array.dtype for name, array in all_data.items() } fout.mktree("tree", branch_types) fout["tree"].extend(all_data) print("Finished writing augmented ROOT file") def do_make_plots(self, arg): import os global plot_data plot_data = [] args = shlex.split(arg) particle = args[0] if len(args) > 0 else "proton" max_events = int(args[1]) if len(args) > 1 else None if self.tree is None: self.do_set_tree("") print(f"Using TTree: {self.tree}") branches = ["Tb", "thetab", "sx3Z"] data = self.tree.arrays( branches, library="np", entry_stop=max_events ) Ei = data["Tb"] theta = np.radians(data["thetab"]) sx3Z = data["sx3Z"] mask = np.sin(theta) != 0 Ei = Ei[mask] theta = theta[mask] sx3Z = sx3Z[mask] update_plot_data(f"{particle}_Ei", Ei) update_plot_data(f"{particle}_sx3Z", sx3Z) sin_theta = np.sin(theta) radii = np.array([3.2, 4.2, 6.6]) dA = radii[0] / sin_theta dC = radii[1] / sin_theta dsx3 = radii[2] / sin_theta print("Computing energies...") EA = energy_loss(particle, Ei, dA) EC = energy_loss(particle, Ei, dC) Esx3 = energy_loss(particle, Ei, dsx3) Elost = Ei - Esx3 Eprop = EA - EC update_plot_data(f"{particle}_EA", EA) update_plot_data(f"{particle}_EC", EC) update_plot_data(f"{particle}_Esx3", Esx3) update_plot_data(f"{particle}_Elost", Elost) update_plot_data(f"{particle}_Eprop", Eprop) base = f"{particle}_plots" os.makedirs(base, exist_ok=True) print(f"Saving plots to folder: {base}") # 1. Histogram: energy loss plt.figure(figsize=(7,5)) plt.hist(Elost, bins=200) plt.xlabel("Energy Loss (MeV)") plt.ylabel("Counts") plt.title(f"{particle} Total Energy Loss Distribution") plt.grid(True) plt.tight_layout() plt.savefig(f"{base}/Elost_hist.png", dpi=300) plt.close() #Histogram: sx3Z plt.figure(figsize=(7,5)) plt.hist(sx3Z, bins=100) plt.xlabel("SX3 Z") plt.ylabel("Counts") plt.title(f"{particle} SX3 Position Distribution") plt.grid(True) plt.tight_layout() plt.savefig(f"{base}/sx3Z_hist.png", dpi=300) plt.close() #2D: Elost vs sx3Z plt.figure(figsize=(7,6)) plt.hist2d(sx3Z, Elost, bins=200) plt.ylabel("Energy Loss (MeV)") plt.xlabel("SX3 Z") plt.title(f"{particle} Energy Loss vs SX3 Position") plt.colorbar(label="Counts") plt.tight_layout() plt.savefig(f"{base}/Elost_vs_sx3Z.png", dpi=300) plt.close() #Anode energy vs sx3 energy plt.figure(figsize=(7,6)) plt.hist2d(EA, Esx3, bins=200) plt.xlabel("EA (MeV)") plt.ylabel("Esx3 (MeV)") plt.title(f"{particle} Anode vs SX3 Energy") plt.colorbar(label="Counts") plt.tight_layout() plt.savefig(f"{base}/EA_vs_Esx3.png", dpi=300) plt.close() #Prop counter energy loss plt.figure(figsize=(7,6)) plt.hist2d(Eprop, sx3Z, bins=200) plt.xlabel("EA - EC (MeV)") plt.ylabel("SX3 Z") plt.title(f"{particle} Energy Propagation Difference vs Position") plt.colorbar(label="Counts") plt.tight_layout() plt.savefig(f"{base}/Eprop_vs_sx3Z.png", dpi=300) plt.close() plt.figure(figsize=(7,6)) plt.hist2d(Esx3, Eprop, bins=200) plt.ylabel("PCEnergy") plt.xlabel("SX3 Energy (MeV)") plt.title(f"{particle} Energy Propagation Difference vs sx3 Energy") plt.colorbar(label="Counts") plt.xlim(0,30) plt.ylim(0,.45) plt.tight_layout() plt.savefig(f"{base}/Eprop_vs_Esx3.png", dpi=300) plt.close() print("Plotting complete.") def do_dual_plotter(self, arg): args = shlex.split(arg) if len(args) < 2: print("Usage: make dual plots proton_data.root alpha_data.root") return file1 = args[0] file2 = args[1] #Helper function #Process both files data1 = process_file(f"../Armory/{file1}") data2 = process_file(f"../Armory/{file2}") outdir = "dual_plots" os.makedirs(outdir, exist_ok=True) print(f"Saving plots to: {outdir}") #Overlay histogram: Elost plt.figure(figsize=(8,6)) plt.hist( data1["Elost"], bins=200, histtype='step', linewidth=2, density=True, label=data1["particle"] ) plt.hist( data2["Elost"], bins=200, histtype='step', linewidth=2, density=True, label=data2["particle"] ) plt.xlabel("Energy Loss (MeV)") plt.ylabel("Normalized Counts") plt.title("Energy Loss Comparison") plt.legend() plt.grid(True) plt.tight_layout() plt.savefig(f"{outdir}/Elost_overlay.png", dpi=300) plt.close() #Overlay histogram: sx3Z plt.figure(figsize=(8,6)) plt.hist( data1["sx3Z"], bins=150, histtype='step', linewidth=2, density=True, label=data1["particle"] ) plt.hist( data2["sx3Z"], bins=150, histtype='step', linewidth=2, density=True, label=data2["particle"] ) plt.xlabel("SX3 Z") plt.ylabel("Normalized Counts") plt.title("SX3 Position Comparison") plt.legend() plt.grid(True) plt.tight_layout() plt.savefig(f"{outdir}/sx3Z_overlay.png", dpi=300) plt.close() #Side-by-side 2D plots fig, axes = plt.subplots(1, 2, figsize=(14,6)) h1 = axes[0].hist2d( data1["sx3Z"], data1["Elost"], bins=200 ) axes[0].set_title(f'{data1["particle"]} Elost vs SX3') axes[0].set_xlabel("SX3 Z") axes[0].set_ylabel("Energy Loss (MeV)") h2 = axes[1].hist2d( data2["sx3Z"], data2["Elost"], bins=200 ) axes[1].set_title(f'{data2["particle"]} Elost vs SX3') axes[1].set_xlabel("SX3 Z") axes[1].set_ylabel("Energy Loss (MeV)") fig.colorbar(h1[3], ax=axes[0], label="Counts") fig.colorbar(h2[3], ax=axes[1], label="Counts") plt.tight_layout() plt.savefig(f"{outdir}/Elost_vs_sx3_comparison.png", dpi=300) plt.close() #EA vs Esx3 overlay scatter plt.figure(figsize=(8,6)) plt.scatter( data1["EA"], data1["Esx3"], s=1, alpha=0.3, label=data1["particle"] ) plt.scatter( data2["EA"], data2["Esx3"], s=1, alpha=0.3, label=data2["particle"] ) plt.xlabel("EA (MeV)") plt.ylabel("Esx3 (MeV)") plt.title("Anode vs SX3 Energy") plt.legend() plt.grid(True) plt.tight_layout() plt.savefig(f"{outdir}/EA_vs_Esx3_overlay.png", dpi=300) plt.close() #PCE vs SiE combined_Esx3 = np.concatenate([ data1["Esx3"], data2["Esx3"] ]) combined_Eprop = np.concatenate([ data1["Eprop"], data2["Eprop"] ]) plt.figure(figsize=(7,6)) plt.hist2d( combined_Esx3, combined_Eprop, bins=100, range=[[0,30],[0,0.45]], cmap='viridis' # same default matplotlib style ) plt.ylabel("PCEnergy") plt.xlabel("SX3 Energy (MeV)") plt.title( f'{data1["particle"]} + {data2["particle"]} ' 'Energy Propagation Difference vs SX3 Energy' ) plt.colorbar(label="Counts") plt.xlim(0,30) plt.ylim(0,0.45) plt.tight_layout() plt.savefig(f"{outdir}/Combined_Eprop_vs_Esx3.png", dpi=300) plt.close() print("Dual plotting complete.") #exec(open("PCEnergyAnalysis.py").read()) if __name__ == "__main__": MyInteractiveApp().cmdloop()