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

1"""These are the coordinate transformations to relate motors, 

2VLM image and display canvas for the ImageViewer component source. 

3 

4All coordinate transformations are done in homogeneous coordinates 

5""" 

6 

7# TODO: this module should go somewhere else 

8 

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 

17 

18 

19logger = logging.getLogger(__name__) 

20 

21 

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]) 

30 

31 

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 

40 

41 

42def polyline(coord): 

43 """Corners for drawing a boundary based on low and high point 

44 

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 

60 

61 

62class MotorsToWorld(maps.Map): 

63 """Maps motors (R^3/R^6) to World (R^4) frame coordinates: 

64 

65 xworld = x + x_fine 

66 yworld = y + y_fine 

67 zworld = z + zfine 

68 

69 Each motor has its own units and the World frame has its own units as well. 

70 """ 

71 

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) 

111 

112 def _init_transform(self, mot, label, units, unitdict, motfine=None): 

113 """Instantiate a transformation object for coarse+fine motor translation 

114 

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 

135 

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 ) 

148 

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 ) 

159 

160 @property 

161 def units(self): 

162 return self._units 

163 

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 

170 

171 def _forward(self, coord): 

172 """Motor positions to World frame coordinates 

173 

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]) 

184 

185 def _inverse(self, icoord): 

186 """World frame coordinates to motor position 

187 

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]) 

196 

197 def motor_world_limits(self, motname): 

198 """Motor limits in the World frame (low, high) 

199 

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) 

213 

214 def motor_world_index(self, motname): 

215 """Motor index in the World frame 

216 

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 

229 

230 @property 

231 def world_limits(self): 

232 """World limits in the World frame 

233 

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) 

239 

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] 

248 

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) 

256 

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]] 

267 

268 @property 

269 def motor_names(self): 

270 return [mot.id() for mot in self.domain.motors] 

271 

272 def motor_name(self, label): 

273 idx = self.motor_labels.index(label) 

274 return self.motor_names[idx] 

275 

276 def motor_label(self, name): 

277 idx = self.motor_names.index(name) 

278 return self.motor_labels[idx] 

279 

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 } 

286 

287 

288class WorldToSample(maps.Isomorphism): 

289 """Maps World (R^4) to Sample (R^4) frame coordinates""" 

290 

291 def __init__(self, sampleoffset=None, motormap=None): 

292 """When sample shift is zero, the sample position is the current motor position 

293 

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) 

302 

303 @property 

304 def sampleoffset(self): 

305 return self._sampleoffset 

306 

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 

315 

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 

323 

324 @property 

325 def _forward_matrix(self): 

326 position = self.motormap.current_world_position 

327 offset = self.sampleoffset - position 

328 

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 ] 

335 

336 

337class VLMToWorld(maps.Isomorphism): 

338 """Maps VLM (R^4) to World (R^4) frame coordinates""" 

339 

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) 

348 

349 @property 

350 def focaloffset(self): 

351 return self._focaloffset 

352 

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 

361 

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 

369 

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 ] 

379 

380 

381class VLMToVI(maps.LinearMap, maps.Surjection): 

382 """Maps R^4 VLM (video light microscope) to R^3 VI frame (video image) coordinates""" 

383 

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) 

400 

401 @property 

402 def imageshape(self): 

403 return self._imageshape 

404 

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 

411 

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 

419 

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 

431 

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 

440 

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)] 

449 

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 

457 

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 

465 

466 @zoomlevel.setter 

467 def zoomlevel(self, value): 

468 self._zoomlevel = value 

469 

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 

477 

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 ] 

487 

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 ) 

497 

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 

508 

509 @property 

510 def vi_corners(self): 

511 """Bounds in the VI frame which the image needs to be displayed. 

512 

513 :returns array: (left, bottom), (right, top) 

514 """ 

515 return numpy.array([[0, 0], self.imageshape.tolist()]) 

516 

517 @property 

518 def vi_polyline(self): 

519 """ 

520 :returns array: (npoints, ndomain) 

521 """ 

522 return polyline(self.vi_corners) 

523 

524 @property 

525 def vi_center(self): 

526 """ 

527 :returns array: (ndomain,) 

528 """ 

529 return self.imageshape / 2.0 

530 

531 @property 

532 def vi_origin(self): 

533 """ 

534 :returns array: (ndomain,) 

535 """ 

