├── .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 |
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 |
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 |
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 |
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 |
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 |
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 |
10 |
--------------------------------------------------------------------------------
/ui/qt_ui/icons/zoom_pan.svg:
--------------------------------------------------------------------------------
1 |
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 |
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
--------------------------------------------------------------------------------