Coverage for /opt/conda/envs/apienv/lib/python3.10/site-packages/daiquiri/core/transform/imageviewer.py: 89%
548 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-14 02:13 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-14 02:13 +0000
1"""These are the coordinate transformations to relate motors,
2VLM image and display canvas for the ImageViewer component source.
4All coordinate transformations are done in homogeneous coordinates
5"""
7# TODO: this module should go somewhere else
9import logging
10import itertools
11import numpy
12from daiquiri.core.transform import hw_sets
13from daiquiri.core.transform import hw_maps
14from daiquiri.core.transform import maps
15from daiquiri.core.transform import sets
16from daiquiri.core.transform.units import get_exponent
19logger = logging.getLogger(__name__)
22def tohomogeneous(coord):
23 """
24 :param array coord: npoints x ndim
25 :returns array: npoints x ndim+1
26 """
27 coord = numpy.atleast_2d(coord)
28 ones = numpy.ones((coord.shape[0], 1))
29 return numpy.hstack([coord, ones])
32def fromhomogeneous(coord):
33 """
34 :param array coord: npoints x ndim+1
35 :returns array: npoints x ndim
36 """
37 coord = numpy.atleast_2d(coord)
38 coord = coord[:, :-1] / coord[:, -1, numpy.newaxis]
39 return coord
42def polyline(coord):
43 """Corners for drawing a boundary based on low and high point
45 :param array coord: shape is (2, ndim)
46 :param array: (ndim*2^(ndim-1)+(ndim>1), ndim)
47 """
48 coord = numpy.atleast_2d(coord)
49 npoints, ndim = coord.shape
50 if npoints != 2:
51 raise ValueError("Provide low and high point")
52 if ndim == 1:
53 return coord
54 elif ndim == 2:
55 x = coord[[0, 1, 1, 0, 0], 0]
56 y = coord[[0, 0, 1, 1, 0], 1]
57 return numpy.vstack([x, y]).T
58 else:
59 raise NotImplementedError
62class MotorsToWorld(maps.Map):
63 """Maps motors (R^3/R^6) to World (R^4) frame coordinates:
65 xworld = x + x_fine
66 yworld = y + y_fine
67 zworld = z + zfine
69 Each motor has its own units and the World frame has its own units as well.
70 """
72 def __init__(
73 self,
74 x=None,
75 y=None,
76 z=None,
77 x_fine=None,
78 y_fine=None,
79 zfine=None,
80 units="nm",
81 unitdict=None,
82 ):
83 """
84 :param Motor x:
85 :param Motor y:
86 :param Motor z:
87 :param Motor x_fine:
88 :param Motor y_fine:
89 :param Motor zfine:
90 :param str units: the World frame units (nm by default)
91 :param dict unitdict: motor units (tries to get them from the hardware when missing)
92 """
93 motor_labels = []
94 self.trnx, labels = self._init_transform(
95 x, "x", units, unitdict, motfine=x_fine
96 )
97 motor_labels += labels
98 self.trny, labels = self._init_transform(
99 y, "y", units, unitdict, motfine=y_fine
100 )
101 motor_labels += labels
102 self.trnz, labels = self._init_transform(z, "z", units, unitdict, motfine=zfine)
103 motor_labels += labels
104 self.motor_labels = motor_labels
105 self.unitdict = unitdict
106 self.units = units
107 domain = hw_sets.CompositeMotorDomain(
108 [self.trnx.domain, self.trny.domain, self.trnz.domain]
109 )
110 super().__init__(domain=domain, codomain=None)
112 def _init_transform(self, mot, label, units, unitdict, motfine=None):
113 """Instantiate a transformation object for coarse+fine motor translation
115 :param Motor mot:
116 :param str label:
117 :param str units: the World frame units
118 :param Motor motfine:
119 :returns MotorLinearCombination, list(str):
120 """
121 if mot is None:
122 raise ValueError(f"Motor '{label}' is missing")
123 if motfine is None:
124 labels = [label]
125 x = [mot]
126 closed = [[True, True]]
127 else:
128 labels = [label, label + "_fine"]
129 x = [mot, motfine]
130 closed = [[True, True], [True, True]]
131 trn = hw_maps.MotorLinearCombination(
132 x, closed=closed, units=units, unitdict=unitdict
133 )
134 return trn, labels
136 @property
137 def codomain(self):
138 fillvalue = [numpy.nan, numpy.nan, numpy.nan, 1]
139 return sets.RealIntervalComposite(
140 [
141 self.trnx.codomain,
142 self.trny.codomain,
143 self.trnz.codomain,
144 sets.RealVectorSpace(ndim=1, fillvalue=1),
145 ],
146 fillvalue=fillvalue,
147 )
149 @property
150 def image(self):
151 return sets.RealIntervalComposite(
152 [
153 self.trnx.image,
154 self.trny.image,
155 self.trnz.image,
156 sets.RealVectorSpace(ndim=1, fillvalue=1),
157 ]
158 )
160 @property
161 def units(self):
162 return self._units
164 @units.setter
165 def units(self, value):
166 self._units = value
167 self.trnx.units = value
168 self.trny.units = value
169 self.trnz.units = value
171 def _forward(self, coord):
172 """Motor positions to World frame coordinates
174 :param array coord: shape is (npoints, ndomain)
175 :returns array: shape is (npoints, ncodomain)
176 """
177 n1 = self.trnx.ndomain
178 n2 = n1 + self.trny.ndomain
179 icoordx = self.trnx.forward(coord[:, :n1])
180 icoordy = self.trny(coord[:, n1:n2])
181 icoordz = self.trnz.forward(coord[:, n2:])
182 ones = numpy.ones((icoordx.shape[0], 1))
183 return numpy.hstack([icoordx, icoordy, icoordz, ones])
185 def _inverse(self, icoord):
186 """World frame coordinates to motor position
188 :param num or array-like icoord: shape is (npoints, ncodomain)
189 :returns array-like: shape is (npoints, ndomain)
190 """
191 icoord = fromhomogeneous(icoord)
192 coordx = self.trnx.inverse(icoord[:, 0, numpy.newaxis])
193 coordy = self.trny.inverse(icoord[:, 1, numpy.newaxis])
194 coordz = self.trnz.inverse(icoord[:, 2, numpy.newaxis])
195 return numpy.hstack([coordx, coordy, coordz])
197 def motor_world_limits(self, motname):
198 """Motor limits in the World frame (low, high)
200 :param str motname:
201 :returns array: shape is (2, ncodomain-1)
202 """
203 axis = self.domain.motor_index(motname)
204 n1 = self.trnx.ndomain
205 n2 = n1 + self.trny.ndomain
206 if axis < n1:
207 trn = self.trnx
208 elif axis >= n1 and axis < n2:
209 trn = self.trny
210 else:
211 trn = self.trnz
212 return trn.codomain_limits(motname)
214 def motor_world_index(self, motname):
215 """Motor index in the World frame
217 :param str motname:
218 :returns int:
219 """
220 axis = self.domain.motor_index(motname)
221 n1 = self.trnx.ndomain
222 n2 = n1 + self.trny.ndomain
223 if axis < n1:
224 return 0
225 elif axis >= n1 and axis < n2:
226 return 1
227 else:
228 return 2
230 @property
231 def world_limits(self):
232 """World limits in the World frame
234 :returns array: shape is (2, ncodomain-1)
235 """
236 limits = self.image.limits
237 limits = [limits.low.tolist()[:-1], limits.high.tolist()[:-1]]
238 return numpy.array(limits)
240 @property
241 # @timed
242 def current_world_position(self):
243 """
244 :returns array: shape is (ncodomain,)
245 """
246 pos = self.forward(self.domain.current_position)
247 return fromhomogeneous(pos)[0]
249 @property
250 def centered_position(self):
251 """Current motor position with the fine axes centered"""
252 x = self._center_fine(self.trnx)
253 y = self._center_fine(self.trny)
254 z = self._center_fine(self.trnz)
255 return numpy.array(x + y + z)
257 @staticmethod
258 def _center_fine(trn):
259 pmotors = trn.current_domain_position
260 if trn.ndomain == 1:
261 return pmotors.tolist()
262 else:
263 pworld = trn.forward(pmotors)[0, 0]
264 pcen = trn.domain.center
265 pcoeff = trn.coefficients.tolist()
266 return [(pworld - pcoeff[1] * pcen[1]) / pcoeff[0], pcen[1]]
268 @property
269 def motor_names(self):
270 return [mot.id() for mot in self.domain.motors]
272 def motor_name(self, label):
273 idx = self.motor_labels.index(label)
274 return self.motor_names[idx]
276 def motor_label(self, name):
277 idx = self.motor_names.index(name)
278 return self.motor_labels[idx]
280 @property
281 def current_motor_positions_dict(self):
282 ret = self.domain.current_position_dict
283 return {
284 label: ret[name] for name, label in zip(self.motor_names, self.motor_labels)
285 }
288class WorldToSample(maps.Isomorphism):
289 """Maps World (R^4) to Sample (R^4) frame coordinates"""
291 def __init__(self, sampleoffset=None, motormap=None):
292 """When sample shift is zero, the sample position is the current motor position
294 :param 3-array sampleoffset: sample shift in the World frame
295 :param MotorsToWorld motormap:
296 """
297 self.sampleoffset = sampleoffset
298 self.motormap = motormap
299 fillvalue = [numpy.nan, numpy.nan, numpy.nan, 1]
300 domain = codomain = sets.RealVectorSpace(ndim=4, fillvalue=fillvalue)
301 super().__init__(domain=domain, codomain=codomain, matrix=self._forward_matrix)
303 @property
304 def sampleoffset(self):
305 return self._sampleoffset
307 @sampleoffset.setter
308 def sampleoffset(self, values):
309 if values is None:
310 values = [0, 0, 0]
311 values = numpy.atleast_1d(values)
312 if values.shape != (3,):
313 raise ValueError("Needs to be an array of 3 values")
314 self._sampleoffset = values
316 @property
317 def matrix(self):
318 """
319 :returns array: shape is (ncodomain, ndomain)
320 """
321 self._matrix = self._parse_matrix(self._forward_matrix)
322 return self._matrix
324 @property
325 def _forward_matrix(self):
326 position = self.motormap.current_world_position
327 offset = self.sampleoffset - position
329 return [
330 [1, 0, 0, offset[0]],
331 [0, 1, 0, offset[1]],
332 [0, 0, 1, offset[2]],
333 [0, 0, 0, 1],
334 ]
337class VLMToWorld(maps.Isomorphism):
338 """Maps VLM (R^4) to World (R^4) frame coordinates"""
340 def __init__(self, focaloffset=None):
341 """
342 :param 3-array focaloffset: VLM focal point in the World frame
343 """
344 self.focaloffset = focaloffset
345 fillvalue = [numpy.nan, numpy.nan, numpy.nan, 1]
346 domain = codomain = sets.RealVectorSpace(ndim=4, fillvalue=fillvalue)
347 super().__init__(domain=domain, codomain=codomain, matrix=self._forward_matrix)
349 @property
350 def focaloffset(self):
351 return self._focaloffset
353 @focaloffset.setter
354 def focaloffset(self, values):
355 if values is None:
356 values = [0, 0, 0]
357 values = numpy.atleast_1d(values)
358 if values.shape != (3,):
359 raise ValueError("Needs to be an array of 3 values")
360 self._focaloffset = values
362 @property
363 def matrix(self):
364 """
365 :returns array: shape is (ncodomain, ndomain)
366 """
367 self._matrix = self._parse_matrix(self._forward_matrix)
368 return self._matrix
370 @property
371 def _forward_matrix(self):
372 offset = self.focaloffset
373 return [
374 [1, 0, 0, offset[0]],
375 [0, 1, 0, offset[1]],
376 [0, 0, 1, offset[2]],
377 [0, 0, 0, 1],
378 ]
381class VLMToVI(maps.LinearMap, maps.Surjection):
382 """Maps R^4 VLM (video light microscope) to R^3 VI frame (video image) coordinates"""
384 def __init__(self, imageshape=None, zoomlevel=None, zoominfo=None):
385 """
386 :param 2-array imageshape: in pixels
387 :param str zoomlevel: key in `zoominfo`
388 :param dict(dict) zoominfo: zoom level -> {"pixelsize":[., .], "focalpoint":[., .]}
389 pixelsize: VLM units per VI unit
390 focalpoint: focal point in VI units relative to the image center
391 """
392 self.imageshape = imageshape
393 self.zoominfo = zoominfo
394 self.zoomlevel = zoomlevel
395 fillvalue = [numpy.nan, numpy.nan, numpy.nan, 1]
396 domain = sets.RealVectorSpace(ndim=4, fillvalue=fillvalue)
397 fillvalue = [numpy.nan, numpy.nan, 1]
398 codomain = sets.RealVectorSpace(ndim=3, fillvalue=fillvalue)
399 super().__init__(domain=domain, codomain=codomain, matrix=self._forward_matrix)
401 @property
402 def imageshape(self):
403 return self._imageshape
405 @imageshape.setter
406 def imageshape(self, values):
407 values = numpy.atleast_1d(values)
408 if values.shape != (2,):
409 raise ValueError("Needs to be an array of 2 values")
410 self._imageshape = values
412 @property
413 def focalpoint(self):
414 """Focal point in the VI frame in VI units (pixels)"""
415 try:
416 return self.zoominfo[self.zoomlevel]["focalpoint"] + self.vi_center
417 except ValueError:
418 return self.vi_center
420 @property
421 def pixelsize(self):
422 """1 VI unit in VLM units"""
423 try:
424 pixelsize = self.zoominfo[self.zoomlevel]["pixelsize"]
425 except ValueError:
426 pixelsize = [1, 1]
427 pixelsize = numpy.atleast_1d(pixelsize)
428 if pixelsize.shape != (2,):
429 raise ValueError("Needs to be an array of 2 values")
430 return pixelsize
432 @property
433 def zoomlevel(self):
434 level = self._zoomlevel
435 if level not in self.zoominfo:
436 msg = f"'{level}' not a known zoom level ([{list(self.zoominfo.keys())}])'"
437 logger.error(msg)
438 raise ValueError(msg)
439 return level
441 @property
442 def zoomlevels(self):
443 arr = []
444 names = []
445 for name, info in self.zoominfo.items():
446 arr.append(info["pixelsize"][0])
447 names.append(name)
448 return [names[i] for i in numpy.argsort(arr)]
450 def zoomin(self):
451 levels = self.zoomlevels
452 i = levels.index(self.zoomlevel)
453 try:
454 self.zoomlevel = levels[i + 1]
455 except IndexError:
456 return
458 def zoomout(self):
459 levels = self.zoomlevels
460 i = max(levels.index(self.zoomlevel), 1)
461 try:
462 self.zoomlevel = levels[i - 1]
463 except IndexError:
464 return
466 @zoomlevel.setter
467 def zoomlevel(self, value):
468 self._zoomlevel = value
470 @property
471 def matrix(self):
472 """
473 :returns array: shape is (ncodomain, ndomain)
474 """
475 self._matrix = self._parse_matrix(self._forward_matrix)
476 return self._matrix
478 @property
479 def _forward_matrix(self):
480 focalpoint = self.focalpoint
481 ipixelsize = 1.0 / self.pixelsize
482 return [
483 [ipixelsize[0], 0, 0, focalpoint[0]],
484 [0, ipixelsize[1], 0, focalpoint[1]],
485 [0, 0, 0, 1],
486 ]
488 @property
489 def _inverse_matrix(self):
490 # Only XY-plane
491 focalpoint = self.focalpoint
492 pixelsize = self.pixelsize
493 shift = -focalpoint * pixelsize
494 return numpy.array(
495 [[pixelsize[0], 0, shift[0]], [0, pixelsize[1], shift[1]], [0, 0, 1]]
496 )
498 def _right_inverse(self, icoord):
499 """
500 :param array icoord: shape is (npoints, ncodomain)
501 :returns array-like: shape is (npoints, ndomain)
502 """
503 coord = self._inverse_matrix.dot(icoord.T).T
504 result = numpy.zeros_like(coord, shape=(coord.shape[0], coord.shape[1] + 1))
505 result[:, :-2] = coord[:, :-1]
506 result[:, -1] = coord[:, -1]
507 return result
509 @property
510 def vi_corners(self):
511 """Bounds in the VI frame which the image needs to be displayed.
513 :returns array: (left, bottom), (right, top)
514 """
515 return numpy.array([[0, 0], self.imageshape.tolist()])
517 @property
518 def vi_polyline(self):
519 """
520 :returns array: (npoints, ndomain)
521 """
522 return polyline(self.vi_corners)
524 @property
525 def vi_center(self):
526 """
527 :returns array: (ndomain,)
528 """
529 return self.imageshape / 2.0
531 @property
532 def vi_origin(self):
533 """
534 :returns array: (ndomain,)
535 """
536 return numpy.array([0, 0])
539class SampleToCanvas(maps.LinearMap, maps.Surjection):
540 """Maps Sample (R^4) to Canvas (R^3) frame coordinates"""
542 def __init__(self, downstream=True):
543 """
544 :param bool downstream: look at the sample in the Z direction (~ beam direction)
545 """
546 self.downstream = downstream
547 fillvalue = [numpy.nan, numpy.nan, numpy.nan, 1]
548 domain = sets.RealVectorSpace(ndim=4, fillvalue=fillvalue)
549 fillvalue = [numpy.nan, numpy.nan, 1]
550 codomain = sets.RealVectorSpace(ndim=3, fillvalue=fillvalue)
551 super().__init__(domain=domain, codomain=codomain, matrix=self._forward_matrix)
553 @property
554 def matrix(self):
555 """
556 :returns array: shape is (ncodomain, ndomain)
557 """
558 self._matrix = self._parse_matrix(self._forward_matrix)
559 return self._matrix
561 @property
562 def _sx(self):
563 if self.downstream:
564 return -1
565 else:
566 return 1
568 @property
569 def _forward_matrix(self):
570 return [[self._sx, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1]]
572 @property
573 def _inverse_matrix(self):
574 return numpy.asarray([[self._sx, 0, 0], [0, 1, 0], [0, 0, 1]])
576 def _right_inverse(self, icoord):
577 """
578 :param array icoord: shape is (npoints, ncodomain)
579 :returns array-like: shape is (npoints, ndomain)
580 """
581 coord = self._inverse_matrix.dot(icoord.T).T
582 result = numpy.zeros_like(coord, shape=(coord.shape[0], coord.shape[1] + 1))
583 result[:, :-2] = coord[:, :-1]
584 result[:, -1] = coord[:, -1]
585 return result
588class BeamToWorld(maps.Isomorphism):
589 """Maps Beam (R^4) to World (R^4) frame coordinates"""
591 def __init__(self, beamangle=0, beamoffset=None, beamsize=None):
592 """
593 :param num beamangle: rotation around the Y-axis (degrees)
594 :param 3-array beamoffset: beam position in the world frame
595 :param 3-array beamsize: beam size in the world frame
596 """
597 self.beamangle = beamangle
598 self.beamoffset = beamoffset
599 self.beamsize = beamsize
600 fillvalue = [numpy.nan, numpy.nan, numpy.nan, 1]
601 domain = codomain = sets.RealVectorSpace(ndim=4, fillvalue=fillvalue)
602 super().__init__(domain=domain, codomain=codomain, matrix=self._forward_matrix)
604 @property
605 def beamoffset(self):
606 return self._beamoffset
608 @beamoffset.setter
609 def beamoffset(self, values):
610 if values is None:
611 values = [0, 0, 0]
612 values = numpy.atleast_1d(values)
613 if values.shape != (3,):
614 raise ValueError("Needs to be an array of 3 values")
615 self._beamoffset = values
617 @property
618 def beamsize(self):
619 return self._beamsize
621 @beamsize.setter
622 def beamsize(self, values):
623 if values is None:
624 values = [0, 0, 0]
625 values = numpy.atleast_1d(values)
626 if values.shape != (3,):
627 raise ValueError("Needs to be an array of 3 values")
628 self._beamsize = values
630 @property
631 def beam_corners(self):
632 """Corners of the beam ellipsoid"""
633 bs = self.beamsize
634 low = -bs / 2
635 low[-1] = 0
636 high = bs / 2
637 return numpy.asarray(list(itertools.product(*zip(low, high))))
639 @property
640 def matrix(self):
641 """
642 :returns array: shape is (ncodomain, ndomain)
643 """
644 self._matrix = self._parse_matrix(self._forward_matrix)
645 return self._matrix
647 @property
648 def _forward_matrix(self):
649 cosa = numpy.cos(numpy.radians(self.beamangle))
650 sina = numpy.sin(numpy.radians(self.beamangle))
651 beamoffset = self.beamoffset
652 return [
653 [cosa, 0, sina, beamoffset[0]],
654 [0, 1, 0, beamoffset[1]],
655 [-sina, 0, cosa, beamoffset[2]],
656 [0, 0, 0, 1],
657 ]
660class ImageViewerCanvas:
661 """This class groups all ImageViewer coordinate transformations
662 and provides an API for dislay, motion and scanning.
664 Coordinates in Canvas frame
665 - markers (beam, VLM center, VLM focus)
666 - borders (VLM, fine, coarse, reach)
668 Actions
669 - move the sample (point beam on in Canvas frame)
670 - move the motors
671 - define the beam
672 - center fine
674 Conversions
675 - convert a group of points to motor positions
676 (e.g. corners of a map)
677 """
679 def __init__(
680 self,
681 motors,
682 units="nm",
683 unitdict=None,
684 sampleoffset=None,
685 beamoffset=None,
686 beamangle=0,
687 beamsize=None,
688 focaloffset=None,
689 vlmimageshape=None,
690 zoomlevel=None,
691 zoominfo=None,
692 downstream=True,
693 ):
694 """
695 :param dict(Motor) motors:
696 :param str units: the World frame units (nm by default)
697 :param dict unitdict: motor units (tries to get them from the hardware when missing)
698 :param 3-array sampleoffset: sample shift in the World frame
699 :param 3-array beamoffset: beam position in the world frame
700 :param 3-array beamsize: beam size in the world frame
701 :param num beamangle: angle (degrees) between world and beam frame
702 :param 3-array focaloffset: VLM focalpoint in the world frame
703 :param 2-array vlmimageshape: in pixels
704 :param str zoomlevel: key in `zoominfo`
705 :param dict(dict) zoominfo: zoom level -> {"pixelsize":[., .], "focalpoint":[., .]}
706 pixelsize: 1 VI unit in VLM units
707 focalpoint: focal point in VI units relative to the image center
708 :param bool downstream: look at the sample downstream
709 """
710 self.motors_to_world = MotorsToWorld(**motors, units=units, unitdict=unitdict)
711 self.world_to_sample = WorldToSample(
712 sampleoffset=sampleoffset, motormap=self.motors_to_world
713 )
714 self.beam_to_world = BeamToWorld(
715 beamangle=beamangle, beamoffset=beamoffset, beamsize=beamsize
716 )
717 self.vlm_to_vi = VLMToVI(
718 imageshape=vlmimageshape, zoomlevel=zoomlevel, zoominfo=zoominfo
719 )
720 self.vlm_to_world = VLMToWorld(focaloffset=focaloffset)
721 self.sample_to_canvas = SampleToCanvas(downstream=downstream)
723 def to_dict(self):
724 return {
725 "units": self.units,
726 "unitdict": self.unitdict,
727 "sampleoffset": self.world_to_sample.sampleoffset,
728 "beamoffset": self.beam_to_world.beamoffset,
729 "beamsize": self.beam_to_world.beamsize,
730 "beamangle": self.beam_to_world.beamangle,
731 "focaloffset": self.vlm_to_world.focaloffset,
732 "vlmimageshape": self.vlm_to_vi.imageshape,
733 "zoomlevel": self.zoomlevel,
734 "zoominfo": self.zoominfo,
735 "downstream": self.downstream,
736 }
738 def from_dict(self, dic):
739 self.units = dic["units"]
740 self.unitdict = dic["unitdict"]
741 self.world_to_sample.sampleoffset = dic["sampleoffset"]
742 self.beam_to_world.beamoffset = dic["beamoffset"]
743 self.beam_to_world.beamsize = dic["beamsize"]
744 self.beam_to_world.beamangle = dic["beamangle"]
745 self.vlm_to_world.focaloffset = dic["focaloffset"]
746 self.vlm_to_vi.imageshape = dic["vlmimageshape"]
747 self.zoomlevel = dic["zoomlevel"]
748 self.zoominfo = dic["zoominfo"]
749 self.downstream = dic["downstream"]
751 @property
752 def align_info(self):
753 return {
754 "sampleoffset": self.world_to_sample.sampleoffset,
755 "beamoffset": self.beam_to_world.beamoffset,
756 "beamsize": self.beam_to_world.beamsize,
757 "focaloffset": self.vlm_to_world.focaloffset,
758 }
760 @property
761 def units(self):
762 return self.motors_to_world.units
764 @units.setter
765 def units(self, value):
766 self.motors_to_world.units = value
768 @property
769 def unitdict(self):
770 return self.motors_to_world.unitdict
772 @unitdict.setter
773 def unitdict(self, value):
774 self.motors_to_world.unitdict = value
776 @property
777 def downstream(self):
778 return self.sample_to_canvas.downstream
780 @downstream.setter
781 def downstream(self, value):
782 self.sample_to_canvas.downstream = value
784 @property
785 def zoomlevel(self):
786 return self.vlm_to_vi.zoomlevel
788 @zoomlevel.setter
789 def zoomlevel(self, value):
790 self.vlm_to_vi.zoomlevel = value
792 @property
793 def zoominfo(self):
794 return self.vlm_to_vi.zoominfo
796 @zoominfo.setter
797 def zoominfo(self, dic):
798 self.vlm_to_vi.zoominfo = dic
800 @property
801 def focalpoint(self):
802 """In the Canvas frame"""
803 fp = self.vlm_to_vi.focalpoint
804 return self._transform_vi_to_canvas(fp)[0]
806 @property
807 def vi_center(self):
808 """In the Canvas frame"""
809 corners = self.vlm_to_vi.vi_center
810 return self._transform_vi_to_canvas(corners)[0]
812 @property
813 def vi_origin(self):
814 """In the Canvas frame"""
815 corners = self.vlm_to_vi.vi_origin
816 return self._transform_vi_to_canvas(corners)[0]
818 @property
819 def vi_corners(self):
820 """In the Canvas frame
822 :returns array: (left, bottom), (right, top)
823 """
824 corners = self.vlm_to_vi.vi_corners
825 return self._transform_vi_to_canvas(corners)
827 @property
828 def vi_polyline(self):
829 """In the Canvas frame"""
830 corners = self.vlm_to_vi.vi_polyline
831 return self._transform_vi_to_canvas(corners)
833 @property
834 def beamposition(self):
835 """In the Canvas frame"""
836 bp = self.beam_to_world.beamoffset
837 return self._transform_world_to_canvas(bp)[0]
839 @beamposition.setter
840 def beamposition(self, bp):
841 """In the Canvas frame"""
842 bp = self.sample_to_canvas.inverse(bp)
843 bp = self.world_to_sample.inverse(bp)
844 bp = fromhomogeneous(bp)[0]
845 self.beam_to_world.beamoffset = bp
847 @property
848 def sampleposition(self):
849 """In the Canvas frame"""
850 coord = [0, 0, 0]
851 coord = self.sample_to_canvas.forward(coord)
852 return fromhomogeneous(coord)[0]
854 @property
855 def vlm_image_info(self):
856 # In VI frame
857 corners1 = self.vlm_to_vi.vi_corners
858 width1 = corners1[1] - corners1[0]
859 # In Canvas frame
860 corners2 = self.vi_corners
861 width2 = corners2[1] - corners2[0]
862 # REMARK: assume image is still a rectangle
863 center = numpy.mean(corners2, axis=0)
864 pixelsize = width2 / width1
866 return {"center": center, "pixelsize": pixelsize}
868 @property
869 def beam_info(self):
870 bp = self.beamposition
871 # TODO: this is not correct
872 # (need intersaction between lines and sample surface)
873 corners = self.beam_to_world.beam_corners
874 corners = self._transform_beam_to_canvas(corners)
875 mi = corners.min(axis=0)
876 ma = corners.max(axis=0)
877 center = corners.mean(axis=0)
878 return {"position": bp, "center": center, "size": ma - mi}
880 @property
881 def current_motor_positions_dict(self):
882 return self.motors_to_world.current_motor_positions_dict
884 def move_motors(self, values, wait=True, timeout=None):
885 """Move the motors directly
887 :param array or dict values:
888 :param bool wait: wait until motion has finished
889 :param num or None timeout: `None` waits indefinitely
890 """
891 self.motors_to_world.domain.move(values, wait=wait, timeout=timeout)
893 def wait_motion_done(self, timeout=None):
894 """Wait until the motors do not move anymore"""
895 self.motors_to_world.domain.wait_motion_done(timeout)
897 def move(self, coord, wait=True, timeout=None):
898 """Move the sample so the beam hits the new
899 position `coord` in the Canvas frame
901 :param array coord: in Canvas frame
902 :param bool wait: wait until motion has finished
903 :param num or None timeout: `None` waits indefinitely
904 """
905 coord = self._transform_motion(coord)
906 self.motors_to_world.domain.move(coord[0], wait=wait, timeout=timeout)
908 def center_fine(self, wait=True, timeout=None):
909 """Stay on the same position but center the fine axes
911 :param bool wait: wait until motion has finished
912 :param num or None timeout: `None` waits indefinitely
913 """
914 motpos = self.motors_to_world.centered_position
915 self.motors_to_world.domain.move(motpos, wait=wait, timeout=timeout)
917 def canvas_to_motor(self, coord):
918 """Get motor positions for a group of points in the Canvas
920 :param array coord: (npoints, 2) in Canvas frame
921 :returns dict: fixed and variable motor positions
922 """
923 coord = self._transform_motion(coord)
924 fixed = {}
925 variable = {}
926 labels = self.motors_to_world.motor_labels
927 unitdict = self.motors_to_world.unitdict
928 for label, mot, motpos in zip(
929 labels, self.motors_to_world.domain.motors, coord.T
930 ):
931 if all(motpos == motpos[0]):
932 destination = motpos[0]
933 dest = fixed
934 else:
935 destination = motpos.tolist()
936 dest = variable
937 dest[label] = {
938 "motor": mot,
939 "destination": destination,
940 "unit": unitdict[mot.id()],
941 "unit_exponent": get_exponent(unitdict[mot.id()]),
942 }
943 return {"fixed": fixed, "variable": variable}
945 @property
946 def reach_polyline(self):
947 """Corners of the reachable Canvas area (see `move`)"""
948 return self._get_polyline()
950 @property
951 def fine_polyline(self):
952 """Corners of the reachable fine motor area (see `move`)"""
953 return self._get_polyline(["x_fine", "y_fine", "z_fine"])
955 @property
956 def coarse_polyline(self):
957 """Corners of the reachable coarse motor area (see `move`)"""
958 return self._get_polyline(["x", "y", "z"])
960 def _get_polyline(self, motors=None):
961 """Corners of reachable area around the beam position
963 :param 3-array motors:
964 :returns array: (npoints, 3)
965 """
966 if motors:
967 limits = self.motors_to_world.current_world_position
968 limits = numpy.tile(limits, (2, 1))
969 for label in motors:
970 try:
971 motname = self.motors_to_world.motor_name(label)
972 except ValueError:
973 continue
974 i = self.motors_to_world.motor_world_index(motname)
975 limits[:, i] = self.motors_to_world.motor_world_limits(motname)
976 else:
977 limits = self.motors_to_world.world_limits
978 limits = polyline(self._transform_world_to_canvas(limits))
979 return self._relative_to_beam(limits)
981 def _transform_vi_to_canvas(self, coord):
982 coord = self.vlm_to_vi.inverse(coord)
983 coord = self.vlm_to_world.forward(coord)
984 return self._transform_world_to_canvas(coord)
986 def _transform_world_to_canvas(self, coord):
987 coord = self.world_to_sample.forward(coord)
988 coord = self.sample_to_canvas.forward(coord)
989 return fromhomogeneous(coord)
991 def _transform_beam_to_canvas(self, coord):
992 coord = self.beam_to_world.forward(coord)
993 coord = self.world_to_sample.forward(coord)
994 coord = self.sample_to_canvas.forward(coord)
995 return fromhomogeneous(coord)
997 def _transform_motion(self, coord):
998 """
999 :param array coord: Canvas coordinates
1000 :returns array: motor coordinates to have the beam on `coord`
1001 """
1002 coord = self._relative_to_beam(coord)
1003 coord = self.sample_to_canvas.inverse(coord)
1004 coord = self.world_to_sample.inverse(coord)
1005 return self.motors_to_world.inverse(coord)
1007 def _relative_to_beam(self, coord):
1008 """
1009 :param array coord: Canvas coordinates
1010 :returns array: relative motion to put beam on coord
1011 """
1012 return numpy.atleast_2d(self.beamposition) - numpy.atleast_2d(coord)