536 return numpy.array([0, 0]) 

537 

538 

539class SampleToCanvas(maps.LinearMap, maps.Surjection): 

540 """Maps Sample (R^4) to Canvas (R^3) frame coordinates""" 

541 

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) 

552 

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 

560 

561 @property 

562 def _sx(self): 

563 if self.downstream: 

564 return -1 

565 else: 

566 return 1 

567 

568 @property 

569 def _forward_matrix(self): 

570 return [[self._sx, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1]] 

571 

572 @property 

573 def _inverse_matrix(self): 

574 return numpy.asarray([[self._sx, 0, 0], [0, 1, 0], [0, 0, 1]]) 

575 

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 

586 

587 

588class BeamToWorld(maps.Isomorphism): 

589 """Maps Beam (R^4) to World (R^4) frame coordinates""" 

590 

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) 

603 

604 @property 

605 def beamoffset(self): 

606 return self._beamoffset 

607 

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 

616 

617 @property 

618 def beamsize(self): 

619 return self._beamsize 

620 

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 

629 

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)))) 

638 

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 

646 

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 ] 

658 

659 

660class ImageViewerCanvas: 

661 """This class groups all ImageViewer coordinate transformations 

662 and provides an API for dislay, motion and scanning. 

663 

664 Coordinates in Canvas frame 

665 - markers (beam, VLM center, VLM focus) 

666 - borders (VLM, fine, coarse, reach) 

667 

668 Actions 

669 - move the sample (point beam on in Canvas frame) 

670 - move the motors 

671 - define the beam 

672 - center fine 

673 

674 Conversions 

675 - convert a group of points to motor positions 

676 (e.g. corners of a map) 

677 """ 

678 

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) 

722 

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 } 

737 

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"] 

750 

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 } 

759 

760 @property 

761 def units(self): 

762 return self.motors_to_world.units 

763 

764 @units.setter 

765 def units(self, value): 

766 self.motors_to_world.units = value 

767 

768 @property 

769 def unitdict(self): 

770 return self.motors_to_world.unitdict 

771 

772 @unitdict.setter 

773 def unitdict(self, value): 

774 self.motors_to_world.unitdict = value 

775 

776 @property 

777 def downstream(self): 

778 return self.sample_to_canvas.downstream 

779 

780 @downstream.setter 

781 def downstream(self, value): 

782 self.sample_to_canvas.downstream = value 

783 

784 @property 

785 def zoomlevel(self): 

786 return self.vlm_to_vi.zoomlevel 

787 

788 @zoomlevel.setter 

789 def zoomlevel(self, value): 

790 self.vlm_to_vi.zoomlevel = value 

791 

792 @property 

793 def zoominfo(self): 

794 return self.vlm_to_vi.zoominfo 

795 

796 @zoominfo.setter 

797 def zoominfo(self, dic): 

798 self.vlm_to_vi.zoominfo = dic 

799 

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] 

805 

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] 

811 

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] 

817 

818 @property 

819 def vi_corners(self): 

820 """In the Canvas frame 

821 

822 :returns array: (left, bottom), (right, top) 

823 """ 

824 corners = self.vlm_to_vi.vi_corners 

825 return self._transform_vi_to_canvas(corners) 

826 

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) 

832 

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] 

838 

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 

846 

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] 

853 

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 

865 

866 return {"center": center, "pixelsize": pixelsize} 

867 

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} 

879 

880 @property 

881 def current_motor_positions_dict(self): 

882 return self.motors_to_world.current_motor_positions_dict 

883 

884 def move_motors(self, values, wait=True, timeout=None): 

885 """Move the motors directly 

886 

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) 

892 

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) 

896 

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 

900 

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) 

907 

908 def center_fine(self, wait=True, timeout=None): 

909 """Stay on the same position but center the fine axes 

910 

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) 

916 

917 def canvas_to_motor(self, coord): 

918 """Get motor positions for a group of points in the Canvas 

919 

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} 

944 

945 @property 

946 def reach_polyline(self): 

947 """Corners of the reachable Canvas area (see `move`)""" 

948 return self._get_polyline() 

949 

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"]) 

954 

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"]) 

959 

960 def _get_polyline(self, motors=None): 

961 """Corners of reachable area around the beam position 

962 

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) 

980 

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) 

985 

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) 

990 

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) 

996 

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) 

1006 

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)