diff --git a/.gitignore b/.gitignore index 8840362..2b68b33 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ __pycache__/ .vs/ *.sps *.spanc -*.csv \ No newline at end of file +*.csv +*.DS_Store \ No newline at end of file diff --git a/spspy/Spanc.py b/spspy/Spanc.py index 30e5b2d..a10b685 100644 --- a/spspy/Spanc.py +++ b/spspy/Spanc.py @@ -126,4 +126,82 @@ class Spanc: for calibration in self.calibrations.values(): rxn = self.reactions[calibration.rxnName] calibration.rho = rxn.convert_ejectile_KE_2_rho(rxn.calculate_ejectile_KE(calibration.excitation)) - calibration.rhoErr = np.abs(rxn.convert_ejectile_KE_2_rho(rxn.calculate_ejectile_KE(calibration.excitation + calibration.excitationErr)) - calibration.rho) \ No newline at end of file + calibration.rhoErr = np.abs(rxn.convert_ejectile_KE_2_rho(rxn.calculate_ejectile_KE(calibration.excitation + calibration.excitationErr)) - calibration.rho) + + def get_excitation_curves(self, + x_min: float = -300.0, + x_max: float = 300.0, + n_bins: int = 601): + """ + Compute excitation-energy curves for every reaction by evaluating + Ex = reaction.calculate_excitation( rho(x) ) across a grid of + focal-plane positions. + + Returns: + x_vals: array of FP bin centers + ex_curves: { rxn_name: Ex_array_in_MeV } + """ + # Bin centers (e.g. 601 values from -300 to 300) + x_vals = np.linspace(x_min, x_max, n_bins) + + # ρ(x) uses your fitted polynomial a0 + a1 x + ... + aN x^N + params = self.fitter.get_parameters() # [a0, a1, ..., aN] + rho_vals = np.polyval(params[::-1], x_vals) # reverse order for numpy poly + + ex_curves: dict[str, np.ndarray] = {} + + for rxn_name, rxn in self.reactions.items(): + ex_vals = np.array( + [rxn.calculate_excitation(rho) for rho in rho_vals], + dtype=float, + ) + ex_curves[rxn_name] = ex_vals + + return x_vals, ex_curves + + def export_excitation_csv(self, + filename: str, + x_min: float = -300.0, + x_max: float = 300.0, + n_bins: int = 601): + """ + Export excitation-energy calibration table as CSV. + + CSV format: + FP_x_mm, Ex_rxn1_MeV, Ex_rxn2_MeV, ... + + FP_x_mm runs from x_min to x_max with n_bins steps (inclusive). + """ + import csv + import numpy as np + + # Generate FP positions (601 values from -300 to 300) + x_vals = np.linspace(x_min, x_max, n_bins) + + # Compute rho(x) using fitted polynomial + params = self.fitter.get_parameters() # [a0, a1, ..., aN] + rho_vals = np.polyval(params[::-1], x_vals) + + # Compute excitation for each reaction + rxn_names = list(self.reactions.keys()) + ex_mev = {rxn: [] for rxn in rxn_names} + + for rho in rho_vals: + for rxn_name, rxn in self.reactions.items(): + ex = rxn.calculate_excitation(rho) + ex_mev[rxn_name].append(ex) + + # Write CSV + with open(filename, "w", newline="") as csvfile: + writer = csv.writer(csvfile) + + # Header row + header = ["x_mm"] + [f"Ex_{rxn}_MeV" for rxn in rxn_names] + writer.writerow(header) + + # Data rows + for i, x in enumerate(x_vals): + row = [f"{x:.6f}"] + for rxn in rxn_names: + row.append(f"{ex_mev[rxn][i]:.9f}") + writer.writerow(row) \ No newline at end of file diff --git a/spspy/SpancUI.py b/spspy/SpancUI.py index af60a0c..33dc3f6 100644 --- a/spspy/SpancUI.py +++ b/spspy/SpancUI.py @@ -95,6 +95,17 @@ class SpancGUI(QMainWindow): fitOptionLayout.addWidget(QLabel("Polynomial Order", self.fitOptionGroup)) fitOptionLayout.addWidget(self.fitOrderBox) fitOptionLayout.addWidget(self.fitButton) + + # NEW: button to plot excitation curves for all reactions + self.exCurveButton = QPushButton("Plot Ex vs x", self.fitOptionGroup) + self.exCurveButton.clicked.connect(self.plot_excitation_curves) + fitOptionLayout.addWidget(self.exCurveButton) + + # Button to export excitation calibration CSV + self.exportCSVButton = QPushButton("Export Ex(x) CSV", self.fitOptionGroup) + self.exportCSVButton.clicked.connect(self.handle_export_excitation_csv) + fitOptionLayout.addWidget(self.exportCSVButton) + self.fitOptionGroup.setLayout(fitOptionLayout) fitLayout.addWidget(QLabel("Fit", self.fitCanvas)) @@ -374,8 +385,42 @@ class SpancGUI(QMainWindow): f"## Parameter Uncertanties (ua0 -> uaN): {np.array_str(self.spanc.fitter.get_parameter_errors(), precision=3)} \n \n" f"## Residuals (x0 -> xN): {np.array_str(residuals, precision=3)} \n \n" f"## Studentized Residuals (x0 -> xN): {np.array_str(studentizedResiduals, precision=3)} \n \n") + self.fitResultText.setMarkdown(markdownString) + def plot_excitation_curves(self): + # Require a fit so rho(x) is defined + if not self.spanc.isFit: + print("Run the calibration fit first before plotting Ex vs x.") + return + + # Compute Ex at 600 bin centers from -300 to 300 for all reactions + x_vals, ex_curves = self.spanc.get_excitation_curves( + x_min=-300.0, + x_max=300.0, + n_bins=600, + ) + + self.fitCanvas.axes.cla() + for rxn_name, ex_vals in ex_curves.items(): + self.fitCanvas.axes.plot(x_vals, ex_vals, label=rxn_name) + + self.fitCanvas.axes.set_xlabel(r"$x$ (mm)") + self.fitCanvas.axes.set_ylabel(r"$E_x$ (MeV)") + self.fitCanvas.axes.set_title("Excitation energy vs focal-plane position") + self.fitCanvas.axes.grid(True) + self.fitCanvas.axes.legend() + self.fitCanvas.fig.tight_layout() + self.fitCanvas.draw() + + def handle_export_excitation_csv(self): + fileName = QFileDialog.getSaveFileName( + self, "Export Excitation CSV", "./", "CSV Files (*.csv)" + ) + if fileName[0]: + self.spanc.export_excitation_csv(fileName[0]) + print(f"Exported excitation calibration to {fileName[0]}") + def run_spanc_ui() : mpl.use("Qt5Agg") app = QApplication.instance()