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.
Files changed (60) hide show
  1. AMS_BP/__init__.py +1 -1
  2. AMS_BP/configio/configmodels.py +26 -12
  3. AMS_BP/configio/convertconfig.py +508 -632
  4. AMS_BP/gui/README.md +77 -0
  5. AMS_BP/gui/__init__.py +0 -0
  6. AMS_BP/gui/assets/__init__.py +0 -0
  7. AMS_BP/gui/assets/drawing.svg +107 -0
  8. AMS_BP/gui/configuration_window.py +333 -0
  9. AMS_BP/gui/help_docs/__init__.py +0 -0
  10. AMS_BP/gui/help_docs/cell_help.md +45 -0
  11. AMS_BP/gui/help_docs/channels_help.md +78 -0
  12. AMS_BP/gui/help_docs/condensate_help.md +59 -0
  13. AMS_BP/gui/help_docs/detector_help.md +57 -0
  14. AMS_BP/gui/help_docs/experiment_help.md +92 -0
  15. AMS_BP/gui/help_docs/fluorophore_help.md +128 -0
  16. AMS_BP/gui/help_docs/general_help.md +43 -0
  17. AMS_BP/gui/help_docs/global_help.md +47 -0
  18. AMS_BP/gui/help_docs/laser_help.md +76 -0
  19. AMS_BP/gui/help_docs/molecule_help.md +78 -0
  20. AMS_BP/gui/help_docs/output_help.md +5 -0
  21. AMS_BP/gui/help_docs/psf_help.md +51 -0
  22. AMS_BP/gui/help_window.py +26 -0
  23. AMS_BP/gui/logging_window.py +93 -0
  24. AMS_BP/gui/main.py +255 -0
  25. AMS_BP/gui/sim_worker.py +58 -0
  26. AMS_BP/gui/template_window_selection.py +100 -0
  27. AMS_BP/gui/widgets/__init__.py +0 -0
  28. AMS_BP/gui/widgets/camera_config_widget.py +213 -0
  29. AMS_BP/gui/widgets/cell_config_widget.py +225 -0
  30. AMS_BP/gui/widgets/channel_config_widget.py +307 -0
  31. AMS_BP/gui/widgets/condensate_config_widget.py +341 -0
  32. AMS_BP/gui/widgets/experiment_config_widget.py +259 -0
  33. AMS_BP/gui/widgets/flurophore_config_widget.py +513 -0
  34. AMS_BP/gui/widgets/general_config_widget.py +47 -0
  35. AMS_BP/gui/widgets/global_config_widget.py +142 -0
  36. AMS_BP/gui/widgets/laser_config_widget.py +255 -0
  37. AMS_BP/gui/widgets/molecule_config_widget.py +714 -0
  38. AMS_BP/gui/widgets/output_config_widget.py +61 -0
  39. AMS_BP/gui/widgets/psf_config_widget.py +128 -0
  40. AMS_BP/gui/widgets/utility_widgets/__init__.py +0 -0
  41. AMS_BP/gui/widgets/utility_widgets/scinotation_widget.py +21 -0
  42. AMS_BP/gui/widgets/utility_widgets/spectrum_widget.py +115 -0
  43. AMS_BP/logging/__init__.py +0 -0
  44. AMS_BP/logging/logutil.py +83 -0
  45. AMS_BP/logging/setup_run_directory.py +35 -0
  46. AMS_BP/{run_cell_simulation.py → main_cli.py} +27 -72
  47. AMS_BP/optics/filters/filters.py +2 -0
  48. AMS_BP/resources/template_configs/metadata_configs.json +20 -0
  49. AMS_BP/resources/template_configs/sim_config.toml +408 -0
  50. AMS_BP/resources/template_configs/twocolor_widefield_timeseries_live.toml +399 -0
  51. AMS_BP/resources/template_configs/twocolor_widefield_zstack_fixed.toml +406 -0
  52. AMS_BP/resources/template_configs/twocolor_widefield_zstack_live.toml +408 -0
  53. AMS_BP/run_sim_util.py +76 -0
  54. {ams_bp-0.3.1.dist-info → ams_bp-0.4.0.dist-info}/METADATA +46 -27
  55. ams_bp-0.4.0.dist-info/RECORD +103 -0
  56. ams_bp-0.4.0.dist-info/entry_points.txt +2 -0
  57. ams_bp-0.3.1.dist-info/RECORD +0 -55
  58. ams_bp-0.3.1.dist-info/entry_points.txt +0 -2
  59. {ams_bp-0.3.1.dist-info → ams_bp-0.4.0.dist-info}/WHEEL +0 -0
  60. {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
- run_cell_simulation.py
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 run_cell_simulation.py config [OPTIONS]
18
- - To run a simulation: python run_cell_simulation.py runsim [CONFIG_FILE]
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 time
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 .configio.convertconfig import ConfigLoader
36
- from .configio.saving import save_config_frames
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 = project_directory / "sim_config.toml"
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:
@@ -6,6 +6,8 @@ from pydantic import BaseModel, Field, field_validator
6
6
 
7
7
  CustomNDarray: TypeAlias = NDArray[np.float64]
8
8
 
9
+ FILTERS = ["bandpass", "allow_all"]
10
+
9
11
 
10
12
  class FilterSpectrum(BaseModel):
11
13
  """Represents the spectral characteristics of an optical filter"""
@@ -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
+ }