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.
- alphanso_gui/__init__.py +3 -0
- alphanso_gui/__main__.py +5 -0
- alphanso_gui/app.py +311 -0
- alphanso_gui/illustrations.py +199 -0
- alphanso_gui/widgets/__init__.py +0 -0
- alphanso_gui/widgets/beam_panel.py +188 -0
- alphanso_gui/widgets/homogeneous_panel.py +94 -0
- alphanso_gui/widgets/interface_panel.py +112 -0
- alphanso_gui/widgets/material_editor.py +104 -0
- alphanso_gui/widgets/results_panel.py +373 -0
- alphanso_gui/widgets/sandwich_panel.py +216 -0
- alphanso_gui/worker.py +31 -0
- alphanso_gui-0.0.2.dist-info/METADATA +129 -0
- alphanso_gui-0.0.2.dist-info/RECORD +18 -0
- alphanso_gui-0.0.2.dist-info/WHEEL +5 -0
- alphanso_gui-0.0.2.dist-info/entry_points.txt +2 -0
- alphanso_gui-0.0.2.dist-info/licenses/LICENSE +661 -0
- alphanso_gui-0.0.2.dist-info/top_level.txt +1 -0
alphanso_gui/__init__.py
ADDED
alphanso_gui/__main__.py
ADDED
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
|