diff --git a/fibsem/alignment.py b/fibsem/alignment.py index e3f2cc2b..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 @@ -485,7 +484,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/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. 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): diff --git a/fibsem/milling/base.py b/fibsem/milling/base.py index b0fa2592..23b00fce 100644 --- a/fibsem/milling/base.py +++ b/fibsem/milling/base.py @@ -1,51 +1,56 @@ +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 Any, 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, Point, MillingAlignment, ImageSettings, CrossSectionPattern +from fibsem.structures import FibsemMillingSettings, MillingAlignment, ImageSettings, CrossSectionPattern + + +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): + +class MillingStrategy(ABC, Generic[TMillingStrategyConfig]): """Abstract base class for different milling strategies""" - name: str = "Milling Strategy" - config = MillingStrategyConfig() + name: str = "Milling Strategy" + config_class: type[TMillingStrategyConfig] - def __init__(self, **kwargs): - pass - - @abstractmethod - def to_dict(self): + 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()} - - @staticmethod - @abstractmethod - def from_dict(d: dict) -> "MillingStrategy": - pass + + @classmethod + def from_dict(cls: type[TMillingStrategy], d: dict[str, Any]) -> 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: @@ -53,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 @@ -71,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 @@ -81,7 +86,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 +98,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"] @@ -120,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())}") @@ -136,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] @@ -172,13 +177,13 @@ 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 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 607396e9..995bab1a 100644 --- a/fibsem/milling/core.py +++ b/fibsem/milling/core.py @@ -1,6 +1,7 @@ +from __future__ import annotations import logging import time -from typing import List, Tuple +from typing import TYPE_CHECKING from fibsem import config as fcfg from fibsem.microscope import FibsemMicroscope @@ -8,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: FibsemImage = None, + ref_image: FibsemImage | None = None, ): """Setup Microscope for FIB Milling. @@ -88,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) @@ -117,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 @@ -144,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.""" @@ -246,8 +249,8 @@ def acquire_images_after_milling( microscope: FibsemMicroscope, milling_stage: FibsemMillingStage, start_time: float, - path: str, -) -> Tuple[FibsemImage, FibsemImage]: + path: str | PathLike[str] | None, +) -> tuple[FibsemImage, FibsemImage]: """Acquire images after milling for reference. Args: microscope (FibsemMicroscope): Fibsem microscope instance @@ -265,7 +268,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 diff --git a/fibsem/milling/patterning/patterns2.py b/fibsem/milling/patterning/patterns2.py index 98cfea1f..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, Union +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,51 +22,68 @@ FibsemBitmapSettings, FibsemCircleSettings, FibsemLineSettings, - FibsemPatternSettings, FibsemRectangleSettings, Point, + 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"} # "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): - # name: str = "BasePattern" - # point: Point = field(default_factory=Point) - # shapes: List[FibsemPatternSettings] = None - # TODO: investigate TypeError: non-default argument 'width' follows default argument when uncommenting the above lines +class BasePattern(ABC, Generic[TFibsemPatternSettings]): + name: ClassVar[str] + point: Point + shapes: Optional[List[TFibsemPatternSettings]] = field(default=None, init=False) - @abstractmethod - def define(self) -> List[FibsemPatternSettings]: - pass + _advanced_attributes: ClassVar[Tuple[str, ...]] = () @abstractmethod - def to_dict(self): + def define(self) -> List[TFibsemPatternSettings]: pass - @classmethod - @abstractmethod - def from_dict(cls, ddict: dict) -> "BasePattern": - pass + def to_dict(self) -> Dict[str, Any]: + 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 + def from_dict(cls: Type[TPattern], ddict: Dict[str, Any]) -> TPattern: + 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 [field.name for field in fields(self) if field.name not in CORE_PATTERN_ATTRIBUTES] + def required_attributes(self) -> Tuple[str, ...]: + return tuple(f.name for f in fields(self) if f.name not in fields(BasePattern)) @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: @@ -63,17 +91,16 @@ 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 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, @@ -87,32 +114,10 @@ def define(self) -> List[FibsemPatternSettings]: self.shapes = [shape] return self.shapes - - def to_dict(self): - 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) -> "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 @@ -121,10 +126,10 @@ 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" + # TODO: add for other patterns + _advanced_attributes: ClassVar[Tuple[str, ...]] = ("time", "passes") def define(self) -> List[FibsemRectangleSettings]: @@ -143,46 +148,17 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [shape] return self.shapes - - def to_dict(self): - 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) -> "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 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( @@ -194,37 +170,15 @@ def define(self) -> List[FibsemLineSettings]: ) self.shapes = [shape] return self.shapes - - def to_dict(self): - 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) -> "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 - name: str = "Circle" - shapes: List[FibsemPatternSettings] = None - point: Point = field(default_factory=Point) + + name: ClassVar[str] = "Circle" def define(self) -> List[FibsemCircleSettings]: @@ -237,27 +191,10 @@ def define(self) -> List[FibsemCircleSettings]: ) self.shapes = [shape] return self.shapes - - def to_dict(self): - 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) -> "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 @@ -266,12 +203,11 @@ 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"] - def define(self) -> List[FibsemRectangleSettings]: + name: ClassVar[str] = "Trench" + _advanced_attributes: ClassVar[Tuple[str, ...]] = ("time", "fillet") + + def define(self) -> List[Union[FibsemRectangleSettings, FibsemCircleSettings]]: point = self.point width = self.width @@ -399,36 +335,9 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self): - 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) -> "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 @@ -438,9 +347,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,39 +409,9 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [lower_pattern, upper_pattern, side_pattern] return self.shapes - def to_dict(self): - 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) -> "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 @@ -542,8 +420,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]: @@ -593,37 +471,10 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [left_pattern,right_pattern, upper_pattern] return self.shapes - - def to_dict(self): - 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) -> "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 @@ -632,12 +483,11 @@ 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]: + def define(self) -> List[FibsemLineSettings]: """Calculate the serial liftout sectioning milling patterns""" point = self.point @@ -705,45 +555,16 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self): - 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) -> "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 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,31 +600,9 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [left_pattern, right_pattern] return self.shapes - def to_dict(self): - 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) -> "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 @@ -811,9 +610,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,43 +660,17 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [top_pattern, rhs_pattern] return self.shapes - def to_dict(self): - 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) -> "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 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""" @@ -928,29 +700,10 @@ def define(self) -> List[FibsemRectangleSettings]: self.shapes = [left_pattern_settings, right_pattern_settings] return self.shapes - - def to_dict(self): - 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) -> "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 @@ -962,9 +715,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,42 +764,9 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self): - 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) -> "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 @@ -1056,9 +775,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,44 +852,15 @@ def define(self) -> List[FibsemRectangleSettings]: return self.shapes - def to_dict(self): - 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) -> "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 - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None - name: str = "Clover" + name: ClassVar[str] = "Clover" - def define(self) -> List[FibsemPatternSettings]: + def define(self) -> List[Union[FibsemCircleSettings, FibsemRectangleSettings]]: point = self.point radius = self.radius @@ -1212,34 +901,16 @@ def define(self) -> List[FibsemPatternSettings]: self.shapes = [top_pattern, right_pattern, left_pattern, stem_pattern] return self.shapes - def to_dict(self): - return { - "name": self.name, - "point": self.point.to_dict(), - "radius": self.radius, - "depth": self.depth - } - - @classmethod - def from_dict(cls, ddict: dict) -> "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 - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None - name: str = "TriForce" - def define(self) -> List[FibsemPatternSettings]: - + name: ClassVar[str] = "TriForce" + + def define(self) -> List[FibsemRectangleSettings]: point = self.point height = self.height width = self.width @@ -1247,7 +918,6 @@ def define(self) -> List[FibsemPatternSettings]: angle = 30 self.shapes = [] - # centre of each triangle points = [ Point(point.x, point.y + height), @@ -1256,7 +926,6 @@ def define(self) -> List[FibsemPatternSettings]: ] for point in points: - triangle_shapes = create_triangle_patterns(width=width, height=height, depth=depth, @@ -1266,26 +935,9 @@ def define(self) -> List[FibsemPatternSettings]: return self.shapes - def to_dict(self): - 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) -> "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 @@ -1293,11 +945,10 @@ class TrapezoidPattern(BasePattern): distance: float n_rectangles: int overlap: float - name: str = "Trapezoid" - point: Point = field(default_factory=Point) - shapes: List[FibsemPatternSettings] = None - def define(self) -> List[FibsemPatternSettings]: + name: ClassVar[str] = "Trapezoid" + + def define(self) -> List[FibsemRectangleSettings]: outer_width = self.outer_width inner_width = self.inner_width @@ -1314,7 +965,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 +979,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, @@ -1340,33 +991,6 @@ def define(self) -> List[FibsemPatternSettings]: ) self.shapes.append(deepcopy(pattern)) return self.shapes - - def to_dict(self): - 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) -> "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( @@ -1407,7 +1031,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 +1053,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 +1085,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 diff --git a/fibsem/milling/patterning/plotting.py b/fibsem/milling/patterning/plotting.py index 3dbb842a..a200ae86 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, 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 - ax: plt.Axes fig, ax = plt.subplots(1, 1, figsize=(10, 10)) ax.imshow(image.data, cmap="gray") 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} diff --git a/fibsem/milling/strategy/overtilt.py b/fibsem/milling/strategy/overtilt.py index a3b04bac..42ad3441 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 pathlib import Path +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: @@ -72,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) diff --git a/fibsem/milling/strategy/standard.py b/fibsem/milling/strategy/standard.py index 00976ddc..0c740f8f 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, @@ -40,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()) diff --git a/fibsem/structures.py b/fibsem/structures.py index 4147281b..84983381 100644 --- a/fibsem/structures.py +++ b/fibsem/structures.py @@ -1,14 +1,20 @@ # 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, fields, asdict 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 ( + Any, + Type, + TypeVar, + TYPE_CHECKING, +) import numpy as np import tifffile as tff @@ -35,11 +41,19 @@ except ImportError: THERMO = False +TFibsemPatternSettings = TypeVar( + "TFibsemPatternSettings", bound="FibsemPatternSettings" +) + +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} @@ -88,7 +102,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... @@ -146,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 = {} @@ -188,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. @@ -219,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( @@ -358,7 +373,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, @@ -402,7 +417,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"] @@ -412,7 +428,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"], @@ -471,19 +487,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: 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): @@ -623,16 +639,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: 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 ( @@ -723,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 @@ -829,9 +845,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 @@ -845,7 +861,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"] @@ -859,18 +875,29 @@ def from_dict(state_dict: dict) -> "MicroscopeState": return microscope_state - ########### Base Pattern Settings @dataclass class FibsemPatternSettings(ABC): + def to_dict(self) -> dict[str, Any]: + ddict = asdict(self) + # Handle any special cases + if "cross_section" in ddict: + ddict["cross_section"] = ddict["cross_section"].name + return ddict - @abstractmethod - def to_dict(self) -> dict: - pass - - @staticmethod - def from_dict(self, data: dict) -> "FibsemPatternSettings": - pass + @classmethod + 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 @@ -897,37 +924,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, - } - - @staticmethod - def from_dict(data: dict) -> "FibsemRectangleSettings": - return FibsemRectangleSettings( - 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 @@ -949,9 +945,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"], @@ -975,33 +971,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, - } - - @staticmethod - def from_dict(data: dict) -> "FibsemCircleSettings": - return FibsemCircleSettings( - 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 @@ -1014,30 +983,7 @@ class FibsemBitmapSettings(FibsemPatternSettings): rotation: float centre_x: float centre_y: float - path: str = 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, - } - - @staticmethod - def from_dict(data: dict) -> "FibsemBitmapSettings": - return FibsemBitmapSettings( - 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"], - ) + path: str | os.PathLike[str] | None = None @property def volume(self) -> float: @@ -1129,9 +1075,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), @@ -1149,7 +1095,7 @@ def from_dict(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)}") @@ -1157,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} @@ -1185,10 +1131,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"], @@ -1209,7 +1154,7 @@ class BeamSystemSettings: eucentric_height: float column_tilt: float plasma: bool = False - plasma_gas: str = None + plasma_gas: str | None = None def to_dict(self): ddict = { @@ -1231,10 +1176,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[str, Any]) -> "BeamSystemSettings": + return cls( beam_type=BeamType[settings["beam_type"]], enabled=settings["enabled"], beam=BeamSettings.from_dict(settings), @@ -1259,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"], @@ -1283,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"], @@ -1301,8 +1246,8 @@ class SystemInfo: hardware_version: str software_version: str fibsem_version: str = fibsem.__version__ - application: str = None - application_version: str = None + application: str | None = None + application_version: str | None = None def to_dict(self): return { @@ -1317,10 +1262,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"), @@ -1351,9 +1296,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 @@ -1388,7 +1333,7 @@ class MicroscopeSettings: system: SystemSettings image: ImageSettings milling: FibsemMillingSettings - protocol: dict = None + protocol: dict[str, Any] | None = None def to_dict(self) -> dict: settings_dict = { @@ -1400,15 +1345,16 @@ def to_dict(self) -> dict: return settings_dict - @staticmethod + @classmethod def from_dict( - settings: dict, protocol: dict = None + cls, settings: dict[str, Any], protocol: dict[str, Any] | None = 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, @@ -1421,14 +1367,14 @@ 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__ - application_version: str = None + application_version: str | None = None - def to_dict(self) -> dict: + def to_dict(self) -> dict[str, Any]: """Converts to a dictionary.""" return { "id": self.id, @@ -1439,10 +1385,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"), @@ -1454,13 +1400,13 @@ def from_dict(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: + def to_dict(self) -> dict[str, Any]: """Converts to a dictionary.""" return { "name": self.name, @@ -1469,8 +1415,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"), @@ -1504,13 +1450,13 @@ class FibsemImageMetadata: image_settings: ImageSettings pixel_size: Point microscope_state: MicroscopeState - system: 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) -> BeamType: + def beam_type(self) -> BeamType | None: return self.image_settings.beam_type @property @@ -1538,8 +1484,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"]) @@ -1557,7 +1503,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, @@ -1570,6 +1516,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: @@ -1640,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: @@ -1660,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] @@ -1673,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: @@ -1696,13 +1645,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: str | os.PathLike[str] | None = 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, @@ -1722,7 +1675,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: str | os.PathLike[str], filename: str) -> None: from ome_types import OME from ome_types.model import ( Channel, @@ -1821,7 +1774,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: str | os.PathLike[str]) -> "FibsemImage": import ome_types # read ome-xml, extract openfibsem metadata @@ -1850,7 +1803,7 @@ def fromAdornedImage( cls, adorned: AdornedImage, image_settings: ImageSettings, - state: MicroscopeState = None, + state: MicroscopeState | None = None, ) -> "FibsemImage": """Creates FibsemImage from an AdornedImage (microscope output format). @@ -1892,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. @@ -1939,7 +1892,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 @@ -1999,7 +1956,7 @@ class FibsemGasInjectionSettings: port: str gas: str duration: float - insert_position: str = None # multichem only + insert_position: str | None = None # multichem only @staticmethod def from_dict(d: dict): @@ -2019,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 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) 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