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/README.md b/README.md index 73aa28b..29ee9a8 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ SPSPy is a Python based package of tools for use with the Super-Enge Split-Pole Spectrograph at FSU. Much of the code here is based on Java programs originally written at Yale University by D.W. Visser, C.M. Deibel, and others. Currently the package contains SPSPlot, a tool aimed at informing users which states should appear at the focal plane of the SESPS, and SPANC, a tool for calibrating the position spectra from the focal plane. ## Depencencies and Requirements -The requirements for running SPSPy are outlined in the requirements.txt file located in the repository. It is recommended to install these to a local virtual environment using `pip install -r requirements.txt`. For conda use the environments.yml file to create a conda environment for SPSPy. Simply run `conda env create -f environment.yml` from the SPSPy directory. conda will make a new virtual environment named spsenv with the dependencies outlined in environments.yml. If you already have an environment named spsenv or would like to change the name simply edit the first line of the enviornments.yml. - -The recommended install for SPSPy dependencies is via pip. +To ensure capability, use python 3.14. The requirements for running SPSPy are outlined in the requirements.txt file located in the repository. It is recommended to install these to a local virtual environment using `pip install -r requirements.txt`. ### Creating a virtual environment with pip To create a virtual environment with pip in the terminal for MacOS or Linux use `python3 -m venv env` to create a local virtual environment named `env` (or whatever name you'd like), or on Windows use `py -m venv env` to do the same. To activate your new environment run `source env/bin/activate` in MacOS or Linux, or `.\env\Scripts\activate`. Now you can run the above `pip` command to install all dependencies to the virtual environment. To leave the virtual environment use the command `deactivate` in your terminal. diff --git a/environment.yml b/environment.yml deleted file mode 100644 index a1e2574..0000000 --- a/environment.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: spsenv -channels: - - defaults -dependencies: - - _libgcc_mutex=0.1=main - - _openmp_mutex=5.1=1_gnu - - bzip2=1.0.8=h7b6447c_0 - - ca-certificates=2022.10.11=h06a4308_0 - - certifi=2022.9.24=py310h06a4308_0 - - ld_impl_linux-64=2.38=h1181459_1 - - libffi=3.4.2=h6a678d5_6 - - libgcc-ng=11.2.0=h1234567_1 - - libgomp=11.2.0=h1234567_1 - - libstdcxx-ng=11.2.0=h1234567_1 - - libuuid=1.41.5=h5eee18b_0 - - ncurses=6.3=h5eee18b_3 - - openssl=1.1.1s=h7f8727e_0 - - pip=22.2.2=py310h06a4308_0 - - python=3.10.8=h7a1cb2a_1 - - readline=8.2=h5eee18b_0 - - setuptools=65.5.0=py310h06a4308_0 - - sqlite=3.40.0=h5082296_0 - - tk=8.6.12=h1ccaba5_0 - - tzdata=2022f=h04d1e81_0 - - wheel=0.37.1=pyhd3eb1b0_0 - - xz=5.2.8=h5eee18b_0 - - zlib=1.2.13=h5eee18b_0 - - pip: - - charset-normalizer==2.1.1 - - contourpy==1.0.6 - - cycler==0.11.0 - - fonttools==4.38.0 - - idna==3.4 - - kiwisolver==1.4.4 - - lxml==4.9.1 - - matplotlib==3.6.2 - - numpy==1.23.5 - - packaging==21.3 - - pillow==9.3.0 - - pycatima==1.71 - - pyparsing==3.0.9 - - pyqtdarktheme==1.2.1 - - pyside6==6.4.1 - - pyside6-addons==6.4.1 - - pyside6-essentials==6.4.1 - - python-dateutil==2.8.2 - - requests==2.28.1 - - scipy==1.9.3 - - shiboken6==6.4.1 - - six==1.16.0 - - urllib3==1.26.13 diff --git a/requirements.txt b/requirements.txt index a190b88..c86e2f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,25 @@ -lxml==4.9.1 -matplotlib==3.6.2 -numpy==1.23.5 -pycatima==1.71 -pyqtdarktheme==1.2.1 -PySide6==6.4.1 -requests==2.28.1 -scipy==1.9.3 \ No newline at end of file +certifi==2025.11.12 +charset-normalizer==3.4.4 +contourpy==1.3.3 +cycler==0.12.1 +fonttools==4.61.0 +idna==3.11 +kiwisolver==1.4.9 +lxml==6.0.2 +matplotlib==3.10.7 +numpy==2.3.5 +packaging==25.0 +pillow==12.0.0 +pycatima==1.981 +pyparsing==3.2.5 +PySide6==6.10.1 +PySide6_Addons==6.10.1 +PySide6_Essentials==6.10.1 +python-dateutil==2.9.0.post0 +QDarkStyle==3.2.3 +QtPy==2.4.3 +requests==2.32.5 +scipy==1.16.3 +shiboken6==6.10.1 +six==1.17.0 +urllib3==2.5.0 diff --git a/spspy/Launcher.py b/spspy/Launcher.py index e77212d..af34e96 100644 --- a/spspy/Launcher.py +++ b/spspy/Launcher.py @@ -9,7 +9,7 @@ from .SpancUI import run_spanc_ui, SpancGUI import sys import matplotlib as mpl -from qdarktheme import load_stylesheet +import qdarkstyle class Launcher(QMainWindow): def __init__(self, parent=None): @@ -43,6 +43,7 @@ def run_launcher() -> None: app = QApplication.instance() if not app: app = QApplication(sys.argv) - app.setStyleSheet(load_stylesheet()) + # app.setStyleSheet(load_stylesheet()) + app.setStyleSheet(qdarkstyle.load_stylesheet()) window = Launcher() sys.exit(app.exec_()) \ No newline at end of file diff --git a/spspy/SPSPlotUI.py b/spspy/SPSPlotUI.py index 25b21a5..577889a 100644 --- a/spspy/SPSPlotUI.py +++ b/spspy/SPSPlotUI.py @@ -13,7 +13,7 @@ from PySide6.QtWidgets import QDoubleSpinBox from PySide6.QtWidgets import QFileDialog from PySide6.QtGui import QAction -from qdarktheme import load_stylesheet +import qdarkstyle from enum import Enum, auto import matplotlib as mpl import sys @@ -299,6 +299,7 @@ def run_spsplot_ui(): app = QApplication.instance() if not app: app = QApplication(sys.argv) - app.setStyleSheet(load_stylesheet()) + # app.setStyleSheet(load_stylesheet()) + app.setStyleSheet(qdarkstyle.load_stylesheet()) window = SPSPlotGUI() sys.exit(app.exec_()) \ 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 0c5584e..33dc3f6 100644 --- a/spspy/SpancUI.py +++ b/spspy/SpancUI.py @@ -12,7 +12,7 @@ from PySide6.QtWidgets import QPushButton, QTextEdit, QSpinBox from PySide6.QtWidgets import QFileDialog from PySide6.QtGui import QAction -from qdarktheme import load_stylesheet, load_palette +import qdarkstyle import matplotlib as mpl import numpy as np from numpy.typing import NDArray @@ -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,13 +385,48 @@ 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() if not app: app = QApplication(sys.argv) - app.setStyleSheet(load_stylesheet()) + # app.setStyleSheet(load_stylesheet()) + app.setStyleSheet(qdarkstyle.load_stylesheet()) window = SpancGUI() sys.exit(app.exec_()) \ No newline at end of file