Skip to content

Commit ca5b87b

Browse files
committed
[feat] add goffset andorshrk
1 parent 5e61817 commit ca5b87b

3 files changed

Lines changed: 163 additions & 13 deletions

File tree

install/linux/usr/share/odemis/sim/sparc2-focus-test.odm.yaml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,12 @@
9393
# On some configuration, there is a dedicated camera for the acquisition of the
9494
# spectra, otherwise 'Camera' is used.
9595
"Spectral Camera": {
96-
class: andorcam2.AndorCam2,
96+
class: simcam.Camera,
9797
role: sp-ccd,
98-
power_supplier: "Power Control Unit",
9998
init: {
100-
device: "fake",
101-
transp: [-1, 2], # if mirrored on X axis
102-
},
103-
properties: {
104-
targetTemperature: -60, # °C
99+
image: "sparc-spec-sim.h5",
105100
},
101+
dependencies: {spectrograph: "Spectrograph"},
106102
}
107103

108104
"Spectrometer IR": {

src/odemis/driver/andorshrk.py

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,11 @@
4949
# SHAMROCK_I24SLITWIDTHMAX 24000
5050
SHUTTERMODEMIN = 0
5151
SHUTTERMODEMAX = 2 # Note: 1 is max on SR303, 2 is max on SR193
52-
# SHAMROCK_DET_OFFSET_MIN -240000
53-
# SHAMROCK_DET_OFFSET_MAX 240000
54-
# SHAMROCK_GRAT_OFFSET_MIN -20000
55-
# SHAMROCK_GRAT_OFFSET_MAX 20000
52+
53+
DET_OFFSET_MIN = -240000
54+
DET_OFFSET_MAX = 240000
55+
GRAT_OFFSET_MIN = -20000
56+
GRAT_OFFSET_MAX = 20000
5657

5758
SLIT_INDEX_MIN = 1
5859
SLIT_INDEX_MAX = 4
@@ -496,7 +497,9 @@ def __init__(self, name: str, role: str,
496497

497498
axes = {"wavelength": model.Axis(unit="m", range=wl_range,
498499
speed=(max_speed, max_speed)),
499-
"grating": model.Axis(choices=gchoices)
500+
"grating": model.Axis(choices=gchoices),
501+
"goffset": model.Axis(unit=None,
502+
range=((GRAT_OFFSET_MIN + DET_OFFSET_MIN), (GRAT_OFFSET_MAX + DET_OFFSET_MAX)))
500503
}
501504

502505
if self.FocusMirrorIsPresent():
@@ -1597,14 +1600,40 @@ def _getGratingChoices(self):
15971600

15981601
return gchoices
15991602

1603+
def GetGoffset(self):
1604+
"""
1605+
Checks the current grating and flip-mirror positions.
1606+
Returns the grating offset, consisting of the grating_offset + the detector offset.
1607+
1608+
The detector offset is the equivalent grating offset that is needed to compensate for the change in
1609+
optical path length, introduced by the flip-mirrors.
1610+
This allows to maintain a stable wavelength calibration across different optical paths, rather than forcing the user
1611+
to recalibrate for every detector configuration.
1612+
"""
1613+
1614+
grating = self.GetGrating()
1615+
if "flip-in" in self.axes:
1616+
flip_in_pos = self.GetFlipperMirror(INPUT_FLIPPER)
1617+
else:
1618+
flip_in_pos = DIRECT_PORT
1619+
1620+
if "flip-out" in self.axes:
1621+
flip_out_pos = self.GetFlipperMirror(OUTPUT_FLIPPER)
1622+
else:
1623+
flip_out_pos = DIRECT_PORT
1624+
1625+
goffset = self.GetGratingOffset(grating) + self.GetDetectorOffset(flip_in_pos, flip_out_pos)
1626+
return goffset
1627+
16001628
# high-level methods (interface)
16011629
def _updatePosition(self, must_notify=False):
16021630
"""
16031631
update the position VA
16041632
"""
16051633
# TODO: support "axes" to limit the axes to update
16061634
pos = {"wavelength": self.GetWavelength(),
1607-
"grating": self.GetGrating()
1635+
"grating": self.GetGrating(),
1636+
"goffset": self.GetGoffset()
16081637
}
16091638

16101639
if "focus" in self.axes:
@@ -1814,6 +1843,8 @@ def moveRel(self, shift):
18141843
actions.append((axis, self._doSetWavelengthRel, s))
18151844
elif axis == "focus":
18161845
actions.append((axis, self._doSetFocusRel, s))
1846+
elif axis == "goffset":
1847+
actions.append((axis, self._doSetGoffsetRel, s))
18171848
elif axis in self._slit_names.values():
18181849
sid = [k for k, v in self._slit_names.items() if v == axis][0]
18191850
actions.append((axis, self._doSetSlitRel, sid, s))
@@ -1851,6 +1882,8 @@ def moveAbs(self, pos):
18511882
actions.append((axis, self._doSetFilter, p, check))
18521883
elif axis == "focus":
18531884
actions.append((axis, self._doSetFocusAbs, p))
1885+
elif axis == "goffset":
1886+
actions.append((axis, self._doSetGoffsetAbs, p))
18541887
elif axis == "flip-in":
18551888
check = self._check_move.get(axis, True)
18561889
actions.append((axis, self._doSetFlipper, INPUT_FLIPPER, p, check))
@@ -2064,6 +2097,77 @@ def _doSetFlipper(self, flipper: int, pos: int, check: bool):
20642097
logging.warning("Failed to update turret position, detector offset might be incorrect", exc_info=True)
20652098
self._updatePosition()
20662099

2100+
2101+
def _doSetGoffsetAbs(self, target_offset, *, allow_grating_offset=True, single_detector_mode = False):
2102+
2103+
"""
2104+
Change grating offset, by either changing the grating offset or the detector offset.
2105+
:param target_offset (float): the new grating offset to set
2106+
:param allow_grating_offset (bool): check to allow changing the grating offset, if false, only change
2107+
detector offset.
2108+
:param single_detector_mode (bool): if true, it will always change the grating offset,
2109+
even if the output flipper is not in the direct port position.
2110+
"""
2111+
2112+
target_offset = int(round(target_offset)) # ensure that we get integers for steps
2113+
grating = self.GetGrating()
2114+
port_index = self.GetFlipperMirror(OUTPUT_FLIPPER)
2115+
2116+
if "flip-in" in self.axes:
2117+
flip_in_pos = self.GetFlipperMirror(INPUT_FLIPPER)
2118+
else:
2119+
flip_in_pos = DIRECT_PORT
2120+
2121+
if "flip-out" in self.axes:
2122+
flip_out_pos = self.GetFlipperMirror(OUTPUT_FLIPPER)
2123+
else:
2124+
flip_out_pos = DIRECT_PORT
2125+
2126+
single_detector = bool(single_detector_mode)
2127+
current_grat_offset = self.GetGratingOffset(grating)
2128+
current_det_offset = self.GetDetectorOffset(flip_in_pos, flip_out_pos)
2129+
logging.debug("Current goffset: %d (Grat: %d, Det: %d)",
2130+
(current_grat_offset + current_det_offset),
2131+
current_grat_offset, current_det_offset)
2132+
2133+
# The detector offset compensates for small naccuracies introduced by the flip-mirror mechanism.
2134+
# This value is normally stable and seldom requires re-adjustment.
2135+
2136+
if port_index == 0 or single_detector:
2137+
logging.debug(
2138+
"Choosing grating offset update (port_index=%s single_detector=%s)",
2139+
port_index, single_detector
2140+
)
2141+
2142+
# primary detector -> modify grating offset
2143+
if not allow_grating_offset:
2144+
logging.debug("Grating offset update disabled (grating=1, target=%d)", target_offset, )
2145+
else:
2146+
grating_offset = target_offset - current_det_offset
2147+
self.SetGratingOffset(grating, grating_offset)
2148+
2149+
# secondary detector (if multiple detectors) -> modify detector offset
2150+
else:
2151+
logging.debug(
2152+
"Choosing detector offset update (port_index=%s single_detector=%s)",
2153+
port_index, single_detector
2154+
)
2155+
detector_offset = target_offset - current_grat_offset
2156+
self.SetDetectorOffset(flip_in_pos, flip_out_pos, detector_offset)
2157+
2158+
self._updatePosition()
2159+
2160+
def _doSetGoffsetRel(self, shift):
2161+
2162+
"""
2163+
Change the grating offset by either changing the grating offset or detector offset.
2164+
:param shift (float): relative change in offset
2165+
"""
2166+
2167+
# We expect the goffset axis to exist
2168+
current_pos = self.position.value["goffset"]
2169+
return self._doSetGoffsetAbs(current_pos + shift)
2170+
20672171
def _updateShutterMode(self, pos):
20682172
"""
20692173
Update the state of the shutter depending on the detector used.
@@ -2451,6 +2555,9 @@ def ShamrockGetDetectorOffsetEx(self, device, entrancePort, exitPort, p_offset):
24512555
offset.value = self._detoffset[_val(entrancePort), _val(exitPort)]
24522556

24532557
def ShamrockSetGratingOffset(self, device, grating, offset):
2558+
cur_offset = self._goffset[_val(grating) - 1]
2559+
new_offset = _val(offset)
2560+
time.sleep(abs(cur_offset - new_offset) / 10000)
24542561
self._goffset[_val(grating) - 1] = _val(offset)
24552562

24562563
def ShamrockGetGratingOffset(self, device, grating, p_offset):

src/odemis/driver/test/andorshrk_test.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,53 @@ def test_iris(self):
589589

590590
sp.moveAbsSync({"iris-in": orig_pos})
591591

592+
def test_goffset(self):
593+
self.assertIn("goffset", self.spectrograph.axes)
594+
sp = self.spectrograph
595+
rng = sp.axes["goffset"].range
596+
597+
orig_offsets = {}
598+
orig_grating = sp.position.value["goffset"]
599+
600+
for g in sp.axes["grating"].choices:
601+
sp.moveAbsSync({"grating": g})
602+
orig_offsets[g] = sp.position.value["goffset"]
603+
604+
try:
605+
# try absolute move for grating 1
606+
sp.moveAbsSync({"grating": 1})
607+
target_abs = rng[0] + 10
608+
sp.moveAbsSync({"goffset": target_abs})
609+
self.assertAlmostEqual(sp.position.value["goffset"], target_abs)
610+
611+
# try relative move for grating 1
612+
shift = 5
613+
expected_rel = sp.position.value["goffset"] + shift
614+
sp.moveRelSync({"goffset": shift})
615+
self.assertAlmostEqual(sp.position.value["goffset"], expected_rel)
616+
617+
# try other gratings
618+
choices = sp.axes["grating"].choices
619+
for g in choices:
620+
if g > 1 and choices[g] != "mirror":
621+
sp.moveAbsSync({"grating": g})
622+
break
623+
else:
624+
self.skipTest("No second grating available to test Detector Offset logic")
625+
626+
target_alt = rng[0] + 20
627+
sp.moveAbsSync({"goffset": target_alt})
628+
self.assertAlmostEqual(sp.position.value["goffset"], target_alt)
629+
630+
finally:
631+
# restore the original offset so other tests aren't affected
632+
for g, offset in orig_offsets.items():
633+
sp.moveAbsSync({"grating": g})
634+
sp.moveAbsSync({"goffset": offset})
635+
636+
# restore original grating
637+
sp.moveAbsSync({"grating": orig_grating})
638+
592639
class TestShamrockAndCCD(SpectrographTestBaseClass, unittest.TestCase):
593640
"""
594641
Test the Shamrock + AndorSpec class

0 commit comments

Comments
 (0)