Skip to content

Commit

Permalink
Merge pull request #3001 from patrickcleeve2/meteor-960-fibsem-client…
Browse files Browse the repository at this point in the history
…-updates

[fix][METEOR-960] Minor client fixes from testing
  • Loading branch information
pieleric authored Feb 17, 2025
2 parents 2a3d8c6 + 523044d commit 64c1cf3
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 65 deletions.
11 changes: 8 additions & 3 deletions install/linux/usr/share/odemis/sim/meteor-fibsem-sim.odm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ METEOR-FIBSEM-Sim: {

"Electron-Detector": {
role: se-detector,
init: {}
init: {},
properties: {
medianFilter: 3,
}

}

Expand All @@ -56,7 +59,10 @@ METEOR-FIBSEM-Sim: {

"Ion-Detector": {
role: "se-detector-ion",
init: {}
init: {},
properties: {
medianFilter: 3,
}
}

"Ion-Focus": {
Expand Down Expand Up @@ -84,7 +90,6 @@ METEOR-FIBSEM-Sim: {
"Sample pre-tilt": 0.6108652381980153}, # 35°
FAV_FM_POS_ACTIVE: {"rx": 0.12213888553625313 , "rz": 3.141592653589793}, # 7° - 270°
FAV_SEM_POS_ACTIVE: {"rx": 0.6108652381980153, "rz": 0}, # pre-tilt 35°
FAV_MILL_POS_ACTIVE: {"rx": 0.314159, "rz": 0}, # Note that milling angle (rx) can be changed per session
},
}

Expand Down
57 changes: 57 additions & 0 deletions src/odemis/driver/autoscript_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import pkg_resources
import Pyro5.api
from Pyro5.errors import CommunicationError
from scipy import ndimage

from odemis import model, util
from odemis.driver.xt_client import check_and_transfer_latest_package
Expand Down Expand Up @@ -1244,6 +1245,10 @@ def _updateSettings(self) -> None:
if dwell_time != self.dwellTime.value:
self.dwellTime._value = dwell_time
self.dwellTime.notify(dwell_time)
res = self.parent.get_resolution(self.channel)
if res != self.resolution.value:
self.resolution._value = res
self.resolution.notify(res)
self._updateResolution()

voltage = self.parent.get_high_voltage(self.channel)
Expand All @@ -1254,6 +1259,10 @@ def _updateSettings(self) -> None:
if voltage != self.accelVoltage.value:
self.accelVoltage._value = voltage
self.accelVoltage.notify(voltage)
beam_current = self.parent.get_beam_current(self.channel)
if beam_current != self.probeCurrent.value:
self.probeCurrent._value = beam_current
self.probeCurrent.notify(beam_current)
beam_shift = self.parent.get_beam_shift(self.channel)
if beam_shift != self.shift.value:
self.shift._value = beam_shift
Expand Down Expand Up @@ -1454,6 +1463,9 @@ def __init__(self, name: str, role: str, parent: SEM, channel: str, **kwargs):
# this makes it pretty annoying to do anything on that side, and error prone.
# disabling this until a better solution is found

# median filter applied to the image (required for cryo data)
self.medianFilter = model.IntContinuous(0, range=(0, 9), setter=self._setMedianFilter)

def terminate(self) -> None:
if self._generator:
self.stop_generate()
Expand Down Expand Up @@ -1520,6 +1532,9 @@ def _acquire(self) -> None:
# TODO: use the metadata from the image acquisition _md once it's available
image, _md = self.parent.acquire_image(self._scanner.channel)

# median filter to remove noise (required for cryo data)
if self.medianFilter.value > 0:
image = ndimage.median_filter(image, self.medianFilter.value)
# non-blocking acquisition (disabled until hw testing)
# logging.debug("Starting one image acquisition")
# # start the acquisition
Expand Down Expand Up @@ -1687,6 +1702,17 @@ def _setDetectorMode(self, mode: str) -> str:
def _setDetectorType(self, detector_type: str) -> str:
self.parent.set_detector_type(detector_type, self._scanner.channel)
return self.parent.get_detector_type(self._scanner.channel)

def _setMedianFilter(self, value: int) -> int:
"""Set the median filter value and update the metadata."""

# if value is 0, remove the filter from the metadata
if value == 0:
self.updateMetadata({model.MD_DATA_FILTER: None})
else:
self.updateMetadata({model.MD_DATA_FILTER: f"median-filter:{value}"})
return value

# TODO: add support for auto functions


Expand Down Expand Up @@ -1748,13 +1774,20 @@ def __init__(self, name: str, role: str, parent: SEM, rng: Optional[Dict[str, Tu
"rz": model.Axis(unit=stage_info["unit"]["r"], range=rng["rz"]),
}

# When raw coordinate system is selected, in theory just z should change
# but in practice x and y change by a fixed offset. When raw coordinate system is used,
# offset correction is applied (in the later part of init)
# such that all axes apart from z, have same values
self._raw_offset = {"x": 0, "y": 0}

model.Actuator.__init__(self, name, role, parent=parent, axes=axes_def,
**kwargs)
# will take care of executing axis move asynchronously
self._executor = CancellableThreadPoolExecutor(max_workers=1) # one task at a time

self.position = model.VigilantAttribute({}, unit=stage_info["unit"],
readonly=True)
self._update_coordinate_system_offset() # to get the offset values for raw coordinate system
self._updatePosition()

# Refresh regularly the position
Expand All @@ -1770,12 +1803,31 @@ def terminate(self):
self._pos_poll.cancel()
self._pos_poll = None

def _update_coordinate_system_offset(self):
"""Calculate the offset values for raw coordinate system. The offset is the difference between the specimen (linked) and raw coordinate system."""
self.parent.set_default_stage_coordinate_system("SPECIMEN")
pos_linked = self._getPosition()
self.parent.set_default_stage_coordinate_system("RAW")
pos = self._getPosition()
for axis in self._raw_offset.keys():
self._raw_offset[axis] = pos_linked[axis] - pos[axis]
# the offset should only be in linear axes, it is not expected in rotational axes
if not all(pos[axis] == pos_linked[axis] for axis in ["rx", "rz"]):
logging.warning(
"Unexpected offset in rotational axes. There should be no difference between raw and linked coordinates for rotational axes. "
"Please check the stage configuration.")
logging.debug(f"The offset values in x and y are {self._raw_offset} when stage is in the raw coordinate "
f"system for raw stage coordinates: {pos}, linked stage coordinates: {pos_linked}")

def _updatePosition(self):
"""
update the position VA
"""
old_pos = self.position.value
pos = self._getPosition()
# Apply the offset to the raw coordinates
pos["x"] += self._raw_offset["x"]
pos["y"] += self._raw_offset["y"]
self.position._set_value(self._applyInversion(pos), force_write=True)
if old_pos != self.position.value:
logging.debug("Updated position to %s", self.position.value)
Expand Down Expand Up @@ -1814,6 +1866,11 @@ def _moveTo(self, future: CancellableFuture, pos: Dict[str, float], rel: bool =
if rel:
logging.debug("Moving by shift {}".format(pos))
else:
# apply the offset to the raw coordinates
if "x" in pos.keys():
pos["x"] -= self._raw_offset["x"]
if "y" in pos.keys():
pos["y"] -= self._raw_offset["y"]
logging.debug("Moving to position {}".format(pos))

if "rx" in pos.keys():
Expand Down
20 changes: 19 additions & 1 deletion src/odemis/driver/test/autoscript_client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,25 @@ def test_scanner_component(self):

### DETECTOR
def test_detector_component(self):
pass
# set median filter value
median_filter = 3
self.detector.medianFilter.value = median_filter

# image acquisition
image = self.detector.data.get()
md = image.metadata
self.assertEqual(md[model.MD_DATA_FILTER], f"median-filter:{median_filter}")

req_keys = [model.MD_BEAM_DWELL_TIME, model.MD_BEAM_SCAN_ROTATION,
model.MD_BEAM_VOLTAGE, model.MD_BEAM_CURRENT, model.MD_BEAM_SHIFT,
model.MD_BEAM_FIELD_OF_VIEW, model.MD_ACQ_TYPE, model.MD_ACQ_DATE]
self.assertTrue(all(k in md for k in req_keys))

# no median filter
self.detector.medianFilter.value = 0
image = self.detector.data.get()
md = image.metadata
self.assertEqual(md[model.MD_DATA_FILTER], None)

### STAGE
def test_stage_component(self):
Expand Down
65 changes: 4 additions & 61 deletions src/odemis/driver/xt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,9 @@ def __init__(self, name: str, role: str, children: Dict[str, dict], address: str
self.server._pyroTimeout = 30 # seconds
self._swVersion = self.server.get_software_version()
self._hwVersion = self.server.get_hardware_version()
if "adapter; autoscript" in self._swVersion:
raise HwError("The connected server is not an xt server, but an autoscript server. Please check the xt adapter configuration."
"The server software version is '%s'." % self._swVersion)
logging.debug(
f"Successfully connected to xtadapter with software version {self._swVersion} and "
f"hardware version {self._hwVersion}")
Expand All @@ -298,7 +301,7 @@ def __init__(self, name: str, role: str, children: Dict[str, dict], address: str

# Transfer latest xtadapter package if available
# The transferred package will be a zip file in the form of bytes
self.check_and_transfer_latest_package()
check_and_transfer_latest_package(self)

# Create the scanner type child(ren)
# Check if at least one of the required scanner types is instantiated
Expand Down Expand Up @@ -358,66 +361,6 @@ def __init__(self, name: str, role: str, children: Dict[str, dict], address: str
self._detector = Detector(parent=self, daemon=daemon, **ckwargs)
self.children.value.add(self._detector)

def transfer_latest_package(self, data: bytes) -> None:
"""
Transfer the latest xtadapter package.
Note:
Pyro has a 1 gigabyte message size limitation.
https://pyro5.readthedocs.io/en/latest/tipstricks.html#binary-data-transfer-file-transfer
:param data: The package's zip file data in bytes.
"""
with self._proxy_access:
self.server._pyroClaimOwnership()
return self.server.transfer_latest_package(data)

def check_and_transfer_latest_package(self) -> None:
"""Check if a latest xtadapter package is available and then transfer it."""
try:
package = None
bitness = re.search(r"bitness:\s*([\da-z]+)", self._swVersion)
bitness = bitness.group(1) if bitness is not None else None
adapter = "xtadapter"
if "xttoolkit" in self._swVersion:
adapter = "fastem-xtadapter"
current_version = re.search(r"xtadapter:\s*([\d.]+)", self._swVersion)
current_version = current_version.group(1) if current_version is not None else None
if current_version is not None and bitness is not None:
package = check_latest_package(
directory=XT_INSTALL_DIR,
current_version=current_version,
adapter=adapter,
bitness=bitness,
is_zip=True,
)
if package is not None:
# Check if it's a proper zip file
zip_file = zipfile.ZipFile(package.path)
ret = zip_file.testzip()
zip_file.close()
if ret is None:
# Open the package's zip file as bytes and transfer them
with open(package.path, mode="rb") as f:
data = f.read()
self.transfer_latest_package(data)
# Notify the user that a newer xtadpater version is available
notify2.init("Odemis")
update = notify2.Notification(
"Update Delmic XT Adapter",
"Newer version {} is available on ThermoFisher Support PC.\n\n"
"How to update?\n\n1. Full stop Odemis and close Delmic XT Adapter.\n"
"2. Restart the Delmic XT Adapter to install it.".format(package.version))
update.set_urgency(notify2.URGENCY_NORMAL)
update.set_timeout(10000) # 10 seconds
update.show()
else:
logging.warning("{} is a bad file in {} not transferring latest package.".format(ret, package.path))

except Exception:
logging.warning("Failure during transfer latest xtadapter package (non critical)", exc_info=True)

def move_stage(self, position: Dict[str, float], rel: bool = False) -> None:
"""
Move the stage the given position in meters. This is non-blocking. Throws an error when the requested position
Expand Down
1 change: 1 addition & 0 deletions src/odemis/model/_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
# position of the stage (in m or rad) for each axis in the chamber (raw hardware values)
MD_STAGE_POSITION_RAW = "Stage position raw" # dict of str -> float,
MD_SAMPLE_PRE_TILT = "pre-tilt" # (rad) pre-tilt of the sample stage / shuttle (tilt)
MD_DATA_FILTER = "Data filter" # data filter applied to acquisition data

MD_STREAK_TIMERANGE = "Streak Time Range" # (s) Time range for one streak/sweep
MD_STREAK_MCPGAIN = "Streak MCP Gain" # (int) Multiplying gain for microchannel plate
Expand Down

0 comments on commit 64c1cf3

Please sign in to comment.