gratinglab 0.1.0__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.
- gratinglab/__init__.py +35 -0
- gratinglab/checks.py +263 -0
- gratinglab/compare.py +233 -0
- gratinglab/convergence.py +455 -0
- gratinglab/geometry.py +483 -0
- gratinglab/gui/__init__.py +0 -0
- gratinglab/gui/app.py +80 -0
- gratinglab/gui/diagram.py +699 -0
- gratinglab/gui/diagram3d.py +609 -0
- gratinglab/gui/docs.py +189 -0
- gratinglab/gui/icon.py +101 -0
- gratinglab/gui/integral_options.py +219 -0
- gratinglab/gui/mathtext.py +209 -0
- gratinglab/gui/orders.py +152 -0
- gratinglab/gui/provenance.py +273 -0
- gratinglab/gui/qt/__init__.py +20 -0
- gratinglab/gui/qt/geometry_panel.py +239 -0
- gratinglab/gui/qt/geometry_tab.py +387 -0
- gratinglab/gui/qt/integral_tab.py +447 -0
- gratinglab/gui/qt/main_window.py +514 -0
- gratinglab/gui/qt/profile_plot_panel.py +67 -0
- gratinglab/gui/qt/scalar_tab.py +498 -0
- gratinglab/gui/qt/scene3d_panel.py +347 -0
- gratinglab/gui/qt/setup_tab.py +215 -0
- gratinglab/gui/qt/sizing.py +25 -0
- gratinglab/gui/qt/theory_viewer.py +89 -0
- gratinglab/gui/qt/worker.py +203 -0
- gratinglab/gui/richtext.py +463 -0
- gratinglab/gui/scalar_options.py +139 -0
- gratinglab/gui/state.py +379 -0
- gratinglab/illumination.py +212 -0
- gratinglab/io/__init__.py +0 -0
- gratinglab/io/efficiency_table.py +382 -0
- gratinglab/io/ggp.py +248 -0
- gratinglab/materials/__init__.py +218 -0
- gratinglab/materials/data/SOURCES.md +53 -0
- gratinglab/materials/fresnel.py +282 -0
- gratinglab/materials/optical.py +319 -0
- gratinglab/materials/refractiveindex.py +306 -0
- gratinglab/metrology/__init__.py +121 -0
- gratinglab/metrology/analyzer.py +875 -0
- gratinglab/metrology/boundary/__init__.py +27 -0
- gratinglab/metrology/boundary/average.py +143 -0
- gratinglab/metrology/boundary/pipeline.py +211 -0
- gratinglab/metrology/cli.py +52 -0
- gratinglab/metrology/config.py +170 -0
- gratinglab/metrology/core/__init__.py +23 -0
- gratinglab/metrology/core/analysis.py +315 -0
- gratinglab/metrology/core/image_flatten.py +131 -0
- gratinglab/metrology/core/processing.py +344 -0
- gratinglab/metrology/core/tip.py +233 -0
- gratinglab/metrology/data/__init__.py +16 -0
- gratinglab/metrology/data/aggregation.py +193 -0
- gratinglab/metrology/gui/__init__.py +56 -0
- gratinglab/metrology/gui/icon.py +136 -0
- gratinglab/metrology/gui/qt/__init__.py +19 -0
- gratinglab/metrology/gui/qt/app.py +28 -0
- gratinglab/metrology/gui/qt/boundary_view.py +324 -0
- gratinglab/metrology/gui/qt/canvas.py +151 -0
- gratinglab/metrology/gui/qt/efficiency_dialog.py +273 -0
- gratinglab/metrology/gui/qt/import_view.py +380 -0
- gratinglab/metrology/gui/qt/main_window.py +398 -0
- gratinglab/metrology/gui/qt/wiki_view.py +99 -0
- gratinglab/metrology/gui/qt/worker.py +59 -0
- gratinglab/metrology/gui/state.py +168 -0
- gratinglab/metrology/io/__init__.py +8 -0
- gratinglab/metrology/io/file_io.py +253 -0
- gratinglab/metrology/io/metrics.py +38 -0
- gratinglab/metrology/io/spm.py +210 -0
- gratinglab/metrology/settings.py +216 -0
- gratinglab/metrology/stats/__init__.py +16 -0
- gratinglab/metrology/stats/analysis.py +329 -0
- gratinglab/metrology/stats/icc.py +128 -0
- gratinglab/metrology/visualization/__init__.py +16 -0
- gratinglab/metrology/visualization/diagnostics.py +111 -0
- gratinglab/metrology/visualization/profiles.py +234 -0
- gratinglab/metrology/visualization/statistics.py +412 -0
- gratinglab/metrology/wiki/__init__.py +91 -0
- gratinglab/metrology/wiki/facet-fitting.md +95 -0
- gratinglab/metrology/wiki/file-formats.md +116 -0
- gratinglab/metrology/wiki/flattening.md +105 -0
- gratinglab/metrology/wiki/icc-correction.md +111 -0
- gratinglab/metrology/wiki/overview.md +51 -0
- gratinglab/metrology/wiki/reading-outputs.md +154 -0
- gratinglab/metrology/wiki/row-groups.md +75 -0
- gratinglab/metrology/wiki/tip-correction.md +101 -0
- gratinglab/metrology/workflows.py +383 -0
- gratinglab/problem.py +79 -0
- gratinglab/profiles.py +531 -0
- gratinglab/resolution.py +198 -0
- gratinglab/result.py +268 -0
- gratinglab/solvers/__init__.py +34 -0
- gratinglab/solvers/base.py +207 -0
- gratinglab/solvers/integral/__init__.py +406 -0
- gratinglab/solvers/integral/_boundary.py +50 -0
- gratinglab/solvers/integral/_core.py +126 -0
- gratinglab/solvers/integral/_finite.py +396 -0
- gratinglab/solvers/integral/_greens.py +234 -0
- gratinglab/solvers/integral/_kernels.py +308 -0
- gratinglab/solvers/integral/_nystrom.py +474 -0
- gratinglab/solvers/scalar.py +1144 -0
- gratinglab-0.1.0.dist-info/METADATA +319 -0
- gratinglab-0.1.0.dist-info/RECORD +107 -0
- gratinglab-0.1.0.dist-info/WHEEL +5 -0
- gratinglab-0.1.0.dist-info/entry_points.txt +4 -0
- gratinglab-0.1.0.dist-info/licenses/LICENSE +28 -0
- gratinglab-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
"""The integral solver's own tab: its knob, its result, its provenance.
|
|
2
|
+
|
|
3
|
+
Deliberately a parallel construction of ``scalar_tab.py`` rather than a
|
|
4
|
+
shared base class: the roadmap's own warning is that every solver seam was
|
|
5
|
+
designed against a sample of one, and the way to learn the actual common
|
|
6
|
+
shape is to build the second tab and *then* extract what proved common. The
|
|
7
|
+
duplication between these two files is that evidence, kept visible.
|
|
8
|
+
|
|
9
|
+
What genuinely differs from scalar: the options group (the boundary
|
|
10
|
+
condition, and the accuracy knob it governs -- no reflectivity or roughness,
|
|
11
|
+
because the material enters through the boundary condition itself rather than
|
|
12
|
+
through a Fresnel factor) and the state type behind ``build_options``.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import csv
|
|
18
|
+
import dataclasses
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Sequence
|
|
21
|
+
|
|
22
|
+
from PySide6.QtCore import Qt, Signal
|
|
23
|
+
from PySide6.QtWidgets import (
|
|
24
|
+
QAbstractItemView,
|
|
25
|
+
QComboBox,
|
|
26
|
+
QFileDialog,
|
|
27
|
+
QFormLayout,
|
|
28
|
+
QGroupBox,
|
|
29
|
+
QHBoxLayout,
|
|
30
|
+
QLabel,
|
|
31
|
+
QLineEdit,
|
|
32
|
+
QListWidget,
|
|
33
|
+
QListWidgetItem,
|
|
34
|
+
QMessageBox,
|
|
35
|
+
QProgressBar,
|
|
36
|
+
QPushButton,
|
|
37
|
+
QScrollArea,
|
|
38
|
+
QSplitter,
|
|
39
|
+
QTextEdit,
|
|
40
|
+
QVBoxLayout,
|
|
41
|
+
QWidget,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
from .. import orders as orders_module
|
|
45
|
+
from .. import provenance
|
|
46
|
+
from ..integral_options import (
|
|
47
|
+
CONDUCTIVITY_MODES,
|
|
48
|
+
CONDUCTIVITY_NOTES,
|
|
49
|
+
IntegralOptionsState,
|
|
50
|
+
)
|
|
51
|
+
from ..integral_options import build_options as build_integral_options
|
|
52
|
+
from .sizing import fit_width_to_contents
|
|
53
|
+
|
|
54
|
+
__all__ = ["IntegralTab"]
|
|
55
|
+
|
|
56
|
+
_ORDER_ROLE = Qt.ItemDataRole.UserRole
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class IntegralTab(QWidget):
|
|
60
|
+
"""Integral's own controls, result, and provenance."""
|
|
61
|
+
|
|
62
|
+
name = "integral"
|
|
63
|
+
|
|
64
|
+
solve_requested = Signal()
|
|
65
|
+
cancel_requested = Signal()
|
|
66
|
+
convergence_requested = Signal()
|
|
67
|
+
|
|
68
|
+
def __init__(self, parent: QWidget | None = None) -> None:
|
|
69
|
+
super().__init__(parent)
|
|
70
|
+
self._fields: dict[str, QWidget] = {}
|
|
71
|
+
self._scan = None
|
|
72
|
+
self._energy = None
|
|
73
|
+
self._lambda_over_period: float | None = None
|
|
74
|
+
self._visible_orders: frozenset[int] = frozenset()
|
|
75
|
+
self._previous_orders: frozenset[int] = frozenset()
|
|
76
|
+
self._build()
|
|
77
|
+
|
|
78
|
+
# -- construction ------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def _build(self) -> None:
|
|
81
|
+
controls = QWidget()
|
|
82
|
+
column = QVBoxLayout(controls)
|
|
83
|
+
column.setContentsMargins(0, 0, 0, 0)
|
|
84
|
+
column.addWidget(self._build_options_group())
|
|
85
|
+
column.addWidget(self._build_orders_group())
|
|
86
|
+
column.addLayout(self._build_action_buttons())
|
|
87
|
+
column.addWidget(self._export_button)
|
|
88
|
+
column.addWidget(self._progress)
|
|
89
|
+
column.addStretch(1)
|
|
90
|
+
|
|
91
|
+
scroller = QScrollArea()
|
|
92
|
+
scroller.setWidget(controls)
|
|
93
|
+
scroller.setWidgetResizable(True)
|
|
94
|
+
fit_width_to_contents(scroller)
|
|
95
|
+
|
|
96
|
+
right = QSplitter(Qt.Orientation.Vertical)
|
|
97
|
+
right.addWidget(self._build_efficiency_plot())
|
|
98
|
+
right.addWidget(self._build_provenance())
|
|
99
|
+
right.setStretchFactor(0, 1)
|
|
100
|
+
right.setStretchFactor(1, 0)
|
|
101
|
+
|
|
102
|
+
body = QSplitter(Qt.Orientation.Horizontal)
|
|
103
|
+
body.addWidget(scroller)
|
|
104
|
+
body.addWidget(right)
|
|
105
|
+
body.setStretchFactor(0, 0)
|
|
106
|
+
body.setStretchFactor(1, 1)
|
|
107
|
+
body.setSizes([260, 700])
|
|
108
|
+
|
|
109
|
+
outer = QVBoxLayout(self)
|
|
110
|
+
outer.setContentsMargins(0, 0, 0, 0)
|
|
111
|
+
outer.addWidget(body)
|
|
112
|
+
|
|
113
|
+
self._check_fields_match_integral_options()
|
|
114
|
+
|
|
115
|
+
def _build_options_group(self) -> QWidget:
|
|
116
|
+
defaults = IntegralOptionsState()
|
|
117
|
+
group = QGroupBox("Integral solver")
|
|
118
|
+
form = QFormLayout(group)
|
|
119
|
+
points = QLineEdit(defaults.boundary_points)
|
|
120
|
+
points.returnPressed.connect(self.solve_requested)
|
|
121
|
+
points.setToolTip(
|
|
122
|
+
"Equal-arc-length nodes on one groove period -- the accuracy\n"
|
|
123
|
+
"knob the convergence sweep varies. Cost grows roughly cubically."
|
|
124
|
+
)
|
|
125
|
+
self._fields["boundary_points"] = points
|
|
126
|
+
form.addRow("Boundary pts", points)
|
|
127
|
+
|
|
128
|
+
conductivity = QComboBox()
|
|
129
|
+
conductivity.addItems(CONDUCTIVITY_MODES)
|
|
130
|
+
conductivity.setToolTip(
|
|
131
|
+
"Which boundary condition the integral equation carries.\n"
|
|
132
|
+
"perfect: perfectly conducting, no material -- efficiencies are\n"
|
|
133
|
+
" relative to a perfect reflector and sum to 1. A coating is\n"
|
|
134
|
+
" not consulted.\n"
|
|
135
|
+
"tabulated: finite conductivity (Goray & Schmidt 2010), reading\n"
|
|
136
|
+
" the coating's complex index at every wavelength --\n"
|
|
137
|
+
" efficiencies are absolute and absorption is recorded.\n"
|
|
138
|
+
"Tabulated costs roughly twice as much per wavelength, and needs\n"
|
|
139
|
+
"a finer mesh: the field oscillates inside the material too."
|
|
140
|
+
)
|
|
141
|
+
conductivity.currentTextChanged.connect(self._show_conductivity_note)
|
|
142
|
+
self._fields["conductivity"] = conductivity
|
|
143
|
+
form.addRow("Conductivity", conductivity)
|
|
144
|
+
|
|
145
|
+
# Dynamic, because the label this replaced asserted a perfectly
|
|
146
|
+
# conducting boundary unconditionally and went stale the moment the
|
|
147
|
+
# solver grew a second one. A claim about the result is exactly what
|
|
148
|
+
# must not be hardcoded beside a control that can falsify it.
|
|
149
|
+
self._conductivity_note = QLabel()
|
|
150
|
+
self._conductivity_note.setWordWrap(True)
|
|
151
|
+
form.addRow(self._conductivity_note)
|
|
152
|
+
self._show_conductivity_note(conductivity.currentText())
|
|
153
|
+
return group
|
|
154
|
+
|
|
155
|
+
def _show_conductivity_note(self, mode: str) -> None:
|
|
156
|
+
"""Say what the selected boundary condition means for the numbers."""
|
|
157
|
+
self._conductivity_note.setText(CONDUCTIVITY_NOTES.get(mode, ""))
|
|
158
|
+
|
|
159
|
+
def _build_orders_group(self) -> QWidget:
|
|
160
|
+
group = QGroupBox("Orders")
|
|
161
|
+
layout = QVBoxLayout(group)
|
|
162
|
+
|
|
163
|
+
self._order_list = QListWidget()
|
|
164
|
+
self._order_list.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
|
|
165
|
+
self._order_list.itemChanged.connect(self._on_order_item_changed)
|
|
166
|
+
layout.addWidget(self._order_list)
|
|
167
|
+
|
|
168
|
+
buttons = QHBoxLayout()
|
|
169
|
+
show_all = QPushButton("All")
|
|
170
|
+
show_all.clicked.connect(self._show_all_orders)
|
|
171
|
+
show_none = QPushButton("None")
|
|
172
|
+
show_none.clicked.connect(self._show_no_orders)
|
|
173
|
+
show_default = QPushButton("Default")
|
|
174
|
+
show_default.clicked.connect(self._show_default_orders)
|
|
175
|
+
for button in (show_all, show_none, show_default):
|
|
176
|
+
buttons.addWidget(button)
|
|
177
|
+
layout.addLayout(buttons)
|
|
178
|
+
|
|
179
|
+
self._order_count_label = QLabel("")
|
|
180
|
+
layout.addWidget(self._order_count_label)
|
|
181
|
+
return group
|
|
182
|
+
|
|
183
|
+
def _build_action_buttons(self) -> QVBoxLayout:
|
|
184
|
+
self._solve_button = QPushButton("Solve")
|
|
185
|
+
self._solve_button.clicked.connect(self.solve_requested)
|
|
186
|
+
self._cancel_button = QPushButton("Cancel")
|
|
187
|
+
self._cancel_button.clicked.connect(self.cancel_requested)
|
|
188
|
+
self._cancel_button.setEnabled(False)
|
|
189
|
+
self._export_button = QPushButton("Export CSV…")
|
|
190
|
+
self._export_button.clicked.connect(self.export_csv)
|
|
191
|
+
|
|
192
|
+
self._converge_button = QPushButton("Check convergence…")
|
|
193
|
+
self._converge_button.setToolTip(
|
|
194
|
+
"Sweep boundary_points until the answer stops moving, and report "
|
|
195
|
+
"the coarsest setting that can be defended."
|
|
196
|
+
)
|
|
197
|
+
self._converge_button.clicked.connect(self.convergence_requested)
|
|
198
|
+
|
|
199
|
+
self._progress = QProgressBar()
|
|
200
|
+
self._progress.setRange(0, 0)
|
|
201
|
+
self._progress.hide()
|
|
202
|
+
|
|
203
|
+
row = QHBoxLayout()
|
|
204
|
+
row.addWidget(self._solve_button)
|
|
205
|
+
row.addWidget(self._cancel_button)
|
|
206
|
+
|
|
207
|
+
column = QVBoxLayout()
|
|
208
|
+
column.addLayout(row)
|
|
209
|
+
column.addWidget(self._converge_button)
|
|
210
|
+
return column
|
|
211
|
+
|
|
212
|
+
def _build_efficiency_plot(self) -> QWidget:
|
|
213
|
+
from matplotlib.backends.backend_qtagg import (
|
|
214
|
+
FigureCanvasQTAgg,
|
|
215
|
+
NavigationToolbar2QT,
|
|
216
|
+
)
|
|
217
|
+
from matplotlib.figure import Figure
|
|
218
|
+
|
|
219
|
+
self._figure = Figure(figsize=(8, 3), layout="constrained")
|
|
220
|
+
self._axes = self._figure.add_subplot(1, 1, 1)
|
|
221
|
+
self._canvas = FigureCanvasQTAgg(self._figure)
|
|
222
|
+
|
|
223
|
+
panel = QWidget()
|
|
224
|
+
layout = QVBoxLayout(panel)
|
|
225
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
226
|
+
layout.addWidget(NavigationToolbar2QT(self._canvas, panel))
|
|
227
|
+
layout.addWidget(self._canvas)
|
|
228
|
+
return panel
|
|
229
|
+
|
|
230
|
+
def _build_provenance(self) -> QWidget:
|
|
231
|
+
panel = QWidget()
|
|
232
|
+
layout = QVBoxLayout(panel)
|
|
233
|
+
layout.setContentsMargins(8, 4, 8, 8)
|
|
234
|
+
heading = QLabel("Provenance")
|
|
235
|
+
heading.setStyleSheet("font-weight: bold")
|
|
236
|
+
self._provenance = QTextEdit()
|
|
237
|
+
self._provenance.setReadOnly(True)
|
|
238
|
+
layout.addWidget(heading)
|
|
239
|
+
layout.addWidget(self._provenance)
|
|
240
|
+
return panel
|
|
241
|
+
|
|
242
|
+
def _check_fields_match_integral_options(self) -> None:
|
|
243
|
+
declared = {f.name for f in dataclasses.fields(IntegralOptionsState)}
|
|
244
|
+
if self._fields.keys() != declared:
|
|
245
|
+
raise AssertionError(
|
|
246
|
+
"integral-option fields do not match IntegralOptionsState: "
|
|
247
|
+
f"missing {sorted(declared - self._fields.keys())}, "
|
|
248
|
+
f"extra {sorted(self._fields.keys() - declared)}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# -- the MainWindow contract ---------------------------------------
|
|
252
|
+
|
|
253
|
+
def build_options(self, problem, illumination, wavelengths) -> dict:
|
|
254
|
+
options = IntegralOptionsState(
|
|
255
|
+
boundary_points=_value(self._fields["boundary_points"]),
|
|
256
|
+
conductivity=_value(self._fields["conductivity"]),
|
|
257
|
+
)
|
|
258
|
+
return build_integral_options(problem, illumination, wavelengths, options)
|
|
259
|
+
|
|
260
|
+
def set_running(self, running: bool) -> None:
|
|
261
|
+
self._solve_button.setEnabled(not running)
|
|
262
|
+
self._converge_button.setEnabled(not running)
|
|
263
|
+
self._cancel_button.setEnabled(running)
|
|
264
|
+
# Only the knob: the sweep chooses it, so the field it would come from
|
|
265
|
+
# is dead for the duration. `conductivity` is passed through to the
|
|
266
|
+
# sweep untouched, so it stays live, like scalar's model selectors.
|
|
267
|
+
self._fields["boundary_points"].setEnabled(not running)
|
|
268
|
+
if not running:
|
|
269
|
+
self._progress.hide()
|
|
270
|
+
|
|
271
|
+
def show_progress(self) -> None:
|
|
272
|
+
self._progress.show()
|
|
273
|
+
|
|
274
|
+
def show_progress_value(self, done: int, total: int) -> None:
|
|
275
|
+
if self._progress.maximum() != total:
|
|
276
|
+
self._progress.setRange(0, total)
|
|
277
|
+
self._progress.setValue(done)
|
|
278
|
+
|
|
279
|
+
def show_solving(self, wavelength_count: int) -> None:
|
|
280
|
+
self._progress.setRange(0, 0)
|
|
281
|
+
self._paint(provenance.solving_lines(self.name, wavelength_count))
|
|
282
|
+
|
|
283
|
+
def show_result(self, scan, energy, lambda_over_period: float) -> None:
|
|
284
|
+
self._scan, self._energy = scan, energy
|
|
285
|
+
self._lambda_over_period = lambda_over_period
|
|
286
|
+
|
|
287
|
+
summaries = orders_module.summarize(scan)
|
|
288
|
+
self._visible_orders = orders_module.carry_over(
|
|
289
|
+
self._visible_orders, self._previous_orders, summaries
|
|
290
|
+
)
|
|
291
|
+
self._previous_orders = frozenset(s.order for s in summaries)
|
|
292
|
+
self._rebuild_order_list(summaries)
|
|
293
|
+
|
|
294
|
+
self._draw_efficiency(scan)
|
|
295
|
+
self._paint(provenance.provenance_lines(scan, energy, lambda_over_period))
|
|
296
|
+
|
|
297
|
+
def show_convergence(self, scan, energy, lambda_over_period: float, report) -> None:
|
|
298
|
+
self.show_result(scan, energy, lambda_over_period)
|
|
299
|
+
if report.converged_at is not None:
|
|
300
|
+
self._fields["boundary_points"].setText(str(report.converged_at))
|
|
301
|
+
|
|
302
|
+
def show_cancelled(self) -> None:
|
|
303
|
+
if self._scan is None:
|
|
304
|
+
return
|
|
305
|
+
self._paint(
|
|
306
|
+
provenance.provenance_lines(
|
|
307
|
+
self._scan, self._energy, self._lambda_over_period, cancelled=True
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def show_error(self, message: str) -> None:
|
|
312
|
+
self._paint((provenance.Line(f"solve failed: {message}\n", "bad"),))
|
|
313
|
+
|
|
314
|
+
def show_field_errors(self, errors) -> None:
|
|
315
|
+
self._paint(provenance.error_lines(errors))
|
|
316
|
+
|
|
317
|
+
# -- orders ----------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
def _rebuild_order_list(self, summaries: Sequence["orders_module.OrderSummary"]) -> None:
|
|
320
|
+
self._order_list.blockSignals(True)
|
|
321
|
+
self._order_list.clear()
|
|
322
|
+
for summary in summaries:
|
|
323
|
+
item = QListWidgetItem(orders_module.describe(summary))
|
|
324
|
+
item.setData(_ORDER_ROLE, summary.order)
|
|
325
|
+
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
|
|
326
|
+
item.setCheckState(
|
|
327
|
+
Qt.CheckState.Checked
|
|
328
|
+
if summary.order in self._visible_orders
|
|
329
|
+
else Qt.CheckState.Unchecked
|
|
330
|
+
)
|
|
331
|
+
if not summary.ever_propagating:
|
|
332
|
+
item.setForeground(self.palette().mid().color())
|
|
333
|
+
self._order_list.addItem(item)
|
|
334
|
+
self._order_list.blockSignals(False)
|
|
335
|
+
self._update_order_count_label(len(summaries))
|
|
336
|
+
|
|
337
|
+
def _update_order_count_label(self, total: int) -> None:
|
|
338
|
+
self._order_count_label.setText(
|
|
339
|
+
f"{len(self._visible_orders)} of {total} shown"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
def _on_order_item_changed(self, item: QListWidgetItem) -> None:
|
|
343
|
+
order = item.data(_ORDER_ROLE)
|
|
344
|
+
if item.checkState() == Qt.CheckState.Checked:
|
|
345
|
+
self._visible_orders = self._visible_orders | {order}
|
|
346
|
+
else:
|
|
347
|
+
self._visible_orders = self._visible_orders - {order}
|
|
348
|
+
self._update_order_count_label(self._order_list.count())
|
|
349
|
+
if self._scan is not None:
|
|
350
|
+
self._draw_efficiency(self._scan)
|
|
351
|
+
|
|
352
|
+
def _show_all_orders(self) -> None:
|
|
353
|
+
self._visible_orders = frozenset(self._previous_orders)
|
|
354
|
+
self._resync_order_checkboxes()
|
|
355
|
+
|
|
356
|
+
def _show_no_orders(self) -> None:
|
|
357
|
+
self._visible_orders = frozenset()
|
|
358
|
+
self._resync_order_checkboxes()
|
|
359
|
+
|
|
360
|
+
def _show_default_orders(self) -> None:
|
|
361
|
+
if self._scan is None:
|
|
362
|
+
return
|
|
363
|
+
self._visible_orders = orders_module.default_visible(
|
|
364
|
+
orders_module.summarize(self._scan)
|
|
365
|
+
)
|
|
366
|
+
self._resync_order_checkboxes()
|
|
367
|
+
|
|
368
|
+
def _resync_order_checkboxes(self) -> None:
|
|
369
|
+
self._order_list.blockSignals(True)
|
|
370
|
+
for i in range(self._order_list.count()):
|
|
371
|
+
item = self._order_list.item(i)
|
|
372
|
+
item.setCheckState(
|
|
373
|
+
Qt.CheckState.Checked
|
|
374
|
+
if item.data(_ORDER_ROLE) in self._visible_orders
|
|
375
|
+
else Qt.CheckState.Unchecked
|
|
376
|
+
)
|
|
377
|
+
self._order_list.blockSignals(False)
|
|
378
|
+
self._update_order_count_label(self._order_list.count())
|
|
379
|
+
if self._scan is not None:
|
|
380
|
+
self._draw_efficiency(self._scan)
|
|
381
|
+
|
|
382
|
+
# -- drawing -----------------------------------------------------
|
|
383
|
+
|
|
384
|
+
def _paint(self, lines) -> None:
|
|
385
|
+
self._provenance.setHtml(provenance.to_html(lines))
|
|
386
|
+
|
|
387
|
+
def _draw_efficiency(self, scan) -> None:
|
|
388
|
+
axes = self._axes
|
|
389
|
+
axes.clear()
|
|
390
|
+
for index, order in enumerate(scan.orders):
|
|
391
|
+
if int(order) not in self._visible_orders:
|
|
392
|
+
continue
|
|
393
|
+
axes.plot(
|
|
394
|
+
scan.wavelengths, scan.efficiency[:, index],
|
|
395
|
+
lw=1.3, label=f"m={int(order):+d}",
|
|
396
|
+
)
|
|
397
|
+
axes.plot(scan.wavelengths, scan.total, "k--", lw=1.0, alpha=0.7, label="Σ")
|
|
398
|
+
if scan.absorption is not None:
|
|
399
|
+
# Under a finite-conductivity solve Σ alone is the reflected
|
|
400
|
+
# fraction and sits below the unity line by exactly the absorbed
|
|
401
|
+
# one. Drawing A and Σ + A puts the R + A = 1 theorem on the same
|
|
402
|
+
# axes as its own discretisation error, rather than leaving a
|
|
403
|
+
# correct result looking like a deficit.
|
|
404
|
+
axes.plot(
|
|
405
|
+
scan.wavelengths, scan.absorption,
|
|
406
|
+
color="#a5370d", lw=1.0, ls="-.", alpha=0.8, label="A",
|
|
407
|
+
)
|
|
408
|
+
axes.plot(
|
|
409
|
+
scan.wavelengths, scan.total + scan.absorption,
|
|
410
|
+
color="k", lw=1.0, ls="-", alpha=0.5, label="Σ + A",
|
|
411
|
+
)
|
|
412
|
+
axes.axhline(1.0, color="#b00020", lw=0.8, ls=":", alpha=0.7)
|
|
413
|
+
axes.set_xlabel("wavelength (nm)")
|
|
414
|
+
axes.set_ylabel("efficiency")
|
|
415
|
+
axes.set_ylim(bottom=0)
|
|
416
|
+
axes.grid(alpha=0.25)
|
|
417
|
+
handles, _ = axes.get_legend_handles_labels()
|
|
418
|
+
if handles:
|
|
419
|
+
axes.legend(fontsize=7, ncol=max(1, len(handles) // 8), loc="upper right")
|
|
420
|
+
self._canvas.draw_idle()
|
|
421
|
+
|
|
422
|
+
# -- export ------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
def export_csv(self) -> None:
|
|
425
|
+
if self._scan is None:
|
|
426
|
+
QMessageBox.information(self, "Nothing to export", "Solve first.")
|
|
427
|
+
return
|
|
428
|
+
path, _ = QFileDialog.getSaveFileName(
|
|
429
|
+
self, "Export efficiencies", "efficiency.csv", "CSV (*.csv)"
|
|
430
|
+
)
|
|
431
|
+
if not path:
|
|
432
|
+
return
|
|
433
|
+
rows = self._scan.to_records()
|
|
434
|
+
with open(path, "w", newline="", encoding="utf-8") as handle:
|
|
435
|
+
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
|
|
436
|
+
writer.writeheader()
|
|
437
|
+
writer.writerows(rows)
|
|
438
|
+
QMessageBox.information(
|
|
439
|
+
self, "Exported", f"{len(rows)} rows to {Path(path).name}"
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _value(widget: QWidget) -> str:
|
|
444
|
+
"""The text of an input, whichever kind it is."""
|
|
445
|
+
if isinstance(widget, QComboBox):
|
|
446
|
+
return widget.currentText()
|
|
447
|
+
return widget.text()
|