Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Integrate OpenFIBSEM Milling #3029

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .github/workflows/unit_tests_cloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,13 @@ jobs:
export PYTHONPATH="$PWD/src:$PYTHONPATH"
python3 -m unittest src/odemis/acq/test/feature_test.py --verbose

- name: Run tests from odemis.acq.milling
if: ${{ !cancelled() }}
run: |
export PYTHONPATH="$PWD/src:$PYTHONPATH"
python3 -m unittest src/odemis/acq/milling/test/patterns_test.py --verbose
python3 -m unittest src/odemis/acq/milling/test/tasks_test.py --verbose

- name: Run tests from odemis.acq.leech
if: ${{ !cancelled() }}
run: |
Expand Down
29 changes: 29 additions & 0 deletions src/odemis/acq/drift/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import numpy
import cv2

from odemis import model
from odemis.acq.align.shift import MeasureShift

MIN_RESOLUTION = (20, 20) # sometimes 8x8 works, but it's not reliable enough
Expand Down Expand Up @@ -347,3 +348,31 @@ def GuessAnchorRegion(whole_img, sample_region):
(occurrences[0, 1] + (dc_shape[1] / 2)) / whole_img.shape[1])

return anchor_roi

def align_reference_image(
ref_image: model.DataArray,
new_image: model.DataArray,
scanner: model.Emitter
) -> None:
"""Align the new image to the reference image using beam shift.
Only supports 2D images with the same resolution.
:param ref_image: The reference image to align with.
:param new_image: The new image to align to the reference.
:param scanner: The scanner to align with.
:return: None
"""
if (ref_image.ndim != 2 or new_image.ndim != 2 or ref_image.shape != new_image.shape):
raise ValueError(f"Only equally sized 2D images are supported for alignment. {ref_image.shape}, {new_image.shape}")

if ref_image.metadata[model.MD_PIXEL_SIZE] != new_image.metadata[model.MD_PIXEL_SIZE]:
raise ValueError("The images must have the same pixel size.")

shift_px = MeasureShift(ref_image, new_image, 2)

pixelsize = ref_image.metadata[model.MD_PIXEL_SIZE]
shift_m = (shift_px[0] * pixelsize[0], shift_px[1] * pixelsize[1])

previous_shift = scanner.shift.value
shift = (shift_m[0] + previous_shift[0], shift_m[1] + previous_shift[1]) # m
scanner.shift.value = shift
logging.debug(f"reference image alignment: previous: {previous_shift}, calculated shift: {shift_m}, beam shift: {scanner.shift.value}")
90 changes: 90 additions & 0 deletions src/odemis/acq/milling/milling_tasks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Contains the configuration for the milling tasks in the format:
#
# task_name:
# milling: Milling parameters
# patterns: List of patterns (and parameters) to mill
'Rough Milling 01':
name: 'Rough Milling 01'
milling:
current: 1.0e-9
voltage: 30000.0
field_of_view: 8e-05
mode: 'Serial'
channel: 'ion'
patterns:
- name: 'Rough Trench'
width: 1.0e-05
height: 6.0e-06
depth: 1.0e-06
spacing: 3.0e-06
center_x: 0
center_y: 0
pattern: 'trench'
'Rough Milling 02':
name: 'Rough Milling 02'
milling:
current: 0.2e-9
voltage: 30000.0
field_of_view: 8e-05
mode: 'Serial'
channel: 'ion'
patterns:
- name: 'Rough Trench 02'
width: 9.5e-6
height: 4.0e-06
depth: 0.8e-06
spacing: 1.5e-06
center_x: 0
center_y: 0
pattern: 'trench'
'Polishing 01':
name: 'Polishing 01'
milling:
current: 60.0e-12
voltage: 30000.0
field_of_view: 8e-05
mode: 'Serial'
channel: 'ion'
patterns:
- name: 'Polishing Trench 01'
width: 9.0e-06
height: 1.0e-06
depth: 0.6e-06
spacing: 600.0e-09
center_x: 0
center_y: 0
pattern: 'trench'
'Polishing 02':
name: 'Polishing 02'
milling:
current: 60.0e-12
voltage: 30000.0
field_of_view: 8e-05
mode: 'Serial'
channel: 'ion'
patterns:
- name: 'Polishing Trench 02'
width: 8.5e-06
height: 0.6e-06
depth: 0.5e-06
spacing: 300.0e-09
center_x: 0
center_y: 0
pattern: 'trench'
'Microexpansion':
name: 'Microexpansion'
milling:
current: 1.0e-9
voltage: 30000.0
field_of_view: 8e-05
mode: 'Serial'
channel: 'ion'
patterns:
- name: 'Microexpansion'
width: 0.5e-06
height: 1.5e-05
depth: 1.0e-06
spacing: 1.0e-05
center_x: 0
center_y: 0
pattern: 'microexpansion'
221 changes: 221 additions & 0 deletions src/odemis/acq/milling/millmng.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
"""
@author: Patrick Cleeve

Copyright © 2025 Delmic

This file is part of Odemis.

Odemis is free software: you can redistribute it and/or modify it under the
terms of the GNU General Public License version 2 as published by the Free
Software Foundation.

Odemis is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
Odemis. If not, see http://www.gnu.org/licenses/.


### Purpose ###

This module contains classes to control the actions related to the milling.

"""
import logging
import threading
import time
from concurrent.futures._base import (
CANCELLED,
FINISHED,
RUNNING,
CancelledError,
Future,
)
from typing import List

