diff --git a/install/linux/usr/share/odemis/sim/meteor-sim-chr.odm.yaml b/install/linux/usr/share/odemis/sim/meteor-sim-chr.odm.yaml new file mode 100644 index 0000000000..508436065e --- /dev/null +++ b/install/linux/usr/share/odemis/sim/meteor-sim-chr.odm.yaml @@ -0,0 +1,236 @@ +# For now, this is esssentially just a FM microscope, as the FIB part is handled +# separately by the SEM + +METEOR-Sim: { + class: Microscope, + role: meteor, + children: [ + "Stage", + "Meteor Stage", + "Optical Objective", + "Optical Focus", + "Light Source", + "Filter Wheel", + "Camera", + "EBeam", + ], +} + +"SEM": { + class: simsem.SimSEM, + role: null, + init: { + image: "simsem-fake-output.h5", # any large 16 bit image is fine + }, + children: { + scanner: "EBeam", + detector0: "SE Detector", + focus: "EBeam Focus", + } +} + +"EBeam": { + # Internal child of SimSEM, so no class + role: e-beam, # Not required for the meteor + init: {}, + affects: ["SE Detector"], +} + +"EBeam Focus": { + # role: ebeam-focus, + role: null, + init: {}, + affects: ["EBeam"] +} + +"SE Detector": { + # Internal child of SimSEM, so no class + # role: se-detector, + role: null, + init: {}, +} + +# Normally provided by the SEM +"Stage": { + class: tmcm.TMCLController, + role: stage-bare, + init: { + port: "/dev/fake6", + address: null, + axes: ["x", "y", "z", "rx", "rz"], + ustepsize: [1.e-7, 1.e-7, 1.e-7, 1.2e-5, 1.2e-5], # unit/µstep + rng: [[-0.1, 0.1], [-0.05, 0.05], [-0.05, 0.1], [-2, 2], [0, 6.28]], + unit: ["m", "m", "m", "rad", "rad"], + refproc: "Standard", + }, + metadata: { + # Loading position: + FAV_POS_DEACTIVE: { 'rx': 0, 'rz': 1.9076449, 'x': -0.01529, 'y': 0.0506, 'z': 0.01975 }, + # XYZ ranges for SEM & METEOR + SEM_IMAGING_RANGE: {"x": [-10.e-3, 10.e-3], "y": [-5.e-3, 10.e-3], "z": [-0.5e-3, 8.e-3]}, + FM_IMAGING_RANGE: {"x": [0.040, 0.054], "y": [-5.e-3, 10.e-3], "z": [-0.5e-3, 8.e-3]}, + # Grid centers in SEM range + # Adjusted so that at init (0,0,0), it's at the Grid 1. + SAMPLE_CENTERS: {"GRID 1": {'x': 0, 'y': 0, 'z': 0}, "GRID 2": {'x': 2.98e-3, 'y': 2.46e-3, 'z': 0}}, + # Mirroring values between SEM - METEOR + POS_COR: [0.02447, -0.000017], + # Active tilting (rx) & rotation (rz) angles positions when switching between SEM & FM, in radians. + # Note: these values are calibrated at installation time. + FAV_FM_POS_ACTIVE: {"rx": 0.12213888553625313 , "rz": 5.06145}, # 7° - 270° + # Typically rz = 110°, but we make it 0 so that at init it looks like in SEM position + FAV_SEM_POS_ACTIVE: {"rx": 0, "rz": 0} # Note that milling angle (rx) can be changed per session + }, +} + +"Linked YZ": { + class: actuator.ConvertStage, + role: null, + dependencies: { + "under": "Stage" + }, + init: { + axes: [ "y", "z" ], # name of the axes in the dependency, mapped to x,y (if identity transformation) + rotation: -0.8034946943806713, # rad , -45° + }, +} + +"Meteor Stage": { + class: actuator.MultiplexActuator, + role: stage, + dependencies: { "x": "Stage", "y": "Linked YZ", "z": "Linked YZ", }, + init: { + axes_map: { "x": "x", "y": "x", "z": "y",}, + }, + affects: ["Camera", "EBeam"], + metadata: { + # Typically, x range is the same as FM_IMAGING_RANGE, and Y has to be converted + POS_ACTIVE_RANGE: {"x": [0.040, 0.054], "y": [-10.e-3, 20.e-3]} + }, +} + +"Light Source": { + class: omicronxx.HubxX, + role: light, + init: { + port: "/dev/fakehub", # Simulator + #port: "/dev/ttyFTDI*", + }, + affects: ["Camera"], +} + +"Optical Objective": { + class: static.OpticalLens, + role: lens, + init: { + mag: 84.0, # ratio, (actually of the complete light path) + na: 0.85, # ratio, numerical aperture + ri: 1.0, # ratio, refractive index + }, + affects: ["Camera"] +} + +# Normally a IDS uEye or Zyla +# Axes: X is horizontal on screen (going left->right), physical: far->close when looking at the door +# Y is vertical on screen (going bottom->top), physical: left->right when looking at the door +"Camera": { + class: simcam.Camera, + role: ccd, + dependencies: {focus: "Optical Focus"}, + init: { + image: "andorcam2-fake-clara.tiff", + transp: [-1, 2], # To swap/invert axes + }, + metadata: { + # To change what the "good" focus position is on the simulator + # It's needed for not using the initial value, which is at deactive position. + FAV_POS_ACTIVE: {'z': 1.7e-3}, # good focus position + ROTATION: -0.099484, # [rad] (=-5.7°) + }, +} + +# Controller for the filter-wheel +# DIP must be configured with address 7 (= 1110000) +"Optical Actuators": { + class: tmcm.TMCLController, + role: null, + init: { + port: "/dev/fake6", # Simulator + address: null, # Simulator + axes: ["fw"], + ustepsize: [1.227184e-3], # [rad/µstep] fake value for simulator + rng: [[-14, 7]], # rad, more than 0->2 Pi, in order to allow one extra rotation in both direction, when quickly switching + unit: ["rad"], + refproc: "Standard", + refswitch: {"fw": 0}, #digital output used to switch on/off sensor + inverted: ["fw"], # for the filter wheel, the direction doesn't matter, as long as the positions are correct + }, +} + +"AntiBacklash for Filter Wheel": { + class: actuator.AntiBacklashActuator, + role: null, + init: { + backlash: { + # Force every move to always finish in the same direction + "fw": 50.e-3, # rad + }, + }, + dependencies: {"slave": "Optical Actuators"}, +} + +"Filter Wheel": { + class: actuator.FixedPositionsActuator, + role: filter, + dependencies: {"band": "AntiBacklash for Filter Wheel"}, + init: { + axis_name: "fw", + # This filter-wheel is made so that the light goes through two "holes": + # the filter, and the opposite hole (left empty). So although it has 8 + # holes, it only supports 4 filters (from 0° to 135°), and there is no + # "fast-path" between the last filter and the first one. + positions: { + # pos (rad) -> m,m + 0.08: [414.e-9, 450.e-9], # FF01-432/36 + 0.865398: [500.e-9, 530.e-9], # FF01-515/30 + 1.650796: [579.5e-9, 610.5e-9], # FF01-595/31 + 2.4361944: [663.e-9, 733.e-9], # FF02-698/70 + }, + cycle: 6.283185, # position of ref switch (0) after a full turn + }, + metadata: { + TRANSFORM_PER_CHANNEL: { 0.08: { "Pixel size cor": [ 1,1 ],"Centre position cor": [ 0,0 ] ,"Rotation cor": 1 ,"Shear cor": 1 }, 0.865398: { "Pixel size cor": [ 1,1 ],"Centre position cor": [ 0,0 ] ,"Rotation cor": 1 ,"Shear cor": 1 }, 1.650796: { "Pixel size cor": [ 1,1 ],"Centre position cor": [ 0,0 ] ,"Rotation cor": 1 ,"Shear cor": 1 }} + }, + # TODO: a way to indicate the best filter to use during alignement and brightfield? via some metadata? + affects: ["Camera"], +} + +# CLS3252dsc-1 +"Optical Focus": { + class: smaract.MCS2, + role: focus, + init: { + locator: "fake", + ref_on_init: True, + # TODO: check speed/accel + speed: 0.003, # m/s + accel: 0.003, # m/s² + #hold_time: 5 # s, default = infinite + # TODO: check the ranges, and the channel + axes: { + 'z': { + # -11.5mm is safely parked (FAV_POS_DEACTIVE) + # 1.7mm is typically in focus (FAV_POS_ACTIVE) + range: [-15.e-3, 5.e-3], + unit: 'm', + channel: 0, + }, + }, + }, + metadata: { + # Loading position to retract lens + FAV_POS_DEACTIVE: {'z': -11.5e-3}, + # Initial active position (close from the sample, but not too close, for safety) + FAV_POS_ACTIVE: {'z': 1.69e-3} + }, + affects: ["Camera"], +} diff --git a/src/odemis/model/_metadata.py b/src/odemis/model/_metadata.py index ae70cc39c1..cd64e84cc8 100644 --- a/src/odemis/model/_metadata.py +++ b/src/odemis/model/_metadata.py @@ -271,3 +271,5 @@ # Fastem: Parameters used for stitching and reconstruction of 3D volumes MD_SLICE_IDX = "Index of slice in volume stack" # int MD_FIELD_SIZE = "Average field of view of a megafield" # tuple (px, px) + +MD_TRANSFORM_PER_CHANNEL = "Transformation parameter per RGB channel" \ No newline at end of file diff --git a/src/odemis/odemisd/mdupdater.py b/src/odemis/odemisd/mdupdater.py index 69fb8cbd5c..f0c656d3eb 100644 --- a/src/odemis/odemisd/mdupdater.py +++ b/src/odemis/odemisd/mdupdater.py @@ -352,6 +352,18 @@ def observeFilter(self, filter, comp_affected): def updateOutWLRange(pos, fl=filter, comp_affected=comp_affected): wl_out = fl.axes["band"].choices[pos["band"]] comp_affected.updateMetadata({model.MD_OUT_WL: wl_out}) + # apply lateral chromatic correction to align with the reference channel + apply_transform = fl.getMetadata().get(model.MD_TRANSFORM_PER_CHANNEL, None) + if apply_transform is not None: + if fl.position.value["band"] in apply_transform.keys(): + comp_affected.updateMetadata( + {model.MD_PIXEL_SIZE_COR: apply_transform[fl.position.value["band"]][model.MD_PIXEL_SIZE_COR]}) + comp_affected.updateMetadata( + {model.MD_POS_COR: apply_transform[fl.position.value["band"]][model.MD_POS_COR]}) + comp_affected.updateMetadata( + {model.MD_ROTATION_COR: apply_transform[fl.position.value["band"]][model.MD_ROTATION_COR]}) + comp_affected.updateMetadata( + {model.MD_SHEAR_COR: apply_transform[fl.position.value["band"]][model.MD_SHEAR_COR]}) filter.position.subscribe(updateOutWLRange, init=True) self._onTerminate.append((filter.position.unsubscribe, (updateOutWLRange,)))