AMS-BP 0.3.1__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 +26 -12
- 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-0.3.1.dist-info → ams_bp-0.4.0.dist-info}/METADATA +46 -27
- 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.1.dist-info/RECORD +0 -55
- ams_bp-0.3.1.dist-info/entry_points.txt +0 -2
- {ams_bp-0.3.1.dist-info → ams_bp-0.4.0.dist-info}/WHEEL +0 -0
- {ams_bp-0.3.1.dist-info → ams_bp-0.4.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,61 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from PyQt6.QtWidgets import (
|
4
|
+
QFormLayout,
|
5
|
+
QLineEdit,
|
6
|
+
QSpinBox,
|
7
|
+
QWidget,
|
8
|
+
)
|
9
|
+
|
10
|
+
|
11
|
+
class OutputConfigWidget(QWidget):
|
12
|
+
def __init__(self):
|
13
|
+
super().__init__()
|
14
|
+
layout = QFormLayout()
|
15
|
+
|
16
|
+
self.output_path = QLineEdit()
|
17
|
+
self.output_path.setPlaceholderText("e.g., ./results/")
|
18
|
+
layout.addRow("Output Path:", self.output_path)
|
19
|
+
|
20
|
+
self.output_name = QLineEdit()
|
21
|
+
self.output_name.setPlaceholderText("e.g., simulation_output")
|
22
|
+
layout.addRow("Output Name:", self.output_name)
|
23
|
+
|
24
|
+
self.subsegment_type = QLineEdit()
|
25
|
+
self.subsegment_type.setPlaceholderText("Not implemented")
|
26
|
+
self.subsegment_type.setDisabled(True)
|
27
|
+
layout.addRow("Subsegment Type:", self.subsegment_type)
|
28
|
+
|
29
|
+
self.subsegment_number = QSpinBox()
|
30
|
+
self.subsegment_number.setMinimum(0)
|
31
|
+
self.subsegment_number.setMaximum(999)
|
32
|
+
self.subsegment_number.setDisabled(True)
|
33
|
+
layout.addRow("Subsegment Number:", self.subsegment_number)
|
34
|
+
|
35
|
+
self.setLayout(layout)
|
36
|
+
|
37
|
+
def is_valid(self):
|
38
|
+
return bool(self.output_path.text().strip()) and bool(
|
39
|
+
self.output_name.text().strip()
|
40
|
+
)
|
41
|
+
|
42
|
+
def get_data(self):
|
43
|
+
return {
|
44
|
+
"output_path": self.output_path.text(),
|
45
|
+
"output_name": self.output_name.text(),
|
46
|
+
"subsegment_type": self.subsegment_type.text(), # Optional, disabled for now
|
47
|
+
"subsegment_number": self.subsegment_number.value(), # Optional, disabled for now
|
48
|
+
}
|
49
|
+
|
50
|
+
def set_data(self, data: dict):
|
51
|
+
if "output_path" in data:
|
52
|
+
self.output_path.setText(data["output_path"])
|
53
|
+
if "output_name" in data:
|
54
|
+
self.output_name.setText(data["output_name"])
|
55
|
+
if "subsegment_type" in data:
|
56
|
+
self.subsegment_type.setText(data["subsegment_type"])
|
57
|
+
if "subsegment_number" in data:
|
58
|
+
self.subsegment_number.setValue(data["subsegment_number"])
|
59
|
+
|
60
|
+
def get_help_path(self) -> Path:
|
61
|
+
return Path(__file__).parent.parent / "help_docs" / "output_help.md"
|
@@ -0,0 +1,128 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
|
3
|
+
from pydantic import ValidationError
|
4
|
+
from PyQt6.QtCore import pyqtSignal
|
5
|
+
from PyQt6.QtWidgets import (
|
6
|
+
QCheckBox,
|
7
|
+
QComboBox,
|
8
|
+
QDoubleSpinBox,
|
9
|
+
QFormLayout,
|
10
|
+
QGroupBox,
|
11
|
+
QLabel,
|
12
|
+
QLineEdit,
|
13
|
+
QMessageBox,
|
14
|
+
QVBoxLayout,
|
15
|
+
QWidget,
|
16
|
+
)
|
17
|
+
|
18
|
+
|
19
|
+
class PSFConfigWidget(QWidget):
|
20
|
+
confocal_mode_changed = pyqtSignal(bool)
|
21
|
+
|
22
|
+
def __init__(self, parent=None):
|
23
|
+
super().__init__(parent)
|
24
|
+
|
25
|
+
layout = QVBoxLayout(self)
|
26
|
+
|
27
|
+
# PSF Type Group
|
28
|
+
self.psf_group = QGroupBox("Point Spread Function (PSF)")
|
29
|
+
psf_form = QFormLayout()
|
30
|
+
|
31
|
+
self.psf_type = QComboBox()
|
32
|
+
self.psf_type.addItems(["gaussian"]) # Future-proofing
|
33
|
+
psf_form.addRow("Type:", self.psf_type)
|
34
|
+
|
35
|
+
self.custom_path = QLineEdit()
|
36
|
+
self.custom_path.setPlaceholderText("Path to custom PSF (not supported)")
|
37
|
+
self.custom_path.setEnabled(False)
|
38
|
+
psf_form.addRow("Custom Path:", self.custom_path)
|
39
|
+
|
40
|
+
# Confocal toggle
|
41
|
+
self.confocal_checkbox = QCheckBox("Confocal (Enable pinhole)")
|
42
|
+
self.confocal_checkbox.toggled.connect(self.toggle_pinhole_visibility)
|
43
|
+
self.confocal_checkbox.toggled.connect(self._on_confocal_toggled)
|
44
|
+
psf_form.addRow(self.confocal_checkbox)
|
45
|
+
|
46
|
+
# PSF Parameters
|
47
|
+
self.numerical_aperture = QDoubleSpinBox()
|
48
|
+
self.numerical_aperture.setRange(0.1, 1.5)
|
49
|
+
self.numerical_aperture.setSingleStep(0.01)
|
50
|
+
psf_form.addRow("Numerical Aperture:", self.numerical_aperture)
|
51
|
+
|
52
|
+
self.refractive_index = QDoubleSpinBox()
|
53
|
+
self.refractive_index.setRange(1.0, 2.0)
|
54
|
+
self.refractive_index.setValue(1.0)
|
55
|
+
self.refractive_index.setSingleStep(0.01)
|
56
|
+
psf_form.addRow("Refractive Index:", self.refractive_index)
|
57
|
+
|
58
|
+
self.pinhole_diameter = QDoubleSpinBox()
|
59
|
+
self.pinhole_diameter.setRange(0.1, 100.0)
|
60
|
+
self.pinhole_diameter.setSuffix(" μm")
|
61
|
+
self.pinhole_diameter.setSingleStep(0.1)
|
62
|
+
self.pinhole_diameter.setVisible(False) # Initially hidden
|
63
|
+
self.pinhole_label = QLabel("Pinhole Diameter:")
|
64
|
+
self.pinhole_label.setVisible(False)
|
65
|
+
psf_form.addRow(self.pinhole_label, self.pinhole_diameter)
|
66
|
+
|
67
|
+
self.psf_group.setLayout(psf_form)
|
68
|
+
layout.addWidget(self.psf_group)
|
69
|
+
|
70
|
+
def _on_confocal_toggled(self, enabled: bool):
|
71
|
+
self.toggle_pinhole_visibility(enabled)
|
72
|
+
self.confocal_mode_changed.emit(enabled)
|
73
|
+
|
74
|
+
def toggle_pinhole_visibility(self, enabled: bool):
|
75
|
+
self.pinhole_diameter.setVisible(enabled)
|
76
|
+
self.pinhole_label.setVisible(enabled)
|
77
|
+
|
78
|
+
def get_data(self):
|
79
|
+
"""Return current PSF config as a dict."""
|
80
|
+
return_dict = {
|
81
|
+
"type": self.psf_type.currentText(),
|
82
|
+
"custom_path": self.custom_path.text(),
|
83
|
+
"parameters": {
|
84
|
+
"numerical_aperture": self.numerical_aperture.value(),
|
85
|
+
"refractive_index": self.refractive_index.value(),
|
86
|
+
},
|
87
|
+
}
|
88
|
+
|
89
|
+
if self.confocal_checkbox.isChecked():
|
90
|
+
return_dict["parameters"]["pinhole_diameter"] = (
|
91
|
+
self.pinhole_diameter.value()
|
92
|
+
)
|
93
|
+
|
94
|
+
return return_dict
|
95
|
+
|
96
|
+
def set_data(self, data):
|
97
|
+
"""Populate fields from a given PSF config dict."""
|
98
|
+
self.psf_type.setCurrentText(data.get("type", "gaussian"))
|
99
|
+
self.custom_path.setText(data.get("custom_path", ""))
|
100
|
+
|
101
|
+
params = data.get("parameters", {})
|
102
|
+
self.numerical_aperture.setValue(params.get("numerical_aperture", 1.0))
|
103
|
+
self.refractive_index.setValue(params.get("refractive_index", 1.0))
|
104
|
+
|
105
|
+
pinhole = params.get("pinhole_diameter", None)
|
106
|
+
if pinhole is not None:
|
107
|
+
self.confocal_checkbox.setChecked(True)
|
108
|
+
self.pinhole_diameter.setValue(pinhole)
|
109
|
+
else:
|
110
|
+
self.confocal_checkbox.setChecked(False)
|
111
|
+
|
112
|
+
def validate(self) -> bool:
|
113
|
+
try:
|
114
|
+
data = self.get_data()
|
115
|
+
# validated = PSFParameters(**data["parameters"])
|
116
|
+
QMessageBox.information(
|
117
|
+
self, "Validation Successful", "PSF parameters are valid."
|
118
|
+
)
|
119
|
+
return True
|
120
|
+
except ValidationError as e:
|
121
|
+
QMessageBox.critical(self, "Validation Error", str(e))
|
122
|
+
return False
|
123
|
+
except ValueError as e:
|
124
|
+
QMessageBox.critical(self, "Validation Error", str(e))
|
125
|
+
return False
|
126
|
+
|
127
|
+
def get_help_path(self) -> Path:
|
128
|
+
return Path(__file__).parent.parent / "help_docs" / "psf_help.md"
|
File without changes
|
@@ -0,0 +1,21 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from PyQt6.QtGui import QDoubleValidator
|
4
|
+
from PyQt6.QtWidgets import (
|
5
|
+
QLineEdit,
|
6
|
+
)
|
7
|
+
|
8
|
+
|
9
|
+
def scientific_input_field(
|
10
|
+
minimum: float = -1e100,
|
11
|
+
maximum: float = 1e100,
|
12
|
+
default: Optional[float] = None,
|
13
|
+
decimals: int = 10,
|
14
|
+
) -> QLineEdit:
|
15
|
+
line = QLineEdit()
|
16
|
+
validator = QDoubleValidator(minimum, maximum, decimals)
|
17
|
+
validator.setNotation(QDoubleValidator.Notation.ScientificNotation)
|
18
|
+
line.setValidator(validator)
|
19
|
+
if default is not None:
|
20
|
+
line.setText(f"{default:.3e}")
|
21
|
+
return line
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# spectrum_editor.py
|
2
|
+
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
3
|
+
from matplotlib.figure import Figure
|
4
|
+
from PyQt6.QtWidgets import (
|
5
|
+
QDialog,
|
6
|
+
QDialogButtonBox,
|
7
|
+
QHBoxLayout,
|
8
|
+
QMessageBox,
|
9
|
+
QPushButton,
|
10
|
+
QTableWidget,
|
11
|
+
QTableWidgetItem,
|
12
|
+
QVBoxLayout,
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
class SpectrumEditorDialog(QDialog):
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
parent=None,
|
20
|
+
wavelengths=None,
|
21
|
+
intensities=None,
|
22
|
+
intensity_name="Intensity",
|
23
|
+
):
|
24
|
+
super().__init__(parent)
|
25
|
+
self.setWindowTitle("Edit Spectrum")
|
26
|
+
self.resize(600, 400)
|
27
|
+
self.wavelengths = wavelengths or []
|
28
|
+
self.intensities = intensities or []
|
29
|
+
self.intensity_name = intensity_name
|
30
|
+
|
31
|
+
self.setup_ui()
|
32
|
+
self.populate_table()
|
33
|
+
self.update_plot()
|
34
|
+
|
35
|
+
def setup_ui(self):
|
36
|
+
layout = QVBoxLayout(self)
|
37
|
+
|
38
|
+
self.table = QTableWidget(0, 2)
|
39
|
+
self.table.setHorizontalHeaderLabels(["Wavelength (nm)", self.intensity_name])
|
40
|
+
self.table.cellChanged.connect(self.update_plot)
|
41
|
+
layout.addWidget(self.table)
|
42
|
+
|
43
|
+
btns = QHBoxLayout()
|
44
|
+
add_btn = QPushButton("Add Row")
|
45
|
+
remove_btn = QPushButton("Remove Selected")
|
46
|
+
btns.addWidget(add_btn)
|
47
|
+
btns.addWidget(remove_btn)
|
48
|
+
layout.addLayout(btns)
|
49
|
+
|
50
|
+
add_btn.clicked.connect(self.add_row)
|
51
|
+
remove_btn.clicked.connect(self.remove_selected_row)
|
52
|
+
|
53
|
+
# Spectrum preview
|
54
|
+
self.figure = Figure(figsize=(4, 2))
|
55
|
+
self.canvas = FigureCanvas(self.figure)
|
56
|
+
layout.addWidget(self.canvas)
|
57
|
+
|
58
|
+
# OK / Cancel
|
59
|
+
buttons = QDialogButtonBox(
|
60
|
+
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
61
|
+
)
|
62
|
+
buttons.accepted.connect(self.accept)
|
63
|
+
buttons.rejected.connect(self.reject)
|
64
|
+
layout.addWidget(buttons)
|
65
|
+
|
66
|
+
def populate_table(self):
|
67
|
+
for w, i in zip(self.wavelengths, self.intensities):
|
68
|
+
self.add_row(w, i)
|
69
|
+
|
70
|
+
def add_row(self, wavelength="", intensity=""):
|
71
|
+
row = self.table.rowCount()
|
72
|
+
self.table.insertRow(row)
|
73
|
+
self.table.setItem(row, 0, QTableWidgetItem(str(wavelength)))
|
74
|
+
self.table.setItem(row, 1, QTableWidgetItem(str(intensity)))
|
75
|
+
|
76
|
+
def remove_selected_row(self):
|
77
|
+
row = self.table.currentRow()
|
78
|
+
if row >= 0:
|
79
|
+
self.table.removeRow(row)
|
80
|
+
self.update_plot()
|
81
|
+
|
82
|
+
def get_spectrum(self):
|
83
|
+
wavelengths = []
|
84
|
+
intensities = []
|
85
|
+
for row in range(self.table.rowCount()):
|
86
|
+
try:
|
87
|
+
w = float(self.table.item(row, 0).text())
|
88
|
+
i = float(self.table.item(row, 1).text())
|
89
|
+
wavelengths.append(w)
|
90
|
+
intensities.append(i)
|
91
|
+
except (ValueError, AttributeError):
|
92
|
+
raise ValueError(f"Invalid entry at row {row + 1}")
|
93
|
+
return wavelengths, intensities
|
94
|
+
|
95
|
+
def update_plot(self):
|
96
|
+
try:
|
97
|
+
wavelengths, intensities = self.get_spectrum()
|
98
|
+
except ValueError:
|
99
|
+
return # Avoid crashing on invalid input
|
100
|
+
|
101
|
+
self.figure.clear()
|
102
|
+
ax = self.figure.add_subplot(111)
|
103
|
+
ax.plot(wavelengths, intensities, marker="o", linestyle="-", color="blue")
|
104
|
+
ax.set_xlabel("Wavelength (nm)")
|
105
|
+
ax.set_ylabel(self.intensity_name)
|
106
|
+
ax.set_title("Spectrum Preview")
|
107
|
+
ax.grid(True)
|
108
|
+
self.canvas.draw()
|
109
|
+
|
110
|
+
def accept(self):
|
111
|
+
try:
|
112
|
+
self.wavelengths, self.intensities = self.get_spectrum()
|
113
|
+
super().accept()
|
114
|
+
except ValueError as e:
|
115
|
+
QMessageBox.critical(self, "Invalid Data", str(e))
|
File without changes
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import shutil
|
2
|
+
import sys
|
3
|
+
|
4
|
+
# logutil.py (continued)
|
5
|
+
import time
|
6
|
+
from datetime import datetime
|
7
|
+
from pathlib import Path
|
8
|
+
|
9
|
+
from PyQt6.QtCore import QObject, pyqtSignal
|
10
|
+
|
11
|
+
|
12
|
+
def cleanup_old_logs(log_dir: Path, max_age_days: int = 7):
|
13
|
+
"""
|
14
|
+
Deletes entire `run_*` directories in `log_dir` if their `sim.log` is older than `max_age_days`.
|
15
|
+
"""
|
16
|
+
if not log_dir.exists():
|
17
|
+
return
|
18
|
+
|
19
|
+
now = time.time()
|
20
|
+
max_age = max_age_days * 86400 # seconds in a day
|
21
|
+
|
22
|
+
for run_dir in log_dir.iterdir():
|
23
|
+
if run_dir.is_dir() and run_dir.name.startswith("run_"):
|
24
|
+
log_file = run_dir / "sim.log"
|
25
|
+
try:
|
26
|
+
if log_file.exists():
|
27
|
+
file_age = now - log_file.stat().st_mtime
|
28
|
+
if file_age > max_age:
|
29
|
+
shutil.rmtree(run_dir)
|
30
|
+
print(f"Deleted old run directory: {run_dir}")
|
31
|
+
except Exception as e:
|
32
|
+
print(f"Error while trying to delete {run_dir}: {e}")
|
33
|
+
|
34
|
+
|
35
|
+
class LogEmitter(QObject):
|
36
|
+
"""Qt signal emitter for log messages."""
|
37
|
+
|
38
|
+
message = pyqtSignal(str)
|
39
|
+
|
40
|
+
|
41
|
+
class LoggerStream:
|
42
|
+
"""Custom stream that writes to file and emits to GUI."""
|
43
|
+
|
44
|
+
def __init__(self, emitter: LogEmitter, log_file_path: Path):
|
45
|
+
self.emitter = emitter
|
46
|
+
self.log_file_path = log_file_path
|
47
|
+
self.log_file_path.parent.mkdir(parents=True, exist_ok=True)
|
48
|
+
self.file = open(self.log_file_path, "a", encoding="utf-8")
|
49
|
+
|
50
|
+
def write(self, text):
|
51
|
+
if text.strip():
|
52
|
+
timestamped = f"[{datetime.now().strftime('%H:%M:%S')}] {text.strip()}"
|
53
|
+
self.file.write(timestamped + "\n")
|
54
|
+
self.file.flush()
|
55
|
+
self.emitter.message.emit(timestamped) # <-- This line is essential
|
56
|
+
|
57
|
+
def flush(self):
|
58
|
+
self.file.flush()
|
59
|
+
|
60
|
+
def close(self):
|
61
|
+
self.file.close()
|
62
|
+
|
63
|
+
|
64
|
+
class LoggerManager:
|
65
|
+
"""Manages stream redirection and GUI+file log capture."""
|
66
|
+
|
67
|
+
def __init__(self, log_path: Path):
|
68
|
+
self.emitter = LogEmitter()
|
69
|
+
self.stream = LoggerStream(self.emitter, log_path)
|
70
|
+
self._stdout = sys.stdout
|
71
|
+
self._stderr = sys.stderr
|
72
|
+
|
73
|
+
def start(self):
|
74
|
+
sys.stdout = self.stream
|
75
|
+
sys.stderr = self.stream
|
76
|
+
|
77
|
+
def stop(self):
|
78
|
+
sys.stdout = self._stdout
|
79
|
+
sys.stderr = self._stderr
|
80
|
+
self.stream.close()
|
81
|
+
|
82
|
+
def get_emitter(self) -> LogEmitter:
|
83
|
+
return self.emitter
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import shutil
|
2
|
+
from datetime import datetime
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
|
6
|
+
def setup_run_directory(config_path: Path, base_dir: Path = None) -> dict:
|
7
|
+
"""
|
8
|
+
Sets up a structured directory for a simulation run.
|
9
|
+
|
10
|
+
Returns a dictionary with paths:
|
11
|
+
{
|
12
|
+
'run_dir': Path,
|
13
|
+
'log_file': Path,
|
14
|
+
'copied_config': Path,
|
15
|
+
}
|
16
|
+
"""
|
17
|
+
if base_dir is None:
|
18
|
+
base_dir = Path.home() / "AMS_runs"
|
19
|
+
|
20
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
21
|
+
run_dir = base_dir / f"run_{timestamp}"
|
22
|
+
run_dir.mkdir(parents=True, exist_ok=False)
|
23
|
+
|
24
|
+
# Copy config into run directory
|
25
|
+
copied_config = run_dir / "config.toml"
|
26
|
+
shutil.copy(config_path, copied_config)
|
27
|
+
|
28
|
+
# Define log file path
|
29
|
+
log_file = run_dir / "sim.log"
|
30
|
+
|
31
|
+
return {
|
32
|
+
"run_dir": run_dir,
|
33
|
+
"log_file": log_file,
|
34
|
+
"copied_config": copied_config,
|
35
|
+
}
|
@@ -1,39 +1,42 @@
|
|
1
1
|
"""
|
2
|
-
|
2
|
+
main_cli.py
|
3
3
|
|
4
4
|
This file contains the command-line interface (CLI) for the AMS_BP package.
|
5
5
|
|
6
6
|
The CLI is built using Typer and provides two main commands:
|
7
7
|
1. 'config': Generates a sample configuration file.
|
8
8
|
2. 'runsim': Runs the cell simulation using a provided configuration file.
|
9
|
+
3. 'gui': starts the PyQT GUI
|
9
10
|
|
10
11
|
Main Components:
|
11
12
|
- typer_app_asms_bp: The main Typer application object.
|
12
13
|
- cell_simulation(): Callback function that displays the version information.
|
13
14
|
- generate_config(): Command to generate a sample configuration file.
|
14
15
|
- run_cell_simulation(): Command to run the cell simulation using a configuration file.
|
16
|
+
- run_gui(): runs the GUI
|
15
17
|
|
16
18
|
Usage:
|
17
|
-
- To generate a config file: python
|
18
|
-
- To run a simulation: python
|
19
|
+
- To generate a config file: python main_cli.py config [OPTIONS]
|
20
|
+
- To run a simulation: python main_cli.py runsim [CONFIG_FILE]
|
19
21
|
|
20
22
|
The file uses Rich for enhanced console output and progress tracking.
|
21
23
|
"""
|
22
24
|
|
23
|
-
import os
|
24
25
|
import shutil
|
25
|
-
import
|
26
|
+
import sys
|
26
27
|
from pathlib import Path
|
27
28
|
from typing import Optional
|
28
29
|
|
29
30
|
import rich
|
30
31
|
import typer
|
32
|
+
from PyQt6.QtWidgets import QApplication
|
31
33
|
from rich.progress import Progress, SpinnerColumn, TextColumn
|
32
34
|
from typing_extensions import Annotated
|
33
35
|
|
34
36
|
from . import __version__
|
35
|
-
from .
|
36
|
-
from .
|
37
|
+
from .gui.main import MainWindow
|
38
|
+
from .logging.logutil import cleanup_old_logs
|
39
|
+
from .run_sim_util import run_simulation_from_file
|
37
40
|
|
38
41
|
cli_help_doc = str(
|
39
42
|
"""
|
@@ -61,10 +64,23 @@ typer_app_asms_bp = typer.Typer(
|
|
61
64
|
def cell_simulation():
|
62
65
|
# print version
|
63
66
|
# find version using the __version__ variable in the __init__.py file
|
67
|
+
cleanup_old_logs(Path.home() / "AMS_runs", max_age_days=7)
|
64
68
|
out_string = f"AMS_BP version: [bold]{__version__}[/bold]"
|
65
69
|
rich.print(out_string)
|
66
70
|
|
67
71
|
|
72
|
+
@typer_app_asms_bp.command(name="gui")
|
73
|
+
def run_gui() -> None:
|
74
|
+
"""Start the PyQt GUI"""
|
75
|
+
# Clean old logs
|
76
|
+
log_dir = Path.home() / "AMS_runs"
|
77
|
+
cleanup_old_logs(log_dir, max_age_days=7)
|
78
|
+
app = QApplication(sys.argv)
|
79
|
+
editor = MainWindow()
|
80
|
+
editor.show()
|
81
|
+
sys.exit(app.exec())
|
82
|
+
|
83
|
+
|
68
84
|
@typer_app_asms_bp.command(name="config")
|
69
85
|
def generate_config(
|
70
86
|
output_path: Annotated[
|
@@ -115,7 +131,9 @@ def generate_config(
|
|
115
131
|
# find the parent dir
|
116
132
|
project_directory = Path(__file__).parent
|
117
133
|
# find the config file
|
118
|
-
config_file =
|
134
|
+
config_file = (
|
135
|
+
project_directory / "resources" / "template_configs" / "sim_config.toml"
|
136
|
+
)
|
119
137
|
output_path = output_path / "sim_config.toml"
|
120
138
|
# copy the config file to the output path
|
121
139
|
|
@@ -135,74 +153,11 @@ def generate_config(
|
|
135
153
|
rich.print(f"Config file saved to {output_path.resolve()}")
|
136
154
|
|
137
155
|
|
138
|
-
# second command to run the simulation using the config file path as argument
|
139
156
|
@typer_app_asms_bp.command(name="runsim")
|
140
157
|
def run_cell_simulation(
|
141
158
|
config_file: Annotated[Path, typer.Argument(help="Path to the configuration file")],
|
142
159
|
) -> None:
|
143
|
-
|
144
|
-
Run the cell simulation using the configuration file provided.
|
145
|
-
"""
|
146
|
-
from contextlib import contextmanager
|
147
|
-
|
148
|
-
@contextmanager
|
149
|
-
def progress_context():
|
150
|
-
progress = Progress(
|
151
|
-
SpinnerColumn(),
|
152
|
-
TextColumn("[progress.description]{task.description}"),
|
153
|
-
transient=True,
|
154
|
-
)
|
155
|
-
try:
|
156
|
-
with progress:
|
157
|
-
yield progress
|
158
|
-
finally:
|
159
|
-
progress.stop()
|
160
|
-
|
161
|
-
# Use in functions
|
162
|
-
with progress_context() as progress:
|
163
|
-
start_task_1 = time.time()
|
164
|
-
task_1 = progress.add_task(
|
165
|
-
description="Processing request to run the simulation ...", total=10
|
166
|
-
)
|
167
|
-
|
168
|
-
# check if the config file is a valid file
|
169
|
-
if not os.path.isfile(config_file):
|
170
|
-
rich.print("FileNotFoundError: Configuration file not found.")
|
171
|
-
raise typer.Abort()
|
172
|
-
|
173
|
-
config_inator = ConfigLoader(config_path=config_file)
|
174
|
-
# find the version flag in the config file
|
175
|
-
if "version" in config_inator.config:
|
176
|
-
version = config_inator.config["version"]
|
177
|
-
rich.print(f"Using config version: [bold]{version}[/bold]")
|
178
|
-
|
179
|
-
setup_config = config_inator.setup_microscope()
|
180
|
-
microscope = setup_config["microscope"]
|
181
|
-
configEXP = setup_config["experiment_config"]
|
182
|
-
functionEXP = setup_config["experiment_func"]
|
183
|
-
|
184
|
-
# complete last progress
|
185
|
-
progress.update(task_1, completed=10)
|
186
|
-
rich.print(
|
187
|
-
"Prep work done in {:.2f} seconds.".format(time.time() - start_task_1)
|
188
|
-
)
|
189
|
-
|
190
|
-
time_task_2 = time.time()
|
191
|
-
task_2 = progress.add_task(description="Running the simulation ...", total=None)
|
192
|
-
|
193
|
-
# run the simulation
|
194
|
-
|
195
|
-
frames, metadata = functionEXP(microscope=microscope, config=configEXP)
|
196
|
-
|
197
|
-
# save
|
198
|
-
save_config_frames(
|
199
|
-
metadata, frames, setup_config["base_config"].OutputParameter
|
200
|
-
)
|
201
|
-
|
202
|
-
progress.update(task_2, completed=None)
|
203
|
-
rich.print(
|
204
|
-
"Simulation completed in {:.2f} seconds.".format(time.time() - time_task_2)
|
205
|
-
)
|
160
|
+
run_simulation_from_file(config_file)
|
206
161
|
|
207
162
|
|
208
163
|
def validate_config(config: dict) -> None:
|
AMS_BP/optics/filters/filters.py
CHANGED
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
"PAmCherry_EGFP_Widefield_Zstack_Fixed": {
|
3
|
+
"label": "PAmCherry with EGFP (Fixed)",
|
4
|
+
"description": "Two-Color Widefield Microscopy of a RodCell with PAmCherry and EGFP as molecules. Cell is fixed and the experiment protocol is a Z-Stack.",
|
5
|
+
"image": "palm_egfp.png",
|
6
|
+
"config": "twocolor_widefield_zstack_fixed.toml"
|
7
|
+
},
|
8
|
+
"PAmCherry_EGFP_Widefield_Zstack_Live": {
|
9
|
+
"label": "PAmCherry with EGFP (Live)",
|
10
|
+
"description": "Two-Color Widefield Microscopy of a RodCell with PAmCherry and EGFP as molecules. Cell is live (with motion described by bounded FBM models (differently for each molecule) and the experiment protocol is a Z-Stack.",
|
11
|
+
"image": "palm_egfp.png",
|
12
|
+
"config": "twocolor_widefield_zstack_live.toml"
|
13
|
+
},
|
14
|
+
"PAmCherry_EGFP_Widefield_Timeseries_Live": {
|
15
|
+
"label": "PAmCherry with EGFP (Live timeseries)",
|
16
|
+
"description": "Two-Color Widefield Microscopy of a RodCell with PAmCherry and EGFP as molecules. Cell is live (with motion described by bounded FBM models (differently for each molecule) and the experiment protocol is a time-series.",
|
17
|
+
"image": "palm_egfp.png",
|
18
|
+
"config": "twocolor_widefield_timeseries_live.toml"
|
19
|
+
}
|
20
|
+
}
|