├── .gitignore ├── COPYING ├── README.md ├── core ├── __init__.py ├── about │ └── license.rtf ├── analysis │ ├── __init__.py │ ├── field_analysis.py │ ├── picket_fence.py │ ├── planar_imaging.py │ ├── starshot.py │ └── wlutz.py ├── calibration │ ├── __init__.py │ └── trs398.py ├── configuration │ ├── config.py │ └── data │ │ ├── chambers.json │ │ ├── phantoms.json │ │ ├── settings.json │ │ └── workspace.json ├── database │ └── __init__.py ├── image │ └── dicom.py └── tools │ ├── __init__.py │ ├── devices.py │ ├── list.json │ ├── report.py │ ├── report_assets │ ├── depth_zmax.pdf │ ├── dw_zmax.pdf │ ├── dw_zref.pdf │ ├── kElec.pdf │ ├── kPol.pdf │ ├── kQQo.pdf │ ├── kS.pdf │ ├── kTP.pdf │ ├── kVol.pdf │ ├── pdd_zref.pdf │ ├── r50.pdf │ ├── tmr_zref.pdf │ └── tpr2010.pdf │ ├── setup.py │ └── toreportlab.py ├── main.py └── ui ├── __init__.py ├── app_main_win.py ├── convertUI.sh ├── linac_qa ├── field_analysis_widgets.py ├── picket_fence_offsets_dialog.py ├── picket_fence_test_dialog.py ├── picket_fence_widgets.py ├── planar_imaging_widgets.py ├── qa_tools_win.py ├── starshot_test_dialog.py ├── starshot_widgets.py ├── trs398_widgets.py ├── winston_lutz_test_dialog.py └── winston_lutz_widgets.py ├── preferences └── prefs_main.py ├── py_ui ├── .py ├── about_dialog_ui.py ├── app_main_win_ui.py ├── electrons_worksheet_ui.py ├── field_analysis_worksheet_ui.py ├── icons_rc.py ├── photons_worksheet_ui.py ├── picket_fence_offsets_dialog_ui.py ├── picket_fence_test_dialog_ui.py ├── picket_fence_worksheet_ui.py ├── planar_imaging_worksheet_ui.py ├── preferences_ui.py ├── qa_main_win_ui.py ├── starshot_test_dialog_ui.py ├── starshot_worksheet_ui.py ├── winston_lutz_test_dialog_ui.py └── winston_lutz_worksheet_ui.py ├── qt_ui ├── about_dialog.ui ├── app_main_win.ui ├── electrons_worksheet.ui ├── field_analysis_worksheet.ui ├── icons.qrc ├── icons │ ├── Animation.svg │ ├── add-file.png │ ├── auto_focus.svg │ ├── bb_shift.png │ ├── circled-left.png │ ├── close_win.png │ ├── correct.png │ ├── delete.png │ ├── deselect.svg │ ├── down_button.png │ ├── error_round.png │ ├── eye.png │ ├── graph-report.png │ ├── ic_app.png │ ├── ic_app.svg │ ├── ic_app_alt.svg │ ├── image-file.png │ ├── import.png │ ├── in_progress.png │ ├── info.png │ ├── ion_chamber.png │ ├── left.png │ ├── loading.png │ ├── logo-github.svg │ ├── minus.png │ ├── module.png │ ├── not_started.png │ ├── picture.png │ ├── pie_chart_report.png │ ├── plus.png │ ├── remove.png │ ├── scan.png │ ├── select_all.svg │ ├── settings.png │ ├── setup_end_anim.svg │ ├── star_profile.svg │ ├── test.png │ ├── tools.png │ ├── transform.svg │ ├── trash_x.svg │ ├── up_button.png │ ├── waiting.png │ ├── warning.png │ ├── zoom_in_area.svg │ └── zoom_pan.svg ├── photons_worksheet.ui ├── picket_fence_offsets_dialog.ui ├── picket_fence_test_dialog.ui ├── picket_fence_worksheet.ui ├── planar_imaging_worksheet.ui ├── preferences.ui ├── pyBeamQA.pyproject ├── qa_main_win.ui ├── starshot_test_dialog.ui ├── starshot_worksheet.ui ├── winston_lutz_test_dialog.ui └── winston_lutz_worksheet.ui ├── util_widgets ├── __init__.py ├── dialogs.py ├── statusbar.py ├── validators.py └── wizard.py ├── web_engine ├── fonts │ └── roboto_regular.ttf ├── katex │ ├── contrib │ │ └── auto-render.min.js │ ├── fonts │ │ ├── KaTeX_AMS-Regular.ttf │ │ ├── KaTeX_AMS-Regular.woff │ │ ├── KaTeX_AMS-Regular.woff2 │ │ ├── KaTeX_Caligraphic-Bold.ttf │ │ ├── KaTeX_Caligraphic-Bold.woff │ │ ├── KaTeX_Caligraphic-Bold.woff2 │ │ ├── KaTeX_Caligraphic-Regular.ttf │ │ ├── KaTeX_Caligraphic-Regular.woff │ │ ├── KaTeX_Caligraphic-Regular.woff2 │ │ ├── KaTeX_Fraktur-Bold.ttf │ │ ├── KaTeX_Fraktur-Bold.woff │ │ ├── KaTeX_Fraktur-Bold.woff2 │ │ ├── KaTeX_Fraktur-Regular.ttf │ │ ├── KaTeX_Fraktur-Regular.woff │ │ ├── KaTeX_Fraktur-Regular.woff2 │ │ ├── KaTeX_Main-Bold.ttf │ │ ├── KaTeX_Main-Bold.woff │ │ ├── KaTeX_Main-Bold.woff2 │ │ ├── KaTeX_Main-BoldItalic.ttf │ │ ├── KaTeX_Main-BoldItalic.woff │ │ ├── KaTeX_Main-BoldItalic.woff2 │ │ ├── KaTeX_Main-Italic.ttf │ │ ├── KaTeX_Main-Italic.woff │ │ ├── KaTeX_Main-Italic.woff2 │ │ ├── KaTeX_Main-Regular.ttf │ │ ├── KaTeX_Main-Regular.woff │ │ ├── KaTeX_Main-Regular.woff2 │ │ ├── KaTeX_Math-BoldItalic.ttf │ │ ├── KaTeX_Math-BoldItalic.woff │ │ ├── KaTeX_Math-BoldItalic.woff2 │ │ ├── KaTeX_Math-Italic.ttf │ │ ├── KaTeX_Math-Italic.woff │ │ ├── KaTeX_Math-Italic.woff2 │ │ ├── KaTeX_SansSerif-Bold.ttf │ │ ├── KaTeX_SansSerif-Bold.woff │ │ ├── KaTeX_SansSerif-Bold.woff2 │ │ ├── KaTeX_SansSerif-Italic.ttf │ │ ├── KaTeX_SansSerif-Italic.woff │ │ ├── KaTeX_SansSerif-Italic.woff2 │ │ ├── KaTeX_SansSerif-Regular.ttf │ │ ├── KaTeX_SansSerif-Regular.woff │ │ ├── KaTeX_SansSerif-Regular.woff2 │ │ ├── KaTeX_Script-Regular.ttf │ │ ├── KaTeX_Script-Regular.woff │ │ ├── KaTeX_Script-Regular.woff2 │ │ ├── KaTeX_Size1-Regular.ttf │ │ ├── KaTeX_Size1-Regular.woff │ │ ├── KaTeX_Size1-Regular.woff2 │ │ ├── KaTeX_Size2-Regular.ttf │ │ ├── KaTeX_Size2-Regular.woff │ │ ├── KaTeX_Size2-Regular.woff2 │ │ ├── KaTeX_Size3-Regular.ttf │ │ ├── KaTeX_Size3-Regular.woff │ │ ├── KaTeX_Size3-Regular.woff2 │ │ ├── KaTeX_Size4-Regular.ttf │ │ ├── KaTeX_Size4-Regular.woff │ │ ├── KaTeX_Size4-Regular.woff2 │ │ ├── KaTeX_Typewriter-Regular.ttf │ │ ├── KaTeX_Typewriter-Regular.woff │ │ └── KaTeX_Typewriter-Regular.woff2 │ ├── katex.min.css │ └── katex.min.js ├── mathView.html └── mathView.js └── wizards └── setup_wizard.py /.gitignore: -------------------------------------------------------------------------------- 1 | **/__pycache__ 2 | .vscode 3 | *.code-workspace 4 | **/pyBeamQA.pyproject.user 5 | ui/qt_ui/*.py 6 | ui/qt_ui/pyBeamQA.pyproject 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # PyBeam QA 6 | 7 | PyBeam QA is a graphical user interface program for performing quality assurance tests in radiotherapy. The program currently runs on Pylinac and PySide6. 8 | 9 | ## Features 10 | The program is still in early development and may contain bugs. Tools are flagged as either 11 | 'Complete', 'In-progress' or 'Planned'. Planned features are those not yet implemented. 12 | 13 | | QA Tool | Status | 14 | | --------------- | --------------- | 15 | | TRS 398 Photon & Electron output calibration | Complete | 16 | | Picket fence | Complete | 17 | | Winston-Lutz analysis | Complete | 18 | | Star-shot analysis | Complete | 19 | | Field analysis | Complete | 20 | | Planar imaging analysis | Complete | 21 | 22 | The program includes some of these complementary features: 23 | 24 | - Interactive QA plots with export capabilities. 25 | - Professional looking QA reports. 26 | - Quick creation of benchmark images and tests. 27 | 28 | ## Requirements 29 | As of current the program depends on the following: 30 | - Python (3.10+) 31 | - PySide6 (6.4+) 32 | - pylinac (3.20.0+) 33 | - pyqtgraph (0.13.3+) 34 | - pdfrw (0.4) 35 | 36 | ## Installation 37 | 1. Download the source code from the repository. 38 | 2. (Optional but highly recommended) Create a virtual environment for PyBeam-QA to avoid dependency conflicts 39 | with existing python libraries. You can use a dependency manager such as `Pipenv` to accomplish this. 40 | 3. Install all the required dependencies using `pip3` (e.g `pip3 install pdfrw==0.4`). 41 | 42 | ## Quick start 43 | To run the application simply navigate to the source code directory and run the following command:\ 44 | `python main.py` or `python3 main.py` 45 | -------------------------------------------------------------------------------- /core/__init__.py: -------------------------------------------------------------------------------- 1 | __app_name__ = "PyBeam QA" 2 | __organisation__ = "" 3 | __version__ = "0.2.0" -------------------------------------------------------------------------------- /core/analysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/analysis/__init__.py -------------------------------------------------------------------------------- /core/analysis/starshot.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import QSizePolicy 18 | from PySide6.QtCore import Signal, Slot, QObject 19 | from PySide6.QtGui import QTransform, QActionGroup, QIcon 20 | 21 | import sys 22 | import copy 23 | import gc 24 | import io 25 | import traceback 26 | import pyqtgraph as pg 27 | import numpy as np 28 | from typing import BinaryIO 29 | from pylinac.core.geometry import Point 30 | from pylinac.starshot import Starshot 31 | from pylinac.settings import get_dicom_cmap 32 | 33 | import matplotlib.pyplot as plt 34 | from ui.py_ui import icons_rc 35 | 36 | pg.setConfigOptions(antialias = True, imageAxisOrder='row-major') 37 | 38 | class QStarshot(Starshot): 39 | 40 | def __init__(self, filepath: str | BinaryIO, **kwargs): 41 | super().__init__(filepath, **kwargs) 42 | 43 | def analyze(self, 44 | radius: float = 0.85, 45 | min_peak_height: float = 0.25, 46 | tolerance: float = 1, 47 | start_point: Point | tuple | None = None, 48 | fwhm: bool = True, 49 | recursive: bool = True, 50 | invert: bool = False): 51 | return super().analyze(radius, min_peak_height, tolerance, start_point, fwhm, recursive, invert) 52 | 53 | def get_publishable_plots(self) -> list[io.BytesIO]: 54 | """ 55 | Custom plot implementation to get smaller, high quality pdf images 56 | """ 57 | 58 | full_plot_data = io.BytesIO() 59 | wobble_plot_data = io.BytesIO() 60 | 61 | fig, ax = plt.subplots() 62 | # show analyzed image 63 | self.image.plot(ax, show=False) 64 | self.lines.plot(ax) 65 | self.wobble.plot2axes(ax, edgecolor="green") 66 | self.circle_profile.plot2axes(ax, edgecolor="green") 67 | 68 | ax.axis('off') 69 | ax.set_aspect('auto') 70 | 71 | # Ensure that we fill the entire pdf page (pad_inches = 0.0 and box_inches = 'tight') 72 | fig.savefig(full_plot_data, format = "pdf", pad_inches = 0.0, bbox_inches='tight') 73 | 74 | x_limits = [self.wobble.center.x + self.wobble.diameter, 75 | self.wobble.center.x - self.wobble.diameter] 76 | 77 | y_limits = [self.wobble.center.y + self.wobble.diameter, 78 | self.wobble.center.y - self.wobble.diameter] 79 | 80 | ax.axis('on') 81 | ax.set_xticklabels([]) 82 | ax.set_yticklabels([]) 83 | ax.set_xlim(x_limits) 84 | ax.set_ylim(y_limits) 85 | 86 | fig.savefig(wobble_plot_data, format = "pdf", pad_inches = 0.0, bbox_inches='tight') 87 | plt.close() 88 | 89 | return [full_plot_data, wobble_plot_data] 90 | 91 | class QStarshotWorker(QObject): 92 | 93 | analysis_progress = Signal(str) 94 | analysis_results_ready = Signal(dict) 95 | analysis_failed = Signal(str) 96 | thread_finished = Signal() 97 | 98 | def __init__(self, filepath: str | list[str], 99 | radius: float = 0.85, 100 | min_peak_height: float = 0.25, 101 | tolerance: float = 1.0, 102 | fwhm: bool = True, 103 | recursive: bool = True, 104 | invert: bool = False, 105 | update_signal: Signal = None, 106 | **kwargs): 107 | super().__init__(parent = None) 108 | 109 | self._filepath = filepath 110 | self._radius = radius 111 | self._min_peak_height = min_peak_height 112 | self._tolerance = tolerance 113 | self._fwhm = fwhm 114 | self._recursive = recursive 115 | self._invert = invert 116 | self._update_signal = update_signal 117 | self._kwargs = kwargs 118 | 119 | def analyze(self): 120 | try: 121 | if type(self._filepath) == str: 122 | self.starshot = QStarshot(self._filepath, **self._kwargs) 123 | else: 124 | self.starshot = QStarshot.from_multiple_images(self._filepath, **self._kwargs) 125 | 126 | self.starshot.analyze(radius = self._radius, 127 | min_peak_height = self._min_peak_height, 128 | tolerance = self._tolerance, 129 | fwhm = self._fwhm, 130 | recursive = self._recursive, 131 | invert = self._invert) 132 | 133 | analysis_data = {}; 134 | analysis_data["image"] = self.starshot.image 135 | analysis_data["wobble"] = self.starshot.wobble 136 | analysis_data["spoke_lines"] = self.starshot.lines 137 | analysis_data["star_profile"] = self.starshot.circle_profile 138 | analysis_data["report_plots"] = self.starshot.get_publishable_plots() 139 | 140 | self.analysis_results_ready.emit(analysis_data) 141 | 142 | del self.starshot 143 | gc.collect() 144 | self.thread_finished.emit() 145 | 146 | except Exception as err: 147 | self.analysis_failed.emit(traceback.format_exception_only(err)[-1]) 148 | self.thread_finished.emit() 149 | raise err 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /core/analysis/wlutz.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import os 18 | import os.path as osp 19 | import copy 20 | import traceback 21 | from typing import Sequence 22 | from PySide6.QtCore import Signal, Slot, QObject 23 | 24 | import pyqtgraph as pg 25 | 26 | from pylinac import WinstonLutz, WinstonLutz2D 27 | from pylinac.winston_lutz import bb_projection_with_rotation, BB3D, BBArrangement 28 | from pylinac.picketfence import Orientation 29 | from pylinac.core.geometry import cos, sin 30 | from pylinac.core.scale import MachineScale 31 | from pylinac.core.image_generator.simulators import Simulator 32 | from pylinac.core.image_generator.layers import (FilteredFieldLayer, 33 | FilterFreeFieldLayer, 34 | PerfectFieldLayer, 35 | PerfectBBLayer, 36 | Layer) 37 | 38 | import gc 39 | from pathlib import Path 40 | import io 41 | import matplotlib.pyplot as plt 42 | from scipy import ndimage 43 | plt.switch_backend('agg') # switch to non-gui backend to avoid runtime error 44 | 45 | pg.setConfigOptions(antialias = True, imageAxisOrder='row-major') 46 | 47 | class QWinstonLutz(WinstonLutz): 48 | 49 | def __init__(self, directory: str | list[str] | Path, 50 | update_signal: Signal = Signal(int), 51 | use_filenames: bool = False, 52 | axis_mapping: dict[str, tuple[int, int, int]] | None = None): 53 | super().__init__(directory, use_filenames, axis_mapping) 54 | 55 | self.update_signal = update_signal 56 | self.image_data = [] 57 | 58 | def analyze(self, 59 | bb_size_mm: float = 5, 60 | machine_scale: MachineScale = MachineScale.IEC61217, 61 | low_density_bb: bool = False, 62 | open_field: bool = False, 63 | apply_virtual_shift: bool = False,): 64 | 65 | # Initial counter value for the progress bar 66 | self.progress_counter = 0 67 | self.machine_scale = machine_scale 68 | 69 | self.machine_scale = machine_scale 70 | if self.is_from_cbct: 71 | low_density_bb = True 72 | open_field = True 73 | for img in self.images: 74 | img.analyze(bb_size_mm, low_density_bb, open_field) 75 | 76 | self.update_image_info(img) 77 | 78 | # we need to construct the BB representation to get the shift vector 79 | bb_config = BBArrangement.ISO[0] 80 | bb_config.bb_size_mm = bb_size_mm 81 | self.bb = BB3D( 82 | bb_config=bb_config, 83 | bb_matches=[img.arrangement_matches["Iso"] for img in self.images], 84 | scale=self.machine_scale, 85 | ) 86 | if apply_virtual_shift: 87 | shift = self.bb_shift_vector 88 | self._virtual_shift = self.bb_shift_instructions() 89 | for img in self.images: 90 | img.analyze(bb_size_mm, low_density_bb, open_field, shift_vector=shift) 91 | 92 | # in the vanilla WL case, the BB can only be represented by non-couch-kick images 93 | # the ray trace cannot handle the kick currently 94 | self.bb = BB3D( 95 | bb_config=bb_config, 96 | bb_matches=[img.arrangement_matches["Iso"] for img in self.images], 97 | scale=self.machine_scale, 98 | ) 99 | self._is_analyzed = True 100 | self._bb_diameter = bb_size_mm 101 | 102 | def update_image_info(self, img: WinstonLutz2D): 103 | self.image_data.append({ 104 | "file_path": str(img.path), 105 | "filename": Path(str(img.path)).name, 106 | "bb_location": {"x": img.bb.x, "y": img.bb.y}, 107 | "bb_outline_coords": img.bb, 108 | "field_cax": {"x": img.field_cax.x, "y": img.field_cax.y}, 109 | "epid": {"x": img.epid.x, "y": img.epid.y}, 110 | "cax_to_bb_dist": img.cax2bb_distance, 111 | "cax_to_epid_dist": img.cax2epid_distance, 112 | "gantry_angle": f"{img.gantry_angle:.2f}", 113 | "collimator_angle": f"{img.collimator_angle:.2f}", 114 | "couch_angle": f"{img.couch_angle:.2f}", 115 | "delta_u": f"{(img.bb.x - img.field_cax.x) / img.dpmm:.2f}", 116 | "delta_v": f"{(img.bb.y - img.field_cax.y) / img.dpmm:.2f}" 117 | }) 118 | 119 | self.progress_counter += 1 120 | self.update_signal.emit(self.progress_counter) 121 | 122 | class QWinstonLutzWorker(QObject): 123 | 124 | images_analyzed = Signal(int) 125 | thread_finished = Signal() 126 | analysis_results_changed = Signal(dict) 127 | analysis_failed = Signal(str) 128 | bb_shift_info_changed = Signal(str) 129 | 130 | def __init__(self, images: list[str], 131 | bb_size: float = 5.0, 132 | use_filenames: bool = False): 133 | super().__init__() 134 | 135 | self.bb_size = bb_size 136 | self._wl = QWinstonLutz(images, update_signal = self.images_analyzed, 137 | use_filenames = use_filenames) 138 | 139 | @Slot() 140 | def analyze(self): 141 | try: 142 | self._wl.analyze(bb_size_mm = self.bb_size) 143 | 144 | wl_data = self._wl.results_data(as_dict=True) 145 | wl_data["image_details"] = self._wl.image_data 146 | 147 | #summary_image_data = io.BytesIO() 148 | #self._wl.save_summary(summary_image_data, format = "pdf", 149 | # pad_inches = 0.0, bbox_inches='tight') 150 | #wl_data["summary_plot"] = summary_image_data 151 | 152 | self.analysis_results_changed.emit(wl_data) 153 | self.bb_shift_info_changed.emit(str(self._wl.bb_shift_instructions())) 154 | del self._wl 155 | gc.collect() 156 | self.thread_finished.emit() 157 | 158 | 159 | except Exception as err: 160 | self.analysis_failed.emit(traceback.format_exception_only(err)[-1]) 161 | self.thread_finished.emit() 162 | 163 | raise err 164 | 165 | def generate_winstonlutz( 166 | simulator: Simulator, 167 | field_layer: type[Layer], 168 | dir_out: str, 169 | field_size_mm: tuple[float, float] = (30, 30), 170 | final_layers: list[Layer] | None = None, 171 | bb_size_mm: float = 5, 172 | offset_mm_left: float = 0, 173 | offset_mm_up: float = 0, 174 | offset_mm_in: float = 0, 175 | image_axes: ((int, int, int), ...) = ( 176 | (0, 0, 0), 177 | (90, 0, 0), 178 | (180, 0, 0), 179 | (270, 0, 0), 180 | ), 181 | gantry_tilt: float = 0, 182 | gantry_sag: float = 0, 183 | clean_dir: bool = True, 184 | field_alpha: float = 1.0, 185 | bb_alpha: float = -0.5, 186 | ) -> list[str]: 187 | 188 | if field_alpha + bb_alpha > 1: 189 | raise ValueError("field_alpha and bb_alpha must sum to <=1") 190 | if field_alpha - bb_alpha < 0: 191 | raise ValueError("field_alpha and bb_alpha must have a sum >=0") 192 | if not osp.isdir(dir_out): 193 | os.mkdir(dir_out) 194 | if clean_dir: 195 | for pdir, _, files in os.walk(dir_out): 196 | [os.remove(osp.join(pdir, f)) for f in files] 197 | file_names = [] 198 | for gantry, coll, couch in image_axes: 199 | sim_single = copy.copy(simulator) 200 | sim_single.add_layer( 201 | field_layer( 202 | field_size_mm=field_size_mm, 203 | cax_offset_mm=(gantry_tilt * cos(gantry), gantry_sag * sin(gantry)), 204 | alpha=field_alpha, 205 | ) 206 | ) 207 | 208 | # Rotate the image now 209 | sim_single.image = ndimage.rotate(sim_single.image, -coll, 210 | reshape = False, mode = 'nearest') 211 | 212 | gplane_offset, long_offset = bb_projection_with_rotation( 213 | offset_left=offset_mm_left, 214 | offset_up=offset_mm_up, 215 | offset_in=offset_mm_in, 216 | gantry=gantry, 217 | couch=couch, 218 | sad=1000, 219 | ) 220 | sim_single.add_layer( 221 | PerfectBBLayer( 222 | cax_offset_mm=(long_offset, gplane_offset), 223 | bb_size_mm=bb_size_mm, 224 | alpha=bb_alpha, 225 | ) 226 | ) 227 | if final_layers is not None: 228 | for layer in final_layers: 229 | sim_single.add_layer(layer) 230 | file_name = f"WL G={gantry}, C={coll}, P={couch}; Field={field_size_mm}mm; " \ 231 | f"BB={bb_size_mm}mm @ left={offset_mm_left}, in={offset_mm_in}, " \ 232 | f"up={offset_mm_up}; Gantry tilt={gantry_tilt}, Gantry sag={gantry_sag}.dcm" 233 | sim_single.generate_dicom( 234 | osp.join(dir_out, file_name), 235 | gantry_angle=gantry, 236 | coll_angle=coll, 237 | table_angle=couch, 238 | ) 239 | file_names.append(file_name) 240 | return file_names 241 | -------------------------------------------------------------------------------- /core/calibration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/calibration/__init__.py -------------------------------------------------------------------------------- /core/calibration/trs398.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import math 18 | import logging 19 | from scipy.interpolate import CubicSpline 20 | 21 | class TRS398: 22 | 23 | __pulsed_constants = {2.0: {"a0": 2.337, "a1": -3.636, "a2": 2.299}, 24 | 2.5: {"a0": 1.474, "a1": -1.587, "a2": 1.114}, 25 | 3.0: {"a0": 1.198, "a1": -0.875, "a2": 0.677}, 26 | 3.5: {"a0": 1.080, "a1": -0.542, "a2": 0.463}, 27 | 4.0: {"a0": 1.022, "a1": -0.363, "a2": 0.341}, 28 | 5.0: {"a0": 0.975, "a1": -0.188, "a2": 0.214}} 29 | 30 | __pulsed_scanned_constants = {2.0: {"a0": 4.711, "a1": -8.242, "a2": 4.533}, 31 | 2.5: {"a0": 2.719, "a1": -3.977, "a2": 2.261}, 32 | 3.0: {"a0": 2.001, "a1": -2.402, "a2": 1.404}, 33 | 3.5: {"a0": 1.665, "a1": -1.647, "a2": 0.984}, 34 | 4.0: {"a0": 1.468, "a1": -1.200, "a2": 0.734}, 35 | 5.0: {"a0": 1.279, "a1": -0.750, "a2": 0.474}} 36 | 37 | def __init__(self, mRaw: float = 1.0, nDW: float = 1.0, kTP: float = 1.0, 38 | kS: float = 1.0, kPol: float = 1.0, kElec: float = 1.0, kQQo: float = 1.0): 39 | self.mRaw = mRaw 40 | self.linac_mu = 100.0 41 | self.nDW = nDW 42 | self.kTP = kTP 43 | self.kS = kS 44 | self.kPol = kPol 45 | self.kElec = kElec 46 | self.kQQo = kQQo 47 | self.kVol = 1.00 48 | 49 | self.refTemp = 20.0 50 | self.refPress = 101.325 51 | self.refHumidity = 50.0 52 | 53 | self.ksCalcMethod = "direct" 54 | 55 | def kTP_corr(self, temp: float, press: float) -> float: 56 | self.kTP = (273.2 + temp) * self.refPress / ((273.2 + self.refTemp) * press) 57 | return self.kTP 58 | 59 | def kPol_corr(self, mPositive: float, mNegative: float, isPositivePref: bool) -> float: 60 | if isPositivePref: self.kPol = (abs(mPositive) + abs(mNegative)) / abs(2.0*mPositive) 61 | else: self.kPol = (abs(mPositive) + abs(mNegative)) / abs(2.0*mNegative) 62 | 63 | return self.kPol 64 | 65 | def kElec_corr(self, kElec: float) -> float: 66 | self.kElec = kElec 67 | return self.kElec 68 | 69 | def kS_corr(self, refVoltage: float, redVoltage: float, refM: float, redM, 70 | isPulsedScanned: bool) -> float: 71 | """ 72 | Calculates the ion recombination correction factor for pulsed and pulsed-scanned beams. 73 | 74 | The algorithm is implemented from the work by Martin S. Weinhous and Jerome A. Meli (1984) 75 | - http://dx.doi.org/10.1118/1.595574 76 | """ 77 | 78 | vRatio = refVoltage / redVoltage 79 | mRatio = refM / redM 80 | 81 | if self.ksCalcMethod == "direct": 82 | max_iter = 10000 83 | 84 | if isPulsedScanned: 85 | #initial conditions 86 | a = -0.1 87 | b = 1.7 88 | delta = 1E-7 89 | 90 | fZeta = lambda x: self.__phi(x)/self.__phi(x*vRatio) - mRatio 91 | fZetaA = fZeta(a) 92 | 93 | #Bisection method 94 | for i in range(1, max_iter+1): 95 | p = (a + b) / 2.0 96 | fZetaB = fZeta(p) 97 | 98 | if fZetaB == 0.0 or abs(b-a) / 2.0 < delta: 99 | self.kS = 1.0 / self.__phi(p) 100 | return self.kS 101 | 102 | if fZetaA * fZetaB > 0: 103 | a = p 104 | fZetaA = fZeta(a) 105 | else: b = p 106 | 107 | else: 108 | #initial conditions 109 | e = mRatio / vRatio 110 | delta = 1E-7 111 | uParam = 0.0005 112 | 113 | #Fixed point iteration 114 | for i in range(0, max_iter+1): 115 | u = pow(1.0 + uParam*vRatio, e) - 1 116 | 117 | if abs(u - uParam) < delta: 118 | self.kS = u / math.log(u + 1.0) 119 | return self.kS 120 | 121 | uParam = u 122 | 123 | elif self.ksCalcMethod == "interpolate": 124 | minRatio = 2.0 125 | maxRatio = 5.0 126 | 127 | try: 128 | if vRatio < minRatio or vRatio > maxRatio: 129 | raise ValueError(f"Voltage ratio %{vRatio} not within allowed range (%{minRatio} - ${maxRatio})") 130 | else: 131 | # Apply spline interpolation to determine the ion recombination correction factor 132 | if isPulsedScanned: fitConstants = self.__pulsed_scanned_constants 133 | else: fitConstants = self.__pulsed_constants 134 | 135 | vRatioData = list(fitConstants.keys()) 136 | a0Data = [x["a0"] for x in fitConstants.values()] 137 | a1Data = [x["a1"] for x in fitConstants.values()] 138 | a2Data = [x["a2"] for x in fitConstants.values()] 139 | 140 | a0Spline = CubicSpline(vRatioData, a0Data) 141 | a1Spline = CubicSpline(vRatioData, a1Data) 142 | a2Spline = CubicSpline(vRatioData, a2Data) 143 | 144 | self.kS = a0Spline(vRatio) + a1Spline(vRatio)*mRatio + a2Spline(vRatio)*pow(mRatio, 2) 145 | return self.kS 146 | 147 | except ValueError as verr: 148 | print("Voltage ratio out of range") 149 | raise 150 | 151 | else: 152 | try: 153 | raise ValueError(f"Invalid option {self.ksCalcMethod} for determining ion recombination correction" + \ 154 | " only the following values are allowed: \'direct\' or \'interpolate\'") 155 | except ValueError: 156 | logging.exception("Invalid option") 157 | 158 | 159 | def __phi(self, zeta: float) -> float: 160 | v = zeta / (zeta + 1.0) 161 | phi = 0.0 162 | preCoeff = 0.0 163 | order = 100 164 | 165 | for i in range(1, order+1): 166 | newCoeff = preCoeff + (1.0 / i) 167 | phi += (1.0 / i) * newCoeff * pow(v, i) 168 | preCoeff = newCoeff 169 | 170 | return phi * (1.0 / zeta) 171 | 172 | def pdd2010_to_tpr2010(self, pdd2010: float) -> float: 173 | return 1.2661*pdd2010 - 0.0595 174 | 175 | def pdd10_to_tpr2010(self, pdd10: float) -> float: 176 | return -0.7898 + 0.0329*pdd10 - 0.000166*pow(pdd10, 2) 177 | 178 | def get_Mcorrected(self) -> float: 179 | return self.mRaw * self.kTP * self.kElec * self.kPol * self.kS * self.kVol 180 | 181 | def get_DwQ_zref(self) -> float: 182 | return self.get_Mcorrected() * self.nDW * self.kQQo / self.linac_mu 183 | 184 | def get_DwQ_zmax_ssdSetup(self, pddZref: float) -> float: 185 | return 100. * self.get_DwQ_zref() / pddZref 186 | 187 | def get_DwQ_zmax_tmrSetup(self, tmrZref: float) -> float: 188 | return self.get_DwQ_zref() / tmrZref 189 | 190 | class TRS398Photons(TRS398): 191 | def __init__(self, mRaw: float = 1.0, nDW: float = 1.0, kTP: float = 1.0, 192 | kS: float = 1.0, kPol: float = 1.0, kElec: float = 1.0, kQQo: float = 1.0): 193 | super().__init__(mRaw = mRaw, nDW = nDW, kTP = kTP,kS = kS, kPol = kPol, kElec = kElec, kQQo = kQQo) 194 | 195 | def tpr2010_to_kQ(self, tpr2010: float, tpr_kQ: dict) -> float: 196 | ''' 197 | Determines the beam quality correction factor from TPR-20,10 (using cubic spline interpolation) 198 | tpr2010 (float) : The beam quality index defined as the ratio of the absorbed doses at 20 and 10 g/cm2 199 | tpr_kQ (dict) : Dictionary with tpr2010 values as dict keys and kQ values as dict values 200 | ''' 201 | tprValues = [float(x) for x in tpr_kQ.keys()] 202 | tprValues.sort() 203 | kQValues = [float(x) for x in tpr_kQ.values()] 204 | kQValues.sort(reverse=True) 205 | 206 | # check if TPR-20,10 value is within bounds 207 | if tpr2010 < tprValues[0] or tpr2010 > tprValues[-1]: 208 | raise ValueError(f"TPR-20,10 value {tpr2010} is not within the acceptable range: {tprValues[0]} - {tprValues[-1]}") 209 | else: 210 | kQSpline = CubicSpline(tprValues, kQValues) 211 | self.kQQo = float(kQSpline(tpr2010)) 212 | return self.kQQo 213 | 214 | def kVol_corr(self, tpr2010: float, cavity_len: float, sdd: float) -> float: 215 | self.kVol = 1 + (0.0062*tpr2010 - 0.0036) * pow(100*cavity_len/sdd, 2) 216 | return self.kVol 217 | 218 | class TRS398Electrons(TRS398): 219 | def __init__(self, mRaw: float = 1.0, nDW: float = 1.0, kTP: float = 1.0, 220 | kS: float = 1.0, kPol: float = 1.0, kElec: float = 1.0, kQQo: float = 1.0): 221 | super().__init__(mRaw = mRaw, nDW = nDW, kTP = kTP,kS = kS, kPol = kPol, kElec = kElec, kQQo = kQQo) 222 | 223 | self.r50 = 1.0 224 | 225 | def r50ion_to_r50(self, r50ion: float, source: str) -> float: 226 | if source == "ion_curves": 227 | if r50ion <= 10.0: 228 | self.r50 = 1.029 * r50ion - 0.06 229 | return self.r50 230 | else: 231 | self.r50 = 1.059 * r50ion - 0.37 232 | return self.r50 233 | else: 234 | self.r50 = r50ion 235 | return self.r50 236 | 237 | def r50_to_kQ(self, r50_kQ: dict) -> float: 238 | r50Values = [float(x) for x in r50_kQ.keys()] 239 | kQValues = [float(x) for x in r50_kQ.values()] 240 | r50Values.sort() 241 | kQValues.sort(reverse=True) 242 | 243 | #check if R50 value is within bounds 244 | if self.r50 < r50Values[0] or self.r50 > r50Values[-1]: 245 | raise ValueError(f"R-50 value {self.r50} is not within an acceptable range: " \ 246 | f"{r50Values[0]} - {r50Values[-1]}") 247 | else: 248 | kQSpline = CubicSpline(r50Values, kQValues) 249 | self.kQQo = float(kQSpline(self.r50)) 250 | return self.kQQo 251 | 252 | def r50_to_kQQint(self, r50: float, r50_kQQint: dict) -> float: 253 | r50Values = [float(x) for x in r50_kQQint.keys()] 254 | kQQintValues = [float(x) for x in r50_kQQint.values()] 255 | r50Values.sort() 256 | kQQintValues.sort(reverse=True) 257 | 258 | #check if R50 value is within bounds 259 | if r50 < r50Values[0] or r50 > r50Values[-1]: 260 | raise ValueError(f"R-50 value {r50} is not within an acceptable range: " \ 261 | f"{r50Values[0]} - {r50Values[-1]}") 262 | else: 263 | kQSpline = CubicSpline(r50Values, kQQintValues) 264 | return float(kQSpline(r50)) 265 | 266 | @property 267 | def ref_depth(self) -> float: 268 | return 0.6*self.r50 - 0.1 269 | -------------------------------------------------------------------------------- /core/configuration/config.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from pathlib import Path 18 | import json 19 | 20 | currDir = Path(__file__).parent 21 | 22 | chambersFile = currDir / "data/chambers.json" 23 | settingsFile = currDir / "data/settings.json" 24 | workspaceFile = currDir / "data/workspace.json" 25 | 26 | with chambersFile.open(encoding="utf-8") as file: 27 | chambersConfig = json.load(file) 28 | 29 | with settingsFile.open(encoding="utf-8") as file: 30 | settingsConfig = json.load(file) 31 | 32 | with workspaceFile.open(encoding="utf-8") as file: 33 | workspaceConfig = json.load(file) 34 | 35 | class Config: 36 | def saveConfig(self, path: Path, config: dict): 37 | with open(path.absolute(), 'w', encoding="utf-8") as file: 38 | json.dump(config, file, indent=4, ensure_ascii=False) 39 | 40 | class ChambersConfig(Config): 41 | path = chambersFile 42 | config = chambersConfig 43 | 44 | def saveConfig(self, config): 45 | self.config = config 46 | return super().saveConfig(self.path, config) 47 | 48 | def getConfig(self) -> dict: 49 | return self.config 50 | 51 | class SettingsConfig(Config): 52 | path = settingsFile 53 | config = settingsConfig 54 | 55 | def saveConfig(self, config): 56 | self.config = config 57 | return super().saveConfig(self.path, config) 58 | 59 | def getConfig(self) -> dict: 60 | return self.config 61 | 62 | class WorkspaceConfig(Config): 63 | path = workspaceFile 64 | config = workspaceConfig 65 | 66 | def saveConfig(self, config): 67 | self.config = config 68 | return super().saveConfig(self.path, config) 69 | 70 | def getConfig(self) -> dict: 71 | return self.config 72 | -------------------------------------------------------------------------------- /core/configuration/data/phantoms.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/configuration/data/phantoms.json -------------------------------------------------------------------------------- /core/configuration/data/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "trs398": { 3 | "tolerance": 1.0, 4 | "reference_temperature": 20.0, 5 | "reference_pressure": 101.325, 6 | "reference_humidity": 50, 7 | "photon_calibration": { 8 | "allowed_chambers": "all" 9 | }, 10 | "electron_calibration": { 11 | "allowed_chambers": [ 12 | "Attix RMI 449", 13 | "Capintec PS-033", 14 | "Exradin P11", 15 | "Holt (Memorial)", 16 | "NACP01", 17 | "NACP02", 18 | "Markus", 19 | "Roos", 20 | "Capintec PR-06C/G Farmer", 21 | "Exradin A2 Spokas (2 mm cap)", 22 | "Exradin T2 Spokas (4 mm cap)", 23 | "Exradin A12 (Farmer)", 24 | "NE 2571 Farmer", 25 | "NE 2581 (Polystyrene cap)", 26 | "NE 2581 (PMMA cap)", 27 | "PTW 30001", 28 | "PTW 30010", 29 | "PTW 30002 / 30011", 30 | "PTW 30004 / 30012", 31 | "PTW 31002 flexible", 32 | "PTW 31003 flexible", 33 | "Victoreen 30-348", 34 | "Victoreen 30-351", 35 | "Victoreen 30-349" 36 | ] 37 | } 38 | }, 39 | "winston_lutz": { 40 | "tolerance": 1.0 41 | } 42 | } -------------------------------------------------------------------------------- /core/configuration/data/workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "workspace_path": "null", 3 | "institution_name": "", 4 | "department_name": "" 5 | } -------------------------------------------------------------------------------- /core/database/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/database/__init__.py -------------------------------------------------------------------------------- /core/image/dicom.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/image/dicom.py -------------------------------------------------------------------------------- /core/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/__init__.py -------------------------------------------------------------------------------- /core/tools/devices.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | import json 18 | 19 | class Device: 20 | ''' Base class for some devices used in radiotherapy/QA 21 | ''' 22 | def __init__(self, name: str, 23 | manufacturer: str, 24 | model_name: str, 25 | serial_num: str): 26 | 27 | self._name = name 28 | self._manufacturer = manufacturer 29 | self._model_name = model_name 30 | self._serial_num = serial_num 31 | 32 | @property 33 | def name(self): 34 | return self._name 35 | 36 | @name.setter 37 | def name(self, name: str): 38 | self._name = name 39 | 40 | @property 41 | def manufacturer(self): 42 | return self._manufacturer 43 | 44 | @manufacturer.setter 45 | def manufacturer(self, manufacturer: str): 46 | self._manufacturer = manufacturer 47 | 48 | @property 49 | def model_name(self): 50 | return self._model_name 51 | 52 | @model_name.setter 53 | def model_name(self, model_name: str): 54 | self._model_name = model_name 55 | 56 | @property 57 | def serial_num(self) -> str: 58 | return self._serial_num 59 | 60 | @serial_num.setter 61 | def serial_num(self, serial_num: str): 62 | self._serial_num = serial_num 63 | 64 | class Linac(Device): 65 | def __init__(self, name: str, 66 | manufacturer: str, 67 | model_name: str, 68 | serial_num: str, 69 | beams: dict): 70 | super().__init__(name=name, manufacturer=manufacturer, serial_num=serial_num, 71 | model_name=model_name) 72 | 73 | self._beams = beams 74 | 75 | @property 76 | def beams(self): 77 | return self._beams 78 | 79 | @beams.setter 80 | def beams(self, beams: dict): 81 | self._beams = beams 82 | 83 | @classmethod 84 | def fromDictionary(cls, name: str, properties: dict): 85 | ''' Loads the device from a dictionary object 86 | ''' 87 | for key in properties.keys(): 88 | if key == "manufacturer": 89 | manufacturer = properties[key] 90 | elif key == "modelName": 91 | model_name = properties[key] 92 | elif key == "serialNum": 93 | serial_num = properties[key] 94 | elif key == "beams": 95 | beams = properties[key] 96 | 97 | #TODO check if the values exist 98 | return cls(name, manufacturer, model_name, serial_num, beams) 99 | 100 | class IonChamber(Device): 101 | 102 | ionChamberTypes = ("Cylindrical", "Plane-parallel") 103 | 104 | def __init__(self, name: str, 105 | manufacturer: str, 106 | model_name: str, 107 | serial_num: str, 108 | calibration_lab: str, 109 | calibration_date: str, 110 | calibration_source: str, 111 | chamber_type: str): 112 | super().__init__(name=name, manufacturer=manufacturer, serial_num=serial_num, 113 | model_name=model_name) 114 | 115 | self._calibration_lab = calibration_lab 116 | self._calibration_date = calibration_date 117 | self._calibration_source = calibration_source 118 | self._chamber_type = chamber_type 119 | 120 | @property 121 | def calibration_lab(self): 122 | return self._calibration_lab 123 | 124 | @calibration_lab.setter 125 | def calibration_lab(self, calibration_lab: str): 126 | self._calibration_lab = calibration_lab 127 | 128 | @property 129 | def calibration_date(self): 130 | return self._calibration_date 131 | 132 | @calibration_date.setter 133 | def calibration_date(self, calibration_date: str): 134 | self._calibration_date = calibration_date 135 | 136 | @property 137 | def calibration_source(self): 138 | return self._calibration_source 139 | 140 | @calibration_source.setter 141 | def calibration_source(self, calibration_source: str): 142 | self._calibration_source = calibration_source 143 | 144 | @classmethod 145 | def fromDictionary(cls, name: str, properties: dict): 146 | ''' Loads the device from a dictionary object 147 | ''' 148 | for key in properties.keys(): 149 | if key == "manufacturer": 150 | manufacturer = properties[key] 151 | elif key == "modelName": 152 | model_name = properties[key] 153 | elif key == "serialNum": 154 | serial_num = properties[key] 155 | elif key == "calibrationDate": 156 | cal_date = properties[key] 157 | elif key == "calibrationLab": 158 | cal_lab = properties[key] 159 | elif key == "calibrationSource": 160 | cal_source = properties[key] 161 | 162 | #TODO check if the values exist 163 | return cls(name, 164 | manufacturer, 165 | model_name, 166 | serial_num, 167 | cal_lab, 168 | cal_date, 169 | cal_source) 170 | 171 | class Electrometer(Device): 172 | def __init__(self, name: str, 173 | manufacturer: str, 174 | model_name: str, 175 | serial_num: str, 176 | calibration_lab: str, 177 | calibration_date: str): 178 | super().__init__(name=name, manufacturer=manufacturer, serial_num=serial_num, model_name=model_name) 179 | 180 | self._calibration_lab = calibration_lab 181 | self._calibration_date = calibration_date 182 | 183 | @property 184 | def calibration_lab(self): 185 | return self.calibration_lab 186 | 187 | @calibration_lab.setter 188 | def calibration_lab(self, calibration_lab: str): 189 | self.calibration_lab = calibration_lab 190 | 191 | @property 192 | def calibration_date(self): 193 | return self._calibration_date 194 | 195 | @calibration_date.setter 196 | def calibration_date(self, calibration_date: str): 197 | self.calibration_date = calibration_date 198 | 199 | class DeviceManager: 200 | device_list = {"linacs": [], "ionChambers": [], "electrometer": []} 201 | 202 | @classmethod 203 | def loadDevices(cls, path: str): 204 | with open(path, 'r') as devicesFile: 205 | saved_devices = json.load(devicesFile) 206 | 207 | for device_type in set(saved_devices).intersection(set(cls.device_list)): 208 | if device_type == "linacs": 209 | for linac in saved_devices[device_type]: 210 | cls.device_list[device_type].append(Linac.fromDictionary(linac, 211 | saved_devices[device_type][linac])) 212 | 213 | @classmethod 214 | def addDevice(cls, device: Device): 215 | if isinstance(device, Linac): 216 | cls.device_list["linacs"] = device 217 | -------------------------------------------------------------------------------- /core/tools/list.json: -------------------------------------------------------------------------------- 1 | { 2 | "linacs": { 3 | "Test Linac 1": { 4 | "serialNum": "SERIAL NUMBER XXX1", 5 | "modelName": "Versa HD", 6 | "manufacturer": "Elekta", 7 | "beams": { 8 | "photons": [6, 9, 12, 18], 9 | "photonsFFF": [6, 9, 12], 10 | "electrons": [6, 9, 12, 15] 11 | } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /core/tools/report_assets/depth_zmax.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/depth_zmax.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/dw_zmax.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/dw_zmax.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/dw_zref.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/dw_zref.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/kElec.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/kElec.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/kPol.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/kPol.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/kQQo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/kQQo.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/kS.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/kS.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/kTP.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/kTP.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/kVol.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/kVol.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/pdd_zref.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/pdd_zref.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/r50.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/r50.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/tmr_zref.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/tmr_zref.pdf -------------------------------------------------------------------------------- /core/tools/report_assets/tpr2010.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/core/tools/report_assets/tpr2010.pdf -------------------------------------------------------------------------------- /core/tools/setup.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import QApplication 18 | from PySide6.QtWidgets import (QWidget, QLabel, QDialog, QHBoxLayout, QFormLayout, 19 | QLineEdit, QPushButton, QSizePolicy, QFileDialog, QGroupBox, 20 | QVBoxLayout,) 21 | from PySide6.QtCore import Qt 22 | from PySide6.QtSvgWidgets import QSvgWidget, QGraphicsSvgItem 23 | 24 | from ui import move_to_screen_center, resize_to_available_screen 25 | from ui.util_widgets.wizard import WizardWindow, WizardPage 26 | from ui.py_ui import icons_rc 27 | 28 | from pathlib import Path 29 | from core.configuration.config import WorkspaceConfig 30 | 31 | import sys 32 | 33 | workspace_config = WorkspaceConfig() 34 | license_path = "core/about/license.rtf" 35 | 36 | def init_setup_wizard() -> None: 37 | 38 | setup_info = {"accepted_terms": False, 39 | "workspace_dir": None} 40 | 41 | setup_wizard = WizardWindow() 42 | resize_to_available_screen(setup_wizard, (0.6, 0.8)) 43 | move_to_screen_center(setup_wizard) 44 | 45 | with open(license_path, mode='r') as license_file: 46 | license_text = license_file.read() 47 | 48 | # License page 49 | license_text_viewer = QLabel(license_text) 50 | license_page = WizardPage(license_text_viewer, "PyBeam QA License", 51 | "First Things First") 52 | setup_wizard.add_page(license_page) 53 | 54 | # Workspace page 55 | workspace_main = QWidget() 56 | workspace_main_layout = QVBoxLayout(workspace_main) 57 | workspace_main_layout.setSpacing(20) 58 | workspace_main.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) 59 | 60 | workspace_dir_grp = QGroupBox("Workspace Directory") 61 | workspace_dir_grp.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) 62 | 63 | institution_grp = QGroupBox("Institution Details") 64 | institution_grp.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) 65 | 66 | workspace_main_layout.addWidget(workspace_dir_grp) 67 | workspace_main_layout.addWidget(institution_grp) 68 | 69 | workspace_form_lay = QFormLayout(workspace_dir_grp) 70 | workspace_form_lay.setHorizontalSpacing(20) 71 | workspace_hbox_lay = QHBoxLayout() 72 | workspace_hbox_lay.setSpacing(6) 73 | folder_label = QLabel("Workspace path:") 74 | folder_field = QLineEdit() 75 | #folder_field.setMinimumWidth(300) 76 | folder_field.setReadOnly(True) 77 | select_button = QPushButton("Select Folder") 78 | select_button.clicked.connect(lambda: select_workspace_folder(folder_field)) 79 | 80 | workspace_hbox_lay.addWidget(folder_field) 81 | workspace_hbox_lay.addWidget(select_button) 82 | workspace_form_lay.addRow(folder_label, workspace_hbox_lay) 83 | 84 | institution_form_lay = QFormLayout() 85 | institution_form_lay.setHorizontalSpacing(20) 86 | institution_label = QLabel("Institution:") 87 | institution_field = QLineEdit() 88 | department_label = QLabel("Department:") 89 | department_field = QLineEdit() 90 | 91 | institution_grp.setLayout(institution_form_lay) 92 | institution_form_lay.addRow(institution_label, institution_field) 93 | institution_form_lay.addRow(department_label, department_field) 94 | 95 | workspace_page = WizardPage(workspace_main, "Workspace Setup", 96 | "Setup Your Workspace") 97 | workspace_page.register_field(folder_field) 98 | workspace_page.register_field(institution_field) 99 | workspace_page.register_field(department_field) 100 | setup_wizard.add_page(workspace_page) 101 | 102 | result = setup_wizard.exec() 103 | 104 | if result == QDialog.DialogCode.Accepted: 105 | config = workspace_config.getConfig() 106 | config["workspace_path"] = folder_field.text() 107 | config["institution_name"] = institution_field.text() 108 | config["department_name"] = department_field.text() 109 | 110 | workspace_config.saveConfig(config) 111 | 112 | Path(config["workspace_path"]).mkdir(exist_ok = True) 113 | else: 114 | sys.exit() 115 | 116 | def select_workspace_folder(line_edit: QLineEdit): 117 | caption = "Select a Folder for the Workspace" 118 | folder = QFileDialog.getExistingDirectory(caption=caption) 119 | 120 | if folder and "PyBeamQA Workspace" not in folder: 121 | line_edit.setText(folder + "/PyBeamQA Workspace") 122 | 123 | else: 124 | line_edit.setText(folder) -------------------------------------------------------------------------------- /core/tools/toreportlab.py: -------------------------------------------------------------------------------- 1 | # A part of pdfrw (https://github.com/pmaupin/pdfrw) 2 | # Copyright (C) 2006-2015 Patrick Maupin, Austin, Texas 3 | # MIT license -- See LICENSE.txt for details 4 | 5 | ''' 6 | Converts pdfrw objects into reportlab objects. 7 | 8 | Designed for and tested with rl 2.3. 9 | 10 | Knows too much about reportlab internals. 11 | What can you do? 12 | 13 | The interface to this function is through the makerl() function. 14 | 15 | Parameters: 16 | canv - a reportlab "canvas" (also accepts a "document") 17 | pdfobj - a pdfrw PDF object 18 | 19 | Returns: 20 | A corresponding reportlab object, or if the 21 | object is a PDF Form XObject, the name to 22 | use with reportlab for the object. 23 | 24 | Will recursively convert all necessary objects. 25 | Be careful when converting a page -- if /Parent is set, 26 | will recursively convert all pages! 27 | 28 | Notes: 29 | 1) Original objects are annotated with a 30 | derived_rl_obj attribute which points to the 31 | reportlab object. This keeps multiple reportlab 32 | objects from being generated for the same pdfobj 33 | via repeated calls to makerl. This is great for 34 | not putting too many objects into the 35 | new PDF, but not so good if you are modifying 36 | objects for different pages. Then you 37 | need to do your own deep copying (of circular 38 | structures). You're on your own. 39 | 40 | 2) ReportLab seems weird about FormXObjects. 41 | They pass around a partial name instead of the 42 | object or a reference to it. So we have to 43 | reach into reportlab and get a number for 44 | a unique name. I guess this is to make it 45 | where you can combine page streams with 46 | impunity, but that's just a guess. 47 | 48 | 3) Updated 1/23/2010 to handle multipass documents 49 | (e.g. with a table of contents). These have 50 | a different doc object on every pass. 51 | 52 | ''' 53 | 54 | from reportlab.pdfbase import pdfdoc as rldocmodule 55 | from pdfrw.objects import PdfDict, PdfArray, PdfName 56 | from pdfrw.py23_diffs import convert_store 57 | 58 | RLStream = rldocmodule.PDFStream 59 | RLDict = rldocmodule.PDFDictionary 60 | RLArray = rldocmodule.PDFArray 61 | 62 | 63 | def _makedict(rldoc, pdfobj): 64 | rlobj = rldict = RLDict() 65 | if pdfobj.indirect: 66 | rlobj.__RefOnly__ = 1 67 | rlobj = rldoc.Reference(rlobj) 68 | pdfobj.derived_rl_obj[rldoc] = rlobj, None 69 | 70 | for key, value in pdfobj.iteritems(): 71 | rldict[key[1:]] = makerl_recurse(rldoc, value) 72 | 73 | return rlobj 74 | 75 | 76 | def _makestream(rldoc, pdfobj, xobjtype=PdfName.XObject): 77 | rldict = RLDict() 78 | rlobj = RLStream(rldict, convert_store(pdfobj.stream)) 79 | 80 | if pdfobj.Type == xobjtype: 81 | shortname = 'pdfrw_%s' % (rldoc.objectcounter + 1) 82 | fullname = rldoc.getXObjectName(shortname) 83 | else: 84 | shortname = fullname = None 85 | result = rldoc.Reference(rlobj, fullname) 86 | pdfobj.derived_rl_obj[rldoc] = result, shortname 87 | 88 | for key, value in pdfobj.iteritems(): 89 | rldict[key[1:]] = makerl_recurse(rldoc, value) 90 | 91 | return result 92 | 93 | 94 | def _makearray(rldoc, pdfobj): 95 | rlobj = rlarray = RLArray([]) 96 | if pdfobj.indirect: 97 | rlobj.__RefOnly__ = 1 98 | rlobj = rldoc.Reference(rlobj) 99 | pdfobj.derived_rl_obj[rldoc] = rlobj, None 100 | 101 | mylist = rlarray.sequence 102 | for value in pdfobj: 103 | mylist.append(makerl_recurse(rldoc, value)) 104 | 105 | return rlobj 106 | 107 | 108 | def _makestr(rldoc, pdfobj): 109 | assert isinstance(pdfobj, (float, int, str)), repr(pdfobj) 110 | # TODO: Add fix for float like in pdfwriter 111 | value = str(getattr(pdfobj, 'encoded', None) or pdfobj) 112 | 113 | try: 114 | value.encode("ascii") # Don't return this, it is just a test. 115 | except UnicodeEncodeError: 116 | value = value.encode("Latin-1") 117 | return value 118 | 119 | 120 | def makerl_recurse(rldoc, pdfobj): 121 | docdict = getattr(pdfobj, 'derived_rl_obj', None) 122 | if docdict is not None: 123 | value = docdict.get(rldoc) 124 | if value is not None: 125 | return value[0] 126 | if isinstance(pdfobj, PdfDict): 127 | if pdfobj.stream is not None: 128 | func = _makestream 129 | else: 130 | func = _makedict 131 | if docdict is None: 132 | pdfobj.private.derived_rl_obj = {} 133 | elif isinstance(pdfobj, PdfArray): 134 | func = _makearray 135 | if docdict is None: 136 | pdfobj.derived_rl_obj = {} 137 | else: 138 | func = _makestr 139 | return func(rldoc, pdfobj) 140 | 141 | 142 | def makerl(canv, pdfobj): 143 | try: 144 | rldoc = canv._doc 145 | except AttributeError: 146 | rldoc = canv 147 | rlobj = makerl_recurse(rldoc, pdfobj) 148 | try: 149 | name = pdfobj.derived_rl_obj[rldoc][1] 150 | except AttributeError: 151 | name = None 152 | return name or rlobj 153 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import QApplication 18 | from PySide6.QtGui import QIcon 19 | 20 | from ui.app_main_win import AppMainWin 21 | from core.tools.devices import DeviceManager 22 | from core.tools.setup import init_setup_wizard 23 | from core.configuration.config import WorkspaceConfig 24 | 25 | import platform 26 | import sys 27 | import json 28 | 29 | app = QApplication(sys.argv) 30 | 31 | app.setWindowIcon(QIcon(u":/misc_icons/icons/ic_app_alt.svg").pixmap(48)) 32 | app.setStyle('Fusion') 33 | app.setStyleSheet("QLineEdit, QDateEdit, QPushButton, QDoubleSpinBox," \ 34 | "QComboBox, QSpinBox { min-height: 20px;}") 35 | 36 | def initFiles(): 37 | DeviceManager.loadDevices("core/tools/list.json") 38 | 39 | workspace = WorkspaceConfig().getConfig() 40 | 41 | if workspace["workspace_path"] == None: 42 | init_setup_wizard() 43 | 44 | if __name__ == "__main__": 45 | 46 | initFiles() 47 | 48 | plt = platform.system() 49 | 50 | if plt == "Windows": 51 | import ctypes 52 | 53 | appID = u"radlab.pybeamqa-0.1.2" 54 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appID) 55 | 56 | QMainWin = AppMainWin() 57 | QMainWin.show() 58 | 59 | app.exec() 60 | 61 | -------------------------------------------------------------------------------- /ui/__init__.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QMainWindow 2 | from PySide6.QtGui import QGuiApplication 3 | 4 | import pylinac.core.array_utils as array_utils 5 | from pylinac.core.decorators import validate 6 | from pylinac import __version__ as pylinac_version 7 | 8 | import sys, os 9 | import numpy as np 10 | 11 | curr_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) 12 | 13 | #Add all paths pointing to icons here 14 | sys.path.insert(0, curr_dir + "/ui/py_ui/") 15 | 16 | #TODO Perform this monkey-patch for pylinac version <= 3.26 17 | 18 | def array_not_empty(array: np.ndarray) -> None: 19 | """Check an array isn't empty""" 20 | if not array.size: 21 | raise ValueError("Array must not be empty") 22 | 23 | @validate(array=array_not_empty) 24 | def normalize(array: np.ndarray, value: float | None = None) -> np.ndarray: 25 | """Normalize an array to the passed value. If not value is passed, normalize to the maximum value""" 26 | if value is None: 27 | val = array.max() 28 | else: 29 | val = value 30 | array = array / val 31 | return array.astype(np.float16) 32 | 33 | pylinac_version = pylinac_version.split(".") 34 | 35 | if int(pylinac_version[0]) == 3 and int(pylinac_version[1]) <= 27: 36 | array_utils.normalize = normalize 37 | 38 | # ---------- Utility functions for windows ------------- 39 | def move_to_screen_center(window: QMainWindow) -> None: 40 | """ 41 | Place window at the center of the current screen 42 | """ 43 | primary_screen = QGuiApplication.primaryScreen() 44 | screen_center = primary_screen.availableGeometry().center() 45 | 46 | window.move(screen_center - window.rect().center()) 47 | 48 | def resize_to_available_screen(window: QMainWindow, value: tuple): 49 | """ 50 | Place window at the center of the current screen 51 | """ 52 | primary_screen = QGuiApplication.primaryScreen() 53 | screen_size = primary_screen.availableSize().toTuple() 54 | 55 | window.resize(screen_size[0] * value[0], screen_size[1] * value[1]) -------------------------------------------------------------------------------- /ui/convertUI.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "----- Converting Qt resources to python files -----" 3 | mkdir py_ui 4 | echo 5 | for file in qt_ui/*.ui 6 | do 7 | base_name=$(basename -- ${file}) 8 | echo "converting ui file: ${base_name} --> ${base_name%.*}_ui.py" 9 | pyside6-uic "${file}" -o "py_ui/${base_name%.*}_ui.py" 10 | done 11 | 12 | for file in qt_ui/*.qrc 13 | do 14 | base_name=$(basename -- ${file}) 15 | echo "converting resource file: ${base_name} --> ${base_name%.*}_rc.py" 16 | pyside6-rcc "${file}" -o "py_ui/${base_name%.*}_rc.py" 17 | done 18 | -------------------------------------------------------------------------------- /ui/linac_qa/picket_fence_offsets_dialog.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import QDialog, QWidget 18 | 19 | from ui.py_ui.picket_fence_offsets_dialog_ui import Ui_PFOffsetDialog 20 | 21 | class PFTestDialog(QDialog): 22 | 23 | def __init__(self, parent: QWidget | None = None): 24 | super().__init__(parent) 25 | self.ui = Ui_PFOffsetDialog() 26 | self.ui.setupUi(self) 27 | 28 | self.setWindowTitle("Set leaf-pair offsets") 29 | -------------------------------------------------------------------------------- /ui/linac_qa/picket_fence_test_dialog.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import QDialog, QWidget, QDialogButtonBox, QFileDialog, QLineEdit 18 | 19 | from ui.py_ui.picket_fence_test_dialog_ui import Ui_PFTestDialog 20 | 21 | import ast 22 | 23 | class PFTestDialog(QDialog): 24 | 25 | def __init__(self, parent: QWidget | None = None): 26 | super().__init__(parent) 27 | self.ui = Ui_PFTestDialog() 28 | self.ui.setupUi(self) 29 | 30 | self.setWindowTitle("Picket Fence Benchmark Testing") 31 | 32 | self.apply_btn = self.ui.button_box.button(QDialogButtonBox.StandardButton.Apply) 33 | self.apply_btn.clicked.connect(self.accept) 34 | 35 | self.ui.picket_offsets_le.textChanged.connect(self.validate_info) 36 | self.ui.num_pickets_sb.valueChanged.connect(self.validate_info) 37 | self.ui.test_name_le.textChanged.connect(self.validate_info) 38 | self.ui.out_file_le.textChanged.connect(self.validate_info) 39 | self.ui.select_file_btn.clicked.connect(lambda: self.save_file_to(self.ui.out_file_le)) 40 | 41 | self.ui.sim_image_cb.addItems(["AS500", "AS1000", "AS1200"]) 42 | self.ui.image_orientation_cb.addItems(["Up-Down", "Left-Right"]) 43 | 44 | self.apply_btn.setEnabled(False) 45 | 46 | self.picket_offset_errors = [] 47 | 48 | def save_file_to(self, line_edit: QLineEdit): 49 | file_path = QFileDialog.getSaveFileName(caption="Save As...", filter="DICOM (*.dcm)") 50 | 51 | if file_path[0] != "": 52 | path = file_path[0].split("/") 53 | 54 | if not path[-1].endswith(".dcm"): 55 | path[-1] = path[-1] + ".dcm" 56 | 57 | line_edit.setText("/".join(path)) 58 | 59 | def validate_info(self): 60 | pf_offset_errors = self.ui.picket_offsets_le.text() 61 | self.picket_offset_errors.clear() 62 | 63 | if pf_offset_errors != "": 64 | offset_error_list = pf_offset_errors.split("; ") 65 | 66 | if len(offset_error_list) == self.ui.num_pickets_sb.value(): 67 | for err_value in offset_error_list: 68 | try: 69 | err_value = ast.literal_eval(err_value) 70 | 71 | if isinstance(err_value, (float, int)): 72 | self.picket_offset_errors.append(err_value) 73 | 74 | else: return self.apply_btn.setEnabled(False) 75 | 76 | except Exception as err: 77 | return self.apply_btn.setEnabled(False) 78 | 79 | else: return self.apply_btn.setEnabled(False) 80 | 81 | elif pf_offset_errors == "": 82 | self.apply_btn.setEnabled(True) 83 | 84 | else: return self.apply_btn.setEnabled(False) 85 | 86 | if self.ui.test_name_le.text() != "" and self.ui.out_file_le.text() != "": 87 | self.apply_btn.setEnabled(True) 88 | 89 | else: self.apply_btn.setEnabled(False) 90 | -------------------------------------------------------------------------------- /ui/linac_qa/qa_tools_win.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import (QLineEdit, QMainWindow, QDialogButtonBox, QDialog, QFileDialog) 18 | from PySide6.QtCore import Qt, Signal, QDate 19 | from PySide6.QtGui import QPixmap, QDesktopServices 20 | 21 | from ui.py_ui import icons_rc 22 | from ui.py_ui.qa_main_win_ui import Ui_MainWindow 23 | from ui.util_widgets.statusbar import AnalysisInfoLabel 24 | from ui.util_widgets.dialogs import MessageDialog 25 | from ui.util_widgets.dialogs import AboutDialog 26 | 27 | class QAToolsWindow(QMainWindow): 28 | 29 | windowClosing = Signal() 30 | 31 | def __init__(self, initData: dict | None = None): 32 | super().__init__() 33 | 34 | self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True) 35 | 36 | self.ui = Ui_MainWindow() 37 | self.ui.setupUi(self) 38 | self.worksheetType = None 39 | self.window_title = "" 40 | self.window_close_signalled = False 41 | 42 | self.untitled_counter = 0 43 | 44 | #self.ui.menuHelp.addAction("Contents", lambda: print(1), "F1") 45 | self.ui.menuHelp.addSeparator() 46 | self.ui.menuHelp.addAction("Report Issue", lambda: QDesktopServices.openUrl( 47 | "https://github.com/Quantico-Bullet/PyBeam-QA/issues")) 48 | self.ui.menuHelp.addAction("Request Feature", lambda: QDesktopServices.openUrl( 49 | "https://github.com/Quantico-Bullet/PyBeam-QA/issues")) 50 | self.ui.menuHelp.addSeparator() 51 | 52 | # Add app about 53 | self.ui.menuHelp.addAction("About PyBeam QA...", self.about_app) 54 | 55 | full_screen_action = self.ui.menuView.addAction("Full Screen") 56 | full_screen_action.setEnabled(False) 57 | full_screen_action.setShortcut("F11") 58 | full_screen_action.setCheckable(True) 59 | #full_screen_action.toggled.connect(self.set_window_state) 60 | 61 | # setup basic dock functionality 62 | self.ui.dockWidget.close() 63 | 64 | #copyright_text = QLabel("PyBeam QA - v0.1.0 (Copyright © 2023 Kagiso Lebang)") 65 | #copyright_text.setAlignment(Qt.AlignmentFlag.AlignCenter) 66 | 67 | #self.ui.statusbar.addPermanentWidget( 68 | # copyright_text, 1) 69 | 70 | #Add analysis message to status bar 71 | self.analysis_info_label = AnalysisInfoLabel() 72 | self.ui.statusbar.addWidget(self.analysis_info_label) 73 | 74 | self.curr_analy_message = None 75 | self.curr_analy_state = AnalysisInfoLabel.IDLE 76 | 77 | self.ui.tabWidget.currentChanged.connect(self.tab_window_changed) 78 | self.ui.tabWidget.tabCloseRequested.connect(self.tab_close_requested) 79 | 80 | self.current_tab_index = 0 81 | self.current_window_state = self.isMaximized() 82 | 83 | def add_new_worksheet(self, worksheet, worksheet_name: str, enable_icon: bool = True): 84 | index = self.ui.tabWidget.addTab(worksheet, worksheet_name) 85 | 86 | if enable_icon: 87 | tab_icon = QPixmap(u":/colorIcons/icons/tools.png") 88 | 89 | self.ui.tabWidget.setCurrentIndex(index) 90 | self.ui.tabWidget.setTabIcon(index, tab_icon) 91 | 92 | def tab_window_changed(self, index: int): 93 | if self.ui.tabWidget.count() > 0: 94 | title = self.ui.tabWidget.tabText(self.ui.tabWidget.currentIndex()) 95 | self.setWindowTitle(title + " | " + self.window_title) 96 | 97 | self.analysis_worksheet = self.ui.tabWidget.widget(index) 98 | analysis_state = self.analysis_worksheet.analysis_state 99 | analysis_message = self.analysis_worksheet.analysis_message 100 | 101 | self.analysis_info_label.set_message(analysis_state, analysis_message) 102 | self.analysis_worksheet.analysis_info_signal.connect(lambda: "dummy disconnect") 103 | self.analysis_worksheet.analysis_info_signal.disconnect() 104 | self.analysis_worksheet.analysis_info_signal.connect(lambda x: 105 | self.analysis_info_label.set_message(x["state"], x["message"])) 106 | 107 | elif self.ui.tabWidget.count() == 0: 108 | self.close() 109 | 110 | def tab_close_requested(self, tab_index: int): 111 | warning_dialog = MessageDialog() 112 | warning_dialog.set_icon(MessageDialog.WARNING_ICON) 113 | warning_dialog.set_title("Close Tab") 114 | warning_dialog.set_header_text("Do you want to close this tab?") 115 | warning_dialog.set_info_text("Closing this tab will remove any unsaved work, proceed?") 116 | 117 | warning_dialog.set_standard_buttons(QDialogButtonBox.StandardButton.Yes | 118 | QDialogButtonBox.StandardButton.No) 119 | 120 | response = warning_dialog.exec_() 121 | 122 | if response == QDialog.DialogCode.Accepted: 123 | self.ui.tabWidget.removeTab(tab_index) 124 | 125 | def about_app(self): 126 | about = AboutDialog() 127 | about.exec() 128 | 129 | del about 130 | -------------------------------------------------------------------------------- /ui/linac_qa/starshot_test_dialog.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import (QDialog, QWidget, QDialogButtonBox, QFileDialog, 18 | QLineEdit) 19 | 20 | from ui.py_ui.starshot_test_dialog_ui import Ui_StarshotTestDialog 21 | 22 | class StarshotTestDialog(QDialog): 23 | 24 | def __init__(self, parent: QWidget | None = None): 25 | super().__init__(parent) 26 | self.ui = Ui_StarshotTestDialog() 27 | self.ui.setupUi(self) 28 | 29 | self.setWindowTitle("Starshot Benchmark Testing") 30 | self.setFixedSize(self.size()) 31 | 32 | self.apply_btn = self.ui.button_box.button(QDialogButtonBox.StandardButton.Apply) 33 | self.apply_btn.clicked.connect(self.accept) 34 | self.apply_btn.setEnabled(False) 35 | self.ui.sim_image_cb.addItems(["AS500", "AS1000", "AS1200"]) 36 | 37 | self.ui.select_file_btn.clicked.connect(lambda: self.save_file_to(self.ui.out_file_le)) 38 | self.ui.test_name_le.textChanged.connect(self.validate_info) 39 | self.ui.out_file_le.textChanged.connect(self.validate_info) 40 | 41 | def save_file_to(self, line_edit: QLineEdit): 42 | file_path = QFileDialog.getSaveFileName(caption="Save As...", filter="DICOM (*.dcm)") 43 | 44 | if file_path[0] != "": 45 | path = file_path[0].split("/") 46 | 47 | if not path[-1].endswith(".dcm"): 48 | path[-1] = path[-1] + ".dcm" 49 | 50 | line_edit.setText("/".join(path)) 51 | 52 | def validate_info(self): 53 | if self.ui.test_name_le.text() != "" and self.ui.out_file_le.text() != "": 54 | self.apply_btn.setEnabled(True) 55 | 56 | else: self.apply_btn.setEnabled(False) 57 | 58 | 59 | -------------------------------------------------------------------------------- /ui/linac_qa/winston_lutz_test_dialog.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import QDialog, QWidget, QDialogButtonBox, QFileDialog 18 | 19 | from ui.py_ui.winston_lutz_test_dialog_ui import Ui_WLTestDialog 20 | 21 | import ast 22 | 23 | class WLTestDialog(QDialog): 24 | 25 | def __init__(self, parent: QWidget | None = None): 26 | super().__init__(parent) 27 | self.ui = Ui_WLTestDialog() 28 | self.ui.setupUi(self) 29 | 30 | self.setWindowTitle("Winston Lutz Benchmark Testing") 31 | 32 | self.apply_btn = self.ui.button_box.button(QDialogButtonBox.StandardButton.Apply) 33 | self.apply_btn.clicked.connect(self.accept) 34 | 35 | self.ui.field_type_cb.currentIndexChanged.connect(self.on_field_type_changed) 36 | self.ui.image_axes_le.textChanged.connect(self.validate_info) 37 | self.ui.test_name_le.textChanged.connect(self.validate_info) 38 | self.ui.out_dir_le.textChanged.connect(self.validate_info) 39 | self.ui.select_dir_btn.clicked.connect(self.select_directory) 40 | 41 | self.ui.sim_image_cb.addItems(["AS500", "AS1000", "AS1200"]) 42 | self.ui.field_type_cb.addItems(["Rectangle", "Cone"]) 43 | 44 | self.ui.final_layer_cb.addItems(["Gaussian filter"]) 45 | self.apply_btn.setEnabled(False) 46 | 47 | self.image_axes = [] 48 | 49 | def select_directory(self): 50 | folder = QFileDialog.getExistingDirectory(self) 51 | 52 | if folder: 53 | self.ui.out_dir_le.setText(folder) 54 | 55 | def on_field_type_changed(self): 56 | if self.ui.field_type_cb.currentText() == "Rectangle": 57 | self.ui.cone_field_size_dsb.hide() 58 | self.ui.rec_field_width_label.show() 59 | self.ui.rec_field_height_label.show() 60 | self.ui.rec_field_width_dsb.show() 61 | self.ui.rec_field_height_dsb.show() 62 | 63 | self.ui.field_layer_cb.clear() 64 | self.ui.field_layer_cb.addItems(["Filtered field", "Filter free field", 65 | "Perfect field"]) 66 | 67 | else: 68 | self.ui.cone_field_size_dsb.show() 69 | self.ui.rec_field_width_label.hide() 70 | self.ui.rec_field_height_label.hide() 71 | self.ui.rec_field_width_dsb.hide() 72 | self.ui.rec_field_height_dsb.hide() 73 | 74 | self.ui.field_layer_cb.clear() 75 | self.ui.field_layer_cb.addItems(["Filter free cone", 76 | "Perfect cone"]) 77 | 78 | def validate_info(self): 79 | image_axes_text = self.ui.image_axes_le.text() 80 | self.image_axes.clear() 81 | 82 | if image_axes_text != "": 83 | axes_list = image_axes_text.split("; ") 84 | 85 | if len(axes_list) > 1: 86 | for axes in axes_list: 87 | try: 88 | axes = ast.literal_eval(axes) 89 | if len(axes) == 3: 90 | valid = isinstance(axes[0], int) \ 91 | and isinstance(axes[1], int) \ 92 | and isinstance(axes[2], int) 93 | 94 | if valid: 95 | self.image_axes.append(axes) 96 | self.apply_btn.setEnabled(True) 97 | 98 | else: 99 | return self.apply_btn.setEnabled(False) 100 | 101 | except Exception as err: 102 | return self.apply_btn.setEnabled(False) 103 | 104 | else: 105 | return self.apply_btn.setEnabled(False) 106 | 107 | else: return 108 | 109 | if self.ui.test_name_le.text() == "" or self.ui.out_dir_le.text() == "": 110 | return self.apply_btn.setEnabled(False) 111 | 112 | else: 113 | self.apply_btn.setEnabled(True) 114 | -------------------------------------------------------------------------------- /ui/preferences/prefs_main.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import (QFormLayout, QDialog, QFileDialog, 18 | QLineEdit, QComboBox, QDialogButtonBox, 19 | QHBoxLayout, QPushButton, QSpacerItem, 20 | QSizePolicy) 21 | 22 | from PySide6.QtCore import Qt 23 | from ui.py_ui.preferences_ui import Ui_PreferencesDialog 24 | 25 | class Preferences(QDialog): 26 | 27 | GENERAL_PAGE = 0 28 | DEVICES_PAGE = 1 29 | REPORTING_PAGE = 2 30 | ANALYSIS_TOOLS_PAGE = 3 31 | 32 | def __init__(self, nav_index: tuple = (GENERAL_PAGE, 0)): 33 | super().__init__() 34 | self.ui = Ui_PreferencesDialog() 35 | self.ui.setupUi(self) 36 | 37 | self.setWindowTitle("Preferences ‒ PyBeam QA") 38 | 39 | # Add slots 40 | self.ui.nav_button_group.buttonClicked.connect( 41 | self.change_preferences_page 42 | ) 43 | 44 | #init pages 45 | self.gen_prefs = GeneralPreferences(self.ui) 46 | self.devices_pref = DevicesPreferences(self.ui) 47 | 48 | # Set default views 49 | self.change_preferences_page() 50 | 51 | # Navigate to the initial set index 52 | self.ui.nav_stacked_widget.setCurrentIndex(nav_index[0]) 53 | 54 | def change_preferences_page(self): 55 | checked_button = self.ui.nav_button_group.checkedButton() 56 | 57 | if checked_button == self.ui.general_btn: 58 | self.ui.nav_stacked_widget.setCurrentIndex(self.GENERAL_PAGE) 59 | self.ui.page_title_label.setText(self.ui.general_btn.text()) 60 | 61 | elif checked_button == self.ui.devices_btn: 62 | self.ui.nav_stacked_widget.setCurrentIndex(self.DEVICES_PAGE) 63 | self.ui.page_title_label.setText(self.ui.devices_btn.text()) 64 | 65 | elif checked_button == self.ui.reporting_btn: 66 | self.ui.nav_stacked_widget.setCurrentIndex(self.REPORTING_PAGE) 67 | self.ui.page_title_label.setText(self.ui.reporting_btn.text()) 68 | 69 | elif checked_button == self.ui.analysis_tools_btn: 70 | self.ui.nav_stacked_widget.setCurrentIndex(self.ANALYSIS_TOOLS_PAGE) 71 | self.ui.page_title_label.setText(self.ui.analysis_tools_btn.text()) 72 | 73 | class GeneralPreferences: 74 | 75 | def __init__(self, ui: Ui_PreferencesDialog): 76 | 77 | self.ui = ui 78 | self.ui.workspace_browse_btn.clicked.connect(self.select_workspace_folder) 79 | 80 | def select_workspace_folder(self): 81 | caption = "Select a Folder for the Workspace" 82 | folder = QFileDialog.getExistingDirectory(caption=caption) 83 | 84 | if folder: 85 | self.ui.workspace_loc_le.setText(folder) 86 | 87 | class DevicesPreferences: 88 | 89 | def __init__(self, ui: Ui_PreferencesDialog): 90 | self.ui = ui 91 | 92 | self.ui.linac_comboB.currentIndexChanged.connect(self.linac_view_changed) 93 | self.ui.add_linac_btn.clicked.connect(self.add_new_linac) 94 | 95 | def add_new_linac(self): 96 | linac_name_le = QLineEdit() 97 | linac_manufacturer_comboB = QComboBox() 98 | linac_model_comboB = QComboBox() 99 | linac_serial_num_le = QLineEdit() 100 | linac_serial_num_le.setFixedWidth(200) 101 | photon_beams_le = QLineEdit("2, 4, 6, 8, 10 FFF, 12 FFF") 102 | photon_beams_le.setReadOnly(True) 103 | photon_beams_le.setFixedWidth(200) 104 | photon_beams_le.setClearButtonEnabled(True) 105 | add_photon_beam_btn = QPushButton("Add/Edit") 106 | electron_beams_le = QLineEdit("1, 3, 5, 7") 107 | electron_beams_le.setReadOnly(True) 108 | electron_beams_le.setFixedWidth(200) 109 | electron_beams_le.setClearButtonEnabled(True) 110 | add_electron_beam_btn = QPushButton("Add/Edit") 111 | 112 | linac_manufacturer_comboB.currentTextChanged.connect(lambda x: 113 | self.add_linac_manufacturer_changed(x, linac_model_comboB)) 114 | linac_manufacturer_comboB.addItems(["Elekta", "Varian"]) 115 | 116 | layout = QFormLayout() 117 | layout.addRow("Linac name:", linac_name_le) 118 | layout.addRow("Manufacturer:", linac_manufacturer_comboB) 119 | layout.addRow("Model:", linac_model_comboB) 120 | layout.addRow("Serial No:", linac_serial_num_le) 121 | 122 | photon_beams_layout = QHBoxLayout() 123 | electron_beams_layout = QHBoxLayout() 124 | photon_beams_layout.addWidget(photon_beams_le) 125 | photon_beams_layout.addWidget(add_photon_beam_btn) 126 | electron_beams_layout.addWidget(electron_beams_le) 127 | electron_beams_layout.addWidget(add_electron_beam_btn) 128 | 129 | layout.addRow("Photon beams (MV):", photon_beams_layout) 130 | layout.addRow("Electron beams (MeV):", electron_beams_layout) 131 | layout.addItem(QSpacerItem(0, 10, QSizePolicy.Policy.Fixed, 132 | QSizePolicy.Policy.Fixed)) 133 | 134 | dialog_buttons = QDialogButtonBox() 135 | apply_btn = dialog_buttons.addButton(QDialogButtonBox.StandardButton.Apply) 136 | cancel_btn = dialog_buttons.addButton(QDialogButtonBox.StandardButton.Cancel) 137 | 138 | layout.addWidget(dialog_buttons) 139 | 140 | add_dialog = QDialog() 141 | add_dialog.setWindowTitle("Add New Linac") 142 | add_dialog.setModal(True) 143 | add_dialog.setLayout(layout) 144 | add_dialog.setMinimumSize(add_dialog.sizeHint()) 145 | add_dialog.setMaximumSize(add_dialog.sizeHint()) 146 | apply_btn.clicked.connect(add_dialog.accept) 147 | cancel_btn.clicked.connect(add_dialog.reject) 148 | 149 | result_code = add_dialog.exec() 150 | 151 | if result_code == QDialog.DialogCode.Accepted: 152 | 153 | data = {"linac_name": linac_name_le.text(), 154 | "linac_manufacturer": linac_manufacturer_comboB.currentText(), 155 | "linac_model": linac_model_comboB.currentText(), 156 | "linac_serial_num": linac_serial_num_le.text(), 157 | "beams": {}} 158 | 159 | photon_beams_data = photon_beams_le.text() 160 | electron_beams_data = electron_beams_le.text() 161 | 162 | data["beams"]["photons"] = [] 163 | data["beams"]["photons_fff"] = [] 164 | data["beams"]["electrons"] = [] 165 | 166 | if photon_beams_data: 167 | photon_beams_data = photon_beams_data.split(", ") 168 | 169 | for beam in photon_beams_data: 170 | if "FFF" in beam: 171 | data["beams"]["photons_fff"].append( 172 | int(beam.split(" ")[0])) 173 | 174 | else: 175 | data["beams"]["photons"].append(int(beam)) 176 | 177 | if electron_beams_data: 178 | electron_beams_data = electron_beams_data.split(", ") 179 | 180 | for beam in electron_beams_data: 181 | data["beams"]["electrons"].append(int(beam)) 182 | 183 | self.ui.linac_comboB.addItem(data["linac_name"], data) 184 | self.ui.linac_comboB.setCurrentText(data["linac_name"]) 185 | 186 | def add_linac_manufacturer_changed(self, 187 | manufacturer: str, 188 | linac_model_comboB: QComboBox): 189 | linac_model_comboB.clear() 190 | 191 | if manufacturer == "Varian": 192 | linac_model_comboB.addItems(["Halycon", "TrueBeam", "VitalBeam"]) 193 | 194 | else: 195 | linac_model_comboB.addItems(["Harmony", "Versa HD", "Synergy", "Infinity"]) 196 | 197 | def linac_view_changed(self, index: int): 198 | item_data = self.ui.linac_comboB.itemData(index, 199 | Qt.ItemDataRole.UserRole) 200 | 201 | self.ui.linac_name_field.setText(item_data["linac_name"]) 202 | self.ui.linac_model_field.setText(item_data["linac_model"]) 203 | self.ui.linac_serial_num_field.setText(item_data["linac_serial_num"]) 204 | 205 | self.ui.photon_beam_field.setText(" ".join( 206 | [str(x) for x in item_data["beams"]["photons"]])) 207 | 208 | self.ui.photon_fff_beam_field.setText(" ".join( 209 | [str(x) for x in item_data["beams"]["photons_fff"]])) 210 | 211 | self.ui.electron_beam_field.setText(" ".join( 212 | [str(x) for x in item_data["beams"]["electrons"]])) 213 | -------------------------------------------------------------------------------- /ui/py_ui/about_dialog_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'about_dialog.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.7.0 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QAbstractButton, QApplication, QDialog, QDialogButtonBox, 19 | QFrame, QGridLayout, QGroupBox, QLabel, 20 | QPushButton, QSizePolicy, QSpacerItem, QTextEdit, 21 | QVBoxLayout, QWidget) 22 | import icons_rc 23 | 24 | class Ui_AboutDialog(object): 25 | def setupUi(self, AboutDialog): 26 | if not AboutDialog.objectName(): 27 | AboutDialog.setObjectName(u"AboutDialog") 28 | AboutDialog.resize(550, 322) 29 | sizePolicy = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) 30 | sizePolicy.setHorizontalStretch(0) 31 | sizePolicy.setVerticalStretch(0) 32 | sizePolicy.setHeightForWidth(AboutDialog.sizePolicy().hasHeightForWidth()) 33 | AboutDialog.setSizePolicy(sizePolicy) 34 | self.gridLayout = QGridLayout(AboutDialog) 35 | self.gridLayout.setObjectName(u"gridLayout") 36 | self.frame_2 = QFrame(AboutDialog) 37 | self.frame_2.setObjectName(u"frame_2") 38 | self.frame_2.setFrameShape(QFrame.NoFrame) 39 | self.frame_2.setFrameShadow(QFrame.Raised) 40 | self.gridLayout_2 = QGridLayout(self.frame_2) 41 | self.gridLayout_2.setObjectName(u"gridLayout_2") 42 | self.verticalSpacer_2 = QSpacerItem(20, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) 43 | 44 | self.gridLayout_2.addItem(self.verticalSpacer_2, 4, 0, 1, 1) 45 | 46 | self.app_name_label = QLabel(self.frame_2) 47 | self.app_name_label.setObjectName(u"app_name_label") 48 | sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) 49 | sizePolicy1.setHorizontalStretch(0) 50 | sizePolicy1.setVerticalStretch(0) 51 | sizePolicy1.setHeightForWidth(self.app_name_label.sizePolicy().hasHeightForWidth()) 52 | self.app_name_label.setSizePolicy(sizePolicy1) 53 | font = QFont() 54 | font.setPointSize(14) 55 | self.app_name_label.setFont(font) 56 | 57 | self.gridLayout_2.addWidget(self.app_name_label, 0, 0, 1, 1, Qt.AlignHCenter) 58 | 59 | self.copyright_label = QLabel(self.frame_2) 60 | self.copyright_label.setObjectName(u"copyright_label") 61 | sizePolicy1.setHeightForWidth(self.copyright_label.sizePolicy().hasHeightForWidth()) 62 | self.copyright_label.setSizePolicy(sizePolicy1) 63 | 64 | self.gridLayout_2.addWidget(self.copyright_label, 2, 0, 1, 1, Qt.AlignHCenter) 65 | 66 | self.app_version_label = QLabel(self.frame_2) 67 | self.app_version_label.setObjectName(u"app_version_label") 68 | sizePolicy.setHeightForWidth(self.app_version_label.sizePolicy().hasHeightForWidth()) 69 | self.app_version_label.setSizePolicy(sizePolicy) 70 | 71 | self.gridLayout_2.addWidget(self.app_version_label, 1, 0, 1, 1, Qt.AlignHCenter) 72 | 73 | self.github_btn = QPushButton(self.frame_2) 74 | self.github_btn.setObjectName(u"github_btn") 75 | icon = QIcon() 76 | icon.addFile(u":/actionIcons/icons/logo-github.svg", QSize(), QIcon.Normal, QIcon.Off) 77 | self.github_btn.setIcon(icon) 78 | 79 | self.gridLayout_2.addWidget(self.github_btn, 3, 0, 1, 1, Qt.AlignHCenter) 80 | 81 | self.oslibs_groubbox = QGroupBox(self.frame_2) 82 | self.oslibs_groubbox.setObjectName(u"oslibs_groubbox") 83 | sizePolicy1.setHeightForWidth(self.oslibs_groubbox.sizePolicy().hasHeightForWidth()) 84 | self.oslibs_groubbox.setSizePolicy(sizePolicy1) 85 | self.oslibs_groubbox.setCheckable(False) 86 | self.verticalLayout = QVBoxLayout(self.oslibs_groubbox) 87 | self.verticalLayout.setObjectName(u"verticalLayout") 88 | self.verticalLayout.setContentsMargins(0, 0, -1, -1) 89 | self.open_source_te = QTextEdit(self.oslibs_groubbox) 90 | self.open_source_te.setObjectName(u"open_source_te") 91 | sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) 92 | sizePolicy2.setHorizontalStretch(0) 93 | sizePolicy2.setVerticalStretch(0) 94 | sizePolicy2.setHeightForWidth(self.open_source_te.sizePolicy().hasHeightForWidth()) 95 | self.open_source_te.setSizePolicy(sizePolicy2) 96 | self.open_source_te.setMaximumSize(QSize(16777215, 90)) 97 | self.open_source_te.setContextMenuPolicy(Qt.NoContextMenu) 98 | self.open_source_te.setStyleSheet(u"background-color: transparent") 99 | self.open_source_te.setReadOnly(True) 100 | self.open_source_te.setTextInteractionFlags(Qt.NoTextInteraction) 101 | 102 | self.verticalLayout.addWidget(self.open_source_te) 103 | 104 | 105 | self.gridLayout_2.addWidget(self.oslibs_groubbox, 5, 0, 1, 1) 106 | 107 | 108 | self.gridLayout.addWidget(self.frame_2, 0, 2, 1, 1) 109 | 110 | self.icon_frame = QFrame(AboutDialog) 111 | self.icon_frame.setObjectName(u"icon_frame") 112 | self.icon_frame.setFrameShape(QFrame.NoFrame) 113 | self.icon_frame.setFrameShadow(QFrame.Raised) 114 | self.gridLayout_3 = QGridLayout(self.icon_frame) 115 | self.gridLayout_3.setObjectName(u"gridLayout_3") 116 | self.app_icon = QLabel(self.icon_frame) 117 | self.app_icon.setObjectName(u"app_icon") 118 | sizePolicy.setHeightForWidth(self.app_icon.sizePolicy().hasHeightForWidth()) 119 | self.app_icon.setSizePolicy(sizePolicy) 120 | self.app_icon.setMinimumSize(QSize(128, 128)) 121 | 122 | self.gridLayout_3.addWidget(self.app_icon, 0, 0, 1, 1) 123 | 124 | 125 | self.gridLayout.addWidget(self.icon_frame, 0, 0, 1, 1) 126 | 127 | self.buttonBox = QDialogButtonBox(AboutDialog) 128 | self.buttonBox.setObjectName(u"buttonBox") 129 | self.buttonBox.setOrientation(Qt.Horizontal) 130 | self.buttonBox.setStandardButtons(QDialogButtonBox.Close) 131 | 132 | self.gridLayout.addWidget(self.buttonBox, 4, 2, 1, 1) 133 | 134 | self.horizontalSpacer = QSpacerItem(10, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) 135 | 136 | self.gridLayout.addItem(self.horizontalSpacer, 0, 1, 1, 1) 137 | 138 | 139 | self.retranslateUi(AboutDialog) 140 | self.buttonBox.accepted.connect(AboutDialog.accept) 141 | self.buttonBox.rejected.connect(AboutDialog.reject) 142 | 143 | QMetaObject.connectSlotsByName(AboutDialog) 144 | # setupUi 145 | 146 | def retranslateUi(self, AboutDialog): 147 | self.app_name_label.setText(QCoreApplication.translate("AboutDialog", u"

PyBeam QA

", None)) 148 | self.copyright_label.setText(QCoreApplication.translate("AboutDialog", u"Copyright \u00a9 2023-2024 Kagiso Lebang", None)) 149 | self.app_version_label.setText(QCoreApplication.translate("AboutDialog", u"version a.b.c", None)) 150 | self.github_btn.setText(QCoreApplication.translate("AboutDialog", u"Github", None)) 151 | self.oslibs_groubbox.setTitle(QCoreApplication.translate("AboutDialog", u"Open source libraries", None)) 152 | pass 153 | # retranslateUi 154 | 155 | -------------------------------------------------------------------------------- /ui/py_ui/picket_fence_offsets_dialog_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'picket_fence_offsets_dialog.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.7.0 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, 19 | QDialogButtonBox, QGridLayout, QHBoxLayout, QHeaderView, 20 | QLabel, QSizePolicy, QSpacerItem, QTableWidget, 21 | QTableWidgetItem, QWidget) 22 | 23 | class Ui_PFOffsetDialog(object): 24 | def setupUi(self, PFOffsetDialog): 25 | if not PFOffsetDialog.objectName(): 26 | PFOffsetDialog.setObjectName(u"PFOffsetDialog") 27 | PFOffsetDialog.resize(369, 400) 28 | self.gridLayout = QGridLayout(PFOffsetDialog) 29 | self.gridLayout.setObjectName(u"gridLayout") 30 | self.buttons_spacer = QSpacerItem(10, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) 31 | 32 | self.gridLayout.addItem(self.buttons_spacer, 3, 0, 1, 1) 33 | 34 | self.button_box = QDialogButtonBox(PFOffsetDialog) 35 | self.button_box.setObjectName(u"button_box") 36 | sizePolicy = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Fixed) 37 | sizePolicy.setHorizontalStretch(0) 38 | sizePolicy.setVerticalStretch(0) 39 | sizePolicy.setHeightForWidth(self.button_box.sizePolicy().hasHeightForWidth()) 40 | self.button_box.setSizePolicy(sizePolicy) 41 | self.button_box.setStyleSheet(u"") 42 | self.button_box.setStandardButtons(QDialogButtonBox.Apply|QDialogButtonBox.Cancel) 43 | 44 | self.gridLayout.addWidget(self.button_box, 4, 0, 1, 1) 45 | 46 | self.horizontalLayout_2 = QHBoxLayout() 47 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") 48 | self.horizontalSpacer_2 = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) 49 | 50 | self.horizontalLayout_2.addItem(self.horizontalSpacer_2) 51 | 52 | self.picket_num_label = QLabel(PFOffsetDialog) 53 | self.picket_num_label.setObjectName(u"picket_num_label") 54 | 55 | self.horizontalLayout_2.addWidget(self.picket_num_label) 56 | 57 | self.picket_num_cb = QComboBox(PFOffsetDialog) 58 | self.picket_num_cb.setObjectName(u"picket_num_cb") 59 | 60 | self.horizontalLayout_2.addWidget(self.picket_num_cb) 61 | 62 | self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) 63 | 64 | self.horizontalLayout_2.addItem(self.horizontalSpacer) 65 | 66 | 67 | self.gridLayout.addLayout(self.horizontalLayout_2, 0, 0, 1, 1) 68 | 69 | self.tableWidget = QTableWidget(PFOffsetDialog) 70 | if (self.tableWidget.columnCount() < 3): 71 | self.tableWidget.setColumnCount(3) 72 | __qtablewidgetitem = QTableWidgetItem() 73 | self.tableWidget.setHorizontalHeaderItem(0, __qtablewidgetitem) 74 | __qtablewidgetitem1 = QTableWidgetItem() 75 | self.tableWidget.setHorizontalHeaderItem(1, __qtablewidgetitem1) 76 | __qtablewidgetitem2 = QTableWidgetItem() 77 | self.tableWidget.setHorizontalHeaderItem(2, __qtablewidgetitem2) 78 | self.tableWidget.setObjectName(u"tableWidget") 79 | self.tableWidget.setShowGrid(True) 80 | self.tableWidget.horizontalHeader().setStretchLastSection(True) 81 | 82 | self.gridLayout.addWidget(self.tableWidget, 1, 0, 1, 1) 83 | 84 | 85 | self.retranslateUi(PFOffsetDialog) 86 | self.button_box.accepted.connect(PFOffsetDialog.accept) 87 | self.button_box.rejected.connect(PFOffsetDialog.reject) 88 | 89 | QMetaObject.connectSlotsByName(PFOffsetDialog) 90 | # setupUi 91 | 92 | def retranslateUi(self, PFOffsetDialog): 93 | self.picket_num_label.setText(QCoreApplication.translate("PFOffsetDialog", u"Picket No:", None)) 94 | ___qtablewidgetitem = self.tableWidget.horizontalHeaderItem(0) 95 | ___qtablewidgetitem.setText(QCoreApplication.translate("PFOffsetDialog", u"Index", None)); 96 | ___qtablewidgetitem1 = self.tableWidget.horizontalHeaderItem(1) 97 | ___qtablewidgetitem1.setText(QCoreApplication.translate("PFOffsetDialog", u"Left Picket (A)", None)); 98 | ___qtablewidgetitem2 = self.tableWidget.horizontalHeaderItem(2) 99 | ___qtablewidgetitem2.setText(QCoreApplication.translate("PFOffsetDialog", u"Right Picket (B)", None)); 100 | pass 101 | # retranslateUi 102 | 103 | -------------------------------------------------------------------------------- /ui/py_ui/qa_main_win_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'qa_main_win.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.7.0 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient, 15 | QCursor, QFont, QFontDatabase, QGradient, 16 | QIcon, QImage, QKeySequence, QLinearGradient, 17 | QPainter, QPalette, QPixmap, QRadialGradient, 18 | QTransform) 19 | from PySide6.QtWidgets import (QApplication, QDockWidget, QGridLayout, QMainWindow, 20 | QMenu, QMenuBar, QSizePolicy, QStatusBar, 21 | QTabWidget, QWidget) 22 | 23 | class Ui_MainWindow(object): 24 | def setupUi(self, MainWindow): 25 | if not MainWindow.objectName(): 26 | MainWindow.setObjectName(u"MainWindow") 27 | MainWindow.resize(640, 480) 28 | MainWindow.setStyleSheet(u"") 29 | self.centralwidget = QWidget(MainWindow) 30 | self.centralwidget.setObjectName(u"centralwidget") 31 | self.gridLayout = QGridLayout(self.centralwidget) 32 | self.gridLayout.setObjectName(u"gridLayout") 33 | self.gridLayout.setContentsMargins(0, 0, 0, 0) 34 | self.tabWidget = QTabWidget(self.centralwidget) 35 | self.tabWidget.setObjectName(u"tabWidget") 36 | sizePolicy = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Expanding) 37 | sizePolicy.setHorizontalStretch(0) 38 | sizePolicy.setVerticalStretch(0) 39 | sizePolicy.setHeightForWidth(self.tabWidget.sizePolicy().hasHeightForWidth()) 40 | self.tabWidget.setSizePolicy(sizePolicy) 41 | self.tabWidget.setAutoFillBackground(True) 42 | self.tabWidget.setStyleSheet(u"") 43 | self.tabWidget.setTabShape(QTabWidget.Rounded) 44 | self.tabWidget.setTabsClosable(True) 45 | self.tabWidget.setMovable(True) 46 | 47 | self.gridLayout.addWidget(self.tabWidget, 0, 0, 1, 1) 48 | 49 | MainWindow.setCentralWidget(self.centralwidget) 50 | self.menubar = QMenuBar(MainWindow) 51 | self.menubar.setObjectName(u"menubar") 52 | self.menubar.setGeometry(QRect(0, 0, 640, 20)) 53 | self.menuFile = QMenu(self.menubar) 54 | self.menuFile.setObjectName(u"menuFile") 55 | self.menuView = QMenu(self.menubar) 56 | self.menuView.setObjectName(u"menuView") 57 | self.menuTools = QMenu(self.menubar) 58 | self.menuTools.setObjectName(u"menuTools") 59 | self.menuHelp = QMenu(self.menubar) 60 | self.menuHelp.setObjectName(u"menuHelp") 61 | self.menuHelp.setTearOffEnabled(False) 62 | MainWindow.setMenuBar(self.menubar) 63 | self.statusbar = QStatusBar(MainWindow) 64 | self.statusbar.setObjectName(u"statusbar") 65 | MainWindow.setStatusBar(self.statusbar) 66 | self.dockWidget = QDockWidget(MainWindow) 67 | self.dockWidget.setObjectName(u"dockWidget") 68 | self.dockWidget.setFloating(False) 69 | self.dockWidget.setFeatures(QDockWidget.DockWidgetFeatureMask) 70 | self.dockWidget.setAllowedAreas(Qt.LeftDockWidgetArea|Qt.RightDockWidgetArea) 71 | self.dockWidgetContents = QWidget() 72 | self.dockWidgetContents.setObjectName(u"dockWidgetContents") 73 | self.dockWidget.setWidget(self.dockWidgetContents) 74 | MainWindow.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.dockWidget) 75 | 76 | self.menubar.addAction(self.menuFile.menuAction()) 77 | self.menubar.addAction(self.menuView.menuAction()) 78 | self.menubar.addAction(self.menuTools.menuAction()) 79 | self.menubar.addAction(self.menuHelp.menuAction()) 80 | 81 | self.retranslateUi(MainWindow) 82 | 83 | self.tabWidget.setCurrentIndex(-1) 84 | 85 | 86 | QMetaObject.connectSlotsByName(MainWindow) 87 | # setupUi 88 | 89 | def retranslateUi(self, MainWindow): 90 | MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) 91 | self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"File", None)) 92 | self.menuView.setTitle(QCoreApplication.translate("MainWindow", u"View", None)) 93 | self.menuTools.setTitle(QCoreApplication.translate("MainWindow", u"Tools", None)) 94 | self.menuHelp.setTitle(QCoreApplication.translate("MainWindow", u"Help", None)) 95 | self.dockWidget.setWindowTitle(QCoreApplication.translate("MainWindow", u"Docket Widget", None)) 96 | # retranslateUi 97 | 98 | -------------------------------------------------------------------------------- /ui/py_ui/starshot_test_dialog_ui.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'starshot_test_dialog.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.7.0 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QAbstractButton, QApplication, QComboBox, QDialog, 19 | QDialogButtonBox, QDoubleSpinBox, QFormLayout, QFrame, 20 | QGridLayout, QHBoxLayout, QLabel, QLineEdit, 21 | QPushButton, QScrollArea, QSizePolicy, QSpacerItem, 22 | QSpinBox, QVBoxLayout, QWidget) 23 | 24 | class Ui_StarshotTestDialog(object): 25 | def setupUi(self, StarshotTestDialog): 26 | if not StarshotTestDialog.objectName(): 27 | StarshotTestDialog.setObjectName(u"StarshotTestDialog") 28 | StarshotTestDialog.resize(522, 241) 29 | self.gridLayout = QGridLayout(StarshotTestDialog) 30 | self.gridLayout.setObjectName(u"gridLayout") 31 | self.buttons_spacer = QSpacerItem(10, 10, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Fixed) 32 | 33 | self.gridLayout.addItem(self.buttons_spacer, 1, 0, 1, 1) 34 | 35 | self.button_box = QDialogButtonBox(StarshotTestDialog) 36 | self.button_box.setObjectName(u"button_box") 37 | sizePolicy = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Fixed) 38 | sizePolicy.setHorizontalStretch(0) 39 | sizePolicy.setVerticalStretch(0) 40 | sizePolicy.setHeightForWidth(self.button_box.sizePolicy().hasHeightForWidth()) 41 | self.button_box.setSizePolicy(sizePolicy) 42 | self.button_box.setStyleSheet(u"") 43 | self.button_box.setStandardButtons(QDialogButtonBox.Apply|QDialogButtonBox.Cancel) 44 | 45 | self.gridLayout.addWidget(self.button_box, 2, 0, 1, 1) 46 | 47 | self.scrollArea = QScrollArea(StarshotTestDialog) 48 | self.scrollArea.setObjectName(u"scrollArea") 49 | sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) 50 | sizePolicy1.setHorizontalStretch(0) 51 | sizePolicy1.setVerticalStretch(0) 52 | sizePolicy1.setHeightForWidth(self.scrollArea.sizePolicy().hasHeightForWidth()) 53 | self.scrollArea.setSizePolicy(sizePolicy1) 54 | self.scrollArea.setFrameShape(QFrame.NoFrame) 55 | self.scrollArea.setWidgetResizable(True) 56 | self.scrollAreaWidgetContents = QWidget() 57 | self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") 58 | self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 504, 177)) 59 | sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.MinimumExpanding) 60 | sizePolicy2.setHorizontalStretch(0) 61 | sizePolicy2.setVerticalStretch(0) 62 | sizePolicy2.setHeightForWidth(self.scrollAreaWidgetContents.sizePolicy().hasHeightForWidth()) 63 | self.scrollAreaWidgetContents.setSizePolicy(sizePolicy2) 64 | self.verticalLayout_3 = QVBoxLayout(self.scrollAreaWidgetContents) 65 | self.verticalLayout_3.setObjectName(u"verticalLayout_3") 66 | self.frame = QFrame(self.scrollAreaWidgetContents) 67 | self.frame.setObjectName(u"frame") 68 | self.frame.setFrameShape(QFrame.NoFrame) 69 | self.frame.setFrameShadow(QFrame.Raised) 70 | self.verticalLayout_2 = QVBoxLayout(self.frame) 71 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 72 | self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) 73 | self.formLayout = QFormLayout() 74 | self.formLayout.setObjectName(u"formLayout") 75 | self.formLayout.setHorizontalSpacing(15) 76 | self.test_name_label = QLabel(self.frame) 77 | self.test_name_label.setObjectName(u"test_name_label") 78 | 79 | self.formLayout.setWidget(0, QFormLayout.LabelRole, self.test_name_label) 80 | 81 | self.test_name_le = QLineEdit(self.frame) 82 | self.test_name_le.setObjectName(u"test_name_le") 83 | sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed) 84 | sizePolicy3.setHorizontalStretch(0) 85 | sizePolicy3.setVerticalStretch(0) 86 | sizePolicy3.setHeightForWidth(self.test_name_le.sizePolicy().hasHeightForWidth()) 87 | self.test_name_le.setSizePolicy(sizePolicy3) 88 | self.test_name_le.setMaximumSize(QSize(350, 16777215)) 89 | 90 | self.formLayout.setWidget(0, QFormLayout.FieldRole, self.test_name_le) 91 | 92 | self.output_file_label = QLabel(self.frame) 93 | self.output_file_label.setObjectName(u"output_file_label") 94 | 95 | self.formLayout.setWidget(1, QFormLayout.LabelRole, self.output_file_label) 96 | 97 | self.horizontalLayout_2 = QHBoxLayout() 98 | self.horizontalLayout_2.setSpacing(6) 99 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") 100 | self.out_file_le = QLineEdit(self.frame) 101 | self.out_file_le.setObjectName(u"out_file_le") 102 | sizePolicy3.setHeightForWidth(self.out_file_le.sizePolicy().hasHeightForWidth()) 103 | self.out_file_le.setSizePolicy(sizePolicy3) 104 | self.out_file_le.setMinimumSize(QSize(245, 0)) 105 | self.out_file_le.setReadOnly(True) 106 | 107 | self.horizontalLayout_2.addWidget(self.out_file_le) 108 | 109 | self.select_file_btn = QPushButton(self.frame) 110 | self.select_file_btn.setObjectName(u"select_file_btn") 111 | 112 | self.horizontalLayout_2.addWidget(self.select_file_btn) 113 | 114 | self.horizontalSpacer = QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) 115 | 116 | self.horizontalLayout_2.addItem(self.horizontalSpacer) 117 | 118 | 119 | self.formLayout.setLayout(1, QFormLayout.FieldRole, self.horizontalLayout_2) 120 | 121 | self.sim_image_label = QLabel(self.frame) 122 | self.sim_image_label.setObjectName(u"sim_image_label") 123 | 124 | self.formLayout.setWidget(2, QFormLayout.LabelRole, self.sim_image_label) 125 | 126 | self.sim_image_cb = QComboBox(self.frame) 127 | self.sim_image_cb.setObjectName(u"sim_image_cb") 128 | sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) 129 | sizePolicy4.setHorizontalStretch(0) 130 | sizePolicy4.setVerticalStretch(0) 131 | sizePolicy4.setHeightForWidth(self.sim_image_cb.sizePolicy().hasHeightForWidth()) 132 | self.sim_image_cb.setSizePolicy(sizePolicy4) 133 | self.sim_image_cb.setMinimumSize(QSize(170, 0)) 134 | 135 | self.formLayout.setWidget(2, QFormLayout.FieldRole, self.sim_image_cb) 136 | 137 | self.num_spokes_label = QLabel(self.frame) 138 | self.num_spokes_label.setObjectName(u"num_spokes_label") 139 | 140 | self.formLayout.setWidget(3, QFormLayout.LabelRole, self.num_spokes_label) 141 | 142 | self.num_spokes_sb = QSpinBox(self.frame) 143 | self.num_spokes_sb.setObjectName(u"num_spokes_sb") 144 | self.num_spokes_sb.setMaximumSize(QSize(100, 16777215)) 145 | self.num_spokes_sb.setMinimum(5) 146 | self.num_spokes_sb.setMaximum(12) 147 | 148 | self.formLayout.setWidget(3, QFormLayout.FieldRole, self.num_spokes_sb) 149 | 150 | self.cax_offset_label = QLabel(self.frame) 151 | self.cax_offset_label.setObjectName(u"cax_offset_label") 152 | 153 | self.formLayout.setWidget(4, QFormLayout.LabelRole, self.cax_offset_label) 154 | 155 | self.cax_offset_dsb = QDoubleSpinBox(self.frame) 156 | self.cax_offset_dsb.setObjectName(u"cax_offset_dsb") 157 | sizePolicy4.setHeightForWidth(self.cax_offset_dsb.sizePolicy().hasHeightForWidth()) 158 | self.cax_offset_dsb.setSizePolicy(sizePolicy4) 159 | self.cax_offset_dsb.setMinimumSize(QSize(100, 0)) 160 | self.cax_offset_dsb.setDecimals(1) 161 | 162 | self.formLayout.setWidget(4, QFormLayout.FieldRole, self.cax_offset_dsb) 163 | 164 | 165 | self.verticalLayout_2.addLayout(self.formLayout) 166 | 167 | 168 | self.verticalLayout_3.addWidget(self.frame) 169 | 170 | self.scrollArea.setWidget(self.scrollAreaWidgetContents) 171 | 172 | self.gridLayout.addWidget(self.scrollArea, 0, 0, 1, 1) 173 | 174 | 175 | self.retranslateUi(StarshotTestDialog) 176 | self.button_box.accepted.connect(StarshotTestDialog.accept) 177 | self.button_box.rejected.connect(StarshotTestDialog.reject) 178 | 179 | QMetaObject.connectSlotsByName(StarshotTestDialog) 180 | # setupUi 181 | 182 | def retranslateUi(self, StarshotTestDialog): 183 | self.test_name_label.setText(QCoreApplication.translate("StarshotTestDialog", u"Test name:", None)) 184 | self.output_file_label.setText(QCoreApplication.translate("StarshotTestDialog", u"Output file:", None)) 185 | self.select_file_btn.setText(QCoreApplication.translate("StarshotTestDialog", u"Save to...", None)) 186 | self.sim_image_label.setText(QCoreApplication.translate("StarshotTestDialog", u"Simulation image:", None)) 187 | self.num_spokes_label.setText(QCoreApplication.translate("StarshotTestDialog", u"Number of spokes:", None)) 188 | self.cax_offset_label.setText(QCoreApplication.translate("StarshotTestDialog", u"CAX offset", None)) 189 | self.cax_offset_dsb.setSuffix(QCoreApplication.translate("StarshotTestDialog", u" mm", None)) 190 | pass 191 | # retranslateUi 192 | 193 | -------------------------------------------------------------------------------- /ui/qt_ui/about_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | AboutDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 550 10 | 322 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 22 | 23 | QFrame::NoFrame 24 | 25 | 26 | QFrame::Raised 27 | 28 | 29 | 30 | 31 | 32 | Qt::Vertical 33 | 34 | 35 | QSizePolicy::Fixed 36 | 37 | 38 | 39 | 20 40 | 10 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 0 50 | 0 51 | 52 | 53 | 54 | 55 | 14 56 | 57 | 58 | 59 | <html><head/><body><p><span style=" font-size:12pt; font-weight:700;">PyBeam QA</span></p></body></html> 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 0 68 | 0 69 | 70 | 71 | 72 | Copyright © 2023-2024 Kagiso Lebang 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 0 81 | 0 82 | 83 | 84 | 85 | version a.b.c 86 | 87 | 88 | 89 | 90 | 91 | 92 | Github 93 | 94 | 95 | 96 | :/actionIcons/icons/logo-github.svg:/actionIcons/icons/logo-github.svg 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 0 105 | 0 106 | 107 | 108 | 109 | Open source libraries 110 | 111 | 112 | false 113 | 114 | 115 | 116 | 0 117 | 118 | 119 | 0 120 | 121 | 122 | 123 | 124 | 125 | 0 126 | 0 127 | 128 | 129 | 130 | 131 | 16777215 132 | 90 133 | 134 | 135 | 136 | Qt::NoContextMenu 137 | 138 | 139 | background-color: transparent 140 | 141 | 142 | true 143 | 144 | 145 | Qt::NoTextInteraction 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | QFrame::NoFrame 159 | 160 | 161 | QFrame::Raised 162 | 163 | 164 | 165 | 166 | 167 | 168 | 0 169 | 0 170 | 171 | 172 | 173 | 174 | 128 175 | 128 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | Qt::Horizontal 187 | 188 | 189 | QDialogButtonBox::Close 190 | 191 | 192 | 193 | 194 | 195 | 196 | Qt::Horizontal 197 | 198 | 199 | QSizePolicy::Fixed 200 | 201 | 202 | 203 | 10 204 | 10 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | buttonBox 217 | accepted() 218 | AboutDialog 219 | accept() 220 | 221 | 222 | 248 223 | 254 224 | 225 | 226 | 157 227 | 274 228 | 229 | 230 | 231 | 232 | buttonBox 233 | rejected() 234 | AboutDialog 235 | reject() 236 | 237 | 238 | 316 239 | 260 240 | 241 | 242 | 286 243 | 274 244 | 245 | 246 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /ui/qt_ui/icons.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/tools.png 4 | icons/close_win.png 5 | icons/circled-left.png 6 | icons/left.png 7 | icons/import.png 8 | icons/image-file.png 9 | icons/module.png 10 | icons/picture.png 11 | icons/plus.png 12 | icons/add-file.png 13 | icons/graph-report.png 14 | icons/minus.png 15 | icons/scan.png 16 | icons/warning.png 17 | icons/bb_shift.png 18 | icons/error_round.png 19 | icons/pie_chart_report.png 20 | icons/down_button.png 21 | icons/up_button.png 22 | icons/test.png 23 | icons/correct.png 24 | icons/loading.png 25 | icons/waiting.png 26 | icons/in_progress.png 27 | icons/settings.png 28 | icons/info.png 29 | icons/not_started.png 30 | 31 | 32 | icons/eye.png 33 | icons/remove.png 34 | icons/delete.png 35 | icons/logo-github.svg 36 | icons/select_all.svg 37 | icons/zoom_in_area.svg 38 | icons/deselect.svg 39 | icons/trash_x.svg 40 | icons/transform.svg 41 | icons/zoom_pan.svg 42 | 43 | 44 | icons/ic_app.svg 45 | icons/ic_app_alt.svg 46 | 47 | 48 | icons/setup_end_anim.svg 49 | 50 | 51 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/Animation.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 25 | 29 | 33 | 34 | 35 | 39 | 42 | 47 | 48 | 51 | 56 | 57 | 58 | 62 | 65 | 70 | 71 | 74 | 79 | 80 | 83 | 88 | 89 | 92 | 97 | 98 | 101 | 106 | 107 | 108 | 111 | 114 | 119 | 120 | 123 | 128 | 129 | 130 | 134 | 144 | 145 | 149 | 159 | 160 | 164 | 174 | 175 | 179 | 189 | 190 | 191 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/add-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/add-file.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/auto_focus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/bb_shift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/bb_shift.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/circled-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/circled-left.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/close_win.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/close_win.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/correct.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/correct.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/delete.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/deselect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/down_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/down_button.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/error_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/error_round.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/eye.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/eye.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/graph-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/graph-report.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/ic_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/ic_app.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/ic_app.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/ic_app_alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 34 | 40 | 46 | 52 | 58 | 64 | 70 | 76 | 77 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/image-file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/image-file.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/import.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/in_progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/in_progress.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/info.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/ion_chamber.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/ion_chamber.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/left.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/loading.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/logo-github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/minus.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/module.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/module.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/not_started.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/not_started.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/picture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/picture.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/pie_chart_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/pie_chart_report.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/plus.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/remove.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/remove.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/scan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/scan.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/select_all.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/settings.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/star_profile.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/test.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/tools.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/transform.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/trash_x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/up_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/up_button.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/waiting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/waiting.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/qt_ui/icons/warning.png -------------------------------------------------------------------------------- /ui/qt_ui/icons/zoom_in_area.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /ui/qt_ui/icons/zoom_pan.svg: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ui/qt_ui/picket_fence_offsets_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | PFOffsetDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 369 10 | 400 11 | 12 | 13 | 14 | 15 | 16 | 17 | Qt::Vertical 18 | 19 | 20 | QSizePolicy::Fixed 21 | 22 | 23 | 24 | 10 25 | 10 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 0 35 | 0 36 | 37 | 38 | 39 | 40 | 41 | 42 | QDialogButtonBox::Apply|QDialogButtonBox::Cancel 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | Qt::Horizontal 52 | 53 | 54 | 55 | 40 56 | 20 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | Picket No: 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | Qt::Horizontal 75 | 76 | 77 | 78 | 40 79 | 20 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | true 90 | 91 | 92 | true 93 | 94 | 95 | 96 | Index 97 | 98 | 99 | 100 | 101 | Left Picket (A) 102 | 103 | 104 | 105 | 106 | Right Picket (B) 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | button_box 117 | accepted() 118 | PFOffsetDialog 119 | accept() 120 | 121 | 122 | 248 123 | 254 124 | 125 | 126 | 157 127 | 274 128 | 129 | 130 | 131 | 132 | button_box 133 | rejected() 134 | PFOffsetDialog 135 | reject() 136 | 137 | 138 | 316 139 | 260 140 | 141 | 142 | 286 143 | 274 144 | 145 | 146 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /ui/qt_ui/pyBeamQA.pyproject: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["icons.qrc","winston_lutz_worksheet.ui","photons_worksheet.ui","qa_main_win.ui","app_main_win.ui", 3 | "electrons_worksheet.ui", "picket_fence_worksheet.ui", "starshot_worksheet.ui", "field_analysis_worksheet.ui", 4 | "planar_imaging_worksheet.ui", "winston_lutz_test_dialog.ui", "about_dialog.ui", "starshot_test_dialog.ui", 5 | "picket_fence_test_dialog.ui", "picket_fence_offsets_dialog.ui", "preferences.ui"] 6 | } 7 | -------------------------------------------------------------------------------- /ui/qt_ui/qa_main_win.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 640 10 | 480 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 0 23 | 24 | 25 | 0 26 | 27 | 28 | 0 29 | 30 | 31 | 0 32 | 33 | 34 | 35 | 36 | 37 | 0 38 | 0 39 | 40 | 41 | 42 | true 43 | 44 | 45 | 46 | 47 | 48 | QTabWidget::Rounded 49 | 50 | 51 | -1 52 | 53 | 54 | true 55 | 56 | 57 | true 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 0 67 | 0 68 | 640 69 | 20 70 | 71 | 72 | 73 | 74 | File 75 | 76 | 77 | 78 | 79 | View 80 | 81 | 82 | 83 | 84 | Tools 85 | 86 | 87 | 88 | 89 | false 90 | 91 | 92 | Help 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | false 104 | 105 | 106 | QDockWidget::DockWidgetFeatureMask 107 | 108 | 109 | Qt::LeftDockWidgetArea|Qt::RightDockWidgetArea 110 | 111 | 112 | Docket Widget 113 | 114 | 115 | 2 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /ui/qt_ui/starshot_test_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | StarshotTestDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 522 10 | 241 11 | 12 | 13 | 14 | 15 | 16 | 17 | Qt::Vertical 18 | 19 | 20 | QSizePolicy::Fixed 21 | 22 | 23 | 24 | 10 25 | 10 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 0 35 | 0 36 | 37 | 38 | 39 | 40 | 41 | 42 | QDialogButtonBox::Apply|QDialogButtonBox::Cancel 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 0 51 | 0 52 | 53 | 54 | 55 | QFrame::NoFrame 56 | 57 | 58 | true 59 | 60 | 61 | 62 | 63 | 0 64 | 0 65 | 504 66 | 177 67 | 68 | 69 | 70 | 71 | 0 72 | 0 73 | 74 | 75 | 76 | 77 | 78 | 79 | QFrame::NoFrame 80 | 81 | 82 | QFrame::Raised 83 | 84 | 85 | 86 | 0 87 | 88 | 89 | 0 90 | 91 | 92 | 0 93 | 94 | 95 | 0 96 | 97 | 98 | 99 | 100 | 15 101 | 102 | 103 | 104 | 105 | Test name: 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 0 114 | 0 115 | 116 | 117 | 118 | 119 | 350 120 | 16777215 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | Output file: 129 | 130 | 131 | 132 | 133 | 134 | 135 | 6 136 | 137 | 138 | 139 | 140 | 141 | 0 142 | 0 143 | 144 | 145 | 146 | 147 | 245 148 | 0 149 | 150 | 151 | 152 | true 153 | 154 | 155 | 156 | 157 | 158 | 159 | Save to... 160 | 161 | 162 | 163 | 164 | 165 | 166 | Qt::Horizontal 167 | 168 | 169 | 170 | 40 171 | 20 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | Simulation image: 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 0 190 | 0 191 | 192 | 193 | 194 | 195 | 170 196 | 0 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | Number of spokes: 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 100 213 | 16777215 214 | 215 | 216 | 217 | 5 218 | 219 | 220 | 12 221 | 222 | 223 | 224 | 225 | 226 | 227 | CAX offset 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 0 236 | 0 237 | 238 | 239 | 240 | 241 | 100 242 | 0 243 | 244 | 245 | 246 | mm 247 | 248 | 249 | 1 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | button_box 268 | accepted() 269 | StarshotTestDialog 270 | accept() 271 | 272 | 273 | 248 274 | 254 275 | 276 | 277 | 157 278 | 274 279 | 280 | 281 | 282 | 283 | button_box 284 | rejected() 285 | StarshotTestDialog 286 | reject() 287 | 288 | 289 | 316 290 | 260 291 | 292 | 293 | 286 294 | 274 295 | 296 | 297 | 298 | 299 | 300 | -------------------------------------------------------------------------------- /ui/util_widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import QWidget, QFileDialog 18 | 19 | def worksheet_save_report(parent: QWidget = None): 20 | file_path = QFileDialog.getSaveFileName(caption="Save To File...", 21 | filter="PDF (*.pdf)", 22 | parent=parent) 23 | 24 | if file_path[0] != "": 25 | path = file_path[0].split("/") 26 | 27 | if not path[-1].endswith(".pdf"): 28 | path[-1] = path[-1] + ".pdf" 29 | 30 | return "/".join(path) 31 | 32 | else: 33 | return "" 34 | -------------------------------------------------------------------------------- /ui/util_widgets/dialogs.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtGui import (QPixmap, QImage, QDesktopServices, QPixmap, QImage, QPainter, 18 | QColor) 19 | from PySide6.QtSvg import QSvgRenderer 20 | from PySide6.QtCore import Qt, QSize 21 | from PySide6.QtWidgets import (QWidget, QDialogButtonBox, QGridLayout, 22 | QSizePolicy, QDialog, QLabel, 23 | QVBoxLayout) 24 | from PySide6.QtGui import QGuiApplication 25 | 26 | from core import __version__ as pybeamqa_version 27 | from pdfrw import __version__ as pdfrw_version 28 | from pylinac import __version__ as pylinac_version 29 | from PySide6 import __version__ as pyside6_version 30 | from pyqtgraph import __version__ as pyqtgraph_version 31 | 32 | from ui.py_ui import icons_rc 33 | from ui.py_ui.about_dialog_ui import Ui_AboutDialog 34 | 35 | class MessageDialog(QDialog): 36 | 37 | NO_ICON = 0 38 | INFO_ICON = 1 39 | WARNING_ICON = 2 40 | CRITICAL_ICON = 3 41 | QUESTION_ICON = 4 42 | 43 | def __init__(self, parent: QWidget | None = None): 44 | super().__init__(parent) 45 | 46 | self.header_text = QLabel() 47 | self.message_text = QLabel() 48 | self.header_text.setWordWrap(True) 49 | self.message_text.setWordWrap(True) 50 | self.header_text.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding) 51 | self.message_text.setSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.MinimumExpanding) 52 | 53 | self.icon = QLabel() 54 | self.icon.setFixedSize(QSize(48, 48)) 55 | self.icon.setScaledContents(True) 56 | self.icon.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed) 57 | 58 | self.button_box = QDialogButtonBox() 59 | 60 | self.button_box.accepted.connect(self.accept) 61 | self.button_box.rejected.connect(self.reject) 62 | 63 | base_layout = QVBoxLayout() 64 | layout = QGridLayout() 65 | layout.setHorizontalSpacing(10) 66 | layout.addWidget(self.icon, 0, 0, 1, 1, Qt.AlignmentFlag.AlignTop) 67 | layout.addWidget(self.header_text, 0, 1, 1, 1, Qt.AlignmentFlag.AlignVCenter) 68 | layout.addWidget(self.message_text, 1, 1, 1, 1, Qt.AlignmentFlag.AlignTop) 69 | 70 | base_layout.addLayout(layout) 71 | base_layout.addWidget(self.button_box) 72 | self.setLayout(base_layout) 73 | 74 | primary_screen = QGuiApplication.primaryScreen() 75 | 76 | self.setFixedWidth(primary_screen.availableSize().toTuple()[0] * 0.35) 77 | 78 | #set default button 79 | self.set_standard_buttons() 80 | 81 | def set_standard_buttons(self, buttons = QDialogButtonBox.StandardButton.Ok): 82 | self.button_box.clear() 83 | self.button_box.setStandardButtons(buttons) 84 | 85 | def set_title(self, title): 86 | return super().setWindowTitle(title) 87 | 88 | def set_header_text(self, text: str) -> None: 89 | text = "

" \ 90 | f"{text}

" 91 | 92 | self.header_text.setText(text) 93 | 94 | def set_info_text(self, text: str) -> None: 95 | self.message_text.setText(text) 96 | 97 | def set_icon(self, pixmap: QPixmap | QImage | str | int, smooth_image: bool = True) -> None: 98 | if isinstance(pixmap, (QPixmap, QImage)) and smooth_image: 99 | pixmap = pixmap 100 | 101 | elif isinstance(pixmap, int): 102 | if pixmap == self.INFO_ICON: 103 | pixmap = QPixmap(u":/colorIcons/icons/info.png") 104 | 105 | elif pixmap == self.WARNING_ICON: 106 | pixmap = QPixmap(u":/colorIcons/icons/warning.png") 107 | 108 | elif pixmap == self.CRITICAL_ICON: 109 | pixmap = QPixmap(u":/colorIcons/icons/error_round.png") 110 | 111 | elif pixmap == self.QUESTION_ICON: 112 | pixmap = QPixmap(u":/colorIcons/icons/question.png") 113 | 114 | else: 115 | return 116 | 117 | self.icon.setPixmap(pixmap) 118 | 119 | class AboutDialog(QDialog): 120 | 121 | app_svg = QSvgRenderer(u":/misc_icons/icons/ic_app.svg") 122 | app_img = QImage(256, 256, QImage.Format.Format_ARGB32) 123 | app_img.fill(QColor(255, 255, 255, 0)) 124 | qpainter = QPainter(app_img) 125 | app_svg.render(qpainter) 126 | qpainter.end() 127 | 128 | def __init__(self, parent: QWidget | None = None): 129 | super().__init__(parent) 130 | 131 | self.setWindowTitle("About PyBeam QA") 132 | 133 | self.ui = Ui_AboutDialog() 134 | self.ui.setupUi(self) 135 | 136 | self.ui.github_btn.clicked.connect(self.open_github) 137 | 138 | self.app_icon = QPixmap.fromImage(self.app_img) 139 | self.app_icon = self.app_icon.scaled(QSize(128, 128), 140 | mode = Qt.TransformationMode.SmoothTransformation) 141 | self.ui.app_icon.setPixmap(self.app_icon) 142 | 143 | self.ui.app_version_label.setText(f"version {pybeamqa_version}") 144 | self.ui.open_source_te.setText(f"⊹ PySide6 ({pyside6_version})\n" \ 145 | f"⊹ Pylinac ({pylinac_version})\n" \ 146 | f"⊹ Pyqtgraph ({pyqtgraph_version})\n" \ 147 | f"⊹ Pdfrw ({pdfrw_version})") 148 | 149 | self.setFixedSize(self.size()) 150 | 151 | def open_github(self): 152 | QDesktopServices.openUrl("https://github.com/Quantico-Bullet/PyBeam-QA/") -------------------------------------------------------------------------------- /ui/util_widgets/statusbar.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtWidgets import (QWidget, QHBoxLayout, QLabel) 18 | from PySide6.QtGui import Qt, QPixmap 19 | 20 | from ui.py_ui import icons_rc 21 | 22 | class AnalysisInfoLabel(QWidget): 23 | 24 | IDLE = -1 25 | IN_PROGRESS = 0 26 | COMPLETE = 1 27 | FAILED = 2 28 | 29 | def __init__(self) -> None: 30 | super().__init__() 31 | self.setContentsMargins(0,0,0,0) 32 | 33 | layout = QHBoxLayout() 34 | layout.setContentsMargins(0,0,0,0) 35 | 36 | self.label = QLabel() 37 | self.icon = QLabel() 38 | self.icon.setScaledContents(True) 39 | self.icon.setMaximumSize(16, 16) 40 | 41 | layout.addWidget(self.icon) 42 | layout.addWidget(self.label) 43 | 44 | self.setLayout(layout) 45 | 46 | def set_message(self, state: int = IDLE, message: str | None = None): 47 | if state == self.IDLE: 48 | self.label.clear() 49 | self.icon.clear() 50 | 51 | elif state == self.IN_PROGRESS: 52 | base_text = "Analysis in progress..." 53 | self.label.setText(base_text + f" ({message}%)" if message else base_text) 54 | 55 | icon_pixmap = QPixmap(u":/colorIcons/icons/in_progress.png") 56 | self.icon.setPixmap(icon_pixmap) 57 | 58 | elif state == self.COMPLETE: 59 | base_text = "Analysis completed" 60 | alt_text = "Analysis completed in " 61 | self.label.setText(alt_text + message if message else base_text) 62 | 63 | icon_pixmap = QPixmap(u":/colorIcons/icons/correct.png") 64 | self.icon.setPixmap(icon_pixmap) 65 | 66 | elif state == self.FAILED: 67 | self.label.setText("Analysis failed (see error message...)") 68 | 69 | icon_pixmap = QPixmap(u":/colorIcons/icons/error_round.png") 70 | self.icon.setPixmap(icon_pixmap) 71 | 72 | else: 73 | raise ValueError("Unknown message state passed in") 74 | -------------------------------------------------------------------------------- /ui/util_widgets/validators.py: -------------------------------------------------------------------------------- 1 | # PyBeam QA 2 | # Copyright (C) 2024 Kagiso Lebang 3 | # 4 | # This program is free software: you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 3 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | from PySide6.QtCore import QObject, QLocale 18 | from typing import Optional, overload 19 | from PySide6.QtGui import QDoubleValidator, QValidator 20 | 21 | class DoubleValidator(QDoubleValidator): 22 | 23 | def __init__(self, parent: Optional[QObject] = None): 24 | super().__init__(parent) 25 | 26 | locale = QLocale.c() 27 | locale.setNumberOptions(QLocale.NumberOption.RejectGroupSeparator) 28 | self.setLocale(locale) 29 | self.setNotation(QDoubleValidator.Notation.StandardNotation) 30 | 31 | @classmethod 32 | def from_args(cls, 33 | bottom: float, 34 | top: float, 35 | decimals: int = 4, 36 | notation = QDoubleValidator.Notation.StandardNotation, 37 | parent: Optional[QObject] = None): 38 | 39 | validator = cls(parent) 40 | validator.setBottom(bottom) 41 | validator.setTop(top) 42 | validator.setDecimals(decimals) 43 | validator.setNotation(notation) 44 | 45 | return validator 46 | 47 | def fixup(self, input: str) -> str: 48 | if input != "" and self.validate(input, 0)[0] == QValidator.State.Intermediate: 49 | if float(input) < self.bottom(): 50 | return str(self.bottom()) 51 | elif float(input) > self.top(): 52 | return str(self.top()) 53 | -------------------------------------------------------------------------------- /ui/web_engine/fonts/roboto_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/web_engine/fonts/roboto_regular.ttf -------------------------------------------------------------------------------- /ui/web_engine/katex/contrib/auto-render.min.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,(function(e){return function(){"use strict";var t={771:function(t){t.exports=e}},r={};function n(e){var i=r[e];if(void 0!==i)return i.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,n),a.exports}n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};var i={};return function(){n.d(i,{default:function(){return s}});var e=n(771),t=n.n(e),r=function(e,t,r){for(var n=r,i=0,a=e.length;n0&&(i.push({type:"text",data:e.slice(0,n)}),e=e.slice(n));var l=t.findIndex((function(t){return e.startsWith(t.left)}));if(-1===(n=r(t[l].right,e,t[l].left.length)))break;var d=e.slice(0,n+t[l].right.length),s=a.test(d)?d:e.slice(t[l].left.length,n);i.push({type:"math",data:s,rawData:d,display:t[l].display}),e=e.slice(n+t[l].right.length)}return""!==e&&i.push({type:"text",data:e}),i},l=function(e,r){var n=o(e,r.delimiters);if(1===n.length&&"text"===n[0].type)return null;for(var i=document.createDocumentFragment(),a=0;a 2 | 3 | 4 | 5 | 6 | 7 | 8 | 34 | 35 | 46 | 47 | 48 | 49 | Test $z_{max}$ 50 | 51 | -------------------------------------------------------------------------------- /ui/web_engine/mathView.js: -------------------------------------------------------------------------------- 1 | function setEquation(text){ 2 | document.body.innerHTML = text 3 | 4 | renderMathInElement(document.body, { 5 | 6 | delimiters: [ 7 | {left: "$$", right: "$$", display: true}, 8 | {left: "$", right: "$", display: false}, 9 | {left: "\\(", right: "\\)", display: false}, 10 | {left: "\\begin{equation}", right: "\\end{equation}", display: true}, 11 | {left: "\\begin{align}", right: "\\end{align}", display: true}, 12 | {left: "\\begin{alignat}", right: "\\end{alignat}", display: true}, 13 | {left: "\\begin{gather}", right: "\\end{gather}", display: true}, 14 | {left: "\\begin{CD}", right: "\\end{CD}", display: true}, 15 | {left: "\\[", right: "\\]", display: true} 16 | ], 17 | throwOnError : false 18 | }); 19 | } 20 | 21 | function setTextStyle(textColor, fontWeight, fontSize){ 22 | document.body.style.color = textColor 23 | document.body.style.fontWeight = fontWeight 24 | document.body.style.fontSize = fontSize 25 | } -------------------------------------------------------------------------------- /ui/wizards/setup_wizard.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Quantico-Bullet/PyBeam-QA/2dc848278ff7caab5192433f5cdc49f8c66063a2/ui/wizards/setup_wizard.py --------------------------------------------------------------------------------