alphanso-gui 0.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ """ALPHONSO GUI — Graphical interface for the ALPHANSO (α,n) neutron source calculator."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """Entry point for ``python -m alphanso_gui``."""
2
+ from alphanso_gui.app import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
alphanso_gui/app.py ADDED
@@ -0,0 +1,311 @@
1
+ """Main application window for ALPHONSO GUI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from PyQt5.QtCore import Qt
8
+ from PyQt5.QtGui import QFont
9
+ from PyQt5.QtWidgets import (
10
+ QApplication,
11
+ QHBoxLayout,
12
+ QLabel,
13
+ QMainWindow,
14
+ QMessageBox,
15
+ QPushButton,
16
+ QSizePolicy,
17
+ QSplitter,
18
+ QStackedWidget,
19
+ QStatusBar,
20
+ QVBoxLayout,
21
+ QWidget,
22
+ )
23
+
24
+ from alphanso_gui.worker import CalculationWorker
25
+ from alphanso_gui.widgets.beam_panel import BeamPanel
26
+ from alphanso_gui.widgets.homogeneous_panel import HomogeneousPanel
27
+ from alphanso_gui.widgets.interface_panel import InterfacePanel
28
+ from alphanso_gui.widgets.sandwich_panel import SandwichPanel
29
+ from alphanso_gui.widgets.results_panel import ResultsPanel
30
+
31
+
32
+ # Ordered list of (calc_type, display_name) for the tab buttons
33
+ _PROBLEM_TYPES: list[tuple[str, str]] = [
34
+ ("beam", "Beam"),
35
+ ("homogeneous", "Homogeneous"),
36
+ ("interface", "Interface"),
37
+ ("sandwich", "Sandwich"),
38
+ ]
39
+
40
+ _DESCRIPTIONS: dict[str, str] = {
41
+ "beam": (
42
+ "<b>Beam</b> — Mono- or poly-energetic alpha beam incident on a thick target. "
43
+ "Yields the (α,n) neutron production rate per incident alpha particle."
44
+ ),
45
+ "homogeneous": (
46
+ "<b>Homogeneous</b> — Uniform mixture of alpha-emitting isotopes and target nuclides. "
47
+ "Yields neutron production rates per gram of material from (α,n) and spontaneous fission."
48
+ ),
49
+ "interface": (
50
+ "<b>Interface</b> — Planar interface between an alpha source region and a target region. "
51
+ "Yields neutron production per cm² of interface area."
52
+ ),
53
+ "sandwich": (
54
+ "<b>Sandwich</b> — Multi-layer geometry: Source | Intermediate layers | Target. "
55
+ "Yields total and per-layer neutron production per cm² of interface."
56
+ ),
57
+ }
58
+
59
+ _STYLE = """
60
+ QMainWindow {
61
+ background-color: #f0f0f0;
62
+ }
63
+ QGroupBox {
64
+ font-weight: bold;
65
+ border: 1px solid #cccccc;
66
+ border-radius: 4px;
67
+ margin-top: 8px;
68
+ padding-top: 6px;
69
+ }
70
+ QGroupBox::title {
71
+ subcontrol-origin: margin;
72
+ left: 8px;
73
+ padding: 0 4px;
74
+ }
75
+ QPushButton#tab_btn {
76
+ border: none;
77
+ border-bottom: 3px solid transparent;
78
+ background: transparent;
79
+ padding: 6px 18px;
80
+ font-size: 13px;
81
+ }
82
+ QPushButton#tab_btn:checked {
83
+ border-bottom: 3px solid #1f77b4;
84
+ color: #1f77b4;
85
+ font-weight: bold;
86
+ }
87
+ QPushButton#tab_btn:hover:!checked {
88
+ border-bottom: 3px solid #aaaaaa;
89
+ }
90
+ QPushButton#run_btn {
91
+ background-color: #1f77b4;
92
+ color: white;
93
+ border-radius: 4px;
94
+ padding: 8px 28px;
95
+ font-size: 13px;
96
+ font-weight: bold;
97
+ }
98
+ QPushButton#run_btn:hover {
99
+ background-color: #155d91;
100
+ }
101
+ QPushButton#run_btn:disabled {
102
+ background-color: #aaaaaa;
103
+ }
104
+ """
105
+
106
+
107
+ class MainWindow(QMainWindow):
108
+ """ALPHONSO GUI main window."""
109
+
110
+ def __init__(self):
111
+ super().__init__()
112
+ self.setWindowTitle("ALPHONSO GUI — (α,n) Neutron Source Calculator")
113
+ self.resize(1200, 750)
114
+ self.setMinimumSize(900, 600)
115
+ self.setStyleSheet(_STYLE)
116
+
117
+ self._worker: CalculationWorker | None = None
118
+
119
+ # ---- Central widget layout -----------------------------------
120
+ central = QWidget()
121
+ self.setCentralWidget(central)
122
+ root_layout = QVBoxLayout(central)
123
+ root_layout.setContentsMargins(8, 8, 8, 4)
124
+ root_layout.setSpacing(6)
125
+
126
+ # ---- Header --------------------------------------------------
127
+ header = QLabel("ALPHONSO — <small>(α,n) Neutron Source Calculator</small>")
128
+ header.setTextFormat(Qt.RichText)
129
+ header_font = QFont()
130
+ header_font.setPointSize(15)
131
+ header_font.setBold(True)
132
+ header.setFont(header_font)
133
+ header.setAlignment(Qt.AlignCenter)
134
+ header.setStyleSheet("color: #1f4e79; padding: 4px 0;")
135
+ root_layout.addWidget(header)
136
+
137
+ # ---- Tab bar (problem type selector) -------------------------
138
+ tab_row = QHBoxLayout()
139
+ tab_row.setContentsMargins(0, 0, 0, 0)
140
+ self._tab_buttons: list[QPushButton] = []
141
+ for i, (ctype, name) in enumerate(_PROBLEM_TYPES):
142
+ btn = QPushButton(name)
143
+ btn.setObjectName("tab_btn")
144
+ btn.setCheckable(True)
145
+ btn.setChecked(i == 0)
146
+ btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
147
+ btn.clicked.connect(lambda checked, idx=i: self._switch_tab(idx))
148
+ tab_row.addWidget(btn)
149
+ self._tab_buttons.append(btn)
150
+ tab_row.addStretch()
151
+ root_layout.addLayout(tab_row)
152
+
153
+ # Description label
154
+ self._desc_label = QLabel()
155
+ self._desc_label.setTextFormat(Qt.RichText)
156
+ self._desc_label.setWordWrap(True)
157
+ self._desc_label.setStyleSheet("color: #444444; font-size: 11px; padding: 2px 0;")
158
+ root_layout.addWidget(self._desc_label)
159
+
160
+ # ---- Main splitter (parameters | results) --------------------
161
+ splitter = QSplitter(Qt.Horizontal)
162
+ splitter.setHandleWidth(6)
163
+
164
+ # --- Left: stacked parameter panels ---------------------------
165
+ self._param_stack = QStackedWidget()
166
+ self._panels: list[QWidget] = [
167
+ BeamPanel(),
168
+ HomogeneousPanel(),
169
+ InterfacePanel(),
170
+ SandwichPanel(),
171
+ ]
172
+ for panel in self._panels:
173
+ self._param_stack.addWidget(panel)
174
+ self._param_stack.setMinimumWidth(380)
175
+ splitter.addWidget(self._param_stack)
176
+
177
+ # --- Right: results panel ------------------------------------
178
+ self._results = ResultsPanel()
179
+ self._results.setMinimumWidth(350)
180
+ splitter.addWidget(self._results)
181
+
182
+ splitter.setSizes([480, 680])
183
+ root_layout.addWidget(splitter, stretch=1)
184
+
185
+ # ---- Bottom bar (run button + status) -----------------------
186
+ bottom = QHBoxLayout()
187
+ self._run_btn = QPushButton("▶ Calculate")
188
+ self._run_btn.setObjectName("run_btn")
189
+ self._run_btn.setFixedWidth(160)
190
+ self._run_btn.clicked.connect(self._run_calculation)
191
+ bottom.addWidget(self._run_btn)
192
+ bottom.addStretch()
193
+ root_layout.addLayout(bottom)
194
+
195
+ # ---- Status bar ---------------------------------------------
196
+ self._status_bar = QStatusBar()
197
+ self.setStatusBar(self._status_bar)
198
+ self._status_bar.showMessage("Ready.")
199
+
200
+ # Initialise description for the first tab
201
+ self._switch_tab(0)
202
+
203
+ # ------------------------------------------------------------------
204
+ # Tab switching
205
+ # ------------------------------------------------------------------
206
+ def _switch_tab(self, index: int):
207
+ for i, btn in enumerate(self._tab_buttons):
208
+ btn.setChecked(i == index)
209
+ self._param_stack.setCurrentIndex(index)
210
+ ctype = _PROBLEM_TYPES[index][0]
211
+ self._desc_label.setText(_DESCRIPTIONS[ctype])
212
+ self._results.clear()
213
+ self._status_bar.showMessage(
214
+ f"{_PROBLEM_TYPES[index][1]} mode — configure parameters and press Calculate."
215
+ )
216
+
217
+ # ------------------------------------------------------------------
218
+ # Calculation
219
+ # ------------------------------------------------------------------
220
+ def _run_calculation(self):
221
+ if self._worker is not None and self._worker.isRunning():
222
+ return # already in progress
223
+
224
+ index = self._param_stack.currentIndex()
225
+ panel = self._panels[index]
226
+ config = panel.get_config()
227
+
228
+ # Basic sanity check
229
+ if not self._validate_config(config):
230
+ return
231
+
232
+ self._run_btn.setEnabled(False)
233
+ self._status_bar.showMessage("Calculating… this may take a moment.")
234
+ self._results.clear()
235
+
236
+ self._worker = CalculationWorker(config)
237
+ self._worker.finished.connect(self._on_finished)
238
+ self._worker.error.connect(self._on_error)
239
+ self._worker.start()
240
+
241
+ def _on_finished(self, results: dict):
242
+ index = self._param_stack.currentIndex()
243
+ calc_type = _PROBLEM_TYPES[index][0]
244
+ self._results.update_results(results, calc_type)
245
+ self._run_btn.setEnabled(True)
246
+ self._status_bar.showMessage("Calculation complete.")
247
+
248
+ def _on_error(self, message: str):
249
+ self._run_btn.setEnabled(True)
250
+ self._status_bar.showMessage(f"Error: {message}")
251
+ QMessageBox.critical(
252
+ self,
253
+ "Calculation Error",
254
+ f"The calculation failed with the following error:\n\n{message}",
255
+ )
256
+
257
+ # ------------------------------------------------------------------
258
+ # Validation helpers
259
+ # ------------------------------------------------------------------
260
+ def _validate_config(self, config: dict) -> bool:
261
+ calc_type = config.get("calc_type", "")
262
+
263
+ def _check_matdef(key: str, label: str) -> bool:
264
+ matdef = config.get(key, {})
265
+ if not matdef:
266
+ QMessageBox.warning(
267
+ self,
268
+ "Incomplete configuration",
269
+ f"The {label} is empty. Please add at least one isotope.",
270
+ )
271
+ return False
272
+ return True
273
+
274
+ if calc_type == "beam":
275
+ if not _check_matdef("matdef", "target material"):
276
+ return False
277
+ elif calc_type == "homogeneous":
278
+ if not _check_matdef("matdef", "material composition"):
279
+ return False
280
+ elif calc_type == "interface":
281
+ if not _check_matdef("source_matdef", "source material"):
282
+ return False
283
+ if not _check_matdef("target_matdef", "target material"):
284
+ return False
285
+ elif calc_type == "sandwich":
286
+ if not _check_matdef("source_matdef", "source material"):
287
+ return False
288
+ if not _check_matdef("target_matdef", "target material"):
289
+ return False
290
+ if not config.get("intermediate_layers"):
291
+ QMessageBox.warning(
292
+ self,
293
+ "Incomplete configuration",
294
+ "Please add at least one intermediate layer.",
295
+ )
296
+ return False
297
+ return True
298
+
299
+
300
+ # ---------------------------------------------------------------------------
301
+ # Application entry point
302
+ # ---------------------------------------------------------------------------
303
+ def main():
304
+ app = QApplication.instance() or QApplication(sys.argv)
305
+ window = MainWindow()
306
+ window.show()
307
+ sys.exit(app.exec_())
308
+
309
+
310
+ if __name__ == "__main__":
311
+ main()
@@ -0,0 +1,199 @@
1
+ """Matplotlib figures illustrating each ALPHANSO geometry type."""
2
+
3
+ import matplotlib.patches as mpatches
4
+ import matplotlib.pyplot as plt
5
+ import numpy as np
6
+ from matplotlib.figure import Figure
7
+
8
+ # Consistent colour palette
9
+ _COL_SOURCE = "#d62728" # red – alpha-emitting source
10
+ _COL_TARGET = "#1f77b4" # blue – neutron target material
11
+ _COL_LAYER = "#2ca02c" # green – intermediate layer
12
+ _COL_ARROW = "#ff7f0e" # orange – beam / neutron arrows
13
+ _COL_BG = "#f8f9fa" # very light grey background
14
+
15
+
16
+ def _base_fig(height: float = 2.8) -> tuple[Figure, plt.Axes]:
17
+ fig = Figure(figsize=(5.0, height), dpi=100, facecolor=_COL_BG)
18
+ ax = fig.add_axes([0.0, 0.0, 1.0, 1.0])
19
+ ax.set_xlim(0, 1)
20
+ ax.set_ylim(0, 1)
21
+ ax.set_aspect("auto")
22
+ ax.axis("off")
23
+ ax.set_facecolor(_COL_BG)
24
+ return fig, ax
25
+
26
+
27
+ def beam_figure() -> Figure:
28
+ """Monoenergetic alpha beam incident on a thick target."""
29
+ fig, ax = _base_fig()
30
+
31
+ # Target rectangle (right half)
32
+ target = mpatches.FancyBboxPatch(
33
+ (0.50, 0.15), 0.40, 0.70,
34
+ boxstyle="round,pad=0.01",
35
+ linewidth=1.5, edgecolor=_COL_TARGET, facecolor="#aec7e8",
36
+ )
37
+ ax.add_patch(target)
38
+ ax.text(0.70, 0.50, "Target\nmaterial", ha="center", va="center",
39
+ fontsize=9, color="#023858", fontweight="bold")
40
+
41
+ # Alpha beam arrows
42
+ for y in [0.35, 0.50, 0.65]:
43
+ ax.annotate(
44
+ "", xy=(0.50, y), xytext=(0.10, y),
45
+ arrowprops=dict(arrowstyle="-|>", color=_COL_SOURCE, lw=2.0),
46
+ )
47
+ ax.text(0.05, 0.50, "α", ha="center", va="center",
48
+ fontsize=16, color=_COL_SOURCE, fontweight="bold")
49
+ ax.text(0.28, 0.82, "α beam", ha="center", va="bottom",
50
+ fontsize=8, color=_COL_SOURCE)
51
+
52
+ # Neutron arrows emerging from target
53
+ rng = np.random.default_rng(42)
54
+ for _ in range(6):
55
+ angle = rng.uniform(-70, 70)
56
+ dx = 0.14 * np.cos(np.radians(angle))
57
+ dy = 0.14 * np.sin(np.radians(angle))
58
+ x0, y0 = 0.90, rng.uniform(0.25, 0.75)
59
+ ax.annotate(
60
+ "", xy=(x0 + dx, y0 + dy), xytext=(x0, y0),
61
+ arrowprops=dict(arrowstyle="-|>", color=_COL_ARROW, lw=1.2),
62
+ )
63
+ ax.text(0.96, 0.88, "n", ha="center", va="center",
64
+ fontsize=11, color=_COL_ARROW, fontweight="bold")
65
+
66
+ ax.set_title("Beam geometry", fontsize=10, pad=4,
67
+ color="#333333", fontstyle="italic")
68
+ return fig
69
+
70
+
71
+ def homogeneous_figure() -> Figure:
72
+ """Uniform mixture of alpha emitters and target nuclei."""
73
+ fig, ax = _base_fig()
74
+
75
+ # Single mixture box
76
+ box = mpatches.FancyBboxPatch(
77
+ (0.10, 0.10), 0.80, 0.72,
78
+ boxstyle="round,pad=0.01",
79
+ linewidth=1.8, edgecolor="#555555", facecolor="#e8f4f8",
80
+ )
81
+ ax.add_patch(box)
82
+
83
+ rng = np.random.default_rng(7)
84
+ # Alpha-emitter dots (red filled circles)
85
+ for x, y in zip(rng.uniform(0.15, 0.85, 14), rng.uniform(0.15, 0.78, 14)):
86
+ if rng.random() > 0.5:
87
+ ax.plot(x, y, "o", ms=7, color=_COL_SOURCE, mec="#7f0000", mew=0.8)
88
+ else:
89
+ ax.plot(x, y, "o", ms=7, mfc="none", color=_COL_TARGET, mew=1.5)
90
+
91
+ # Legend patches
92
+ src_patch = mpatches.Patch(color=_COL_SOURCE, label="α emitter")
93
+ tgt_patch = mpatches.Patch(facecolor="none", edgecolor=_COL_TARGET,
94
+ linewidth=1.5, label="Target nucleus")
95
+ ax.legend(handles=[src_patch, tgt_patch], loc="lower center",
96
+ fontsize=7, ncol=2, framealpha=0.7, handlelength=1.2)
97
+
98
+ ax.set_title("Homogeneous geometry", fontsize=10, pad=4,
99
+ color="#333333", fontstyle="italic")
100
+ return fig
101
+
102
+
103
+ def interface_figure() -> Figure:
104
+ """Planar interface between source (left) and target (right) regions."""
105
+ fig, ax = _base_fig()
106
+
107
+ # Source region
108
+ src_box = mpatches.FancyBboxPatch(
109
+ (0.05, 0.10), 0.42, 0.72,
110
+ boxstyle="square,pad=0.0",
111
+ linewidth=1.5, edgecolor=_COL_SOURCE, facecolor="#ffd0d0",
112
+ )
113
+ ax.add_patch(src_box)
114
+ ax.text(0.26, 0.74, "Source (A)", ha="center", va="center",
115
+ fontsize=8, color="#7f0000", fontweight="bold")
116
+
117
+ # Target region
118
+ tgt_box = mpatches.FancyBboxPatch(
119
+ (0.53, 0.10), 0.42, 0.72,
120
+ boxstyle="square,pad=0.0",
121
+ linewidth=1.5, edgecolor=_COL_TARGET, facecolor="#d0e8ff",
122
+ )
123
+ ax.add_patch(tgt_box)
124
+ ax.text(0.74, 0.74, "Target (B)", ha="center", va="center",
125
+ fontsize=8, color="#023858", fontweight="bold")
126
+
127
+ # Interface line
128
+ ax.plot([0.50, 0.50], [0.08, 0.84], "--", color="#444444", lw=2.0)
129
+ ax.text(0.50, 0.88, "Interface", ha="center", va="bottom",
130
+ fontsize=7.5, color="#444444")
131
+
132
+ # Alpha particles in source
133
+ rng = np.random.default_rng(3)
134
+ for x, y in zip(rng.uniform(0.08, 0.44, 8), rng.uniform(0.14, 0.68, 8)):
135
+ ax.plot(x, y, "o", ms=6, color=_COL_SOURCE, mec="#7f0000", mew=0.7)
136
+ ax.text(0.26, 0.58, "α", fontsize=13, color=_COL_SOURCE,
137
+ ha="center", va="center", fontweight="bold")
138
+
139
+ # Neutron arrows leaving interface into target
140
+ for y in [0.28, 0.46, 0.64]:
141
+ ax.annotate(
142
+ "", xy=(0.72, y), xytext=(0.52, y),
143
+ arrowprops=dict(arrowstyle="-|>", color=_COL_ARROW, lw=1.5),
144
+ )
145
+ ax.text(0.82, 0.46, "n", ha="center", va="center",
146
+ fontsize=12, color=_COL_ARROW, fontweight="bold")
147
+
148
+ ax.set_title("Interface geometry", fontsize=10, pad=4,
149
+ color="#333333", fontstyle="italic")
150
+ return fig
151
+
152
+
153
+ def sandwich_figure() -> Figure:
154
+ """Multi-layer sandwich geometry: Source | B1 | B2 | … | Target."""
155
+ fig, ax = _base_fig()
156
+
157
+ regions = [
158
+ (0.03, 0.20, "Source\n(A)", _COL_SOURCE, "#ffd0d0"),
159
+ (0.23, 0.18, "Layer\nB1", _COL_LAYER, "#d0f0d0"),
160
+ (0.41, 0.18, "Layer\nB2", _COL_LAYER, "#b8e8b8"),
161
+ (0.59, 0.38, "Target\n(C)", _COL_TARGET, "#d0e8ff"),
162
+ ]
163
+ y0, height = 0.10, 0.72
164
+ for x, width, label, edge, face in regions:
165
+ box = mpatches.FancyBboxPatch(
166
+ (x, y0), width, height,
167
+ boxstyle="square,pad=0.0",
168
+ linewidth=1.5, edgecolor=edge, facecolor=face,
169
+ )
170
+ ax.add_patch(box)
171
+ ax.text(x + width / 2, y0 + height / 2, label,
172
+ ha="center", va="center", fontsize=7.5,
173
+ color="#222222", fontweight="bold")
174
+
175
+ # Alpha arrows (source → layers)
176
+ for y in [0.30, 0.46, 0.62]:
177
+ ax.annotate(
178
+ "", xy=(0.96, y), xytext=(0.03, y),
179
+ arrowprops=dict(arrowstyle="-|>", color=_COL_ARROW, lw=1.0,
180
+ connectionstyle="arc3,rad=0.0"),
181
+ )
182
+
183
+ ax.text(0.50, 0.90, "α →", ha="center", va="bottom",
184
+ fontsize=9, color=_COL_ARROW)
185
+ ax.text(0.50, 0.94, "neutron production", ha="center", va="bottom",
186
+ fontsize=7, color="#555555")
187
+
188
+ ax.set_title("Sandwich geometry", fontsize=10, pad=4,
189
+ color="#333333", fontstyle="italic")
190
+ return fig
191
+
192
+
193
+ # Map calc_type → factory function
194
+ ILLUSTRATIONS: dict[str, callable] = {
195
+ "beam": beam_figure,
196
+ "homogeneous": homogeneous_figure,
197
+ "interface": interface_figure,
198
+ "sandwich": sandwich_figure,
199
+ }
File without changes