AMS-BP 0.3.0__py3-none-any.whl → 0.4.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.
- AMS_BP/__init__.py +1 -1
- AMS_BP/configio/configmodels.py +32 -18
- AMS_BP/configio/convertconfig.py +508 -632
- AMS_BP/gui/README.md +77 -0
- AMS_BP/gui/__init__.py +0 -0
- AMS_BP/gui/assets/__init__.py +0 -0
- AMS_BP/gui/assets/drawing.svg +107 -0
- AMS_BP/gui/configuration_window.py +333 -0
- AMS_BP/gui/help_docs/__init__.py +0 -0
- AMS_BP/gui/help_docs/cell_help.md +45 -0
- AMS_BP/gui/help_docs/channels_help.md +78 -0
- AMS_BP/gui/help_docs/condensate_help.md +59 -0
- AMS_BP/gui/help_docs/detector_help.md +57 -0
- AMS_BP/gui/help_docs/experiment_help.md +92 -0
- AMS_BP/gui/help_docs/fluorophore_help.md +128 -0
- AMS_BP/gui/help_docs/general_help.md +43 -0
- AMS_BP/gui/help_docs/global_help.md +47 -0
- AMS_BP/gui/help_docs/laser_help.md +76 -0
- AMS_BP/gui/help_docs/molecule_help.md +78 -0
- AMS_BP/gui/help_docs/output_help.md +5 -0
- AMS_BP/gui/help_docs/psf_help.md +51 -0
- AMS_BP/gui/help_window.py +26 -0
- AMS_BP/gui/logging_window.py +93 -0
- AMS_BP/gui/main.py +255 -0
- AMS_BP/gui/sim_worker.py +58 -0
- AMS_BP/gui/template_window_selection.py +100 -0
- AMS_BP/gui/widgets/__init__.py +0 -0
- AMS_BP/gui/widgets/camera_config_widget.py +213 -0
- AMS_BP/gui/widgets/cell_config_widget.py +225 -0
- AMS_BP/gui/widgets/channel_config_widget.py +307 -0
- AMS_BP/gui/widgets/condensate_config_widget.py +341 -0
- AMS_BP/gui/widgets/experiment_config_widget.py +259 -0
- AMS_BP/gui/widgets/flurophore_config_widget.py +513 -0
- AMS_BP/gui/widgets/general_config_widget.py +47 -0
- AMS_BP/gui/widgets/global_config_widget.py +142 -0
- AMS_BP/gui/widgets/laser_config_widget.py +255 -0
- AMS_BP/gui/widgets/molecule_config_widget.py +714 -0
- AMS_BP/gui/widgets/output_config_widget.py +61 -0
- AMS_BP/gui/widgets/psf_config_widget.py +128 -0
- AMS_BP/gui/widgets/utility_widgets/__init__.py +0 -0
- AMS_BP/gui/widgets/utility_widgets/scinotation_widget.py +21 -0
- AMS_BP/gui/widgets/utility_widgets/spectrum_widget.py +115 -0
- AMS_BP/logging/__init__.py +0 -0
- AMS_BP/logging/logutil.py +83 -0
- AMS_BP/logging/setup_run_directory.py +35 -0
- AMS_BP/{run_cell_simulation.py → main_cli.py} +27 -72
- AMS_BP/optics/filters/filters.py +2 -0
- AMS_BP/resources/template_configs/metadata_configs.json +20 -0
- AMS_BP/resources/template_configs/sim_config.toml +408 -0
- AMS_BP/resources/template_configs/twocolor_widefield_timeseries_live.toml +399 -0
- AMS_BP/resources/template_configs/twocolor_widefield_zstack_fixed.toml +406 -0
- AMS_BP/resources/template_configs/twocolor_widefield_zstack_live.toml +408 -0
- AMS_BP/run_sim_util.py +76 -0
- AMS_BP/sim_microscopy.py +2 -2
- {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/METADATA +59 -34
- ams_bp-0.4.0.dist-info/RECORD +103 -0
- ams_bp-0.4.0.dist-info/entry_points.txt +2 -0
- ams_bp-0.3.0.dist-info/RECORD +0 -55
- ams_bp-0.3.0.dist-info/entry_points.txt +0 -2
- {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/WHEEL +0 -0
- {ams_bp-0.3.0.dist-info → ams_bp-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,51 @@
|
|
1
|
+
# Point Spread Function (PSF) Configuration Help
|
2
|
+
|
3
|
+
The PSF (Point Spread Function) models how light emitted from a point source is blurred by the microscope optics. This section defines the parameters of the optical model used for image formation.
|
4
|
+
|
5
|
+
---
|
6
|
+
|
7
|
+
## `type`
|
8
|
+
- **Type**: String
|
9
|
+
- **Allowed**: `"gaussian"` *(currently the only supported type)*
|
10
|
+
- **Description**: Specifies the PSF model. A Gaussian PSF approximates diffraction-limited optics and is sufficient for many simulation scenarios.
|
11
|
+
|
12
|
+
---
|
13
|
+
|
14
|
+
## `custom_path`
|
15
|
+
- **Type**: String
|
16
|
+
- **Default**: Empty
|
17
|
+
- **Description**: Placeholder for loading a custom PSF file. Currently not supported.
|
18
|
+
|
19
|
+
---
|
20
|
+
|
21
|
+
## `confocal`
|
22
|
+
- **Type**: Boolean
|
23
|
+
- **Description**: Enables confocal mode by introducing a pinhole before the detector to reject out-of-focus light. When enabled, additional parameters become relevant (e.g., pinhole diameter).
|
24
|
+
|
25
|
+
---
|
26
|
+
|
27
|
+
## `parameters`
|
28
|
+
|
29
|
+
### `numerical_aperture`
|
30
|
+
- **Type**: Float
|
31
|
+
- **Typical Range**: 0.1 – 1.5
|
32
|
+
- **Description**: The numerical aperture of the objective lens, which affects the resolution and depth of field of the microscope.
|
33
|
+
|
34
|
+
### `refractive_index`
|
35
|
+
- **Type**: Float
|
36
|
+
- **Typical Range**: 1.0 – 2.0
|
37
|
+
- **Description**: The refractive index of the imaging medium (e.g., air: 1.0, water: 1.33, immersion oil: ~1.5).
|
38
|
+
|
39
|
+
### `pinhole_diameter`
|
40
|
+
- **Type**: Float (optional)
|
41
|
+
- **Units**: Micrometers (μm)
|
42
|
+
- **Visible only when confocal mode is enabled**
|
43
|
+
- **Description**: The diameter of the confocal pinhole that restricts the depth of detection, improving axial resolution by eliminating out-of-focus light.
|
44
|
+
|
45
|
+
---
|
46
|
+
|
47
|
+
## Notes
|
48
|
+
|
49
|
+
- Confocal simulation is optional and intended for advanced users modeling high-resolution 3D optical sectioning.
|
50
|
+
- You can adjust the `numerical_aperture` and `refractive_index` even in widefield mode to simulate different objective lens setups.
|
51
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QTextBrowser, QVBoxLayout
|
4
|
+
|
5
|
+
|
6
|
+
class HelpWindow(QDialog):
|
7
|
+
def __init__(self, help_path: Path, parent=None):
|
8
|
+
super().__init__(parent)
|
9
|
+
self.setWindowTitle("Help")
|
10
|
+
self.resize(500, 400)
|
11
|
+
|
12
|
+
layout = QVBoxLayout()
|
13
|
+
browser = QTextBrowser()
|
14
|
+
|
15
|
+
try:
|
16
|
+
with open(help_path, "r", encoding="utf-8") as f:
|
17
|
+
browser.setMarkdown(f.read())
|
18
|
+
except Exception as e:
|
19
|
+
browser.setPlainText(f"Failed to load help: {e}")
|
20
|
+
|
21
|
+
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
22
|
+
buttons.rejected.connect(self.reject)
|
23
|
+
|
24
|
+
layout.addWidget(browser)
|
25
|
+
layout.addWidget(buttons)
|
26
|
+
self.setLayout(layout)
|
@@ -0,0 +1,93 @@
|
|
1
|
+
from PyQt6.QtCore import Qt, QTimer
|
2
|
+
from PyQt6.QtWidgets import (
|
3
|
+
QDialog,
|
4
|
+
QHBoxLayout,
|
5
|
+
QPlainTextEdit,
|
6
|
+
QProgressBar,
|
7
|
+
QPushButton,
|
8
|
+
QVBoxLayout,
|
9
|
+
)
|
10
|
+
|
11
|
+
|
12
|
+
class LogWindow(QDialog):
|
13
|
+
cancel_requested = False # Flag to check cancel request
|
14
|
+
|
15
|
+
def __init__(self, parent=None):
|
16
|
+
super().__init__(parent)
|
17
|
+
self.setWindowTitle("Simulation Log")
|
18
|
+
self.setMinimumSize(600, 300)
|
19
|
+
|
20
|
+
self.log_output = QPlainTextEdit()
|
21
|
+
self.log_output.setReadOnly(True)
|
22
|
+
|
23
|
+
# Progress bar (indeterminate)
|
24
|
+
self.progress = QProgressBar()
|
25
|
+
self.progress.setRange(0, 0) # Infinite/indeterminate animation
|
26
|
+
|
27
|
+
# Cancel button
|
28
|
+
self.cancel_button = QPushButton("Cancel")
|
29
|
+
self.cancel_button.clicked.connect(self.on_cancel_clicked)
|
30
|
+
|
31
|
+
button_layout = QHBoxLayout()
|
32
|
+
button_layout.addStretch()
|
33
|
+
button_layout.addWidget(self.cancel_button)
|
34
|
+
|
35
|
+
layout = QVBoxLayout()
|
36
|
+
layout.addWidget(self.log_output)
|
37
|
+
layout.addWidget(self.progress)
|
38
|
+
layout.addLayout(button_layout)
|
39
|
+
|
40
|
+
self.setLayout(layout)
|
41
|
+
|
42
|
+
def append_text(self, text: str):
|
43
|
+
self.log_output.appendPlainText(text)
|
44
|
+
QTimer.singleShot(0, self.scroll_to_bottom)
|
45
|
+
|
46
|
+
def scroll_to_bottom(self):
|
47
|
+
self.log_output.verticalScrollBar().setValue(
|
48
|
+
self.log_output.verticalScrollBar().maximum()
|
49
|
+
)
|
50
|
+
|
51
|
+
def on_cancel_clicked(self):
|
52
|
+
self.cancel_requested = True
|
53
|
+
self.append_text("Cancellation requested by user...")
|
54
|
+
|
55
|
+
def mark_success(self):
|
56
|
+
self.progress.setRange(0, 1)
|
57
|
+
self.progress.setValue(1)
|
58
|
+
self.progress.setStyleSheet("""
|
59
|
+
QProgressBar {
|
60
|
+
border: 1px solid #555;
|
61
|
+
border-radius: 5px;
|
62
|
+
background-color: #2d2d2d;
|
63
|
+
text-align: center;
|
64
|
+
color: white;
|
65
|
+
font-weight: bold;
|
66
|
+
font-size: 14px;
|
67
|
+
}
|
68
|
+
QProgressBar::chunk {
|
69
|
+
background-color: #4CAF50;
|
70
|
+
}
|
71
|
+
""")
|
72
|
+
self.append_text("Simulation completed successfully.")
|
73
|
+
|
74
|
+
def mark_failure(self):
|
75
|
+
self.progress.setRange(0, 1)
|
76
|
+
self.progress.setValue(0) # No fill
|
77
|
+
self.progress.setFormat("Failed, share logs with developer.") # Custom text
|
78
|
+
self.progress.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
79
|
+
self.progress.setStyleSheet("""
|
80
|
+
QProgressBar {
|
81
|
+
border: 1px solid #800000;
|
82
|
+
border-radius: 5px;
|
83
|
+
background-color: #2d2d2d;
|
84
|
+
color: #e53935;
|
85
|
+
font-weight: bold;
|
86
|
+
font-size: 13px;
|
87
|
+
text-align: center;
|
88
|
+
}
|
89
|
+
QProgressBar::chunk {
|
90
|
+
background-color: transparent; /* no fill */
|
91
|
+
}
|
92
|
+
""")
|
93
|
+
self.append_text("Simulation failed. Please send logs to the developer.")
|
AMS_BP/gui/main.py
ADDED
@@ -0,0 +1,255 @@
|
|
1
|
+
import webbrowser
|
2
|
+
from pathlib import Path
|
3
|
+
from zipfile import ZipFile
|
4
|
+
|
5
|
+
import napari
|
6
|
+
import tifffile
|
7
|
+
from PyQt6.QtCore import Qt, QThread
|
8
|
+
from PyQt6.QtGui import QPainter, QPixmap
|
9
|
+
from PyQt6.QtSvg import QSvgRenderer
|
10
|
+
from PyQt6.QtWidgets import (
|
11
|
+
QFileDialog,
|
12
|
+
QLabel,
|
13
|
+
QMainWindow,
|
14
|
+
QMessageBox,
|
15
|
+
QPushButton,
|
16
|
+
QVBoxLayout,
|
17
|
+
QWidget,
|
18
|
+
)
|
19
|
+
|
20
|
+
from ..logging.logutil import LoggerManager
|
21
|
+
from ..logging.setup_run_directory import setup_run_directory
|
22
|
+
from .logging_window import LogWindow
|
23
|
+
from .sim_worker import SimulationWorker
|
24
|
+
from .template_window_selection import TemplateSelectionWindow
|
25
|
+
|
26
|
+
LOGO_PATH = str(Path(__file__).parent / "assets" / "drawing.svg")
|
27
|
+
|
28
|
+
|
29
|
+
class MainWindow(QMainWindow):
|
30
|
+
def __init__(self):
|
31
|
+
super().__init__()
|
32
|
+
self.setWindowTitle("Welcome to AMS!")
|
33
|
+
|
34
|
+
# Set up the main layout
|
35
|
+
layout = QVBoxLayout()
|
36
|
+
|
37
|
+
# Add logo as a placeholder (SVG format)
|
38
|
+
self.logo_label = QLabel() # Label to hold the logo
|
39
|
+
self.set_svg_logo(LOGO_PATH) # Set the SVG logo
|
40
|
+
layout.addWidget(self.logo_label, alignment=Qt.AlignmentFlag.AlignCenter)
|
41
|
+
|
42
|
+
# Add the maintainer's name under the image
|
43
|
+
self.maintainer_label = QLabel(
|
44
|
+
"Maintainer: Baljyot Parmar \n baljyotparmar@hotmail.com"
|
45
|
+
)
|
46
|
+
self.maintainer_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
47
|
+
layout.addWidget(self.maintainer_label)
|
48
|
+
self.lab_label = QLabel(
|
49
|
+
"Brought to you by: " + '<a href="https://weberlab.ca">The WeberLab</a>'
|
50
|
+
)
|
51
|
+
self.lab_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
52
|
+
self.lab_label.setOpenExternalLinks(True) # Enable external links
|
53
|
+
self.lab_label.linkActivated.connect(self.on_link_activated)
|
54
|
+
layout.addWidget(self.lab_label)
|
55
|
+
|
56
|
+
# Button to open the Configuration Creation window
|
57
|
+
self.config_button = QPushButton("Create Configuration File")
|
58
|
+
self.config_button.clicked.connect(self.open_config_editor)
|
59
|
+
layout.addWidget(self.config_button)
|
60
|
+
|
61
|
+
# Create a central widget and set the layout
|
62
|
+
central_widget = QWidget()
|
63
|
+
central_widget.setLayout(layout)
|
64
|
+
self.setCentralWidget(central_widget)
|
65
|
+
|
66
|
+
# Button to run simulation with a TOML config file
|
67
|
+
self.run_sim_button = QPushButton("Run Simulation from Config")
|
68
|
+
self.run_sim_button.clicked.connect(self.run_simulation_from_config)
|
69
|
+
layout.addWidget(self.run_sim_button)
|
70
|
+
|
71
|
+
# Button to open Napari viewer
|
72
|
+
self.view_button = QPushButton("Visualize Microscopy Data (Napari)")
|
73
|
+
self.view_button.clicked.connect(self.open_napari_viewer)
|
74
|
+
layout.addWidget(self.view_button)
|
75
|
+
|
76
|
+
self.package_logs_button = QPushButton("Package Logs for Sharing")
|
77
|
+
self.package_logs_button.clicked.connect(self.package_logs)
|
78
|
+
layout.addWidget(self.package_logs_button)
|
79
|
+
|
80
|
+
def package_logs(self):
|
81
|
+
log_dir = Path.home() / "AMS_runs"
|
82
|
+
|
83
|
+
# Step 1: Open dialog to select folders
|
84
|
+
folder_paths = QFileDialog.getExistingDirectory(
|
85
|
+
self,
|
86
|
+
"Select Folder Containing Run Logs",
|
87
|
+
str(log_dir),
|
88
|
+
QFileDialog.Option.ShowDirsOnly | QFileDialog.Option.DontResolveSymlinks,
|
89
|
+
)
|
90
|
+
|
91
|
+
if not folder_paths:
|
92
|
+
return
|
93
|
+
|
94
|
+
# QFileDialog.getExistingDirectory() returns a single path.
|
95
|
+
# For now, let's treat this as a single run_* folder being selected.
|
96
|
+
|
97
|
+
run_dir = Path(folder_paths)
|
98
|
+
if not run_dir.is_dir() or not run_dir.name.startswith("run_"):
|
99
|
+
QMessageBox.warning(
|
100
|
+
self, "Invalid Selection", "Please select a valid run_* folder."
|
101
|
+
)
|
102
|
+
return
|
103
|
+
|
104
|
+
# Step 2: Ask for destination .zip archive
|
105
|
+
zip_path_str, _ = QFileDialog.getSaveFileName(
|
106
|
+
self,
|
107
|
+
"Save Zipped Folder As",
|
108
|
+
str(log_dir / f"{run_dir.name}.zip"),
|
109
|
+
"Zip Archive (*.zip)",
|
110
|
+
)
|
111
|
+
|
112
|
+
if not zip_path_str:
|
113
|
+
return
|
114
|
+
|
115
|
+
zip_path = Path(zip_path_str)
|
116
|
+
if not zip_path.suffix == ".zip":
|
117
|
+
zip_path = zip_path.with_suffix(".zip")
|
118
|
+
|
119
|
+
try:
|
120
|
+
with ZipFile(zip_path, "w") as archive:
|
121
|
+
for path in run_dir.rglob("*"):
|
122
|
+
archive.write(path, arcname=path.relative_to(run_dir.parent))
|
123
|
+
|
124
|
+
QMessageBox.information(
|
125
|
+
self,
|
126
|
+
"Logs Packaged",
|
127
|
+
f"Folder '{run_dir.name}' successfully packaged to:\n{zip_path}",
|
128
|
+
)
|
129
|
+
|
130
|
+
except Exception as e:
|
131
|
+
QMessageBox.critical(self, "Error", f"Failed to package folder:\n{e}")
|
132
|
+
|
133
|
+
def run_simulation_from_config(self):
|
134
|
+
config_path_str, _ = QFileDialog.getOpenFileName(
|
135
|
+
self,
|
136
|
+
"Select Configuration File",
|
137
|
+
"",
|
138
|
+
"TOML Files (*.toml);;All Files (*)",
|
139
|
+
)
|
140
|
+
|
141
|
+
if config_path_str:
|
142
|
+
config_path = Path(config_path_str)
|
143
|
+
|
144
|
+
# Structured run dir setup
|
145
|
+
run_info = setup_run_directory(config_path)
|
146
|
+
log_path = run_info["log_file"]
|
147
|
+
|
148
|
+
# Create logger
|
149
|
+
self.logger_manager = LoggerManager(log_path)
|
150
|
+
self.logger_manager.start()
|
151
|
+
|
152
|
+
# Log window
|
153
|
+
self.log_window = LogWindow(self)
|
154
|
+
self.log_window.show()
|
155
|
+
|
156
|
+
# Connect logger to GUI window
|
157
|
+
# Get emitter from logger
|
158
|
+
emitter = self.logger_manager.get_emitter()
|
159
|
+
|
160
|
+
# Hook up emitter to the GUI
|
161
|
+
emitter.message.connect(self.log_window.append_text)
|
162
|
+
|
163
|
+
self.sim_worker = SimulationWorker(
|
164
|
+
config_path,
|
165
|
+
emitter,
|
166
|
+
cancel_callback=lambda: self.log_window.cancel_requested,
|
167
|
+
)
|
168
|
+
|
169
|
+
self.sim_thread = QThread()
|
170
|
+
self.sim_worker.moveToThread(self.sim_thread)
|
171
|
+
|
172
|
+
self.sim_thread.started.connect(self.sim_worker.run)
|
173
|
+
self.sim_worker.finished.connect(self.sim_thread.quit)
|
174
|
+
self.sim_worker.finished.connect(self.sim_worker.deleteLater)
|
175
|
+
self.sim_thread.finished.connect(self.sim_thread.deleteLater)
|
176
|
+
|
177
|
+
self.sim_worker.finished.connect(lambda: print("Simulation finished."))
|
178
|
+
self.sim_worker.finished.connect(self.logger_manager.stop)
|
179
|
+
|
180
|
+
def handle_finished():
|
181
|
+
if self.sim_worker.failed:
|
182
|
+
self.log_window.mark_failure()
|
183
|
+
else:
|
184
|
+
self.log_window.mark_success()
|
185
|
+
|
186
|
+
self.log_window.cancel_button.setText("Close")
|
187
|
+
self.log_window.cancel_button.clicked.disconnect()
|
188
|
+
self.log_window.cancel_button.clicked.connect(self.log_window.close)
|
189
|
+
|
190
|
+
self.sim_worker.finished.connect(handle_finished)
|
191
|
+
self.sim_worker.error_occurred.connect(emitter.message.emit)
|
192
|
+
|
193
|
+
self.sim_thread.start()
|
194
|
+
|
195
|
+
def open_napari_viewer(self):
|
196
|
+
"""Open a file dialog to select a microscopy image and visualize it with Napari."""
|
197
|
+
# Allow user to select an image file
|
198
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
199
|
+
self,
|
200
|
+
"Open Microscopy Image",
|
201
|
+
"",
|
202
|
+
"Image Files (*.tif *.tiff *.nd2 *.png *.jpg *.zarr);;All Files (*)",
|
203
|
+
)
|
204
|
+
|
205
|
+
if file_path:
|
206
|
+
try:
|
207
|
+
# Load the image (expand here if you want ND2 or Zarr support)
|
208
|
+
image = tifffile.imread(file_path)
|
209
|
+
|
210
|
+
# Open Napari viewer and display the image
|
211
|
+
viewer = napari.Viewer()
|
212
|
+
viewer.add_image(image, name=Path(file_path).stem)
|
213
|
+
napari.run()
|
214
|
+
|
215
|
+
except Exception as e:
|
216
|
+
print(f"Failed to open image: {e}")
|
217
|
+
|
218
|
+
def set_svg_logo(self, svg_path):
|
219
|
+
"""Set an SVG logo to the QLabel, maintaining the aspect ratio."""
|
220
|
+
# Create a QSvgRenderer to render the SVG
|
221
|
+
renderer = QSvgRenderer(svg_path)
|
222
|
+
if renderer.isValid():
|
223
|
+
# Get the size of the SVG image
|
224
|
+
image_size = renderer.defaultSize()
|
225
|
+
|
226
|
+
# Create a QPixmap to hold the rendered SVG with the same size as the SVG image
|
227
|
+
pixmap = QPixmap(image_size * 2)
|
228
|
+
pixmap.fill(Qt.GlobalColor.transparent) # Fill the pixmap with transparency
|
229
|
+
|
230
|
+
# Use QPainter to paint the SVG onto the pixmap
|
231
|
+
painter = QPainter(pixmap)
|
232
|
+
renderer.render(painter)
|
233
|
+
painter.end()
|
234
|
+
|
235
|
+
# Scale the pixmap to fit the desired size while maintaining the aspect ratio
|
236
|
+
scaled_pixmap = pixmap.scaled(
|
237
|
+
200,
|
238
|
+
200,
|
239
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
240
|
+
Qt.TransformationMode.SmoothTransformation,
|
241
|
+
)
|
242
|
+
|
243
|
+
# Set the scaled pixmap as the QLabel content
|
244
|
+
self.logo_label.setPixmap(scaled_pixmap)
|
245
|
+
else:
|
246
|
+
print("Failed to load SVG file.")
|
247
|
+
|
248
|
+
def open_config_editor(self):
|
249
|
+
"""Launch template selection first, then open ConfigEditor."""
|
250
|
+
self.template_window = TemplateSelectionWindow()
|
251
|
+
self.template_window.show()
|
252
|
+
|
253
|
+
def on_link_activated(self, url):
|
254
|
+
"""Handle the link activation (clicking the hyperlink)."""
|
255
|
+
webbrowser.open(url) # Open the URL in the default web browser
|
AMS_BP/gui/sim_worker.py
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from PyQt6.QtCore import QObject, pyqtSignal
|
4
|
+
|
5
|
+
from ..logging.logutil import LogEmitter
|
6
|
+
|
7
|
+
|
8
|
+
class SimulationWorker(QObject):
|
9
|
+
finished = pyqtSignal()
|
10
|
+
error_occurred = pyqtSignal(str)
|
11
|
+
|
12
|
+
def __init__(self, config_path: Path, emitter: LogEmitter, cancel_callback=None):
|
13
|
+
super().__init__()
|
14
|
+
self.config_path = config_path
|
15
|
+
self.emitter = emitter
|
16
|
+
self.cancel_callback = cancel_callback or (lambda: False)
|
17
|
+
self.failed = False
|
18
|
+
|
19
|
+
def run(self):
|
20
|
+
try:
|
21
|
+
self.emitter.message.emit(f"Starting simulation for {self.config_path}")
|
22
|
+
|
23
|
+
from ..configio.convertconfig import load_config, setup_microscope
|
24
|
+
from ..configio.saving import save_config_frames
|
25
|
+
|
26
|
+
loadedconfig = load_config(self.config_path)
|
27
|
+
|
28
|
+
if "version" in loadedconfig:
|
29
|
+
version = loadedconfig["version"]
|
30
|
+
self.emitter.message.emit(f"Using config version: {version}")
|
31
|
+
|
32
|
+
setup_config = setup_microscope(loadedconfig)
|
33
|
+
microscope = setup_config["microscope"]
|
34
|
+
configEXP = setup_config["experiment_config"]
|
35
|
+
functionEXP = setup_config["experiment_func"]
|
36
|
+
|
37
|
+
if self.cancel_callback():
|
38
|
+
self.emitter.message.emit("Simulation canceled before execution.")
|
39
|
+
return
|
40
|
+
|
41
|
+
frames, metadata = functionEXP(microscope=microscope, config=configEXP)
|
42
|
+
|
43
|
+
if self.cancel_callback():
|
44
|
+
self.emitter.message.emit("Simulation canceled after run.")
|
45
|
+
return
|
46
|
+
|
47
|
+
save_config_frames(
|
48
|
+
metadata, frames, setup_config["base_config"].OutputParameter
|
49
|
+
)
|
50
|
+
|
51
|
+
self.emitter.message.emit("Simulation data saved successfully.")
|
52
|
+
self.finished.emit()
|
53
|
+
|
54
|
+
except Exception as e:
|
55
|
+
self.failed = True
|
56
|
+
self.error_occurred.emit(f"Simulation failed: {e}")
|
57
|
+
finally:
|
58
|
+
self.finished.emit()
|
@@ -0,0 +1,100 @@
|
|
1
|
+
import json
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
import tomli
|
5
|
+
from PyQt6.QtCore import Qt
|
6
|
+
from PyQt6.QtGui import QPixmap
|
7
|
+
from PyQt6.QtWidgets import (
|
8
|
+
QGroupBox,
|
9
|
+
QHBoxLayout,
|
10
|
+
QLabel,
|
11
|
+
QMessageBox,
|
12
|
+
QPushButton,
|
13
|
+
QScrollArea,
|
14
|
+
QVBoxLayout,
|
15
|
+
QWidget,
|
16
|
+
)
|
17
|
+
|
18
|
+
from .configuration_window import ConfigEditor
|
19
|
+
|
20
|
+
TEMPLATE_DIR = Path(__file__).parent.parent / "resources" / "template_configs"
|
21
|
+
METADATA_PATH = TEMPLATE_DIR / "metadata_configs.json"
|
22
|
+
|
23
|
+
|
24
|
+
class TemplateSelectionWindow(QWidget):
|
25
|
+
def __init__(self):
|
26
|
+
super().__init__()
|
27
|
+
self.setWindowTitle("Choose a Simulation Template")
|
28
|
+
self.setMinimumSize(700, 500)
|
29
|
+
layout = QVBoxLayout()
|
30
|
+
layout.addWidget(QLabel("<h2>Select a Template to Begin</h2>"))
|
31
|
+
|
32
|
+
scroll_area = QScrollArea()
|
33
|
+
scroll_area.setWidgetResizable(True)
|
34
|
+
|
35
|
+
content_widget = QWidget()
|
36
|
+
content_layout = QVBoxLayout(content_widget)
|
37
|
+
|
38
|
+
try:
|
39
|
+
with open(METADATA_PATH, "r", encoding="utf-8") as f:
|
40
|
+
metadata = json.load(f)
|
41
|
+
|
42
|
+
for key, entry in metadata.items():
|
43
|
+
card = self.create_template_card(entry)
|
44
|
+
content_layout.addWidget(card)
|
45
|
+
except Exception as e:
|
46
|
+
error_label = QLabel(f"Failed to load templates: {e}")
|
47
|
+
layout.addWidget(error_label)
|
48
|
+
|
49
|
+
scroll_area.setWidget(content_widget)
|
50
|
+
layout.addWidget(scroll_area)
|
51
|
+
self.setLayout(layout)
|
52
|
+
|
53
|
+
def create_template_card(self, entry: dict) -> QWidget:
|
54
|
+
group = QGroupBox(entry["label"])
|
55
|
+
layout = QHBoxLayout()
|
56
|
+
|
57
|
+
# Image
|
58
|
+
img_label = QLabel()
|
59
|
+
img_path = TEMPLATE_DIR / entry["image"]
|
60
|
+
if img_path.exists():
|
61
|
+
pixmap = QPixmap(str(img_path)).scaled(
|
62
|
+
200, 200, Qt.AspectRatioMode.KeepAspectRatio
|
63
|
+
)
|
64
|
+
img_label.setPixmap(pixmap)
|
65
|
+
else:
|
66
|
+
img_label.setText("[Missing image assets]")
|
67
|
+
layout.addWidget(img_label)
|
68
|
+
|
69
|
+
# Description + Button
|
70
|
+
vbox = QVBoxLayout()
|
71
|
+
|
72
|
+
description_label = QLabel(entry.get("description", ""))
|
73
|
+
description_label.setWordWrap(True) # <--- this is the key!
|
74
|
+
vbox.addWidget(description_label)
|
75
|
+
|
76
|
+
btn = QPushButton("Use This Template")
|
77
|
+
btn.clicked.connect(
|
78
|
+
lambda _, config=entry["config"]: self.load_template(config)
|
79
|
+
)
|
80
|
+
vbox.addWidget(btn)
|
81
|
+
layout.addLayout(vbox)
|
82
|
+
|
83
|
+
group.setLayout(layout)
|
84
|
+
return group
|
85
|
+
|
86
|
+
def load_template(self, config_filename: str):
|
87
|
+
config_path = TEMPLATE_DIR / config_filename
|
88
|
+
try:
|
89
|
+
with open(config_path, "rb") as f:
|
90
|
+
config = tomli.load(f)
|
91
|
+
|
92
|
+
self.launch_config_editor(config)
|
93
|
+
except Exception as e:
|
94
|
+
QMessageBox.critical(self, "Error", f"Failed to load template:\n{e}")
|
95
|
+
|
96
|
+
def launch_config_editor(self, config_dict: dict):
|
97
|
+
self.editor = ConfigEditor()
|
98
|
+
self.editor.set_data(config_dict)
|
99
|
+
self.editor.show()
|
100
|
+
self.close()
|
File without changes
|