diff --git a/pyproject.toml b/pyproject.toml index 9663db2..292940b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "pickmix" -version = "0.3.0" +version = "0.3.1" description = "Multi-kind particle picking GUI for cryo-ET sub-tomogram averaging workflows." readme = "README.md" authors = [ diff --git a/src/pickmix/gui/dialogs/kind_dialog.py b/src/pickmix/gui/dialogs/kind_dialog.py index 4c67f89..62df9b1 100644 --- a/src/pickmix/gui/dialogs/kind_dialog.py +++ b/src/pickmix/gui/dialogs/kind_dialog.py @@ -33,7 +33,7 @@ class KindDialog(QDialog): # __init__: build the form; name and kind are None when adding, others are the kinds besides this one (so the parent choices and the names this kind cannot take), and check returns why a name and kind cannot be saved, or None def __init__( self, parent: QWidget | None, name: str | None, kind: Kind | None, others: list[str], - check: Callable[[str, Kind], str | None], + check: Callable[[str, Kind], str | None], *, tomo_angpix: float | None = None, ) -> None: super().__init__(parent) self._check = check @@ -61,7 +61,12 @@ def __init__( self.tilt_box.setChecked(kind.tilt_prior_deg is not None) self.tilt_spin.setEnabled(self.tilt_box.isChecked()) self.tilt_box.toggled.connect(self.tilt_spin.setEnabled) - self.angpix_spin = _spin(kind.extract_angpix, minimum=0.01) + self.angpix_spin = _spin(kind.extract_angpix, minimum=0.01, decimals=3) + self.tomo_button = QPushButton('Tomogram') + self.tomo_button.setVisible(tomo_angpix is not None) + if tomo_angpix is not None: + self.tomo_button.setToolTip(f'Use the tomogram pixel size, {tomo_angpix:g} Å/px') + self.tomo_button.clicked.connect(lambda: self.angpix_spin.setValue(tomo_angpix)) self.error = QLabel() error_look(self.error) self.error.setWordWrap(True) @@ -74,6 +79,9 @@ def __init__( colour_row = QHBoxLayout() colour_row.addWidget(self.colour_edit) colour_row.addWidget(choose) + angpix_row = QHBoxLayout() + angpix_row.addWidget(self.angpix_spin, 1) + angpix_row.addWidget(self.tomo_button) tilt_row = QHBoxLayout() tilt_row.addWidget(self.tilt_box) tilt_row.addWidget(self.tilt_spin) @@ -88,7 +96,7 @@ def __init__( layout.addWidget(self._section('Estimation', [ ('estimator', self.estimator_box), ('parent', self.parent_box), ('parent_tolerance_a', self.tolerance_spin), ])) - layout.addWidget(self._section('Extraction', [('tilt_prior_deg', tilt_row), ('extract_angpix', self.angpix_spin)])) + layout.addWidget(self._section('Extraction', [('tilt_prior_deg', tilt_row), ('extract_angpix', angpix_row)])) layout.addWidget(self.error) layout.addLayout(footer) @@ -142,10 +150,10 @@ def _choose_colour(self) -> None: if colour.isValid(): self.colour_edit.setText(colour.name()) -# _spin: a float spin box with two decimals -def _spin(value: float, *, minimum: float = 0.0, maximum: float = 1_000_000.0) -> QDoubleSpinBox: +# _spin: a float spin box, two decimals unless asked for more +def _spin(value: float, *, minimum: float = 0.0, maximum: float = 1_000_000.0, decimals: int = 2) -> QDoubleSpinBox: box = QDoubleSpinBox() box.setRange(minimum, maximum) - box.setDecimals(2) + box.setDecimals(decimals) box.setValue(value) return box diff --git a/src/pickmix/gui/panes/kinds_pane.py b/src/pickmix/gui/panes/kinds_pane.py index faa0ba9..7ae43bd 100644 --- a/src/pickmix/gui/panes/kinds_pane.py +++ b/src/pickmix/gui/panes/kinds_pane.py @@ -147,7 +147,7 @@ def _edit(self, name: str | None) -> None: if self.project is None: return kinds = self.project.config.kinds - dialog = KindDialog(self, name, None if name is None else kinds[name], [k for k in kinds if k != name], lambda new, kind: self._problem(new, kind, name)) + dialog = KindDialog(self, name, None if name is None else kinds[name], [k for k in kinds if k != name], lambda new, kind: self._problem(new, kind, name), tomo_angpix=self.project.common_pixel_size) result = dialog.exec() if result == DELETED and name is not None: self._delete(name) diff --git a/src/pickmix/project.py b/src/pickmix/project.py index 0aa0c12..05f29e7 100644 --- a/src/pickmix/project.py +++ b/src/pickmix/project.py @@ -3,6 +3,7 @@ ''' # Import external dependencies +from collections import Counter from collections.abc import Sequence from dataclasses import dataclass from pathlib import Path @@ -27,6 +28,12 @@ def update(self, **changes: object) -> None: save_config(config, self.path) self.config = config + # common_pixel_size: pixel size most tomograms share in A/px + @property + def common_pixel_size(self) -> float | None: + sizes = Counter(round(t.pixel_size, 3) for t in self.tomos.values()) + return sizes.most_common(1)[0][0] if sizes else None + # display_name: the configured name, else the name of the directory holding config.toml @property def display_name(self) -> str: diff --git a/tests/gui/test_dialogs.py b/tests/gui/test_dialogs.py index 4d41555..0daf324 100644 --- a/tests/gui/test_dialogs.py +++ b/tests/gui/test_dialogs.py @@ -34,6 +34,17 @@ def test_defaults_when_adding(self, qtbot: QtBot) -> None: dialog.name_edit.setText('yellow') assert dialog.values() == ('yellow', Kind(marker_a=100, box_a=100)) + # test_tomogram_pixel_size_button: button fills the extract pixel size with tomogram's, and only shows when it is known + def test_tomogram_pixel_size_button(self, qtbot: QtBot) -> None: + dialog = KindDialog(None, 'cage', Kind(marker_a=600), [], _ok, tomo_angpix=5.36) + qtbot.addWidget(dialog) + assert not dialog.tomo_button.isHidden() and dialog.values()[1].extract_angpix == 10.68 + dialog.tomo_button.click() + assert dialog.values()[1].extract_angpix == 5.36 + plain = KindDialog(None, 'cage', Kind(marker_a=600), [], _ok) + qtbot.addWidget(plain) + assert plain.tomo_button.isHidden() + # test_delete_only_when_editing: an existing kind can be deleted, a new one cannot def test_delete_only_when_editing(self, qtbot: QtBot) -> None: dialog = KindDialog(None, 'cage', Kind(marker_a=600), [], _ok) diff --git a/tests/test_project.py b/tests/test_project.py index f465b33..a63a1a0 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -60,3 +60,10 @@ def test_create_with_name(self, tmp_path: Path, relion_project: Callable[[Path, (tmp_path / 'raw').mkdir() path, _ = create_project(tmp_path / 'proj', [('r1', tmp_path / 'p')], 'Tomograms/job006', tmp_path / 'raw', name='Cages') assert load_config(path).name == 'Cages' + + # test_common_pixel_size: the pixel size most tomograms share, None when there are none + def test_common_pixel_size(self, make_config: Callable[..., Path]) -> None: + project = open_project(make_config()) + assert project.common_pixel_size == 5.36 + project.tomos = {} + assert project.common_pixel_size is None \ No newline at end of file diff --git a/uv.lock b/uv.lock index 9b71180..7f3e8a7 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.14" [[package]] @@ -137,7 +137,7 @@ wheels = [ [[package]] name = "pickmix" -version = "0.3.0" +version = "0.3.1" source = { editable = "." } dependencies = [ { name = "imageio" },