Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fb16dce
base typing improvements
thomasmfish Jun 6, 2025
be596fb
conversions type improvements
thomasmfish Jun 6, 2025
e357aad
Update MillingStrategy and MillingStrategyConfig
thomasmfish Jun 6, 2025
d1a0552
image_settings.path is expected to be of type Path
thomasmfish Jun 6, 2025
84116bb
Add some milling Optionals
thomasmfish Jun 6, 2025
3318914
Improve MIllingStrategy typing
thomasmfish Jun 6, 2025
e79f044
Fix acquire_images_after_milling typing
thomasmfish Jun 6, 2025
7c2d63e
Add missing milling_voltage argument to run_milling
thomasmfish Jun 6, 2025
42abe3a
Raise errors for unexpected types handling strategy widgets
thomasmfish Jun 6, 2025
5359dce
Handle if ref_image attribute is unset in StandardMillingStrategy
thomasmfish Jun 6, 2025
25a7831
Improve FibsemPatternSettings typing
thomasmfish Jun 6, 2025
0dd1d2f
Improve pattern typing (and fix dataclass default argument issue)
thomasmfish Jun 6, 2025
510eb9e
Smaller plotting type fixes
thomasmfish Jun 6, 2025
5d89856
Various typing improvements for structures
thomasmfish Jun 6, 2025
4c8641d
Change default Nones to "Unknown"s to match from_dict
thomasmfish Jun 6, 2025
235e2d4
Fix typing changes
thomasmfish Jun 9, 2025
806efb9
MillingStrategy.to_dict no longer an abstract method
thomasmfish Jun 9, 2025
c1806f1
Fix BasePattern inheritance issues
thomasmfish Jun 10, 2025
1a7dd25
Improve alignment typing
thomasmfish Jun 10, 2025
5982ff8
estimate_milling_time: simplify cross_section check
thomasmfish Jun 10, 2025
f49b99a
Handle to_dict and from_dict in FibsemPatternSettings
thomasmfish Jun 10, 2025
1fe3963
Use collections.abc.Generator type hint
thomasmfish Jun 10, 2025
2d9fad4
Update to newer typing style thanks to __future__.annotations
thomasmfish Jun 10, 2025
9ea26ff
Update more typing with __future__.annotations
thomasmfish Jun 10, 2025
86e4536
Type can also be updated to type
thomasmfish Jun 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 32 additions & 33 deletions fibsem/alignment.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 6 additions & 3 deletions fibsem/conversions.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ 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


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:
Expand All @@ -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"]
Expand Down Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions fibsem/microscope.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
89 changes: 47 additions & 42 deletions fibsem/milling/base.py
Original file line number Diff line number Diff line change
@@ -1,59 +1,64 @@
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:
pass


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

Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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"]
Expand All @@ -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())}")

Expand All @@ -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]

Expand Down Expand Up @@ -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]
Expand Down
Loading