"""FIB-SEM semi-aautomated creation of ion beam milling patterns."""
from collections import namedtuple
import logging
import matplotlib.pyplot as plt
import numpy as np
import fibsem
from fibsem.user_input import response_yes, response_no, response_cancel
log = logging.getLogger(__name__)
def _n_rois(axis_coords, roi_width, roi_overlap):
"""Number of ROIs needed to cover the sample.
Parameters
----------
axis_coords : tuple of coordinates
Coordinates are in row, column format.
roi_width : float
Width of ROI milling regions, in units of meters.
roi_overlap : float
Amount each milling region should overlap with the last. Meter units.
Returns
-------
n_rois
Integer. How many ROIs are needed to cover the sample.
"""
length = fibsem.orientation.line_length(axis_coords)
n_rois = int(np.ceil(length / (roi_width - roi_overlap)))
return n_rois
def _single_roi_centroid_position(i, axis_coords, roi_width, roi_overlap):
"""Return center point coordinate for ROI.
Parameters
----------
i : ROI index
i.e. which number ROI in the sequence this one is.
axis_coords :
Coordinates are in row, column format.
roi_width : float
Width of ROI milling regions, in units of meters.
roi_overlap : float
Amount each milling region should overlap with the last. Meter units.
Returns
-------
center_coordinate : numpy array
Coordinate is in row, column format.
"""
theta = fibsem.orientation.rotation_angle(axis_coords)
distance = (i * (roi_width - roi_overlap)) + (roi_width / 2)
center_coordinate = fibsem.orientation._point_given_angle_and_distance(
axis_coords[0], theta, distance)
return center_coordinate
[docs]def all_roi_coordinates(sample, settings):
""""Calculate all coordinates for rectangle milling patterns.
Parameters
----------
sample : Embryo sample object.
settings : dict
User input arguments from config yml file.
Returns
-------
all_roi_coords
List of all ROI coordinates.
"""
head2tail_axis, tail2head_axis = _dual_milling_directions(
sample.major_axis, settings)
length_head2tail = fibsem.orientation.line_length(head2tail_axis)
length_tail2head = fibsem.orientation.line_length(tail2head_axis)
if length_head2tail > settings['roi_width'] * 0.5:
head2tail_coords = _roi_coordinates(head2tail_axis,
sample,
settings)
for head_coords in head2tail_coords:
head_coords[-1] += np.pi / 2 # effectively a LeftToRight scan dir
else:
head2tail_coords = []
log.info("Head to tail milling axis is too short, don't mill here.")
if length_tail2head > settings['roi_width'] * 0.5:
tail2head_coords = _roi_coordinates(tail2head_axis,
sample,
settings)
for tail_coords in tail2head_coords:
tail_coords[-1] -= np.pi / 2 # effectively a RightToLeft scan dir
else:
tail2head_coords = []
log.info("Tail to head milling axis is too short, don't mill here.")
all_roi_coords = head2tail_coords + tail2head_coords
return all_roi_coords
def _roi_centroid_positions(milling_axis, args):
"""Return list of ROI centroid coordinates for the milling axis.
ROI centroids are calculated from the outside in, so the edge is always
in the right position closest to the lamella
(we don't care if we mill a little extra on the outside edge).
The returned list is flipped, so ROIs are created from the outside in.
Parameters
----------
milling_axis : Axis coordinates; in row, column format.
args : dict
User input settings from config yml file.
Returns
-------
list of coordinates
Centroid coordinates are in row, column format.
"""
n_rois = _n_rois(milling_axis, args['roi_width'], args['roi_overlap'])
reversed_axis = np.flip(milling_axis, axis=0)
results = []
for i in range(n_rois):
roi_centroid = _single_roi_centroid_position(
i, reversed_axis, args['roi_width'], args['roi_overlap'])
results.append(roi_centroid)
results = np.flip(results, axis=0)
return results
def _roi_coordinates(milling_axis, sample, args):
"""Calculate coordinates for rectangle milling patterns.
start_coord is in row, column format.
"""
theta = fibsem.orientation.rotation_angle(sample.major_axis)
roi_centroids = _roi_centroid_positions(milling_axis, args)
results = []
for roi_centroid in roi_centroids:
center_y, center_x = roi_centroid
roi_height = fibsem.orientation._calculate_roi_height(roi_centroid,
sample)
roi_height_buffer = 0.1 * \
fibsem.orientation.line_length(sample.minor_axis)
roi_height += roi_height_buffer
result = [center_x, center_y, roi_height, args['roi_width'],
args['depth'], theta] # swap width/height, scan_dir bugfix
results.append(result)
return results
def _dual_milling_directions(major_axis, settings):
"""Milling goes from the outside edges in, create axis for this.
Parameters
----------
major_axis : major axis of sample segmentation.
Coordinates are in row, column format.
settings : User input settings. Includes:
start_lamella : float, in range 0 to 1.0 (1 = 100%)
Where the lamella starts, as percentage of sample length from head.
end_lamella : float, in range 0 to 1.0 (1 = 100%)
Where the lamella ends, as a percentage of sample length from head.
buffer_around_lamella :
Returns
-------
head_to_tail_axis, tail_to_head_to_axis
"""
start_lamella = settings['start_lamella']
end_lamella = settings['end_lamella']
subtract_dist = settings['buffer_around_lamella'] + settings['roi_overlap']
theta = fibsem.orientation.rotation_angle(major_axis)
sample_length = fibsem.orientation.line_length(major_axis)
distance_to_start = (start_lamella * sample_length) - subtract_dist
distance_to_stop = (end_lamella * sample_length) - subtract_dist
# head_to_tail
start_coord = fibsem.orientation._point_given_angle_and_distance(
major_axis[0], theta, distance_to_start)
head_to_tail_axis = (major_axis[0], start_coord)
# tail_to_head
stop_coord = fibsem.orientation._point_given_angle_and_distance(
major_axis[0], theta, distance_to_stop)
tail_to_head_to_axis = (major_axis[1], stop_coord)
return head_to_tail_axis, tail_to_head_to_axis
[docs]def mill_single_sample(sample, microscope, settings, wait_for_user=False):
"""Ion beam milling a single sample.
Parameters
----------
sample : fibsem.sample.Sample()
Sample object for ion beam milling.
microscope : autoscript_sdb_microscope_client.SdbMicroscopeClient()
Autoscript microscope client.
settings : dict
User input settings from config yml file.
wait_for_user : bool, optional
Ask the user to confirm (the default is False, which cancels milling).
Returns
-------
milled_image
The ion beam image of the sample after milling is finished.
"""
import fibsem.autoscript
fibsem.autoscript.reset_patterning_state(microscope)
fibsem.autoscript.create_milling_patterns(microscope,
sample.milling_rois,
settings)
if wait_for_user:
user_continue = fibsem.user_input.response(
'Do you want to mill these patterns? y/n')
if user_continue in response_no or user_continue in response_cancel:
print('Aborting ion beam milling')
microscope.patterning.clear_patterns()
return
log.info('Ion beam milling in progress, this may take some time...')
current_for_milling = settings['current_for_milling']
if microscope.beams.ion_beam.beam_current.value != current_for_milling:
microscope.beams.ion_beam.beam_current.value = current_for_milling
microscope.patterning.run()
microscope.patterning.clear_patterns() # inconsistent in clearing patterns
log.info('Single sample milled.')
[docs]def mill_all_samples(samples, microscope, settings):
"""Ion beam milling for all samples in list.
Parameters
----------
samples : list
List of sample objects for ion beam milling.
microscope : autoscript_sdb_microscope_client.SdbMicroscopeClient()
Autoscript microscope client.
settings : dict
User input arguments from config yml file.
"""
message = 'Do you want to mill all samples? y/n'
user_continue = fibsem.user_input.response(message)
if user_continue in response_yes:
for i, sample in enumerate(samples):
log.info('Sample {}'.format(i))
if settings['display_matplotlib'] is True:
sample.display_milling_rois()
sample.move_to_stage_position(microscope)
mill_single_sample(sample, microscope, settings,
wait_for_user=True)
log.info('All ion beam milling complete.')
else:
log.info('Cancelling ion milling.')