From fb16dce4ee30a109c1156925751d42442db4ee3c Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:57:55 +0100 Subject: [PATCH 01/25] base typing improvements --- fibsem/milling/base.py | 60 ++++++++++++++++++++++++------------------ 1 file changed, 34 insertions(+), 26 deletions(-) diff --git a/fibsem/milling/base.py b/fibsem/milling/base.py index b0fa2592..870d7c33 100644 --- a/fibsem/milling/base.py +++ b/fibsem/milling/base.py @@ -1,50 +1,58 @@ +from __future__ import annotations from abc import ABC, abstractmethod from copy import deepcopy -from dataclasses import dataclass, fields, field -from typing import List, Union, Dict, Any, Tuple, Optional +from dataclasses import dataclass, fields, field, asdict +from typing import TYPE_CHECKING from fibsem.microscope import FibsemMicroscope from fibsem.milling.config import MILLING_SPUTTER_RATE from fibsem.milling.patterning.patterns2 import BasePattern as BasePattern, get_pattern as get_pattern -from fibsem.structures import FibsemMillingSettings, Point, MillingAlignment, ImageSettings, CrossSectionPattern +from fibsem.structures import FibsemMillingSettings, MillingAlignment, ImageSettings, CrossSectionPattern, NumericalDisplayInfo + +if TYPE_CHECKING: + from typing import List, Dict, Any, Tuple, Optional, Type, TypeVar, ClassVar + + TMillingStrategyConfig = TypeVar( + "TMillingStrategyConfig", bound="MillingStrategyConfig" + ) + TMillingStrategy = TypeVar("TMillingStrategy", bound="MillingStrategy") + @dataclass class MillingStrategyConfig(ABC): """Abstract base class for milling strategy configurations""" - - def to_dict(self): - return {} + _advanced_attributes: ClassVar[Tuple[str, ...]] = () + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + @classmethod + def from_dict( + cls: Type[TMillingStrategyConfig], d: Dict[str, Any] + ) -> TMillingStrategyConfig: + return cls(**d) - @staticmethod - def from_dict(d: dict) -> "MillingStrategyConfig": - return MillingStrategyConfig() - - @property - def required_attributes(self) -> Tuple[str]: - return [field.name for field in fields(self)] - @property - def advanced_attributes(self) -> List[str]: - if hasattr(self, "_advanced_attributes"): - return self._advanced_attributes - return [] + def required_attributes(self) -> Tuple[str, ...]: + return tuple(f.name for f in fields(self)) + @dataclass class MillingStrategy(ABC): """Abstract base class for different milling strategies""" - name: str = "Milling Strategy" + name: str = "Milling Strategy" config = MillingStrategyConfig() def __init__(self, **kwargs): pass - + @abstractmethod - def to_dict(self): + def to_dict(self) -> dict[str, Any]: return {"name": self.name, "config": self.config.to_dict()} - - @staticmethod + + @classmethod @abstractmethod - def from_dict(d: dict) -> "MillingStrategy": + def from_dict(cls: Type[TMillingStrategy], d: dict[str, Any]) -> TMillingStrategy: pass @abstractmethod @@ -81,7 +89,7 @@ def __post_init__(self): if self.imaging.resolution is None: self.imaging.resolution = [1536, 1024] # default resolution for imaging - def to_dict(self): + def to_dict(self) -> dict[str, Any]: return { "name": self.name, "num": self.num, @@ -93,7 +101,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, data: dict): + def from_dict(cls, data: dict) -> "FibsemMillingStage": strategy_config = data.get("strategy", {}) strategy_name = strategy_config.get("name", "Standard") pattern_name = data["pattern"]["name"] From be596fbc98a2db4f6638c347c836f880b79d0ac4 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 12:58:35 +0100 Subject: [PATCH 02/25] conversions type improvements --- fibsem/conversions.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/fibsem/conversions.py b/fibsem/conversions.py index ae9e9679..78e356c7 100644 --- a/fibsem/conversions.py +++ b/fibsem/conversions.py @@ -34,7 +34,7 @@ def image_to_microscope_image_coordinates( dy = float(-(coord.y - cy)) # neg = down dx = float(coord.x - cx) # neg = left - point_m = convert_point_from_pixel_to_metres(Point(dx, dy), pixelsize) + point_m = convert_point_from_pixel_to_metres(Point(x=dx, y=dy), pixelsize) # point_m = Point(dx, dy)._to_metres(pixel_size=pixelsize) return point_m @@ -42,7 +42,7 @@ def image_to_microscope_image_coordinates( def get_lamella_size_in_pixels( img: FibsemImage, protocol: dict, use_trench_height: bool = False -) -> Tuple[int]: +) -> Tuple[int, int]: """Get the relative size of the lamella in pixels based on the hfw of the image. Args: @@ -53,6 +53,9 @@ def get_lamella_size_in_pixels( Returns: Tuple[int]: A tuple containing the height and width of the lamella in pixels. """ + if img.metadata is None: + raise ValueError("Image has no metadata") + # get real size from protocol lamella_width = protocol["lamella_width"] lamella_height = protocol["lamella_height"] @@ -89,7 +92,7 @@ def convert_metres_to_pixels(distance: float, pixelsize: float) -> int: return int(distance / pixelsize) -def convert_pixels_to_metres(pixels: int, pixelsize: float) -> float: +def convert_pixels_to_metres(pixels: float, pixelsize: float) -> float: """ Convert a distance in pixels to metres based on a given pixel size. From e357aad67030b65416806686ba44a2037baa5b83 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:02:15 +0100 Subject: [PATCH 03/25] Update MillingStrategy and MillingStrategyConfig Fixes some static typing issues Should be easier to subclass MillingStrategy is no longer a dataclass (instead has class attributes) MillingStrategy now has class_config class attribute MIllingStrategy and MillingStrategyConfig have methods that don't need subclassing --- fibsem/milling/base.py | 17 ++++++++------- fibsem/milling/strategy/overtilt.py | 32 +++++------------------------ fibsem/milling/strategy/standard.py | 13 +----------- 3 files changed, 14 insertions(+), 48 deletions(-) diff --git a/fibsem/milling/base.py b/fibsem/milling/base.py index 870d7c33..761ced4a 100644 --- a/fibsem/milling/base.py +++ b/fibsem/milling/base.py @@ -10,7 +10,7 @@ from fibsem.structures import FibsemMillingSettings, MillingAlignment, ImageSettings, CrossSectionPattern, NumericalDisplayInfo if TYPE_CHECKING: - from typing import List, Dict, Any, Tuple, Optional, Type, TypeVar, ClassVar + from typing import List, Dict, Any, Tuple, Optional, Type, TypeVar, ClassVar, Generic TMillingStrategyConfig = TypeVar( "TMillingStrategyConfig", bound="MillingStrategyConfig" @@ -37,23 +37,22 @@ def required_attributes(self) -> Tuple[str, ...]: return tuple(f.name for f in fields(self)) -@dataclass -class MillingStrategy(ABC): +class MillingStrategy(ABC, Generic[TMillingStrategyConfig]): """Abstract base class for different milling strategies""" name: str = "Milling Strategy" - config = MillingStrategyConfig() + config_class: Type[TMillingStrategyConfig] - def __init__(self, **kwargs): - pass + def __init__(self, config: Optional[TMillingStrategyConfig] = None): + self.config: TMillingStrategyConfig = config or self.config_class() @abstractmethod def to_dict(self) -> dict[str, Any]: return {"name": self.name, "config": self.config.to_dict()} @classmethod - @abstractmethod - def from_dict(cls: Type[TMillingStrategy], d: dict[str, Any]) -> TMillingStrategy: - pass + def from_dict(cls: Type[TMillingStrategy], d: dict) -> TMillingStrategy: + config=cls.config_class.from_dict(d.get("config", {})) + return cls(config=config) @abstractmethod def run(self, microscope: FibsemMicroscope, stage: "FibsemMillingStage", asynch: bool = False, parent_ui = None) -> None: diff --git a/fibsem/milling/strategy/overtilt.py b/fibsem/milling/strategy/overtilt.py index a3b04bac..3172d386 100644 --- a/fibsem/milling/strategy/overtilt.py +++ b/fibsem/milling/strategy/overtilt.py @@ -1,8 +1,8 @@ import logging import os -from dataclasses import dataclass -from typing import Tuple, List +from dataclasses import dataclass, field +from typing import List import numpy as np from fibsem import acquire, alignment @@ -18,36 +18,14 @@ @dataclass class OvertiltTrenchMillingConfig(MillingStrategyConfig): overtilt: float = 1 - resolution: List[int] = None + resolution: List[int] = field(default_factory=lambda: [1536, 1024]) - def __post_init__(self): - if self.resolution is None: - self.resolution = [1536, 1024] - @staticmethod - def from_dict(d: dict) -> "OvertiltTrenchMillingConfig": - return OvertiltTrenchMillingConfig(**d) - - def to_dict(self): - return {"overtilt": self.overtilt, } - -@dataclass -class OvertiltTrenchMillingStrategy(MillingStrategy): +class OvertiltTrenchMillingStrategy(MillingStrategy[OvertiltTrenchMillingConfig]): """Overtilt milling strategy for trench milling""" name: str = "Overtilt" fullname: str = "Overtilt Trench Milling" - - def __init__(self, config: OvertiltTrenchMillingConfig = None): - self.config = config or OvertiltTrenchMillingConfig() - - def to_dict(self): - return {"name": self.name, "config": self.config.to_dict()} - - @staticmethod - def from_dict(d: dict) -> "OvertiltTrenchMillingStrategy": - config = OvertiltTrenchMillingConfig.from_dict(d["config"]) - - return OvertiltTrenchMillingStrategy(config=config) + config_class = OvertiltTrenchMillingConfig def run(self, microscope: FibsemMicroscope, stage: "FibsemMillingStage", asynch: bool = False, parent_ui = None) -> None: diff --git a/fibsem/milling/strategy/standard.py b/fibsem/milling/strategy/standard.py index 00976ddc..2e76cdc6 100644 --- a/fibsem/milling/strategy/standard.py +++ b/fibsem/milling/strategy/standard.py @@ -15,22 +15,11 @@ class StandardMillingConfig(MillingStrategyConfig): pass -@dataclass class StandardMillingStrategy(MillingStrategy): """Basic milling strategy that mills continuously until completion""" name: str = "Standard" fullname: str = "Standard Milling" - - def __init__(self, config: StandardMillingConfig = None): - self.config = config or StandardMillingConfig() - - def to_dict(self): - return {"name": self.name, "config": self.config.to_dict()} - - @staticmethod - def from_dict(d: dict) -> "StandardMillingStrategy": - config=StandardMillingConfig.from_dict(d.get("config", {})) - return StandardMillingStrategy(config=config) + config_class = StandardMillingConfig def run( self, From d1a0552ba63b6d2598aa2d0e87fb129752d2934c Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:03:09 +0100 Subject: [PATCH 04/25] image_settings.path is expected to be of type Path image_settings.path is supposed to be of type Path --- fibsem/milling/strategy/overtilt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fibsem/milling/strategy/overtilt.py b/fibsem/milling/strategy/overtilt.py index 3172d386..42ad3441 100644 --- a/fibsem/milling/strategy/overtilt.py +++ b/fibsem/milling/strategy/overtilt.py @@ -1,6 +1,6 @@ import logging -import os +from pathlib import Path from dataclasses import dataclass, field from typing import List import numpy as np @@ -50,7 +50,7 @@ def run(self, microscope: FibsemMicroscope, stage: "FibsemMillingStage", asynch: resolution=[1536, 1024], beam_type=stage.milling.milling_channel) image_settings.reduced_area = stage.alignment.rect - image_settings.path = os.getcwd() + image_settings.path = Path.cwd() image_settings.filename = f"ref_{stage.name}_overtilt_alignment" ref_image = acquire.acquire_image(microscope, image_settings) From 84116bba9c897489bdf5f11a2e650b022fe0bd05 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:07:15 +0100 Subject: [PATCH 05/25] Add some milling Optionals --- fibsem/alignment.py | 2 +- fibsem/milling/core.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fibsem/alignment.py b/fibsem/alignment.py index e3f2cc2b..6f77c20b 100644 --- a/fibsem/alignment.py +++ b/fibsem/alignment.py @@ -485,7 +485,7 @@ def multi_step_alignment_v2( microscope: FibsemMicroscope, ref_image: FibsemImage, beam_type: BeamType, - alignment_current: float = None, + alignment_current: Optional[float] = None, steps: int = 3, use_autocontrast: bool = False, subsystem: Optional[str] = None, diff --git a/fibsem/milling/core.py b/fibsem/milling/core.py index 607396e9..cc9f873d 100644 --- a/fibsem/milling/core.py +++ b/fibsem/milling/core.py @@ -23,7 +23,7 @@ def setup_milling( microscope: FibsemMicroscope, milling_stage: FibsemMillingStage, - ref_image: FibsemImage = None, + ref_image: Optional[FibsemImage] = None, ): """Setup Microscope for FIB Milling. From 3318914e14abf23324e25544ba33a3793ce4f933 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:07:38 +0100 Subject: [PATCH 06/25] Improve MIllingStrategy typing --- fibsem/milling/strategy/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fibsem/milling/strategy/__init__.py b/fibsem/milling/strategy/__init__.py index a3045684..da811a55 100644 --- a/fibsem/milling/strategy/__init__.py +++ b/fibsem/milling/strategy/__init__.py @@ -13,10 +13,10 @@ StandardMillingStrategy.name: StandardMillingStrategy, OvertiltTrenchMillingStrategy.name: OvertiltTrenchMillingStrategy, } -REGISTERED_STRATEGIES: typing.Dict[str, type[MillingStrategy]] = {} +REGISTERED_STRATEGIES: typing.Dict[str, type[MillingStrategy[typing.Any]]] = {} -def get_strategies() -> typing.Dict[str, type[MillingStrategy]]: +def get_strategies() -> typing.Dict[str, type[MillingStrategy[typing.Any]]]: # This order means that builtins > registered > plugins if there are any name clashes return {**_get_plugin_strategies(), **REGISTERED_STRATEGIES, **BUILTIN_STRATEGIES} From e79f0447c3d6402654faf83ed4e150cb6985ac7e Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:08:34 +0100 Subject: [PATCH 07/25] Fix acquire_images_after_milling typing --- fibsem/milling/core.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/fibsem/milling/core.py b/fibsem/milling/core.py index cc9f873d..878c2511 100644 --- a/fibsem/milling/core.py +++ b/fibsem/milling/core.py @@ -1,6 +1,7 @@ import logging import time -from typing import List, Tuple +from os import PathLike +from typing import List, Tuple, Optional, Union from fibsem import config as fcfg from fibsem.microscope import FibsemMicroscope @@ -246,7 +247,7 @@ def acquire_images_after_milling( microscope: FibsemMicroscope, milling_stage: FibsemMillingStage, start_time: float, - path: str, + path: Optional[Union[str, PathLike]], ) -> Tuple[FibsemImage, FibsemImage]: """Acquire images after milling for reference. Args: @@ -265,7 +266,7 @@ def acquire_images_after_milling( # set imaging parameters (filename, path, etc.) if milling_stage.imaging.path is None: - milling_stage.imaging.path = path + milling_stage.imaging.path = str(path) milling_stage.imaging.filename = f"ref_milling_{milling_stage.name.replace(' ', '-')}_finished_{str(start_time).replace('.', '_')}" # from pprint import pprint From 7c2d63eca591c743a2d0832de86d20356b66c369 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:09:11 +0100 Subject: [PATCH 08/25] Add missing milling_voltage argument to run_milling --- fibsem/microscope.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fibsem/microscope.py b/fibsem/microscope.py index a1b6540a..06f9b673 100644 --- a/fibsem/microscope.py +++ b/fibsem/microscope.py @@ -330,7 +330,7 @@ def setup_milling(self, mill_settings: FibsemMillingSettings) -> None: pass @abstractmethod - def run_milling(self, milling_current: float, asynch: bool) -> None: + def run_milling(self, milling_current: float, milling_voltage: float, asynch: bool) -> None: pass @abstractmethod @@ -1079,7 +1079,7 @@ class ThermoMicroscope(FibsemMicroscope): setup_milling(self, mill_settings: FibsemMillingSettings): Configure the microscope for milling using the ion beam. - run_milling(self, milling_current: float, asynch: bool = False): + run_milling(self, milling_current: float, milling_voltage: float, asynch: bool = False): Run ion beam milling using the specified milling current. finish_milling(self, imaging_current: float): From 42abe3a84a256082ec5b42ddf69eefe42d6216fe Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:14:58 +0100 Subject: [PATCH 09/25] Raise errors for unexpected types handling strategy widgets --- fibsem/ui/FibsemMillingWidget.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/fibsem/ui/FibsemMillingWidget.py b/fibsem/ui/FibsemMillingWidget.py index 50f6d2be..470aa1db 100644 --- a/fibsem/ui/FibsemMillingWidget.py +++ b/fibsem/ui/FibsemMillingWidget.py @@ -490,7 +490,10 @@ def set_milling_strategy_ui(self): # default None val = getattr(strategy.config, key, None) - if isinstance(val, (int, float)) and not isinstance(val, bool): + if isinstance(val, bool): + control_widget = QtWidgets.QCheckBox() + control_widget.setChecked(bool(val)) + elif isinstance(val, (int, float)): # limits min_val = -1000 @@ -505,18 +508,18 @@ def set_milling_strategy_ui(self): if key in ["overtilt"]: control_widget.setSuffix(" °") control_widget.setValue(val) - if isinstance(val, str): + elif isinstance(val, str): control_widget = QtWidgets.QLineEdit() control_widget.setText(val) - if isinstance(val, bool): - control_widget = QtWidgets.QCheckBox() - control_widget.setChecked(bool(val)) - if isinstance(val, (tuple, list)): + + elif isinstance(val, (tuple, list)): # dont handle for now if "resolution" in key: control_widget = QtWidgets.QComboBox() control_widget.addItems(cfg.STANDARD_RESOLUTIONS) control_widget.setCurrentText(f"{val[0]}x{val[1]}") # TODO: check if in list + else: + raise TypeError(f"{strategy.name} config '{key}' is unsupported type '{type(val)}'") # TODO: add support for scaling, str, bool, etc. # TODO: attached events @@ -544,14 +547,16 @@ def get_milling_strategy_from_ui(self): if isinstance(widget, QtWidgets.QDoubleSpinBox): value = scale_value_for_display(key, widget.value(), constants.MICRO_TO_SI) # TODO: support other scales - if isinstance(widget, QtWidgets.QLineEdit): + elif isinstance(widget, QtWidgets.QLineEdit): value = widget.text() - if isinstance(widget, QtWidgets.QCheckBox): + elif isinstance(widget, QtWidgets.QCheckBox): value = widget.isChecked() - if isinstance(widget, QtWidgets.QComboBox): + elif isinstance(widget, QtWidgets.QComboBox): value = widget.currentText() if "resolution" in key: value = tuple(map(int, value.split("x"))) + else: + raise TypeError(f"Unexpected widget type {type(widget)} in milling strategy UI") # TODO: add support for scaling, str, bool, etc. setattr(strategy.config, key, value) From 5359dceffca8958bdfaf7b262834cb1a0b01871d Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 14:16:07 +0100 Subject: [PATCH 10/25] Handle if ref_image attribute is unset in StandardMillingStrategy --- fibsem/milling/strategy/standard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fibsem/milling/strategy/standard.py b/fibsem/milling/strategy/standard.py index 2e76cdc6..0c740f8f 100644 --- a/fibsem/milling/strategy/standard.py +++ b/fibsem/milling/strategy/standard.py @@ -29,7 +29,7 @@ def run( parent_ui = None, ) -> None: logging.info(f"Running {self.name} Milling Strategy for {stage.name}") - setup_milling(microscope, milling_stage=stage, ref_image=stage.ref_image) + setup_milling(microscope, milling_stage=stage, ref_image=getattr(stage, "ref_image", None)) draw_patterns(microscope, stage.pattern.define()) From 25a7831cef6c32255d19e0324525277ba392d472 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:40:32 +0100 Subject: [PATCH 11/25] Improve FibsemPatternSettings typing --- fibsem/structures.py | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/fibsem/structures.py b/fibsem/structures.py index 4147281b..2e3a8d62 100644 --- a/fibsem/structures.py +++ b/fibsem/structures.py @@ -1,5 +1,6 @@ # fibsem structures +from __future__ import annotations import json import os import sys @@ -8,7 +9,7 @@ from datetime import datetime from enum import Enum, auto from pathlib import Path -from typing import List, Optional, Tuple, Union, Set, Any, Dict +from typing import List, Optional, Tuple, Union, Set, Any, Dict, TypeVar, TYPE_CHECKING import numpy as np import tifffile as tff @@ -35,6 +36,10 @@ except ImportError: THERMO = False +if TYPE_CHECKING: + TFibsemPatternSettings = TypeVar("TFibsemPatternSettings", bound="FibsemPatternSettings") + + @dataclass class Point: x: float = 0.0 @@ -867,9 +872,10 @@ class FibsemPatternSettings(ABC): @abstractmethod def to_dict(self) -> dict: pass - - @staticmethod - def from_dict(self, data: dict) -> "FibsemPatternSettings": + + @abstractmethod + @classmethod + def from_dict(cls: type[TFibsemPatternSettings], data: dict) -> TFibsemPatternSettings: pass @property @@ -912,9 +918,9 @@ def to_dict(self) -> dict: "is_exclusion": self.is_exclusion, } - @staticmethod - def from_dict(data: dict) -> "FibsemRectangleSettings": - return FibsemRectangleSettings( + @classmethod + def from_dict(cls, data: dict) -> "FibsemRectangleSettings": + return cls( width=data["width"], height=data["height"], depth=data["depth"], @@ -949,9 +955,9 @@ def to_dict(self) -> dict: "depth": self.depth, } - @staticmethod - def from_dict(data: dict) -> "FibsemLineSettings": - return FibsemLineSettings( + @classmethod + def from_dict(cls, data: dict) -> "FibsemLineSettings": + return cls( start_x=data["start_x"], end_x=data["end_x"], start_y=data["start_y"], @@ -988,9 +994,9 @@ def to_dict(self) -> dict: "is_exclusion": self.is_exclusion, } - @staticmethod - def from_dict(data: dict) -> "FibsemCircleSettings": - return FibsemCircleSettings( + @classmethod + def from_dict(cls, data: dict) -> "FibsemCircleSettings": + return cls( radius=data["radius"], depth=data["depth"], centre_x=data["centre_x"], @@ -1026,10 +1032,9 @@ def to_dict(self) -> dict: "centre_y": self.centre_y, "path": self.path, } - - @staticmethod - def from_dict(data: dict) -> "FibsemBitmapSettings": - return FibsemBitmapSettings( + @classmethod + def from_dict(cls, data: dict) -> "FibsemBitmapSettings": + return cls( width=data["width"], height=data["height"], depth=data["depth"], From 0dd1d2f289b0bb4d8faff88068b12ff392e4919b Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:43:18 +0100 Subject: [PATCH 12/25] Improve pattern typing (and fix dataclass default argument issue) --- fibsem/milling/patterning/patterns2.py | 205 ++++++++++++------------- 1 file changed, 101 insertions(+), 104 deletions(-) diff --git a/fibsem/milling/patterning/patterns2.py b/fibsem/milling/patterning/patterns2.py index 98cfea1f..32bf56ad 100644 --- a/fibsem/milling/patterning/patterns2.py +++ b/fibsem/milling/patterning/patterns2.py @@ -1,7 +1,8 @@ +from __future__ import annotations from copy import deepcopy from abc import ABC, abstractmethod from dataclasses import dataclass, fields, field -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple, Any, Type, ClassVar, TypeVar, Generic, TYPE_CHECKING import numpy as np @@ -16,6 +17,10 @@ Point, ) +if TYPE_CHECKING: + from fibsem.structures import TFibsemPatternSettings + TPattern = TypeVar("TPattern", bound="BasePattern") + # TODO: define the configuration for each key, # e.g. # "width": {"type": "float", "min": 0, "max": 1000, "default": 100, "description": "Width of the rectangle"} @@ -28,34 +33,43 @@ CORE_PATTERN_ATTRIBUTES = ["name", "point", "shapes"] @dataclass -class BasePattern(ABC): - # name: str = "BasePattern" - # point: Point = field(default_factory=Point) - # shapes: List[FibsemPatternSettings] = None +class BasePattern(ABC, Generic[TFibsemPatternSettings]): + name: ClassVar[str] + point: Point + shapes: List[TFibsemPatternSettings] = field(init=False) + + _advanced_attributes: ClassVar[Tuple[str, ...]] = field(default_factory=tuple) # TODO: investigate TypeError: non-default argument 'width' follows default argument when uncommenting the above lines + def __post_init__(self): + # This is solved by the 'kw_only' flag in 3.10+ (see + # https://bugs.python.org/issue43532) but this workaround works in + # earlier versions too. + if not hasattr(self, "point"): + setattr(self, "point", Point()) + if not hasattr(self, "shapes"): + setattr(self, "shapes", []) + @abstractmethod - def define(self) -> List[FibsemPatternSettings]: + def define(self) -> List[TFibsemPatternSettings]: pass @abstractmethod - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: pass @classmethod @abstractmethod - def from_dict(cls, ddict: dict) -> "BasePattern": + def from_dict(cls: Type[TPattern], ddict: Dict[str, Any]) -> TPattern: pass @property - def required_attributes(self) -> Tuple[str]: - return [field.name for field in fields(self) if field.name not in CORE_PATTERN_ATTRIBUTES] + def required_attributes(self) -> Tuple[str, ...]: + return tuple(field.name for field in fields(self) if field.name not in CORE_PATTERN_ATTRIBUTES) @property - def advanced_attributes(self) -> List[str]: - if hasattr(self, "_advanced_attributes"): - return self._advanced_attributes - return [] + def advanced_attributes(self) -> Tuple[str, ...]: + return self._advanced_attributes @property def volume(self) -> float: @@ -69,11 +83,10 @@ class BitmapPattern(BasePattern): depth: float rotation: float = 0 path: str = "" - shapes: List[FibsemPatternSettings] = None - point: Point = field(default_factory=Point) - name: str = "Bitmap" - def define(self) -> List[FibsemPatternSettings]: + name: ClassVar[str] = "Bitmap" + + def define(self) -> List[FibsemBitmapSettings]: shape = FibsemBitmapSettings( width=self.width, @@ -88,7 +101,7 @@ def define(self) -> List[FibsemPatternSettings]: self.shapes = [shape] return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -100,7 +113,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "BitmapPattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "BitmapPattern": return cls( width=ddict["width"], height=ddict["height"], @@ -121,10 +134,9 @@ class RectanglePattern(BasePattern): passes: int = 0 scan_direction: str = "TopToBottom" cross_section: CrossSectionPattern = CrossSectionPattern.Rectangle - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None - name: str = "Rectangle" - _advanced_attributes = ["time", "passes"] # TODO: add for other patterns + + name: ClassVar[str] = "Rectangle" + _advanced_attributes: ClassVar[Tuple[str, ...]] = ("time", "passes") # TODO: add for other patterns def define(self) -> List[FibsemRectangleSettings]: @@ -144,7 +156,7 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [shape] return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -159,7 +171,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "RectanglePattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "RectanglePattern": return cls( width=ddict["width"], height=ddict["height"], @@ -180,9 +192,8 @@ class LinePattern(BasePattern): start_y: float end_y: float depth: float - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None - name: str = "Line" + + name: ClassVar[str] = "Line" def define(self) -> List[FibsemLineSettings]: shape = FibsemLineSettings( @@ -195,7 +206,7 @@ def define(self) -> List[FibsemLineSettings]: self.shapes = [shape] return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -207,7 +218,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "LinePattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "LinePattern": return cls( start_x=ddict["start_x"], end_x=ddict["end_x"], @@ -222,9 +233,8 @@ class CirclePattern(BasePattern): radius: float depth: float thickness: float = 0 - name: str = "Circle" - shapes: List[FibsemPatternSettings] = None - point: Point = field(default_factory=Point) + + name: ClassVar[str] = "Circle" def define(self) -> List[FibsemCircleSettings]: @@ -238,7 +248,7 @@ def define(self) -> List[FibsemCircleSettings]: self.shapes = [shape] return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -248,7 +258,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "CirclePattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "CirclePattern": return cls( radius=ddict["radius"], depth=ddict["depth"], @@ -266,10 +276,9 @@ class TrenchPattern(BasePattern): cross_section: CrossSectionPattern = CrossSectionPattern.Rectangle time: float = 0.0 fillet: float = 0.0 - point: Point = field(default_factory=Point) - name: str = "Trench" - shapes: List[FibsemPatternSettings] = None - _advanced_attributes = ["time", "fillet"] + + name: ClassVar[str] = "Trench" + _advanced_attributes: ClassVar[Tuple[str, ...]] = ("time", "fillet") def define(self) -> List[FibsemRectangleSettings]: @@ -399,7 +408,7 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -414,7 +423,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "TrenchPattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "TrenchPattern": return cls( width=ddict["width"], depth=ddict["depth"], @@ -438,9 +447,8 @@ class HorseshoePattern(BasePattern): inverted: bool = False scan_direction: str = "TopToBottom" cross_section: CrossSectionPattern = CrossSectionPattern.Rectangle - shapes: List[FibsemPatternSettings] = None - point: Point = field(default_factory=Point) - name: str = "Horseshoe" + + name: ClassVar[str] = "Horseshoe" # ref: "horseshoe" terminology https://www.researchgate.net/publication/351737991_A_Modular_Platform_for_Streamlining_Automated_Cryo-FIB_Workflows#pf14 def define(self) -> List[FibsemRectangleSettings]: @@ -501,7 +509,7 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [lower_pattern, upper_pattern, side_pattern] return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -517,7 +525,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "HorseshoePattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "HorseshoePattern": return cls( width=ddict["width"], spacing=ddict["spacing"], @@ -542,8 +550,8 @@ class HorseshoePatternVertical(BasePattern): scan_direction: str = "TopToBottom" inverted: bool = False cross_section: CrossSectionPattern = CrossSectionPattern.Rectangle - point: Point = field(default_factory=Point) - name: str = "HorseshoeVertical" + + name: ClassVar[str] = "HorseshoeVertical" # ref: "horseshoe" terminology https://www.researchgate.net/publication/351737991_A_Modular_Platform_for_Streamlining_Automated_Cryo-FIB_Workflows#pf14 def define(self) -> List[FibsemRectangleSettings]: @@ -594,7 +602,7 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [left_pattern,right_pattern, upper_pattern] return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -609,7 +617,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "HorseshoePatternVertical": + def from_dict(cls, ddict: Dict[str, Any]) -> "HorseshoePatternVertical": return cls( width=ddict["width"], height=ddict["height"], @@ -632,9 +640,8 @@ class SerialSectionPattern(BasePattern): side_depth: float = 0 inverted: bool = False use_side_patterns: bool = True - name: str = "SerialSection" - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None + + name: ClassVar[str] = "SerialSection" # ref: "serial-liftout section" https://www.nature.com/articles/s41592-023-02113-5 def define(self) -> List[FibsemRectangleSettings]: @@ -705,7 +712,7 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -720,7 +727,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "SerialSectionPattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "SerialSectionPattern": return cls( section_thickness=ddict["section_thickness"], section_width=ddict["section_width"], @@ -741,9 +748,8 @@ class FiducialPattern(BasePattern): depth: float rotation: float = 0 cross_section: CrossSectionPattern = CrossSectionPattern.Rectangle - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None - name: str = "Fiducial" + + name: ClassVar[str] = "Fiducial" def define(self) -> List[FibsemRectangleSettings]: """Draw a fiducial milling pattern (cross shape)""" @@ -779,7 +785,7 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [left_pattern, right_pattern] return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -791,7 +797,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "FiducialPattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "FiducialPattern": return cls( width=ddict["width"], height=ddict["height"], @@ -811,9 +817,8 @@ class UndercutPattern(BasePattern): rhs_height: float h_offset: float cross_section: CrossSectionPattern = CrossSectionPattern.Rectangle - name: str = "Undercut" - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None + + name: ClassVar[str] = "Undercut" def define(self) -> List[FibsemRectangleSettings]: @@ -862,7 +867,7 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [top_pattern, rhs_pattern] return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -876,7 +881,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "UndercutPattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "UndercutPattern": return cls( width=ddict["width"], height=ddict["height"], @@ -894,11 +899,10 @@ class MicroExpansionPattern(BasePattern): height: float depth: float distance: float - name: str = "MicroExpansion" - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None + name: ClassVar[str] = "MicroExpansion" # ref: https://www.nature.com/articles/s41467-022-29501-3 + def define(self) -> List[FibsemRectangleSettings]: """Draw the microexpansion joints for stress relief of lamella""" @@ -929,7 +933,7 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [left_pattern_settings, right_pattern_settings] return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -940,7 +944,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "MicroExpansionPattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "MicroExpansionPattern": return cls( width=ddict["width"], height=ddict["height"], @@ -962,9 +966,8 @@ class ArrayPattern(BasePattern): rotation: float = 0 scan_direction: str = "TopToBottom" cross_section: CrossSectionPattern = CrossSectionPattern.Rectangle - name: str = "ArrayPattern" - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None + + name: ClassVar[str] = "ArrayPattern" # ref: spotweld terminology https://www.researchgate.net/publication/351737991_A_Modular_Platform_for_Streamlining_Automated_Cryo-FIB_Workflows#pf14 # ref: weld cross-section/ passes: https://www.nature.com/articles/s41592-023-02113-5 @@ -1012,7 +1015,7 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -1030,7 +1033,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "ArrayPattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "ArrayPattern": return cls( width=ddict["width"], height=ddict["height"], @@ -1056,9 +1059,8 @@ class WaffleNotchPattern(BasePattern): distance: float inverted: bool = False cross_section: CrossSectionPattern = CrossSectionPattern.Rectangle - name: str = "WaffleNotch" - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None + + name: ClassVar[str] = "WaffleNotch" # ref: https://www.nature.com/articles/s41467-022-29501-3 def define(self) -> List[FibsemRectangleSettings]: @@ -1134,7 +1136,7 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -1149,7 +1151,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "WaffleNotchPattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "WaffleNotchPattern": return cls( vheight=ddict["vheight"], vwidth=ddict["vwidth"], @@ -1166,10 +1168,8 @@ def from_dict(cls, ddict: dict) -> "WaffleNotchPattern": class CloverPattern(BasePattern): radius: float depth: float - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None - name: str = "Clover" + name: ClassVar[str] = "Clover" def define(self) -> List[FibsemPatternSettings]: @@ -1212,7 +1212,7 @@ def define(self) -> List[FibsemPatternSettings]: self.shapes = [top_pattern, right_pattern, left_pattern, stem_pattern] return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -1221,7 +1221,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "CloverPattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "CloverPattern": return cls( radius=ddict["radius"], depth=ddict["depth"], @@ -1234,12 +1234,10 @@ class TriForcePattern(BasePattern): width: float height: float depth: float - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None - name: str = "TriForce" + + name: ClassVar[str] = "TriForce" def define(self) -> List[FibsemPatternSettings]: - point = self.point height = self.height width = self.width @@ -1247,7 +1245,6 @@ def define(self) -> List[FibsemPatternSettings]: angle = 30 self.shapes = [] - # centre of each triangle points = [ Point(point.x, point.y + height), @@ -1256,7 +1253,6 @@ def define(self) -> List[FibsemPatternSettings]: ] for point in points: - triangle_shapes = create_triangle_patterns(width=width, height=height, depth=depth, @@ -1266,7 +1262,7 @@ def define(self) -> List[FibsemPatternSettings]: return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -1276,7 +1272,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "TriForcePattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "TriForcePattern": return cls( width=ddict["width"], height=ddict["height"], @@ -1293,9 +1289,8 @@ class TrapezoidPattern(BasePattern): distance: float n_rectangles: int overlap: float - name: str = "Trapezoid" - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None + + name: ClassVar[str] = "Trapezoid" def define(self) -> List[FibsemPatternSettings]: @@ -1314,7 +1309,7 @@ def define(self) -> List[FibsemPatternSettings]: # bottom half for i in range(n_rectangles): width = outer_width - i * width_increments - y = point.y + (i * dict["trench_height"] * (1-overlap)) - distance - trench_height + y = point.y + (i * self.trench_height * (1-overlap)) - distance - trench_height centre = Point(point.x, y) pattern = FibsemRectangleSettings( width=width, @@ -1328,7 +1323,7 @@ def define(self) -> List[FibsemPatternSettings]: # top half for i in range(n_rectangles): width = outer_width - i * width_increments - y = point.y - (i * dict["trench_height"] * (1-overlap)) + distance + trench_height + y = point.y - (i * self.trench_height * (1-overlap)) + distance + trench_height centre = Point(point.x, y) pattern = FibsemRectangleSettings( width=width, @@ -1341,7 +1336,7 @@ def define(self) -> List[FibsemPatternSettings]: self.shapes.append(deepcopy(pattern)) return self.shapes - def to_dict(self): + def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "point": self.point.to_dict(), @@ -1355,7 +1350,7 @@ def to_dict(self): } @classmethod - def from_dict(cls, ddict: dict) -> "TrapezoidPattern": + def from_dict(cls, ddict: Dict[str, Any]) -> "TrapezoidPattern": return cls( inner_width=ddict["inner_width"], @@ -1407,7 +1402,7 @@ def create_triangle_patterns( return [left_pattern, right_pattern, bottom_pattern] -MILLING_PATTERNS: Dict[str, BasePattern] = { +MILLING_PATTERNS: Dict[str, Type[BasePattern]] = { RectanglePattern.name.lower(): RectanglePattern, LinePattern.name.lower(): LinePattern, CirclePattern.name.lower(): CirclePattern, @@ -1429,7 +1424,7 @@ def create_triangle_patterns( DEFAULT_MILLING_PATTERN = RectanglePattern.name # legacy mapping -PROTOCOL_MILL_MAP = { +PROTOCOL_MILL_MAP: Dict[str, Type[BasePattern]] = { "cut": RectanglePattern, "fiducial": FiducialPattern, "flatten": RectanglePattern, @@ -1461,7 +1456,9 @@ def create_triangle_patterns( "mill_polishing": TrenchPattern, } -def get_pattern(name: str, config: dict) -> BasePattern: +def get_pattern(name: str, config: dict[str, Any]) -> BasePattern: cls_pattern = MILLING_PATTERNS.get(name.lower()) + if cls_pattern is None: + raise ValueError(f"Pattern '{name}' is not defined") pattern = cls_pattern.from_dict(config) return pattern \ No newline at end of file From 510eb9e8da7f60816cd1b68e1a513f3d417fd9b2 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:44:31 +0100 Subject: [PATCH 13/25] Smaller plotting type fixes --- fibsem/milling/patterning/plotting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fibsem/milling/patterning/plotting.py b/fibsem/milling/patterning/plotting.py index 3dbb842a..f14faaa3 100644 --- a/fibsem/milling/patterning/plotting.py +++ b/fibsem/milling/patterning/plotting.py @@ -1,7 +1,7 @@ import logging import math from dataclasses import dataclass -from typing import Callable, List, Tuple +from typing import Callable, List, Tuple, Optional import matplotlib.patches as mpatches import matplotlib.pyplot as plt @@ -131,7 +131,7 @@ def _draw_rectangle_pattern( return patches -def get_drawing_function(name: str) -> Callable: +def get_drawing_function(name: str) -> Optional[Callable]: if name in ["Circle", "Bitmap", "Line", "SerialSection"]: return None @@ -146,7 +146,7 @@ def draw_milling_patterns( title: str = "Milling Patterns", show_current: bool = False, show_preset: bool = False, -) -> plt.Figure: +) -> Tuple[plt.figure.Figure, plt.axes.Axes]: """ Draw milling patterns on an image. Args: @@ -157,8 +157,8 @@ def draw_milling_patterns( Returns: plt.Figure: Figure with patterns drawn. """ - fig: plt.Figure - ax: plt.Axes + fig: plt.figure.Figure + ax: plt.axes.Axes fig, ax = plt.subplots(1, 1, figsize=(10, 10)) ax.imshow(image.data, cmap="gray") From 5d898563ff9fd4d7b8b6c820ed8592176ed47fed Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:56:11 +0100 Subject: [PATCH 14/25] Various typing improvements for structures --- fibsem/structures.py | 182 ++++++++++++++++++++++++------------------- 1 file changed, 102 insertions(+), 80 deletions(-) diff --git a/fibsem/structures.py b/fibsem/structures.py index 2e3a8d62..ba2bf37e 100644 --- a/fibsem/structures.py +++ b/fibsem/structures.py @@ -9,7 +9,18 @@ from datetime import datetime from enum import Enum, auto from pathlib import Path -from typing import List, Optional, Tuple, Union, Set, Any, Dict, TypeVar, TYPE_CHECKING +from typing import ( + List, + Optional, + Tuple, + Union, + Set, + Any, + Dict, + TypeVar, + Generator, + TYPE_CHECKING, +) import numpy as np import tifffile as tff @@ -93,7 +104,7 @@ def distance(self, other: "Point") -> "Point": def euclidean(self, other: "Point") -> float: """Calculate the euclidean distance between two points.""" - return np.linalg.norm(self.distance(other).to_list()) + return float(np.linalg.norm(self.distance(other).to_list())) # TODO: convert these to match autoscript... @@ -363,7 +374,7 @@ def to_autoscript_position(self) -> ManipulatorPosition: ) @classmethod - def from_autoscript_position(cls, position: ManipulatorPosition) -> None: + def from_autoscript_position(cls, position: ManipulatorPosition) -> "FibsemManipulatorPosition": return cls( x=position.x, y=position.y, @@ -407,7 +418,8 @@ def __post_init__(self): self.height, int ), f"type {type(self.height)} is unsupported for height, must be int or floar" - def from_dict(settings: dict) -> "FibsemRectangle": + @classmethod + def from_dict(cls, settings: dict) -> "FibsemRectangle": if settings is None: return None points = ["left", "top", "width", "height"] @@ -417,7 +429,7 @@ def from_dict(settings: dict) -> "FibsemRectangle": assert isinstance(value, float) or isinstance(value, int) or value is None - return FibsemRectangle( + return cls( left=settings["left"], top=settings["top"], width=settings["width"], @@ -476,19 +488,19 @@ class ImageSettings: Converts the ImageSettings object to a dictionary of image settings. """ - resolution: list = None - dwell_time: float = None - hfw: float = None - autocontrast: bool = None - beam_type: BeamType = None - save: bool = None - filename: str = None - autogamma: bool = None - path: Path = None - reduced_area: FibsemRectangle = None - line_integration: int = None # (int32) 2 - 255 - scan_interlacing: int = None # (int32) 2 - 8 - frame_integration: int = None # (int32) 2 - 512 + resolution: Optional[list] = None + dwell_time: Optional[float] = None + hfw: Optional[float] = None + autocontrast: Optional[bool] = None + beam_type: Optional[BeamType] = None + save: Optional[bool] = None + filename: Optional[str] = None + autogamma: Optional[bool] = None + path: Optional[Path] = None + reduced_area: Optional[FibsemRectangle] = None + line_integration: Optional[int] = None # (int32) 2 - 255 + scan_interlacing: Optional[int] = None # (int32) 2 - 8 + frame_integration: Optional[int] = None # (int32) 2 - 512 drift_correction: bool = False # (bool) # requires frame_integration > 1 def __post_init__(self): @@ -628,16 +640,16 @@ class BeamSettings: """ beam_type: BeamType - working_distance: float = None - beam_current: float = None - voltage: float = None - hfw: float = None - resolution: list = None - dwell_time: float = None - stigmation: Point = None - shift: Point = None - scan_rotation: float = None - preset: str = None + working_distance: Optional[float] = None + beam_current: Optional[float] = None + voltage: Optional[float] = None + hfw: Optional[float] = None + resolution: Optional[list] = None + dwell_time: Optional[float] = None + stigmation: Optional[Point] = None + shift: Optional[Point] = None + scan_rotation: Optional[float] = None + preset: Optional[str] = None def __post_init__(self): assert ( @@ -834,9 +846,9 @@ def to_dict(self) -> dict: return state_dict - @staticmethod - def from_dict(state_dict: dict) -> "MicroscopeState": - + @classmethod + def from_dict(cls, state_dict: dict) -> "MicroscopeState": + # beam, and detector settings are now optional electron_beam, electron_detector = None, None ion_beam, ion_detector = None, None @@ -850,7 +862,7 @@ def from_dict(state_dict: dict) -> "MicroscopeState": if state_dict.get("ion_detector", None) is not None: ion_detector = FibsemDetectorSettings.from_dict(state_dict["ion_detector"]) - microscope_state = MicroscopeState( + microscope_state = cls( timestamp=state_dict["timestamp"], stage_position=FibsemStagePosition.from_dict( state_dict["stage_position"] @@ -1020,7 +1032,7 @@ class FibsemBitmapSettings(FibsemPatternSettings): rotation: float centre_x: float centre_y: float - path: str = None + path: Optional[Union[str, os.PathLike]] = None def to_dict(self) -> dict: return { @@ -1134,9 +1146,9 @@ def to_dict(self) -> dict: return settings_dict - @staticmethod - def from_dict(settings: dict) -> "FibsemMillingSettings": - milling_settings = FibsemMillingSettings( + @classmethod + def from_dict(cls, settings: dict) -> "FibsemMillingSettings": + milling_settings = cls( milling_current=settings.get("milling_current", 20.0e-12), spot_size=settings.get("spot_size", 5.0e-8), rate=settings.get("rate", 3.0e-11), @@ -1190,10 +1202,9 @@ def to_dict(self): "tilt": self.tilt, "milling_angle": self.milling_angle, } - - @staticmethod - def from_dict(settings: dict): - return StageSystemSettings( + @classmethod + def from_dict(cls, settings: dict) -> "StageSystemSettings": + return cls( rotation_reference=settings["rotation_reference"], rotation_180=settings["rotation_180"], shuttle_pre_tilt=settings["shuttle_pre_tilt"], @@ -1214,7 +1225,7 @@ class BeamSystemSettings: eucentric_height: float column_tilt: float plasma: bool = False - plasma_gas: str = None + plasma_gas: Optional[str] = None def to_dict(self): ddict = { @@ -1236,10 +1247,10 @@ def to_dict(self): ddict["current"] = ddict.pop("beam_current") return ddict - - @staticmethod - def from_dict(settings: dict) -> 'BeamSystemSettings': - return BeamSystemSettings( + + @classmethod + def from_dict(cls, settings: dict) -> 'BeamSystemSettings': + return cls( beam_type=BeamType[settings["beam_type"]], enabled=settings["enabled"], beam=BeamSettings.from_dict(settings), @@ -1306,8 +1317,8 @@ class SystemInfo: hardware_version: str software_version: str fibsem_version: str = fibsem.__version__ - application: str = None - application_version: str = None + application: Optional[str] = None + application_version: Optional[str] = None def to_dict(self): return { @@ -1322,10 +1333,10 @@ def to_dict(self): "application": self.application, "application_version": self.application_version, } - - @staticmethod - def from_dict(settings: dict): - return SystemInfo( + + @classmethod + def from_dict(cls, settings: dict) -> "SystemInfo": + return cls( name=settings.get("name", "Unknown"), ip_address=settings.get("ip_address", "Unknown"), manufacturer=settings.get("manufacturer", "Unknown"), @@ -1356,9 +1367,9 @@ def to_dict(self): "gis": self.gis.to_dict(), "info": self.info.to_dict(), } - - @staticmethod - def from_dict(settings: dict): + + @classmethod + def from_dict(cls, settings: dict) -> "SystemSettings": # TODO: remove this once the settings are updated settings["electron"]["beam_type"] = BeamType.ELECTRON.name @@ -1393,7 +1404,7 @@ class MicroscopeSettings: system: SystemSettings image: ImageSettings milling: FibsemMillingSettings - protocol: dict = None + protocol: Optional[Dict[str, Any]] = None def to_dict(self) -> dict: settings_dict = { @@ -1405,15 +1416,17 @@ def to_dict(self) -> dict: return settings_dict - @staticmethod + @classmethod def from_dict( - settings: dict, protocol: dict = None + cls, settings: Dict[str, Any], protocol: Optional[Dict[str, Any]] = None ) -> "MicroscopeSettings": - + if protocol is None: - protocol = settings.get("protocol", {"name": "demo"}) - - return MicroscopeSettings( + protocol = settings.get("protocol") + if not isinstance(protocol, dict): + protocol = {"name": "demo"} + + return cls( system=SystemSettings.from_dict(settings), image=ImageSettings.from_dict(settings["imaging"]), protocol=protocol, @@ -1431,9 +1444,9 @@ class FibsemExperiment: date: float = datetime.timestamp(datetime.now()) application: str = "OpenFIBSEM" fibsem_version: str = fibsem.__version__ - application_version: str = None + application_version: Optional[str] = None - def to_dict(self) -> dict: + def to_dict(self) -> Dict[str, Any]: """Converts to a dictionary.""" return { "id": self.id, @@ -1444,10 +1457,10 @@ def to_dict(self) -> dict: "application_version": self.application_version, } - @staticmethod - def from_dict(settings: dict) -> "FibsemExperiment": + @classmethod + def from_dict(cls, settings: dict) -> "FibsemExperiment": """Converts from a dictionary.""" - return FibsemExperiment( + return cls( id=settings.get("id", "Unknown"), method=settings.get("method", "Unknown"), date=settings.get("date", "Unknown"), @@ -1465,7 +1478,7 @@ class FibsemUser: hostname: str = None # TODO: add host_ip_address - def to_dict(self) -> dict: + def to_dict(self) -> Dict[str, Any]: """Converts to a dictionary.""" return { "name": self.name, @@ -1474,8 +1487,8 @@ def to_dict(self) -> dict: "hostname": self.hostname, } - @staticmethod - def from_dict(settings: dict) -> "FibsemUser": + @classmethod + def from_dict(cls, settings: dict) -> "FibsemUser": """Converts from a dictionary.""" return FibsemUser( name=settings.get("name", "Unknown"), @@ -1509,13 +1522,13 @@ class FibsemImageMetadata: image_settings: ImageSettings pixel_size: Point microscope_state: MicroscopeState - system: SystemSettings = None + system: Optional[SystemSettings] = None version: str = METADATA_VERSION user: FibsemUser = field(default_factory=FibsemUser) experiment: FibsemExperiment = field(default_factory=FibsemExperiment) @property - def beam_type(self) -> BeamType: + def beam_type(self) -> Optional[BeamType]: return self.image_settings.beam_type @property @@ -1543,8 +1556,8 @@ def to_dict(self) -> dict: return settings_dict - @staticmethod - def from_dict(settings: dict) -> "ImageSettings": + @classmethod + def from_dict(cls, settings: dict) -> "FibsemImageMetadata": """Converts a dictionary to metadata.""" image_settings = ImageSettings.from_dict(settings["image"]) @@ -1562,7 +1575,7 @@ def from_dict(settings: dict) -> "ImageSettings": if system_dict: system_settings = SystemSettings.from_dict(system_dict) - metadata = FibsemImageMetadata( + metadata = cls( image_settings=image_settings, version=version, pixel_size=pixel_size, @@ -1575,6 +1588,7 @@ def from_dict(settings: dict) -> "ImageSettings": if THERMO: # TODO: move to ImageSettings + @staticmethod def image_settings_from_adorned( image=AdornedImage, beam_type: BeamType = BeamType.ELECTRON ) -> ImageSettings: @@ -1701,13 +1715,17 @@ def load(cls, tiff_path: str) -> "FibsemImage": # traceback.print_exc() return cls(data=data, metadata=metadata) - def save(self, path: Path = None) -> None: + def save(self, path: Optional[Union[str, os.PathLike]] = None) -> None: """Saves a FibsemImage to a tiff file. Inputs: path (path): path to save directory and filename """ if path is None: + if self.metadata is None: + raise ValueError( + "Cannot save as no path was specified and attribute 'metadata' is None" + ) path = os.path.join( self.metadata.image_settings.path, self.metadata.image_settings.filename, @@ -1727,7 +1745,7 @@ def save(self, path: Path = None) -> None: ### EXPERIMENTAL START #### - def _save_ome_tiff(self, path: str, filename: str) -> None: + def _save_ome_tiff(self, path: Union[str, os.PathLike], filename: str) -> None: from ome_types import OME from ome_types.model import ( Channel, @@ -1826,7 +1844,7 @@ def _save_ome_tiff(self, path: str, filename: str) -> None: tif.overwrite_description(ome.to_xml()) @classmethod - def _load_from_ome_tiff(cls, path: str) -> 'FibsemImage': + def _load_from_ome_tiff(cls, path: Union[str, os.PathLike]) -> 'FibsemImage': import ome_types # read ome-xml, extract openfibsem metadata @@ -1855,7 +1873,7 @@ def fromAdornedImage( cls, adorned: AdornedImage, image_settings: ImageSettings, - state: MicroscopeState = None, + state: Optional[MicroscopeState] = None, ) -> "FibsemImage": """Creates FibsemImage from an AdornedImage (microscope output format). @@ -1944,7 +1962,11 @@ class ReferenceImages: low_res_ib: FibsemImage high_res_ib: FibsemImage - def __iter__(self) -> List[FibsemImage]: + def __iter__( + self, + ) -> Generator[ + Tuple[FibsemImage, FibsemImage, FibsemImage, FibsemImage], None, None + ]: yield self.low_res_eb, self.high_res_eb, self.low_res_ib, self.high_res_ib @@ -2004,7 +2026,7 @@ class FibsemGasInjectionSettings: port: str gas: str duration: float - insert_position: str = None # multichem only + insert_position: Optional[str] = None # multichem only @staticmethod def from_dict(d: dict): From 4c8641df6e710df35f2a30b61ece1ad9b58feae3 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Fri, 6 Jun 2025 18:58:03 +0100 Subject: [PATCH 15/25] Change default Nones to "Unknown"s to match from_dict --- fibsem/structures.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/fibsem/structures.py b/fibsem/structures.py index ba2bf37e..bc9b93e2 100644 --- a/fibsem/structures.py +++ b/fibsem/structures.py @@ -1439,8 +1439,8 @@ def from_dict( @dataclass class FibsemExperiment: - id: str = None - method: str = None + id: str = "Unknown" + method: str = "Unknown" date: float = datetime.timestamp(datetime.now()) application: str = "OpenFIBSEM" fibsem_version: str = fibsem.__version__ @@ -1472,10 +1472,10 @@ def from_dict(cls, settings: dict) -> "FibsemExperiment": @dataclass class FibsemUser: - name: str = None - email: str = None - organization: str = None - hostname: str = None + name: str = "Unknown" + email: str = "Unknown" + organization: str = "Unknown" + hostname: str = "Unknown" # TODO: add host_ip_address def to_dict(self) -> Dict[str, Any]: From 235e2d474b30c1cd5f40578e64515e9b20458548 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:18:39 +0100 Subject: [PATCH 16/25] Fix typing changes --- fibsem/milling/base.py | 15 ++++++--------- fibsem/milling/patterning/patterns2.py | 12 +++++------- fibsem/milling/patterning/plotting.py | 4 +--- fibsem/structures.py | 22 ++++++++++------------ 4 files changed, 22 insertions(+), 31 deletions(-) diff --git a/fibsem/milling/base.py b/fibsem/milling/base.py index 761ced4a..653bc432 100644 --- a/fibsem/milling/base.py +++ b/fibsem/milling/base.py @@ -1,21 +1,18 @@ -from __future__ import annotations from abc import ABC, abstractmethod from copy import deepcopy from dataclasses import dataclass, fields, field, asdict -from typing import TYPE_CHECKING +from typing import List, Dict, Any, Tuple, Optional, Type, TypeVar, ClassVar, Generic from fibsem.microscope import FibsemMicroscope from fibsem.milling.config import MILLING_SPUTTER_RATE from fibsem.milling.patterning.patterns2 import BasePattern as BasePattern, get_pattern as get_pattern -from fibsem.structures import FibsemMillingSettings, MillingAlignment, ImageSettings, CrossSectionPattern, NumericalDisplayInfo +from fibsem.structures import FibsemMillingSettings, MillingAlignment, ImageSettings, CrossSectionPattern -if TYPE_CHECKING: - from typing import List, Dict, Any, Tuple, Optional, Type, TypeVar, ClassVar, Generic - TMillingStrategyConfig = TypeVar( - "TMillingStrategyConfig", bound="MillingStrategyConfig" - ) - TMillingStrategy = TypeVar("TMillingStrategy", bound="MillingStrategy") +TMillingStrategyConfig = TypeVar( + "TMillingStrategyConfig", bound="MillingStrategyConfig" +) +TMillingStrategy = TypeVar("TMillingStrategy", bound="MillingStrategy") @dataclass diff --git a/fibsem/milling/patterning/patterns2.py b/fibsem/milling/patterning/patterns2.py index 32bf56ad..44a0589d 100644 --- a/fibsem/milling/patterning/patterns2.py +++ b/fibsem/milling/patterning/patterns2.py @@ -1,8 +1,7 @@ -from __future__ import annotations from copy import deepcopy from abc import ABC, abstractmethod from dataclasses import dataclass, fields, field -from typing import Dict, List, Tuple, Any, Type, ClassVar, TypeVar, Generic, TYPE_CHECKING +from typing import Dict, List, Tuple, Any, Type, ClassVar, TypeVar, Generic import numpy as np @@ -15,11 +14,10 @@ FibsemPatternSettings, FibsemRectangleSettings, Point, + TFibsemPatternSettings ) -if TYPE_CHECKING: - from fibsem.structures import TFibsemPatternSettings - TPattern = TypeVar("TPattern", bound="BasePattern") +TPattern = TypeVar("TPattern", bound="BasePattern") # TODO: define the configuration for each key, # e.g. @@ -37,8 +35,8 @@ class BasePattern(ABC, Generic[TFibsemPatternSettings]): name: ClassVar[str] point: Point shapes: List[TFibsemPatternSettings] = field(init=False) - - _advanced_attributes: ClassVar[Tuple[str, ...]] = field(default_factory=tuple) + + _advanced_attributes: ClassVar[Tuple[str, ...]] = () # TODO: investigate TypeError: non-default argument 'width' follows default argument when uncommenting the above lines def __post_init__(self): diff --git a/fibsem/milling/patterning/plotting.py b/fibsem/milling/patterning/plotting.py index f14faaa3..a200ae86 100644 --- a/fibsem/milling/patterning/plotting.py +++ b/fibsem/milling/patterning/plotting.py @@ -146,7 +146,7 @@ def draw_milling_patterns( title: str = "Milling Patterns", show_current: bool = False, show_preset: bool = False, -) -> Tuple[plt.figure.Figure, plt.axes.Axes]: +) -> Tuple[plt.Figure, plt.Axes]: """ Draw milling patterns on an image. Args: @@ -157,8 +157,6 @@ def draw_milling_patterns( Returns: plt.Figure: Figure with patterns drawn. """ - fig: plt.figure.Figure - ax: plt.axes.Axes fig, ax = plt.subplots(1, 1, figsize=(10, 10)) ax.imshow(image.data, cmap="gray") diff --git a/fibsem/structures.py b/fibsem/structures.py index bc9b93e2..be853d7d 100644 --- a/fibsem/structures.py +++ b/fibsem/structures.py @@ -1,11 +1,10 @@ # fibsem structures -from __future__ import annotations import json import os import sys from abc import ABC, abstractmethod -from dataclasses import dataclass, field +from dataclasses import dataclass, field, asdict from datetime import datetime from enum import Enum, auto from pathlib import Path @@ -19,7 +18,6 @@ Dict, TypeVar, Generator, - TYPE_CHECKING, ) import numpy as np @@ -47,8 +45,9 @@ except ImportError: THERMO = False -if TYPE_CHECKING: - TFibsemPatternSettings = TypeVar("TFibsemPatternSettings", bound="FibsemPatternSettings") +TFibsemPatternSettings = TypeVar( + "TFibsemPatternSettings", bound="FibsemPatternSettings" +) @dataclass @@ -876,18 +875,17 @@ def from_dict(cls, state_dict: dict) -> "MicroscopeState": return microscope_state - ########### Base Pattern Settings @dataclass class FibsemPatternSettings(ABC): + def to_dict(self) -> Dict[str, Any]: + return asdict(self) - @abstractmethod - def to_dict(self) -> dict: - pass - - @abstractmethod @classmethod - def from_dict(cls: type[TFibsemPatternSettings], data: dict) -> TFibsemPatternSettings: + @abstractmethod + def from_dict( + cls: type[TFibsemPatternSettings], data: Dict[str, Any] + ) -> TFibsemPatternSettings: pass @property From 806efb906c2cd5fd0ef29acdd2c8f942175d1bfa Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Mon, 9 Jun 2025 19:34:04 +0100 Subject: [PATCH 17/25] MillingStrategy.to_dict no longer an abstract method --- fibsem/milling/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/fibsem/milling/base.py b/fibsem/milling/base.py index 653bc432..bd0c8391 100644 --- a/fibsem/milling/base.py +++ b/fibsem/milling/base.py @@ -42,7 +42,6 @@ class MillingStrategy(ABC, Generic[TMillingStrategyConfig]): def __init__(self, config: Optional[TMillingStrategyConfig] = None): self.config: TMillingStrategyConfig = config or self.config_class() - @abstractmethod def to_dict(self) -> dict[str, Any]: return {"name": self.name, "config": self.config.to_dict()} From c1806f10a8e3b1314801178983affc076edadee7 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:49:10 +0100 Subject: [PATCH 18/25] Fix BasePattern inheritance issues --- fibsem/milling/patterning/patterns2.py | 501 ++++--------------------- tests/milling/test_base.py | 2 +- tests/milling/test_patterns.py | 16 +- 3 files changed, 80 insertions(+), 439 deletions(-) diff --git a/fibsem/milling/patterning/patterns2.py b/fibsem/milling/patterning/patterns2.py index 44a0589d..fb571569 100644 --- a/fibsem/milling/patterning/patterns2.py +++ b/fibsem/milling/patterning/patterns2.py @@ -1,7 +1,18 @@ from copy import deepcopy from abc import ABC, abstractmethod -from dataclasses import dataclass, fields, field -from typing import Dict, List, Tuple, Any, Type, ClassVar, TypeVar, Generic +from dataclasses import dataclass, fields, field, asdict +from typing import ( + Dict, + List, + Tuple, + Any, + Union, + Optional, + Type, + ClassVar, + TypeVar, + Generic, +) import numpy as np @@ -11,10 +22,9 @@ FibsemBitmapSettings, FibsemCircleSettings, FibsemLineSettings, - FibsemPatternSettings, FibsemRectangleSettings, Point, - TFibsemPatternSettings + TFibsemPatternSettings, ) TPattern = TypeVar("TPattern", bound="BasePattern") @@ -24,46 +34,52 @@ # "width": {"type": "float", "min": 0, "max": 1000, "default": 100, "description": "Width of the rectangle"} # "cross_section": {"type": "str", "options": [cs.name for cs in CrossSectionPattern], "default": "Rectangle", "description": "Cross section of the milling pattern"} -DEFAULT_POINT_DDICT = {"x": 0.0, "y": 0.0} ####### Combo Patterns -CORE_PATTERN_ATTRIBUTES = ["name", "point", "shapes"] @dataclass class BasePattern(ABC, Generic[TFibsemPatternSettings]): name: ClassVar[str] point: Point - shapes: List[TFibsemPatternSettings] = field(init=False) + shapes: Optional[List[TFibsemPatternSettings]] = field(default=None, init=False) _advanced_attributes: ClassVar[Tuple[str, ...]] = () - # TODO: investigate TypeError: non-default argument 'width' follows default argument when uncommenting the above lines - - def __post_init__(self): - # This is solved by the 'kw_only' flag in 3.10+ (see - # https://bugs.python.org/issue43532) but this workaround works in - # earlier versions too. - if not hasattr(self, "point"): - setattr(self, "point", Point()) - if not hasattr(self, "shapes"): - setattr(self, "shapes", []) @abstractmethod def define(self) -> List[TFibsemPatternSettings]: pass - @abstractmethod def to_dict(self) -> Dict[str, Any]: - pass + ddict = asdict(self) + # Handle any special cases + if "cross_section" in ddict: + ddict["cross_section"] = ddict["cross_section"].name + ddict["name"] = self.name + del ddict["shapes"] + return ddict @classmethod - @abstractmethod def from_dict(cls: Type[TPattern], ddict: Dict[str, Any]) -> TPattern: - pass - + kwargs = {} + for f in fields(cls): + if f.name in ddict: + # Handle any special cases + if f.name == "cross_section": + value = CrossSectionPattern[ddict.get("cross_section", "Rectangle")] + else: + value = ddict[f.name] + kwargs[f.name] = value + + # Set defaults + point = kwargs.get("point", {"x": 0.0, "y": 0.0}) + kwargs["point"] = Point.from_dict(point) + + return cls(**kwargs) + @property def required_attributes(self) -> Tuple[str, ...]: - return tuple(field.name for field in fields(self) if field.name not in CORE_PATTERN_ATTRIBUTES) + return tuple(f.name for f in fields(self) if f.name not in fields(BasePattern)) @property def advanced_attributes(self) -> Tuple[str, ...]: @@ -75,7 +91,7 @@ def volume(self) -> float: return sum([shape.volume for shape in self.define()]) @dataclass -class BitmapPattern(BasePattern): +class BitmapPattern(BasePattern[FibsemBitmapSettings]): width: float height: float depth: float @@ -98,32 +114,10 @@ def define(self) -> List[FibsemBitmapSettings]: self.shapes = [shape] return self.shapes - - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "width": self.width, - "height": self.height, - "depth": self.depth, - "rotation": self.rotation, - "path": self.path - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "BitmapPattern": - return cls( - width=ddict["width"], - height=ddict["height"], - depth=ddict["depth"], - rotation=ddict.get("rotation", 0), - path=ddict.get("path", ""), - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) @dataclass -class RectanglePattern(BasePattern): +class RectanglePattern(BasePattern[FibsemRectangleSettings]): width: float height: float depth: float @@ -134,7 +128,8 @@ class RectanglePattern(BasePattern): cross_section: CrossSectionPattern = CrossSectionPattern.Rectangle name: ClassVar[str] = "Rectangle" - _advanced_attributes: ClassVar[Tuple[str, ...]] = ("time", "passes") # TODO: add for other patterns + # TODO: add for other patterns + _advanced_attributes: ClassVar[Tuple[str, ...]] = ("time", "passes") def define(self) -> List[FibsemRectangleSettings]: @@ -153,38 +148,10 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [shape] return self.shapes - - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "width": self.width, - "height": self.height, - "depth": self.depth, - "rotation": self.rotation, - "time": self.time, - "passes": self.passes, - "scan_direction": self.scan_direction, - "cross_section": self.cross_section.name - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "RectanglePattern": - return cls( - width=ddict["width"], - height=ddict["height"], - depth=ddict["depth"], - rotation=ddict.get("rotation", 0), - time=ddict.get("time", 0), - passes=ddict.get("passes", 0), - scan_direction=ddict.get("scan_direction", "TopToBottom"), - cross_section=CrossSectionPattern[ddict.get("cross_section", "Rectangle")], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) @dataclass -class LinePattern(BasePattern): +class LinePattern(BasePattern[FibsemLineSettings]): start_x: float end_x: float start_y: float @@ -203,31 +170,10 @@ def define(self) -> List[FibsemLineSettings]: ) self.shapes = [shape] return self.shapes - - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "start_x": self.start_x, - "end_x": self.end_x, - "start_y": self.start_y, - "end_y": self.end_y, - "depth": self.depth - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "LinePattern": - return cls( - start_x=ddict["start_x"], - end_x=ddict["end_x"], - start_y=ddict["start_y"], - end_y=ddict["end_y"], - depth=ddict["depth"], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) + @dataclass -class CirclePattern(BasePattern): +class CirclePattern(BasePattern[FibsemCircleSettings]): radius: float depth: float thickness: float = 0 @@ -245,27 +191,10 @@ def define(self) -> List[FibsemCircleSettings]: ) self.shapes = [shape] return self.shapes - - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "radius": self.radius, - "depth": self.depth, - "thickness": self.thickness - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "CirclePattern": - return cls( - radius=ddict["radius"], - depth=ddict["depth"], - thickness=ddict.get("thickness", 0), - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) + @dataclass -class TrenchPattern(BasePattern): +class TrenchPattern(BasePattern[Union[FibsemRectangleSettings, FibsemCircleSettings]]): width: float depth: float spacing: float @@ -278,7 +207,7 @@ class TrenchPattern(BasePattern): name: ClassVar[str] = "Trench" _advanced_attributes: ClassVar[Tuple[str, ...]] = ("time", "fillet") - def define(self) -> List[FibsemRectangleSettings]: + def define(self) -> List[Union[FibsemRectangleSettings, FibsemCircleSettings]]: point = self.point width = self.width @@ -406,36 +335,9 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "width": self.width, - "depth": self.depth, - "spacing": self.spacing, - "upper_trench_height": self.upper_trench_height, - "lower_trench_height": self.lower_trench_height, - "cross_section": self.cross_section.name, - "time": self.time, - "fillet": self.fillet, - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "TrenchPattern": - return cls( - width=ddict["width"], - depth=ddict["depth"], - spacing=ddict["spacing"], - upper_trench_height=ddict["upper_trench_height"], - lower_trench_height=ddict["lower_trench_height"], - fillet=ddict.get("fillet", 0), - cross_section=CrossSectionPattern[ddict.get("cross_section", "Rectangle")], - time=ddict.get("time", 0), - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) @dataclass -class HorseshoePattern(BasePattern): +class HorseshoePattern(BasePattern[FibsemRectangleSettings]): width: float upper_trench_height: float lower_trench_height: float @@ -507,39 +409,9 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [lower_pattern, upper_pattern, side_pattern] return self.shapes - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "width": self.width, - "spacing": self.spacing, - "depth": self.depth, - "upper_trench_height": self.upper_trench_height, - "lower_trench_height": self.lower_trench_height, - "side_width": self.side_width, - "inverted": self.inverted, - "scan_direction": self.scan_direction, - "cross_section": self.cross_section.name - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "HorseshoePattern": - return cls( - width=ddict["width"], - spacing=ddict["spacing"], - depth=ddict["depth"], - upper_trench_height=ddict["upper_trench_height"], - lower_trench_height=ddict["lower_trench_height"], - side_width=ddict["side_width"], - inverted=ddict.get("inverted", False), - scan_direction=ddict.get("scan_direction", "TopToBottom"), - cross_section=CrossSectionPattern[ddict.get("cross_section", "Rectangle")], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) - @dataclass -class HorseshoePatternVertical(BasePattern): +class HorseshoePatternVertical(BasePattern[FibsemRectangleSettings]): width: float height: float side_trench_width: float @@ -599,37 +471,10 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [left_pattern,right_pattern, upper_pattern] return self.shapes - - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "width": self.width, - "height": self.height, - "side_trench_width": self.side_trench_width, - "top_trench_height": self.top_trench_height, - "depth": self.depth, - "scan_direction": self.scan_direction, - "inverted": self.inverted, - "cross_section": self.cross_section.name - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "HorseshoePatternVertical": - return cls( - width=ddict["width"], - height=ddict["height"], - side_trench_width=ddict["side_trench_width"], - top_trench_height=ddict["top_trench_height"], - depth=ddict["depth"], - scan_direction=ddict.get("scan_direction", "TopToBottom"), - inverted=ddict.get("inverted", False), - cross_section=CrossSectionPattern[ddict.get("cross_section", "Rectangle")], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) + @dataclass -class SerialSectionPattern(BasePattern): +class SerialSectionPattern(BasePattern[FibsemLineSettings]): section_thickness: float section_width: float section_depth: float @@ -642,7 +487,7 @@ class SerialSectionPattern(BasePattern): name: ClassVar[str] = "SerialSection" # ref: "serial-liftout section" https://www.nature.com/articles/s41592-023-02113-5 - def define(self) -> List[FibsemRectangleSettings]: + def define(self) -> List[FibsemLineSettings]: """Calculate the serial liftout sectioning milling patterns""" point = self.point @@ -710,37 +555,9 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "section_thickness": self.section_thickness, - "section_width": self.section_width, - "section_depth": self.section_depth, - "side_width": self.side_width, - "side_height": self.side_height, - "side_depth": self.side_depth, - "inverted": self.inverted, - "use_side_patterns": self.use_side_patterns - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "SerialSectionPattern": - return cls( - section_thickness=ddict["section_thickness"], - section_width=ddict["section_width"], - section_depth=ddict["section_depth"], - side_width=ddict["side_width"], - side_height=ddict.get("side_height", 0), - side_depth=ddict.get("side_depth", 0), - inverted=ddict.get("inverted", False), - use_side_patterns=ddict.get("use_side_patterns", True), - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) - @dataclass -class FiducialPattern(BasePattern): +class FiducialPattern(BasePattern[FibsemRectangleSettings]): width: float height: float depth: float @@ -783,31 +600,9 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [left_pattern, right_pattern] return self.shapes - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "width": self.width, - "height": self.height, - "depth": self.depth, - "rotation": self.rotation, - "cross_section": self.cross_section.name - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "FiducialPattern": - return cls( - width=ddict["width"], - height=ddict["height"], - depth=ddict["depth"], - rotation=ddict.get("rotation", 0), - cross_section=CrossSectionPattern[ddict.get("cross_section", "Rectangle")], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) - @dataclass -class UndercutPattern(BasePattern): +class UndercutPattern(BasePattern[FibsemRectangleSettings]): width: float height: float depth: float @@ -865,34 +660,9 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [top_pattern, rhs_pattern] return self.shapes - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "width": self.width, - "height": self.height, - "depth": self.depth, - "trench_width": self.trench_width, - "rhs_height": self.rhs_height, - "h_offset": self.h_offset, - "cross_section": self.cross_section.name - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "UndercutPattern": - return cls( - width=ddict["width"], - height=ddict["height"], - depth=ddict["depth"], - trench_width=ddict["trench_width"], - rhs_height=ddict["rhs_height"], - h_offset=ddict["h_offset"], - cross_section=CrossSectionPattern[ddict.get("cross_section", "Rectangle")], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) @dataclass -class MicroExpansionPattern(BasePattern): +class MicroExpansionPattern(BasePattern[FibsemRectangleSettings]): width: float height: float depth: float @@ -930,29 +700,10 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [left_pattern_settings, right_pattern_settings] return self.shapes - - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "width": self.width, - "height": self.height, - "depth": self.depth, - "distance": self.distance - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "MicroExpansionPattern": - return cls( - width=ddict["width"], - height=ddict["height"], - depth=ddict["depth"], - distance=ddict["distance"], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) + @dataclass -class ArrayPattern(BasePattern): +class ArrayPattern(BasePattern[FibsemRectangleSettings]): width: float height: float depth: float @@ -1013,42 +764,9 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "width": self.width, - "height": self.height, - "depth": self.depth, - "n_columns": self.n_columns, - "n_rows": self.n_rows, - "pitch_vertical": self.pitch_vertical, - "pitch_horizontal": self.pitch_horizontal, - "passes": self.passes, - "rotation": self.rotation, - "scan_direction": self.scan_direction, - "cross_section": self.cross_section.name - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "ArrayPattern": - return cls( - width=ddict["width"], - height=ddict["height"], - depth=ddict["depth"], - n_columns=ddict["n_columns"], - n_rows=ddict["n_rows"], - pitch_vertical=ddict["pitch_vertical"], - pitch_horizontal=ddict["pitch_horizontal"], - passes=ddict.get("passes", 0), - rotation=ddict.get("rotation", 0), - scan_direction=ddict.get("scan_direction", "TopToBottom"), - cross_section=CrossSectionPattern[ddict.get("cross_section", "Rectangle")], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) @dataclass -class WaffleNotchPattern(BasePattern): +class WaffleNotchPattern(BasePattern[FibsemRectangleSettings]): vheight: float vwidth: float hheight: float @@ -1134,42 +852,15 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "vheight": self.vheight, - "vwidth": self.vwidth, - "hheight": self.hheight, - "hwidth": self.hwidth, - "depth": self.depth, - "distance": self.distance, - "inverted": self.inverted, - "cross_section": self.cross_section.name - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "WaffleNotchPattern": - return cls( - vheight=ddict["vheight"], - vwidth=ddict["vwidth"], - hheight=ddict["hheight"], - hwidth=ddict["hwidth"], - depth=ddict["depth"], - distance=ddict["distance"], - inverted=ddict.get("inverted", False), - cross_section=CrossSectionPattern[ddict.get("cross_section", "Rectangle")], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) @dataclass -class CloverPattern(BasePattern): +class CloverPattern(BasePattern[Union[FibsemCircleSettings, FibsemRectangleSettings]]): radius: float depth: float name: ClassVar[str] = "Clover" - def define(self) -> List[FibsemPatternSettings]: + def define(self) -> List[Union[FibsemCircleSettings, FibsemRectangleSettings]]: point = self.point radius = self.radius @@ -1210,32 +901,16 @@ def define(self) -> List[FibsemPatternSettings]: self.shapes = [top_pattern, right_pattern, left_pattern, stem_pattern] return self.shapes - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "radius": self.radius, - "depth": self.depth - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "CloverPattern": - return cls( - radius=ddict["radius"], - depth=ddict["depth"], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) - @dataclass -class TriForcePattern(BasePattern): +class TriForcePattern(BasePattern[FibsemRectangleSettings]): width: float height: float depth: float name: ClassVar[str] = "TriForce" - def define(self) -> List[FibsemPatternSettings]: + def define(self) -> List[FibsemRectangleSettings]: point = self.point height = self.height width = self.width @@ -1260,26 +935,9 @@ def define(self) -> List[FibsemPatternSettings]: return self.shapes - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "width": self.width, - "height": self.height, - "depth": self.depth - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "TriForcePattern": - return cls( - width=ddict["width"], - height=ddict["height"], - depth=ddict["depth"], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) @dataclass -class TrapezoidPattern(BasePattern): +class TrapezoidPattern(BasePattern[FibsemRectangleSettings]): inner_width: float outer_width: float trench_height: float @@ -1290,7 +948,7 @@ class TrapezoidPattern(BasePattern): name: ClassVar[str] = "Trapezoid" - def define(self) -> List[FibsemPatternSettings]: + def define(self) -> List[FibsemRectangleSettings]: outer_width = self.outer_width inner_width = self.inner_width @@ -1333,33 +991,6 @@ def define(self) -> List[FibsemPatternSettings]: ) self.shapes.append(deepcopy(pattern)) return self.shapes - - def to_dict(self) -> Dict[str, Any]: - return { - "name": self.name, - "point": self.point.to_dict(), - "inner_width": self.inner_width, - "outer_width": self.outer_width, - "trench_height": self.trench_height, - "depth": self.depth, - "distance": self.distance, - "n_rectangles": self.n_rectangles, - "overlap": self.overlap - } - - @classmethod - def from_dict(cls, ddict: Dict[str, Any]) -> "TrapezoidPattern": - - return cls( - inner_width=ddict["inner_width"], - outer_width=ddict["outer_width"], - trench_height=ddict["trench_height"], - depth=ddict["depth"], - distance=ddict["distance"], - n_rectangles=ddict["n_rectangles"], - overlap=ddict["overlap"], - point=Point.from_dict(ddict.get("point", DEFAULT_POINT_DDICT)) - ) def create_triangle_patterns( diff --git a/tests/milling/test_base.py b/tests/milling/test_base.py index 9ea62d4a..816f284f 100644 --- a/tests/milling/test_base.py +++ b/tests/milling/test_base.py @@ -10,7 +10,7 @@ def test_milling_stage(): milling_settings = FibsemMillingSettings() - pattern = RectanglePattern(width=10, height=5, depth=1) + pattern = RectanglePattern(Point(), width=10, height=5, depth=1) strategy = get_strategy("Standard") alignment = MillingAlignment(enabled=True) diff --git a/tests/milling/test_patterns.py b/tests/milling/test_patterns.py index def093a8..8a6f393e 100644 --- a/tests/milling/test_patterns.py +++ b/tests/milling/test_patterns.py @@ -4,7 +4,6 @@ import pytest from fibsem.milling.patterning.patterns2 import ( - DEFAULT_POINT_DDICT, CirclePattern, FibsemCircleSettings, FibsemLineSettings, @@ -159,6 +158,7 @@ def test_circle_pattern(): radius = 5e-6 depth = 1e-6 circle = CirclePattern( + Point(), radius=radius, depth=depth, ) @@ -175,9 +175,11 @@ def test_circle_pattern(): assert ddict["point"]["x"] == 0 assert ddict["point"]["y"] == 0 + del ddict["point"] # Check default circle2 = CirclePattern.from_dict(ddict) assert circle2.radius == radius assert circle2.depth == depth + assert circle2.point == Point() def test_line_pattern(): @@ -197,6 +199,7 @@ def test_line_pattern(): end_y = 2e-6 depth = 1e-6 line = LinePattern( + Point(), start_x=start_x, start_y=start_y, end_x=end_x, @@ -217,13 +220,17 @@ def test_line_pattern(): assert ddict["end_x"] == end_x assert ddict["end_y"] == end_y assert ddict["depth"] == depth + assert ddict["point"]["x"] == 0 + assert ddict["point"]["y"] == 0 + del ddict["point"] # Check default line2 = LinePattern.from_dict(ddict) assert line2.start_x == start_x assert line2.start_y == start_y assert line2.end_x == end_x assert line2.end_y == end_y assert line2.depth == depth + assert line2.point == Point() def test_rectangle_pattern(): @@ -242,6 +249,7 @@ def test_rectangle_pattern(): rotation = 0 rect = RectanglePattern( + Point(), width=width, height=height, depth=depth, @@ -268,13 +276,13 @@ def test_rectangle_pattern(): assert ddict["point"]["y"] == 0 # test deserialization + del ddict["point"] # Check default rect2 = RectanglePattern.from_dict(ddict) assert rect2.width == width assert rect2.height == height assert rect2.depth == depth assert rect2.rotation == rotation - - + assert rect2.point == Point() class TestTrenchPattern: @@ -477,6 +485,7 @@ class TestArrayPattern: def test_init(self): # Test default initialization array = ArrayPattern( + Point(), width=10.0, height=20.0, depth=30.0, @@ -719,6 +728,7 @@ class TestFiducialPattern: def test_init(self): # Test initialization fiducial = FiducialPattern( + Point(), width=10.0, height=20.0, depth=5.0 From 1a7dd25b871ec8762fd60283e8670c2a883c8a7f Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:02:00 +0100 Subject: [PATCH 19/25] Improve alignment typing --- fibsem/alignment.py | 63 ++++++++++++++++++++++----------------------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/fibsem/alignment.py b/fibsem/alignment.py index 6f77c20b..e5dc84e3 100644 --- a/fibsem/alignment.py +++ b/fibsem/alignment.py @@ -1,20 +1,21 @@ +from __future__ import annotations import logging -from typing import Optional, Tuple, Union +from typing import Optional, Tuple, Union, Any, TYPE_CHECKING import numpy as np from fibsem import acquire, utils, validation from fibsem.imaging import masks from fibsem.imaging import utils as image_utils -from fibsem.microscope import FibsemMicroscope -from fibsem.structures import ( - BeamType, - FibsemImage, - FibsemRectangle, - ImageSettings, - MicroscopeSettings, - ReferenceImages, -) + +from fibsem.structures import BeamType, ImageSettings + +if TYPE_CHECKING: + from numpy.typing import NDArray + + from fibsem.microscope import FibsemMicroscope + from fibsem.structures import FibsemImage, MicroscopeSettings, ReferenceImages + def auto_eucentric_correction( microscope: FibsemMicroscope, @@ -115,7 +116,7 @@ def correct_stage_drift( alignment: Tuple[BeamType, BeamType] = (BeamType.ELECTRON, BeamType.ELECTRON), rotate: bool = False, ref_mask_rad: int = 512, - xcorr_limit: Union[Tuple[int, int], None] = None, + xcorr_limit: Optional[Union[Tuple[int, int], Tuple[None, None]]] = None, constrain_vertical: bool = False, use_beam_shift: bool = False, ) -> bool: @@ -197,8 +198,8 @@ def align_using_reference_images( microscope: FibsemMicroscope, ref_image: FibsemImage, new_image: FibsemImage, - ref_mask: np.ndarray = None, - xcorr_limit: int = None, + ref_mask: Optional[NDArray[np.bool_]] = None, + xcorr_limit: Optional[int] = None, constrain_vertical: bool = False, use_beam_shift: bool = False, ) -> bool: @@ -275,8 +276,8 @@ def shift_from_crosscorrelation( highpass: int = 6, sigma: int = 6, use_rect_mask: bool = False, - ref_mask: np.ndarray = None, - xcorr_limit: int = None, + ref_mask: Optional[NDArray[np.bool_]] = None, + xcorr_limit: Optional[int] = None, ) -> Tuple[float, float, np.ndarray]: """Calculates the shift between two images by cross-correlating them and finding the position of maximum correlation. @@ -374,8 +375,8 @@ def shift_from_crosscorrelation( def crosscorrelation_v2( - img1: np.ndarray, img2: np.ndarray, bandpass: np.ndarray = None -) -> np.ndarray: + img1: NDArray[Any], img2: NDArray[Any], bandpass: Optional[NDArray[Any]] = None +) -> NDArray[Any]: """ Cross-correlate two images using Fourier convolution matching. @@ -426,23 +427,21 @@ def crosscorrelation_v2( def _save_alignment_data( ref_image: FibsemImage, new_image: FibsemImage, - bandpass: np.ndarray, - xcorr: np.ndarray, - ref_mask: np.ndarray = None, - lowpass: float = None, - highpass: float = None, - sigma: float = None, - xcorr_limit: float = None, + bandpass: NDArray[Any], + xcorr: NDArray[Any], + ref_mask: Optional[NDArray[Any]] = None, + lowpass: Optional[float] = None, + highpass: Optional[float] = None, + sigma: Optional[float] = None, + xcorr_limit: Optional[float] = None, use_rect_mask: bool = False, - dx: float = None, - dy: float = None, - pixelsize_x: float = None, - pixelsize_y: float = None, - - -): + dx: Optional[float] = None, + dy: Optional[float] = None, + pixelsize_x: Optional[float] = None, + pixelsize_y: Optional[float] = None, +) -> None: """Save alignment data to disk.""" - + import os import pandas as pd From 5982ff89ebfa8432c0bd839a17a04da302ddba36 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:02:40 +0100 Subject: [PATCH 20/25] estimate_milling_time: simplify cross_section check --- fibsem/milling/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fibsem/milling/base.py b/fibsem/milling/base.py index bd0c8391..74c06842 100644 --- a/fibsem/milling/base.py +++ b/fibsem/milling/base.py @@ -175,7 +175,7 @@ def estimate_milling_time(pattern: BasePattern, milling_current: float) -> float sputter_rate = sputter_rate * (milling_current / sp_keys[0]) volume = pattern.volume # m3 - if hasattr(pattern, "cross_section") and pattern.cross_section is CrossSectionPattern.CleaningCrossSection: + if getattr(pattern, "cross_section") is CrossSectionPattern.CleaningCrossSection: volume *= 0.66 # ccs is approx 2/3 of the volume of a rectangle time = (volume *1e6**3) / sputter_rate From f49b99a50bf3706b8442dc63a694f257ab0317fc Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:27:52 +0100 Subject: [PATCH 21/25] Handle to_dict and from_dict in FibsemPatternSettings --- fibsem/structures.py | 106 ++++++++----------------------------------- 1 file changed, 19 insertions(+), 87 deletions(-) diff --git a/fibsem/structures.py b/fibsem/structures.py index be853d7d..ff6686b3 100644 --- a/fibsem/structures.py +++ b/fibsem/structures.py @@ -4,7 +4,7 @@ import os import sys from abc import ABC, abstractmethod -from dataclasses import dataclass, field, asdict +from dataclasses import dataclass, field, fields, asdict from datetime import datetime from enum import Enum, auto from pathlib import Path @@ -16,6 +16,7 @@ Set, Any, Dict, + Type, TypeVar, Generator, ) @@ -879,14 +880,25 @@ def from_dict(cls, state_dict: dict) -> "MicroscopeState": @dataclass class FibsemPatternSettings(ABC): def to_dict(self) -> Dict[str, Any]: - return asdict(self) + ddict = asdict(self) + # Handle any special cases + if "cross_section" in ddict: + ddict["cross_section"] = ddict["cross_section"].name + return ddict @classmethod - @abstractmethod - def from_dict( - cls: type[TFibsemPatternSettings], data: Dict[str, Any] - ) -> TFibsemPatternSettings: - pass + def from_dict(cls: Type[TFibsemPatternSettings], data: Dict[str, Any]) -> TFibsemPatternSettings: + kwargs = {} + for f in fields(cls): + if f.name in data: + # Handle any special cases + if f.name == "cross_section": + value = CrossSectionPattern[data.get("cross_section", "Rectangle")] + else: + value = data[f.name] + kwargs[f.name] = value + + return cls(**kwargs) @property @abstractmethod @@ -913,37 +925,6 @@ class FibsemRectangleSettings(FibsemPatternSettings): time: float = 0.0 is_exclusion: bool = False - def to_dict(self) -> dict: - return { - "width": self.width, - "height": self.height, - "depth": self.depth, - "rotation": self.rotation, - "centre_x": self.centre_x, - "centre_y": self.centre_y, - "scan_direction": self.scan_direction, - "cross_section": self.cross_section.name, - "passes": self.passes, - "time": self.time, - "is_exclusion": self.is_exclusion, - } - - @classmethod - def from_dict(cls, data: dict) -> "FibsemRectangleSettings": - return cls( - width=data["width"], - height=data["height"], - depth=data["depth"], - centre_x=data["centre_x"], - centre_y=data["centre_y"], - rotation=data.get("rotation", 0), - scan_direction=data.get("scan_direction", "TopToBottom"), - cross_section=CrossSectionPattern[data.get("cross_section", "Rectangle")], - passes=data.get("passes", 0), - time=data.get("time", 0.0), - is_exclusion=data.get("is_exclusion", False), - ) - @property def volume(self) -> float: return self.width * self.height * self.depth @@ -991,33 +972,6 @@ class FibsemCircleSettings(FibsemPatternSettings): rotation: float = 0.0 # annulus -> thickness !=0 is_exclusion: bool = False - def to_dict(self) -> dict: - return { - "radius": self.radius, - "depth": self.depth, - "centre_x": self.centre_x, - "centre_y": self.centre_y, - "start_angle": self.start_angle, - "end_angle": self.end_angle, - "rotation": self.rotation, - "thickness": self.thickness, - "is_exclusion": self.is_exclusion, - } - - @classmethod - def from_dict(cls, data: dict) -> "FibsemCircleSettings": - return cls( - radius=data["radius"], - depth=data["depth"], - centre_x=data["centre_x"], - centre_y=data["centre_y"], - start_angle=data.get("start_angle", 0), - end_angle=data.get("end_angle", 360), - rotation=data.get("rotation", 0), - thickness=data.get("thickness", 0), - is_exclusion=data.get("is_exclusion", False), - ) - @property def volume(self) -> float: return np.pi * self.radius**2 * self.depth @@ -1032,28 +986,6 @@ class FibsemBitmapSettings(FibsemPatternSettings): centre_y: float path: Optional[Union[str, os.PathLike]] = None - def to_dict(self) -> dict: - return { - "width": self.width, - "height": self.height, - "depth": self.depth, - "rotation": self.rotation, - "centre_x": self.centre_x, - "centre_y": self.centre_y, - "path": self.path, - } - @classmethod - def from_dict(cls, data: dict) -> "FibsemBitmapSettings": - return cls( - width=data["width"], - height=data["height"], - depth=data["depth"], - rotation=data["rotation"], - centre_x=data["centre_x"], - centre_y=data["centre_y"], - path=data["path"], - ) - @property def volume(self) -> float: # NOTE: this is a VERY rough estimate From 1fe39634680cdd108c615c0f6b724ff97934b81b Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:46:49 +0100 Subject: [PATCH 22/25] Use collections.abc.Generator type hint --- fibsem/structures.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fibsem/structures.py b/fibsem/structures.py index ff6686b3..c757669a 100644 --- a/fibsem/structures.py +++ b/fibsem/structures.py @@ -1,4 +1,5 @@ # fibsem structures +from __future__ import annotations import json import os @@ -18,7 +19,7 @@ Dict, Type, TypeVar, - Generator, + TYPE_CHECKING, ) import numpy as np @@ -50,6 +51,8 @@ "TFibsemPatternSettings", bound="FibsemPatternSettings" ) +if TYPE_CHECKING: + from collections.abc import Generator @dataclass class Point: From 2d9fad44e69d1e034e83d04d8142cfbbc31d5a86 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:16:05 +0100 Subject: [PATCH 23/25] Update to newer typing style thanks to __future__.annotations --- fibsem/structures.py | 147 +++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 75 deletions(-) diff --git a/fibsem/structures.py b/fibsem/structures.py index c757669a..84983381 100644 --- a/fibsem/structures.py +++ b/fibsem/structures.py @@ -10,13 +10,7 @@ from enum import Enum, auto from pathlib import Path from typing import ( - List, - Optional, - Tuple, - Union, - Set, Any, - Dict, Type, TypeVar, TYPE_CHECKING, @@ -53,12 +47,13 @@ if TYPE_CHECKING: from collections.abc import Generator + from numpy.typing import NDArray @dataclass class Point: x: float = 0.0 y: float = 0.0 - name: Optional[str] = None + name: str | None = None def to_dict(self) -> dict: return {"x": self.x, "y": self.y} @@ -165,13 +160,13 @@ class FibsemStagePosition: from_autoscript_position(position: StagePosition) -> None: Create a new FibsemStagePosition object from a StagePosition object that is compatible with Autoscript. """ - name: str = None - x: float = None - y: float = None - z: float = None - r: float = None - t: float = None - coordinate_system: str = None + name: str | None = None + x: float | None = None + y: float | None = None + z: float | None = None + r: float | None = None + t: float | None = None + coordinate_system: str | None = None def to_dict(self) -> dict: position_dict = {} @@ -207,7 +202,7 @@ def from_dict(cls, data: dict) -> "FibsemStagePosition": if THERMO: - def to_autoscript_position(self, compustage: bool = False) -> Union[StagePosition, CompustagePosition]: + def to_autoscript_position(self, compustage: bool = False) -> StagePosition | CompustagePosition: """Converts the stage position to a StagePosition object that is compatible with Autoscript. Args: compustage: bool = False: Whether or not the stage is a compustage. @@ -238,9 +233,10 @@ def to_autoscript_position(self, compustage: bool = False) -> Union[StagePositio return stage_position - @classmethod # TODO: convert this to staticmethod? - def from_autoscript_position(cls, position: Union[StagePosition, CompustagePosition]) -> 'FibsemStagePosition': - + @classmethod # TODO: convert this to staticmethod? + def from_autoscript_position( + cls, position: StagePosition | CompustagePosition + ) -> "FibsemStagePosition": # compustage position if isinstance(position, CompustagePosition): return cls( @@ -491,19 +487,19 @@ class ImageSettings: Converts the ImageSettings object to a dictionary of image settings. """ - resolution: Optional[list] = None - dwell_time: Optional[float] = None - hfw: Optional[float] = None - autocontrast: Optional[bool] = None - beam_type: Optional[BeamType] = None - save: Optional[bool] = None - filename: Optional[str] = None - autogamma: Optional[bool] = None - path: Optional[Path] = None - reduced_area: Optional[FibsemRectangle] = None - line_integration: Optional[int] = None # (int32) 2 - 255 - scan_interlacing: Optional[int] = None # (int32) 2 - 8 - frame_integration: Optional[int] = None # (int32) 2 - 512 + resolution: list | None = None + dwell_time: float | None = None + hfw: float | None = None + autocontrast: bool | None = None + beam_type: BeamType | None = None + save: bool | None = None + filename: str | None = None + autogamma: bool | None = None + path: str | os.PathLike[str] | None = None + reduced_area: FibsemRectangle | None = None + line_integration: int | None = None # (int32) 2 - 255 + scan_interlacing: int | None = None # (int32) 2 - 8 + frame_integration: int | None = None # (int32) 2 - 512 drift_correction: bool = False # (bool) # requires frame_integration > 1 def __post_init__(self): @@ -643,16 +639,16 @@ class BeamSettings: """ beam_type: BeamType - working_distance: Optional[float] = None - beam_current: Optional[float] = None - voltage: Optional[float] = None - hfw: Optional[float] = None - resolution: Optional[list] = None - dwell_time: Optional[float] = None - stigmation: Optional[Point] = None - shift: Optional[Point] = None - scan_rotation: Optional[float] = None - preset: Optional[str] = None + working_distance: float | None = None + beam_current: float | None = None + voltage: float | None = None + hfw: float | None = None + resolution: list | None = None + dwell_time: float | None = None + stigmation: Point | None = None + shift: Point | None = None + scan_rotation: float | None = None + preset: str | None = None def __post_init__(self): assert ( @@ -743,8 +739,8 @@ def from_dict(state_dict: dict) -> "BeamSettings": @dataclass class FibsemDetectorSettings: - type: str = None - mode: str = None + type: str | None = None + mode: str | None = None brightness: float = 0.5 contrast: float = 0.5 @@ -882,7 +878,7 @@ def from_dict(cls, state_dict: dict) -> "MicroscopeState": ########### Base Pattern Settings @dataclass class FibsemPatternSettings(ABC): - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: ddict = asdict(self) # Handle any special cases if "cross_section" in ddict: @@ -890,7 +886,7 @@ def to_dict(self) -> Dict[str, Any]: return ddict @classmethod - def from_dict(cls: Type[TFibsemPatternSettings], data: Dict[str, Any]) -> TFibsemPatternSettings: + def from_dict(cls: Type[TFibsemPatternSettings], data: dict[str, Any]) -> TFibsemPatternSettings: kwargs = {} for f in fields(cls): if f.name in data: @@ -987,7 +983,7 @@ class FibsemBitmapSettings(FibsemPatternSettings): rotation: float centre_x: float centre_y: float - path: Optional[Union[str, os.PathLike]] = None + path: str | os.PathLike[str] | None = None @property def volume(self) -> float: @@ -1099,7 +1095,7 @@ def from_dict(cls, settings: dict) -> "FibsemMillingSettings": return milling_settings @classmethod - def get_parameters_for_manufacturer(cls, manufacturer: str) -> Set[str]: + def get_parameters_for_manufacturer(cls, manufacturer: str) -> set[str]: """Get all parameter names for a specific manufacturer.""" if manufacturer not in cls._SUPPORTED_MANUFACTURERS: raise ValueError(f"Manufacturer must be one of: {', '.join(cls._SUPPORTED_MANUFACTURERS)}") @@ -1107,7 +1103,7 @@ def get_parameters_for_manufacturer(cls, manufacturer: str) -> Set[str]: # TODO: ensure this returns in the canonical order return cls._PARAMETERS["COMMON"] | cls._PARAMETERS[manufacturer] - def get_parameters(self, manufacturer: str) -> Dict[str, Any]: + def get_parameters(self, manufacturer: str) -> dict[str, Any]: """Get parameter values for a specific manufacturer.""" required_params = self.get_parameters_for_manufacturer(manufacturer) return {param: getattr(self, param) for param in required_params} @@ -1158,7 +1154,7 @@ class BeamSystemSettings: eucentric_height: float column_tilt: float plasma: bool = False - plasma_gas: Optional[str] = None + plasma_gas: str | None = None def to_dict(self): ddict = { @@ -1182,7 +1178,7 @@ def to_dict(self): return ddict @classmethod - def from_dict(cls, settings: dict) -> 'BeamSystemSettings': + def from_dict(cls, settings: dict[str, Any]) -> "BeamSystemSettings": return cls( beam_type=BeamType[settings["beam_type"]], enabled=settings["enabled"], @@ -1208,7 +1204,7 @@ def to_dict(self): } @staticmethod - def from_dict(settings: dict): + def from_dict(settings: dict[str, Any]): return ManipulatorSystemSettings( enabled=settings["enabled"], rotation=settings["rotation"], @@ -1232,7 +1228,7 @@ def to_dict(self): } @staticmethod - def from_dict(settings: dict): + def from_dict(settings: dict[str, Any]): return GISSystemSettings( enabled=settings["enabled"], multichem=settings["multichem"], @@ -1250,8 +1246,8 @@ class SystemInfo: hardware_version: str software_version: str fibsem_version: str = fibsem.__version__ - application: Optional[str] = None - application_version: Optional[str] = None + application: str | None = None + application_version: str | None = None def to_dict(self): return { @@ -1337,7 +1333,7 @@ class MicroscopeSettings: system: SystemSettings image: ImageSettings milling: FibsemMillingSettings - protocol: Optional[Dict[str, Any]] = None + protocol: dict[str, Any] | None = None def to_dict(self) -> dict: settings_dict = { @@ -1351,9 +1347,8 @@ def to_dict(self) -> dict: @classmethod def from_dict( - cls, settings: Dict[str, Any], protocol: Optional[Dict[str, Any]] = None + cls, settings: dict[str, Any], protocol: dict[str, Any] | None = None ) -> "MicroscopeSettings": - if protocol is None: protocol = settings.get("protocol") if not isinstance(protocol, dict): @@ -1377,9 +1372,9 @@ class FibsemExperiment: date: float = datetime.timestamp(datetime.now()) application: str = "OpenFIBSEM" fibsem_version: str = fibsem.__version__ - application_version: Optional[str] = None + application_version: str | None = None - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Converts to a dictionary.""" return { "id": self.id, @@ -1411,7 +1406,7 @@ class FibsemUser: hostname: str = "Unknown" # TODO: add host_ip_address - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """Converts to a dictionary.""" return { "name": self.name, @@ -1455,13 +1450,13 @@ class FibsemImageMetadata: image_settings: ImageSettings pixel_size: Point microscope_state: MicroscopeState - system: Optional[SystemSettings] = None + system: SystemSettings | None = None version: str = METADATA_VERSION user: FibsemUser = field(default_factory=FibsemUser) experiment: FibsemExperiment = field(default_factory=FibsemExperiment) @property - def beam_type(self) -> Optional[BeamType]: + def beam_type(self) -> BeamType | None: return self.image_settings.beam_type @property @@ -1592,7 +1587,7 @@ class FibsemImage: Has in built methods to deal with image types of TESCAN and ThermoFisher API Args: - data (np.ndarray): The image data stored in a numpy array. + data (NDArray[Any]): The image data stored in a numpy array. metadata (FibsemImageMetadata, optional): The metadata associated with the image. Defaults to None. Methods: @@ -1612,7 +1607,9 @@ class FibsemImage: path (path): path to save directory and filename """ - def __init__(self, data: np.ndarray, metadata: FibsemImageMetadata = None): + def __init__( + self, data: NDArray[Any], metadata: FibsemImageMetadata | None = None + ) -> None: if check_data_format(data): if data.ndim == 3 and data.shape[2] == 1: data = data[:, :, 0] @@ -1625,7 +1622,7 @@ def __init__(self, data: np.ndarray, metadata: FibsemImageMetadata = None): self.metadata = None @classmethod - def load(cls, tiff_path: str) -> "FibsemImage": + def load(cls, tiff_path: str | os.PathLike[str]) -> "FibsemImage": """Loads a FibsemImage from a tiff file. Args: @@ -1648,7 +1645,7 @@ def load(cls, tiff_path: str) -> "FibsemImage": # traceback.print_exc() return cls(data=data, metadata=metadata) - def save(self, path: Optional[Union[str, os.PathLike]] = None) -> None: + def save(self, path: str | os.PathLike[str] | None = None) -> None: """Saves a FibsemImage to a tiff file. Inputs: @@ -1678,7 +1675,7 @@ def save(self, path: Optional[Union[str, os.PathLike]] = None) -> None: ### EXPERIMENTAL START #### - def _save_ome_tiff(self, path: Union[str, os.PathLike], filename: str) -> None: + def _save_ome_tiff(self, path: str | os.PathLike[str], filename: str) -> None: from ome_types import OME from ome_types.model import ( Channel, @@ -1777,7 +1774,7 @@ def _save_ome_tiff(self, path: Union[str, os.PathLike], filename: str) -> None: tif.overwrite_description(ome.to_xml()) @classmethod - def _load_from_ome_tiff(cls, path: Union[str, os.PathLike]) -> 'FibsemImage': + def _load_from_ome_tiff(cls, path: str | os.PathLike[str]) -> "FibsemImage": import ome_types # read ome-xml, extract openfibsem metadata @@ -1806,7 +1803,7 @@ def fromAdornedImage( cls, adorned: AdornedImage, image_settings: ImageSettings, - state: Optional[MicroscopeState] = None, + state: MicroscopeState | None = None, ) -> "FibsemImage": """Creates FibsemImage from an AdornedImage (microscope output format). @@ -1848,15 +1845,15 @@ def fromAdornedImage( @staticmethod def generate_blank_image( - resolution: List[int] = [1536, 1024], + resolution: list[int] = [1536, 1024], hfw: float = 100e-6, - pixel_size: Optional[Point] = None, + pixel_size: Point | None = None, random: bool = False, dtype: np.dtype = np.uint8, ) -> 'FibsemImage': """Generate a blank image with a given resolution and field of view. Args: - resolution: List[int]: Resolution of the image. + resolution: list[int]: Resolution of the image. hfw: float: Horizontal field width of the image. pixel_size: Point: Pixel size of the image. random: bool: If True, generate a random (noise) image. @@ -1898,7 +1895,7 @@ class ReferenceImages: def __iter__( self, ) -> Generator[ - Tuple[FibsemImage, FibsemImage, FibsemImage, FibsemImage], None, None + tuple[FibsemImage, FibsemImage, FibsemImage, FibsemImage], None, None ]: yield self.low_res_eb, self.high_res_eb, self.low_res_ib, self.high_res_ib @@ -1959,7 +1956,7 @@ class FibsemGasInjectionSettings: port: str gas: str duration: float - insert_position: Optional[str] = None # multichem only + insert_position: str | None = None # multichem only @staticmethod def from_dict(d: dict): @@ -1979,7 +1976,7 @@ def to_dict(self): } -def calculate_fiducial_area_v2(image: FibsemImage, fiducial_centre: Point, fiducial_length:float)->Tuple[FibsemRectangle, bool]: +def calculate_fiducial_area_v2(image: FibsemImage, fiducial_centre: Point, fiducial_length: float) -> tuple[FibsemRectangle, bool]: from fibsem import conversions pixelsize = image.metadata.pixel_size.x From 9ea26ff3c352b3915698e28f0c45252907057de3 Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:27:05 +0100 Subject: [PATCH 24/25] Update more typing with __future__.annotations --- fibsem/milling/base.py | 30 ++++++++++++++++-------------- fibsem/milling/core.py | 24 +++++++++++++----------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/fibsem/milling/base.py b/fibsem/milling/base.py index 74c06842..342adb90 100644 --- a/fibsem/milling/base.py +++ b/fibsem/milling/base.py @@ -1,7 +1,9 @@ +from __future__ import annotations + from abc import ABC, abstractmethod from copy import deepcopy from dataclasses import dataclass, fields, field, asdict -from typing import List, Dict, Any, Tuple, Optional, Type, TypeVar, ClassVar, Generic +from typing import Any, Type, TypeVar, ClassVar, Generic from fibsem.microscope import FibsemMicroscope from fibsem.milling.config import MILLING_SPUTTER_RATE @@ -18,19 +20,19 @@ @dataclass class MillingStrategyConfig(ABC): """Abstract base class for milling strategy configurations""" - _advanced_attributes: ClassVar[Tuple[str, ...]] = () + _advanced_attributes: ClassVar[tuple[str, ...]] = () - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: return asdict(self) @classmethod def from_dict( - cls: Type[TMillingStrategyConfig], d: Dict[str, Any] + cls: Type[TMillingStrategyConfig], d: dict[str, Any] ) -> TMillingStrategyConfig: return cls(**d) @property - def required_attributes(self) -> Tuple[str, ...]: + def required_attributes(self) -> tuple[str, ...]: return tuple(f.name for f in fields(self)) @@ -39,14 +41,14 @@ class MillingStrategy(ABC, Generic[TMillingStrategyConfig]): name: str = "Milling Strategy" config_class: Type[TMillingStrategyConfig] - def __init__(self, config: Optional[TMillingStrategyConfig] = None): + def __init__(self, config: TMillingStrategyConfig | None = None): self.config: TMillingStrategyConfig = config or self.config_class() def to_dict(self) -> dict[str, Any]: return {"name": self.name, "config": self.config.to_dict()} @classmethod - def from_dict(cls: Type[TMillingStrategy], d: dict) -> TMillingStrategy: + def from_dict(cls: Type[TMillingStrategy], d: dict[str, Any]) -> TMillingStrategy: config=cls.config_class.from_dict(d.get("config", {})) return cls(config=config) @@ -56,7 +58,7 @@ def run(self, microscope: FibsemMicroscope, stage: "FibsemMillingStage", asynch: def get_strategy( - name: str = "Standard", config: Optional[Dict[str, Any]] = None + name: str = "Standard", config: dict[str, Any] | None = None ) -> MillingStrategy: from fibsem.milling.strategy import get_strategies, DEFAULT_STRATEGY @@ -74,7 +76,7 @@ class FibsemMillingStage: milling: FibsemMillingSettings = field(default_factory=FibsemMillingSettings) pattern: BasePattern = field(default_factory=lambda: get_pattern("Rectangle", config={"width": 10e-6, "height": 5e-6, "depth": 1e-6})) - patterns: List[BasePattern] = None # unused + patterns: list[BasePattern] | None = None # unused strategy: MillingStrategy = field(default_factory=lambda: get_strategy("Standard")) alignment: MillingAlignment = field(default_factory=MillingAlignment) imaging: ImageSettings = field(default_factory=ImageSettings) # settings for post-milling acquisition @@ -123,13 +125,13 @@ def run(self, microscope: FibsemMicroscope, asynch: bool = False, parent_ui = No self.strategy.run(microscope=microscope, stage=self, asynch=asynch, parent_ui=parent_ui) -def get_milling_stages(key: str, protocol: Dict[str, List[Dict[str, Any]]]) -> List[FibsemMillingStage]: +def get_milling_stages(key: str, protocol: dict[str, list[dict[str, Any]]]) -> list[FibsemMillingStage]: """Get the milling stages for specific key from the protocol. Args: key: the key to get the milling stages for protocol: the protocol to get the milling stages from Returns: - List[FibsemMillingStage]: the milling stages for the given key""" + list[FibsemMillingStage]: the milling stages for the given key""" if key not in protocol: raise ValueError(f"Key {key} not found in protocol. Available keys: {list(protocol.keys())}") @@ -139,12 +141,12 @@ def get_milling_stages(key: str, protocol: Dict[str, List[Dict[str, Any]]]) -> L stages.append(stage) return stages -def get_protocol_from_stages(stages: List[FibsemMillingStage]) -> List[Dict[str, Any]]: +def get_protocol_from_stages(stages: list[FibsemMillingStage]) -> list[dict[str, Any]]: """Convert a list of milling stages to a protocol dictionary. Args: stages: the list of milling stages to convert Returns: - List[Dict[str, Any]]: the protocol dictionary""" + list[dict[str, Any]]: the protocol dictionary""" if not isinstance(stages, list): stages = [stages] @@ -181,7 +183,7 @@ def estimate_milling_time(pattern: BasePattern, milling_current: float) -> float time = (volume *1e6**3) / sputter_rate return time * 0.75 # QUERY: accuracy of this estimate? -def estimate_total_milling_time(stages: List[FibsemMillingStage]) -> float: +def estimate_total_milling_time(stages: list[FibsemMillingStage]) -> float: """Estimate the total milling time for a list of milling stages""" if not isinstance(stages, list): stages = [stages] diff --git a/fibsem/milling/core.py b/fibsem/milling/core.py index 878c2511..995bab1a 100644 --- a/fibsem/milling/core.py +++ b/fibsem/milling/core.py @@ -1,7 +1,7 @@ +from __future__ import annotations import logging import time -from os import PathLike -from typing import List, Tuple, Optional, Union +from typing import TYPE_CHECKING from fibsem import config as fcfg from fibsem.microscope import FibsemMicroscope @@ -9,22 +9,24 @@ from fibsem.structures import ( FibsemBitmapSettings, FibsemCircleSettings, - FibsemImage, FibsemLineSettings, - FibsemPatternSettings, FibsemRectangleSettings, ImageSettings, BeamType, ) from fibsem.utils import current_timestamp_v2 +if TYPE_CHECKING: + from os import PathLike + from fibsem.structures import FibsemImage, FibsemPatternSettings + ########################### SETUP def setup_milling( microscope: FibsemMicroscope, milling_stage: FibsemMillingStage, - ref_image: Optional[FibsemImage] = None, + ref_image: FibsemImage | None = None, ): """Setup Microscope for FIB Milling. @@ -89,11 +91,11 @@ def finish_milling( microscope.finish_milling(imaging_current=imaging_current, imaging_voltage=imaging_voltage) logging.info("Finished Ion Beam Milling.") -def draw_patterns(microscope: FibsemMicroscope, patterns: List[FibsemPatternSettings]) -> None: +def draw_patterns(microscope: FibsemMicroscope, patterns: list[FibsemPatternSettings]) -> None: """Draw milling patterns on the microscope from the list of settings Args: microscope (FibsemMicroscope): Fibsem microscope instance - patterns (List[FibsemPatternSettings]): List of milling patterns + patterns (list[FibsemPatternSettings]): List of milling patterns """ for pattern in patterns: draw_pattern(microscope, pattern) @@ -118,7 +120,7 @@ def draw_pattern(microscope: FibsemMicroscope, pattern: FibsemPatternSettings): elif isinstance(pattern, FibsemBitmapSettings): microscope.draw_bitmap_pattern(pattern, pattern.path) -def convert_to_bitmap_format(path): +def convert_to_bitmap_format(path: str | PathLike[str]) -> str: import os from PIL import Image @@ -145,7 +147,7 @@ def mill_stage(microscope: FibsemMicroscope, stage: FibsemMillingStage, asynch: def mill_stages( microscope: FibsemMicroscope, - stages: List[FibsemMillingStage], + stages: list[FibsemMillingStage], parent_ui=None, ): """Run a list of milling stages, with a progress bar and notifications.""" @@ -247,8 +249,8 @@ def acquire_images_after_milling( microscope: FibsemMicroscope, milling_stage: FibsemMillingStage, start_time: float, - path: Optional[Union[str, PathLike]], -) -> Tuple[FibsemImage, FibsemImage]: + path: str | PathLike[str] | None, +) -> tuple[FibsemImage, FibsemImage]: """Acquire images after milling for reference. Args: microscope (FibsemMicroscope): Fibsem microscope instance From 86e4536e1c99a0218cbb6e41889b960699976ffc Mon Sep 17 00:00:00 2001 From: Thomas M Fish <50103643+thomasmfish@users.noreply.github.com> Date: Tue, 10 Jun 2025 18:28:33 +0100 Subject: [PATCH 25/25] Type can also be updated to type --- fibsem/milling/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/fibsem/milling/base.py b/fibsem/milling/base.py index 342adb90..23b00fce 100644 --- a/fibsem/milling/base.py +++ b/fibsem/milling/base.py @@ -3,7 +3,7 @@ from abc import ABC, abstractmethod from copy import deepcopy from dataclasses import dataclass, fields, field, asdict -from typing import Any, Type, TypeVar, ClassVar, Generic +from typing import Any, TypeVar, ClassVar, Generic from fibsem.microscope import FibsemMicroscope from fibsem.milling.config import MILLING_SPUTTER_RATE @@ -27,7 +27,7 @@ def to_dict(self) -> dict[str, Any]: @classmethod def from_dict( - cls: Type[TMillingStrategyConfig], d: dict[str, Any] + cls: type[TMillingStrategyConfig], d: dict[str, Any] ) -> TMillingStrategyConfig: return cls(**d) @@ -39,7 +39,7 @@ def required_attributes(self) -> tuple[str, ...]: class MillingStrategy(ABC, Generic[TMillingStrategyConfig]): """Abstract base class for different milling strategies""" name: str = "Milling Strategy" - config_class: Type[TMillingStrategyConfig] + config_class: type[TMillingStrategyConfig] def __init__(self, config: TMillingStrategyConfig | None = None): self.config: TMillingStrategyConfig = config or self.config_class() @@ -48,7 +48,7 @@ def to_dict(self) -> dict[str, Any]: return {"name": self.name, "config": self.config.to_dict()} @classmethod - def from_dict(cls: Type[TMillingStrategy], d: dict[str, Any]) -> TMillingStrategy: + def from_dict(cls: type[TMillingStrategy], d: dict[str, Any]) -> TMillingStrategy: config=cls.config_class.from_dict(d.get("config", {})) return cls(config=config)