from odemis import model
from odemis.acq.acqmng import acquire
from odemis.acq.drift import align_reference_image
from odemis.acq.milling.patterns import (
RectanglePatternParameters,
)
from odemis.acq.milling.tasks import MillingTaskSettings
from odemis.acq.stream import FIBStream
from odemis.util import executeAsyncTask


class TFSMillingTaskManager:
"""This class manages running milling tasks."""

def __init__(self, future: Future, tasks: List[MillingTaskSettings], fib_stream: FIBStream):
"""
:param future: the future that will be executing the task
:param tasks: The milling tasks to run (in order)
"""

self.fibsem = model.getComponent(role="fibsem")
self.tasks = tasks

# for reference image alignment
self.fib_stream = fib_stream

self._future = future
if future is not None:
self._future.running_subf = model.InstantaneousFuture()
self._future._task_lock = threading.Lock()

def cancel(self, future: Future) -> bool:
"""
Canceler of acquisition task.
:param future: the future that will be executing the task
:return: True if it successfully cancelled (stopped) the future
"""
logging.debug("Canceling milling procedure...")

with future._task_lock:
if future._task_state == FINISHED:
return False
future._task_state = CANCELLED
future.running_subf.cancel()
self.fibsem.stop_milling()
logging.debug("Milling procedure cancelled.")
return True

def estimate_milling_time(self) -> float:
"""
Estimates the milling time for the given patterns.
:return: (float > 0): the estimated time is in seconds
"""
return self.fibsem.estimate_milling_time()

def run_milling(self, settings: MillingTaskSettings):
"""Run the milling task with the given settings. ThermoFisher implementation"""

# get the milling settings
milling_current = settings.milling.current.value
milling_voltage = settings.milling.voltage.value
milling_fov = settings.milling.field_of_view.value
milling_channel = settings.milling.channel.value
milling_mode = settings.milling.mode.value
align_at_milling_current = settings.milling.align.value

# get initial imaging settings
imaging_current = self.fibsem.get_beam_current(milling_channel)
imaging_voltage = self.fibsem.get_high_voltage(milling_channel)
imaging_fov = self.fibsem.get_field_of_view(milling_channel)

try:

# acquire a reference image at the imaging settings
if align_at_milling_current:
self._future.running_subf = acquire([self.fib_stream])
data, _ = self._future.running_subf.result()
ref_image = data[0]

# set the milling state
self.fibsem.clear_patterns()
self.fibsem.set_default_patterning_beam_type(milling_channel)
self.fibsem.set_high_voltage(milling_voltage, milling_channel)
self.fibsem.set_beam_current(milling_current, milling_channel)
# self.fibsem.set_field_of_view(milling_fov, milling_channel) # tmp: disable until matched in gui
self.fibsem.set_patterning_mode(milling_mode)

# acquire a new image at the milling settings and align
if align_at_milling_current:
self._future.running_subf = acquire([self.fib_stream])
data, _ = self._future.running_subf.result()
new_image = data[0]
align_reference_image(ref_image, new_image, self.ion_beam)

# draw milling patterns to microscope
for pattern in settings.generate():
if isinstance(pattern, RectanglePatternParameters):
self.fibsem.create_rectangle(pattern.to_dict())
else:
raise NotImplementedError(f"Pattern {pattern} not supported") # TODO: support other patterns

# estimate the milling time
estimated_time = self.fibsem.estimate_milling_time()
self._future.set_end_time(time.time() + estimated_time)

# start patterning (async)
self.fibsem.start_milling()

# wait for milling to finish
elapsed_time = 0
wait_time = 5
while self.fibsem.get_patterning_state() == "Running":

with self._future._task_lock:
if self._future.cancelled() == CANCELLED:
raise CancelledError()

logging.debug(f"Milling in progress... elapsed time: {elapsed_time} s, estimated time: {estimated_time} s")
time.sleep(wait_time)
elapsed_time += wait_time

except CancelledError as ce:
logging.debug(f"Cancelled milling: {ce}")
raise
except Exception as e:
logging.exception(f"Error while milling: {e}")
raise
finally:
# restore imaging state
self.fibsem.set_beam_current(imaging_current, milling_channel)
self.fibsem.set_high_voltage(imaging_voltage, milling_channel)
self.fibsem.set_field_of_view(imaging_fov, milling_channel)
self.fibsem.clear_patterns()
return

def run(self):
"""
The main function of the task class, which will be called by the future asynchronously
"""
self._future._task_state = RUNNING

try:
for task in self.tasks:

with self._future._task_lock:
if self._future._task_state == CANCELLED:
raise CancelledError()

logging.debug(f"Running milling task: {task.name}")

self.run_milling(task)
logging.debug("The milling completed")

except CancelledError:
logging.debug("Stopping because milling was cancelled")
raise
except Exception:
logging.warning("The milling failed")
raise
finally:
self._future._task_state = FINISHED


# TODO: replace with run_milling_tasks_openfibsem
def run_milling_tasks(tasks: List[MillingTaskSettings], fib_stream: FIBStream) -> Future:
"""
Run multiple milling tasks in order.
:param tasks: List of milling tasks to be executed in order.
:return: ProgressiveFuture
"""
# Create a progressive future with running sub future
future = model.ProgressiveFuture()
# create acquisition task
milling_task_manager = TFSMillingTaskManager(future, tasks, fib_stream)
# add the ability of cancelling the future during execution
future.task_canceller = milling_task_manager.cancel

# set the progress of the future (TODO: fix dummy time estimate)
future.set_end_time(time.time() + 10 * len(tasks))

# assign the acquisition task to the future
executeAsyncTask(future, milling_task_manager.run)

return future
Loading
Loading