Coverage for /opt/conda/envs/apienv/lib/python3.10/site-packages/daiquiri/core/components/imageviewer/__init__.py: 39%

649 statements  

« prev     ^ index     » next       coverage.py v7.6.5, created at 2024-11-15 02:12 +0000

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3import json 

4import time 

5import os 

6import pprint 

7from datetime import datetime 

8from typing import Any, Dict, List 

9import gevent 

10from marshmallow import fields 

11from flask import g 

12import numpy 

13from PIL import Image 

14 

15from daiquiri.core import marshal, require_control 

16from daiquiri.core.logging import log 

17from daiquiri.core.components import ( 

18 Component, 

19 ComponentResource, 

20 actor, 

21 ComponentActorKilled, 

22) 

23from daiquiri.core.schema import ErrorSchema, MessageSchema 

24from daiquiri.core.schema.components.imageviewer import ( 

25 ImageSource, 

26 SourceSettings, 

27 MapAdditionalSchema, 

28 MapSettings, 

29 MoveToReferenceSchema, 

30 SelectMatrixSchema, 

31 ExportReferenceSchema, 

32) 

33from daiquiri.core.schema.metadata import paginated 

34from daiquiri.core.components.dcutilsmixin import DCUtilsMixin 

35from daiquiri.core.components.imageviewer.source import Source 

36from daiquiri.core.components.imageviewer.annotate import AnnotateImage 

37from daiquiri.core.components.imageviewer.transform import ( 

38 calculate_transform_matrix, 

39 export_reference_to_sampleimage, 

40) 

41 

42import logging 

43 

44logger = logging.getLogger(__name__) 

45pp = pprint.PrettyPrinter() 

46 

47# For large mosaics need to disable pixel check 

48# https://stackoverflow.com/questions/51152059/pillow-in-python-wont-let-me-open-image-exceeds-limit 

49Image.MAX_IMAGE_PIXELS = None 

50 

51 

52class SourcesResource(ComponentResource): 

53 @marshal(out=[[200, paginated(ImageSource), "A list of image/video sources"]]) 

54 def get(self, **kwargs): 

55 """Get a list of image sources defined in the current 2d viewer""" 

56 return self._parent.get_sources() 

57 

58 

59class SourcesSettingsResource(ComponentResource): 

60 @marshal(out=[[200, SourceSettings(), "Source Settings"]]) 

61 def get(self): 

62 return { 

63 "has_fine": self._parent.origin_defining_source.has_fine, 

64 "fine_fixed": self._parent.origin_defining_source.fine_fixed, 

65 "coarse_fixed": self._parent.origin_defining_source.coarse_fixed, 

66 "config": self._parent.origin_defining_source.config, 

67 } 

68 

69 @marshal( 

70 inp=SourceSettings, 

71 out=[ 

72 [200, SourceSettings(), "Updated source settings"], 

73 [400, ErrorSchema(), "Could not update source settings"], 

74 ], 

75 ) 

76 def patch(self, fine_fixed: bool = None, coarse_fixed: bool = None, **kwargs): 

77 if fine_fixed is not None: 

78 self._parent.origin_defining_source.fine_fixed = fine_fixed 

79 if coarse_fixed is not None: 

80 self._parent.origin_defining_source.coarse_fixed = coarse_fixed 

81 

82 return { 

83 "has_fine": self._parent.origin_defining_source.has_fine, 

84 "fine_fixed": self._parent.origin_defining_source.fine_fixed, 

85 "coarse_fixed": self._parent.origin_defining_source.coarse_fixed, 

86 "config": self._parent.origin_defining_source.config, 

87 } 

88 

89 

90class SourceImageResource(ComponentResource): 

91 @marshal( 

92 inp={ 

93 "sampleid": fields.Int( 

94 metadata={"description": "The sampleid"}, required=True 

95 ), 

96 "subsampleid": fields.Int( 

97 metadata={"description": "Optionally a subsampleid"} 

98 ), 

99 }, 

100 out=[ 

101 [200, MessageSchema(), "Source image created"], 

102 [400, ErrorSchema(), "Could not create source image"], 

103 ], 

104 ) 

105 def post(self, **kwargs): 

106 """Capture an image from a source""" 

107 try: 

108 ret = self._parent.save_sample_image(**kwargs) 

109 if ret: 

110 return {"message": "Source image created"}, 200 

111 else: 

112 return {"error": "Could not create source image"}, 400 

113 

114 except Exception as e: 

115 logger.exception("Could not save image") 

116 log.get("user").exception("Could not save source image", type="hardware") 

117 return {"error": f"Could not create source image: {str(e)}"}, 400 

118 

119 

120class GenerateMapsResource(ComponentResource): 

121 @marshal( 

122 inp={ 

123 "datacollectionid": fields.Int( 

124 metadata={"description": "Optionally a datacollectionid"}, 

125 required=False, 

126 ), 

127 }, 

128 out=[ 

129 [200, MessageSchema(), "Maps generated"], 

130 [400, ErrorSchema(), "Could not create maps"], 

131 ], 

132 ) 

133 def post(self, subsampleid, **kwargs): 

134 """Generate a new map for a subsampleid""" 

135 maps = self._parent.generate_maps( 

136 subsampleid, datacollectionid=kwargs.get("datacollectionid", None) 

137 ) 

138 if maps: 

139 return {"message": "Maps created"}, 200 

140 else: 

141 return {"error": "Could not create maps"}, 400 

142 

143 

144class CreateMapAdditionalResource(ComponentResource): 

145 @marshal( 

146 inp=MapAdditionalSchema, 

147 out=[ 

148 [200, MessageSchema(), "Map generated"], 

149 [400, ErrorSchema(), "Could not create map"], 

150 ], 

151 ) 

152 def post(self, **kwargs): 

153 """Generate a new map from additional scalars""" 

154 amap = self._parent.generate_additional_map(**kwargs) 

155 if amap: 

156 return {"message": "Map created"}, 200 

157 else: 

158 return {"error": "Could not create additional map"}, 400 

159 

160 

161class MoveResource(ComponentResource): 

162 @require_control 

163 @marshal( 

164 inp={ 

165 "x": fields.Float(required=True, metadata={"title": "X Position"}), 

166 "y": fields.Float(required=True, metadata={"title": "Y Position"}), 

167 }, 

168 out=[ 

169 [200, MessageSchema(), "Move to position ok"], 

170 [400, ErrorSchema(), "Could not move to position"], 

171 ], 

172 ) 

173 def post(self, **kwargs): 

174 """Move the cursor position to the current origin marking position""" 

175 try: 

176 self._parent.move(kwargs) 

177 return {"message": "ok"}, 200 

178 except Exception as e: 

179 message = f"Couldn't move to position: {str(e)}" 

180 log.get("user").error(message, type="hardware") 

181 return {"error": message}, 400 

182 

183 

184class MoveToResource(ComponentResource): 

185 @require_control 

186 @marshal( 

187 out=[ 

188 [200, MessageSchema(), "Moved to subsample"], 

189 [400, ErrorSchema(), "Could not move to subsample"], 

190 ], 

191 ) 

192 def post(self, subsampleid, **kwargs): 

193 """Move the specified subsample to the current origin marking position""" 

194 try: 

195 self._parent.move_to(subsampleid) 

196 return {"message": "ok"}, 200 

197 except Exception as e: 

198 message = f"Couldn't move to subsample `{subsampleid}`: {str(e)}" 

199 log.get("user").error(message, type="hardware") 

200 return {"error": message}, 400 

201 

202 

203class MoveToReferenceResource(ComponentResource): 

204 @require_control 

205 @marshal( 

206 inp={ 

207 "x": fields.Float(required=True, metadata={"title": "X Position"}), 

208 "y": fields.Float(required=True, metadata={"title": "Y Position"}), 

209 "execute": fields.Bool(), 

210 }, 

211 out=[ 

212 [200, MoveToReferenceSchema(), "Moved to position"], 

213 [400, ErrorSchema(), "Could not move to position"], 

214 ], 

215 ) 

216 def post(self, **kwargs): 

217 """Move the cursor position to a position from a reference image""" 

218 try: 

219 execute = kwargs.pop("execute", True) 

220 positions = self._parent.move_to_reference(kwargs, execute=execute) 

221 print("positions", positions) 

222 return {"moveid": f"move{time.time()}", "positions": positions}, 200 

223 except Exception as e: 

224 logger.exception("Couldnt move to reference position") 

225 return {"error": str(e)}, 400 

226 

227 

228class SelectReferenceMatrixResource(ComponentResource): 

229 @require_control 

230 @marshal( 

231 inp={ 

232 "sampleactionid": fields.Int( 

233 required=True, metadata={"title": "Sample Action"} 

234 ), 

235 }, 

236 out=[ 

237 [200, SelectMatrixSchema(), "Calculated transformation matrix"], 

238 [400, ErrorSchema(), "Could not calculate transformation matrix"], 

239 ], 

240 ) 

241 def post(self, sampleactionid, **kwargs): 

242 """Select a sampleaction and calculate transformation matrix""" 

243 try: 

244 self._parent.select_reference_matrix(sampleactionid) 

245 return {"matrixid": f"matrix{time.time()}"}, 200 

246 except Exception as e: 

247 logger.exception("Couldnt select reference matrix") 

248 return {"error": str(e)}, 400 

249 

250 

251class ExportReferenceResource(ComponentResource): 

252 @require_control 

253 @marshal( 

254 inp=ExportReferenceSchema, 

255 out=[ 

256 [200, MessageSchema(), "Exported reference to sample image"], 

257 [400, ErrorSchema(), "Could not export reference to sample image"], 

258 ], 

259 ) 

260 def post(self, sampleactionid, crop=None): 

261 """Export a reference image to a sample image 

262 

263 Transforms the image into the current 2dview coordinate space 

264 using the selected transformation matrix 

265 """ 

266 try: 

267 self._parent.export_reference_to_sampleimage(sampleactionid, crop=crop) 

268 return {"message": "Exported reference to sample image"}, 200 

269 except Exception as e: 

270 logger.exception("Could not export reference to sample image") 

271 return {"error": str(e)}, 400 

272 

273 

274class MosaicResource(ComponentResource): 

275 @require_control 

276 @actor("mosaic", enqueue=False, preprocess=True) 

277 def post(self, **kwargs): 

278 """Create a tiled mosaic actor""" 

279 pass 

280 

281 def preprocess(self, **kwargs): 

282 kwargs["absol"] = self._parent.get_absolute_fp( 

283 {"x": kwargs["x1"], "y": kwargs["y1"]}, 

284 {"x": kwargs["x2"], "y": kwargs["y2"]}, 

285 ) 

286 

287 sample = self._metadata.get_samples(sampleid=kwargs["sampleid"]) 

288 if not sample: 

289 raise AttributeError(f"No such sample {kwargs['sampleid']}") 

290 

291 sessionid = g.blsession.get("sessionid") 

292 

293 def save_image(x, y): 

294 return self._parent.save_image( 

295 sessionid=sessionid, 

296 sampleid=kwargs["sampleid"], 

297 file_prefix=f"mosaic_{x}_{y}_", 

298 ) 

299 

300 kwargs["sessionid"] = sessionid 

301 kwargs["save"] = save_image 

302 kwargs["camera"] = self._parent.origin_defining_source.device 

303 

304 return kwargs 

305 

306 

307class UploadImage(ComponentResource): 

308 @marshal( 

309 inp={ 

310 "image": fields.Str( 

311 required=True, metadata={"description": "Base64 encoded image"} 

312 ), 

313 "sampleid": fields.Int( 

314 required=True, metadata={"description": "Sample this image belongs to"} 

315 ), 

316 }, 

317 out=[ 

318 [200, MessageSchema(), "Image uploaded"], 

319 [400, ErrorSchema(), "Could not upload image"], 

320 ], 

321 ) 

322 def post(self, **kwargs): 

323 """Upload an image and send it to an actor""" 

324 success = self._parent.upload_image(**kwargs) 

325 if success: 

326 return {"message": "Image uploaded"}, 200 

327 else: 

328 return {"error": "Could not upload image"}, 400 

329 

330 

331class AutoFocusImageResource(ComponentResource): 

332 @require_control 

333 @actor("autofocus", enqueue=False, preprocess=True) 

334 def post(self, **kwargs): 

335 """Autofocus the sample image""" 

336 pass 

337 

338 def preprocess(self, **kwargs): 

339 kwargs["camera"] = self._parent.origin_defining_source.device 

340 kwargs["z_increment"] = self._parent.origin_defining_source._get_from_config( 

341 "motor_z_autofocus_increment" 

342 ) 

343 kwargs["z_iterations"] = self._parent.origin_defining_source._get_from_config( 

344 "motor_z_autofocus_iterations" 

345 ) 

346 kwargs["z_motor"] = self._parent.origin_defining_source._get_hwobj_from_config( 

347 "motor_z" 

348 ) 

349 

350 return kwargs 

351 

352 

353class ExportSubSamplesResource(ComponentResource): 

354 @marshal( 

355 inp={ 

356 "subsampleids": fields.List( 

357 fields.Int(), 

358 required=True, 

359 metadata={"description": "A list of subsamples to export"}, 

360 ) 

361 }, 

362 out=[ 

363 [200, MessageSchema(), "Subsamples exported"], 

364 [400, ErrorSchema(), "Could not export subsamples"], 

365 ], 

366 ) 

367 def post(self, **kwargs): 

368 """Export the selected subsamples to json""" 

369 try: 

370 dirname = self._parent.export_subsamples(kwargs["subsampleids"]) 

371 message = f"Sub samples exported to '{dirname}'" 

372 log.get("user").info(message, type="actor") 

373 return {"message": message}, 200 

374 except Exception as e: 

375 return {"error": f"Could not export subsamples: {str(e)}"}, 400 

376 

377 

378class MapSettingsResource(ComponentResource): 

379 @marshal(out=[[200, MapSettings(), "Map Settings"]]) 

380 def get(self): 

381 return { 

382 "during_scan": self._parent._generate_during_scan, 

383 "scalar_maps": self._parent._scalar_maps, 

384 } 

385 

386 @marshal( 

387 inp=MapSettings, 

388 out=[ 

389 [200, MapSettings(), "Updated map settings"], 

390 [400, ErrorSchema(), "Could not update map settings"], 

391 ], 

392 ) 

393 def patch(self, **kwargs): 

394 if kwargs.get("during_scan") is not None: 

395 self._parent._generate_during_scan = kwargs["during_scan"] 

396 

397 if kwargs.get("scalar_maps") is not None: 

398 self._parent._scalar_maps = kwargs["scalar_maps"] 

399 

400 return { 

401 "during_scan": self._parent._generate_during_scan, 

402 "scalar_maps": self._parent._scalar_maps, 

403 }, 200 

404 

405 

406class ReferenceImageResource(ComponentResource): 

407 @require_control 

408 @actor("reference", enqueue=False, preprocess=True) 

409 def post(self, **kwargs): 

410 """Import a reference image of the sample""" 

411 pass 

412 

413 def preprocess(self, **kwargs): 

414 if "sampleid" not in kwargs: 

415 raise AttributeError("No sample provided") 

416 

417 sample = self._metadata.get_samples(sampleid=kwargs["sampleid"]) 

418 if not sample: 

419 raise AttributeError(f"No such sample {kwargs['sampleid']}") 

420 

421 kwargs["sessionid"] = g.blsession.get("sessionid") 

422 return kwargs 

423 

424 

425class Imageviewer(Component, DCUtilsMixin): 

426 _actors = [ 

427 "createmap", 

428 "mosaic", 

429 "move", 

430 "upload_canvas", 

431 "autofocus", 

432 "export", 

433 "reference", 

434 ] 

435 _config_export = ["options", "scantypes", "upload_canvas"] 

436 

437 def setup(self): 

438 self._scan_actors = [] 

439 self._map_actors = [] 

440 self._in_generate = False 

441 self._generate_during_scan = self._config.get("generate_maps_during_scan", True) 

442 self._scalar_maps = self._config.get("automatic_scalar_maps", []) 

443 

444 self.register_route(SourcesResource, "/sources") 

445 self.register_route(SourcesSettingsResource, "/sources/origin") 

446 self.register_route(SourceImageResource, "/sources/image") 

447 self.register_route(MapSettingsResource, "/maps") 

448 self.register_route(GenerateMapsResource, "/maps/generate/<int:subsampleid>") 

449 self.register_route(CreateMapAdditionalResource, "/maps/additional") 

450 self.register_route(MoveResource, "/move") 

451 self.register_route(MoveToReferenceResource, "/move/reference") 

452 self.register_route(SelectReferenceMatrixResource, "/move/reference/matrix") 

453 self.register_route(MoveToResource, "/move/<int:subsampleid>") 

454 self.register_route(UploadImage, "/image/<int:sampleid>") 

455 self.register_route(ExportSubSamplesResource, "/export") 

456 self.register_actor_route(MosaicResource, "/mosaic") 

457 self.register_actor_route(ReferenceImageResource, "/reference") 

458 self.register_actor_route(ExportReferenceResource, "/reference/export") 

459 self.register_actor_route(AutoFocusImageResource, "/sources/autofocus") 

460 self._generate_scan_actors() 

461 self._sources: List[Source] = [] 

462 for i, src in enumerate(self._config["sources"]): 

463 self._sources.append( 

464 Source( 

465 src, 

466 i + 1, 

467 self._hardware, 

468 self.emit, 

469 config_file=self._config.resource, 

470 ) 

471 ) 

472 self._create_maps = self._config.get("createmaps", {}) 

473 self._check_running_actors = True 

474 self._check_actor_thread = gevent.spawn(self.check_running_actors) 

475 

476 self._reference_matrix = None 

477 self._reference_inverse_matrix = None 

478 

479 def reload(self): 

480 self._generate_scan_actors() 

481 for i, src in enumerate(self._config["sources"]): 

482 if i < len(self._sources): 

483 self._sources[i].update_config(src) 

484 else: 

485 self._sources.append( 

486 Source( 

487 src, 

488 i + 1, 

489 self._hardware, 

490 self.emit, 

491 config_file=self._config.resource, 

492 ) 

493 ) 

494 

495 def _generate_scan_actors(self): 

496 """Dynamically generate scan actor resources""" 

497 

498 def post(self, **kwargs): 

499 pass 

500 

501 def preprocess(self, **kwargs): 

502 subsample = self._parent._metadata.get_subsamples(kwargs["subsampleid"]) 

503 if not subsample: 

504 raise AttributeError(f"No such subsample {kwargs['subsampleid']}") 

505 kwargs["sampleid"] = subsample["sampleid"] 

506 kwargs["sample"] = subsample["sample"] 

507 sample = self._parent._metadata.get_samples(kwargs["sampleid"]) 

508 kwargs["extrametadata"] = { 

509 **(sample["extrametadata"] if sample["extrametadata"] else {}), 

510 "subsample": subsample["extrametadata"], 

511 } 

512 kwargs["sessionid"] = g.blsession.get("sessionid") 

513 # Check whether position can be reached before marking the subsample queued 

514 # as this can raise 

515 absol = self._parent.get_absolute(kwargs["subsampleid"]) 

516 ( 

517 kwargs["containerqueuesampleid"], 

518 kwargs["datacollectionplanid"], 

519 ) = self._parent._metadata.queue_subsample( 

520 kwargs["subsampleid"], scanparameters=kwargs 

521 ) 

522 kwargs["absol"] = absol 

523 kwargs["before_scan_starts"] = self._parent.before_scan_starts 

524 kwargs["update_datacollection"] = self._parent.update_datacollection 

525 kwargs["next_datacollection"] = self._parent.next_datacollection 

526 kwargs["generate_maps"] = self._parent.generate_maps 

527 kwargs["open_attachment"] = self._parent._open_dc_attachment 

528 kwargs["add_scanqualityindicators"] = self._parent.add_scanqualityindicators 

529 kwargs["scans"] = self._parent.get_component("scans") 

530 kwargs["beamsize"] = self._parent.beamsize 

531 

532 def get_rois(): 

533 return { 

534 "rois": self._metadata.get_xrf_map_rois( 

535 sampleid=kwargs["sampleid"], 

536 no_context=True, 

537 )["rows"], 

538 "conversion": self._parent.get_component("scans")._config["mca"][ 

539 "conversion" 

540 ], 

541 } 

542 

543 kwargs["get_rois"] = get_rois 

544 

545 kwargs["enqueue"] = kwargs.get("enqueue", True) 

546 return kwargs 

547 

548 for key, scans in self._config.get("scantypes", {}).items(): 

549 for scanname in scans: 

550 if scanname in self._actors: 

551 continue 

552 

553 self._actors.append(scanname) 

554 self._scan_actors.append(scanname) 

555 act_res = type( 

556 scanname, 

557 (ComponentResource,), 

558 { 

559 "post": require_control(actor(scanname, preprocess=True)(post)), 

560 "preprocess": preprocess, 

561 }, 

562 ) 

563 self.register_actor_route(act_res, f"/scan/{scanname}") 

564 

565 def before_scan_starts(self, actor, save_image=True): 

566 """Saving directory is created""" 

567 if actor.get("datacollectionid"): 

568 self._save_dc_params(actor) 

569 if save_image: 

570 self._save_dc_image(actor) 

571 

572 def _save_dc_image(self, actor): 

573 """Save an image for a datacollection 

574 

575 Includes origin and scalebar, and subsample location. 

576 Should ideally be called once the subsample has been moved to the 

577 origin location 

578 """ 

579 try: 

580 details = self.save_image( 

581 sessionid=actor["sessionid"], 

582 sampleid=actor["sampleid"], 

583 subsampleid=actor["subsampleid"], 

584 savemeta=False, 

585 annotate=True, 

586 file_prefix="snapshot1_", 

587 ) 

588 

589 extra = {} 

590 if details.get("subsample"): 

591 subsample = details["subsample"] 

592 extra["pixelspermicronx"] = details["scale"]["x"] * 1e-9 / 1e-6 

593 extra["pixelspermicrony"] = details["scale"]["y"] * 1e-9 / 1e-6 

594 extra["snapshot_offsetxpixel"] = subsample["x"] 

595 extra["snapshot_offsetypixel"] = subsample["y"] 

596 

597 self.update_datacollection( 

598 actor, xtalsnapshotfullpath1=details["path"], **extra 

599 ) 

600 except Exception: 

601 logger.exception("Could not save image") 

602 log.get("user").exception( 

603 "Could not save data collection image", type="actor" 

604 ) 

605 

606 def save_sample_image(self, **kwargs): 

607 """Saves a sample image 

608 

609 Sets up file saving, and saves an image from the source image 

610 

611 Kwargs: 

612 sampleid (int): Sample id 

613 Returns: 

614 path (str): Path to the new image 

615 """ 

616 self._saving.set_filename( 

617 set_metadata=False, 

618 extra_saving_args=self._config.get( 

619 "sample_image_saving", {"data_filename": "{sampleid.name}_image{time}"} 

620 ), 

621 **{ 

622 "sampleid": kwargs["sampleid"], 

623 "sessionid": g.blsession.get("sessionid"), 

624 "time": int(time.time()), 

625 }, 

626 ) 

627 self._saving.create_root_path() 

628 return self.save_image( 

629 sampleid=kwargs.get("sampleid", None), 

630 subsampleid=kwargs.get("subsampleid", None), 

631 sessionid=g.blsession.get("sessionid"), 

632 file_prefix="sampleimage_", 

633 ) 

634 

635 def upload_image(self, **kwargs): 

636 """Send an image to an actor 

637 

638 Kwargs: 

639 sampleid (int): The associated sampleid 

640 image (str): The base64 encoded image 

641 """ 

642 sample = self._metadata.get_samples(sampleid=kwargs["sampleid"]) 

643 self.actor( 

644 "upload_canvas", 

645 error=self._upload_failed, 

646 spawn=True, 

647 actargs={"image": kwargs["image"], "sample": sample}, 

648 ) 

649 

650 return True 

651 

652 def _upload_failed(self, actid, exception, actor): 

653 logger.error( 

654 f"Could not upload image for {actor['sampleid']} exception was {exception}" 

655 ) 

656 log.get("user").exception( 

657 f"Could not upload image for {actor['sampleid']} exception was {exception}", 

658 type="queue", 

659 ) 

660 

661 def actor_started(self, actid, actor): 

662 """Callback when an actor starts 

663 

664 For scan actors this will generate a datacollection 

665 """ 

666 if actor.name in self._scan_actors: 

667 self.start_datacollection(actor) 

668 

669 if actor.name in ["mosaic", "reference"]: 

670 args = { 

671 "sessionid": actor["sessionid"], 

672 "sampleid": actor["sampleid"], 

673 "starttime": datetime.now(), 

674 "actiontype": actor.name, 

675 } 

676 sampleaction = self._metadata.add_sampleaction(**args, no_context=True) 

677 actor.update(sampleactionid=sampleaction["sampleactionid"]) 

678 

679 self._saving.set_filename( 

680 extra_saving_args=actor.saving_args, **actor.all_data 

681 ) 

682 self._saving.create_root_path(wait_exists=True) 

683 actor.update(base_path=self._saving.dirname) 

684 

685 logger.info(f"Actor '{actor.name}' with id '{actid}' started") 

686 

687 def actor_success(self, actid, response, actor): 

688 """Callback when an actor finishes successfully 

689 

690 For scan actors this update the datacollection with the endtime and 

691 'success' status 

692 

693 For ROI scans it will launch map generation 

694 """ 

695 if actor.name in self._scan_actors: 

696 self.update_datacollection( 

697 actor, endtime=datetime.now(), runstatus="Successful", emit_end=True 

698 ) 

699 self._save_dc_log(actor) 

700 

701 if actor.name in self._create_maps: 

702 self.generate_maps(actor["subsampleid"], actor["datacollectionid"]) 

703 

704 if actor.name in ["mosaic", "reference"]: 

705 snapshot = {} 

706 if actor.get("full"): 

707 snapshot["xtalsnapshotafter"] = self.save_full_mosaic(actor["full"]) 

708 

709 self._metadata.update_sampleaction( 

710 sampleactionid=actor["sampleactionid"], 

711 no_context=True, 

712 endtimestamp=datetime.now(), 

713 status="success", 

714 **snapshot, 

715 ) 

716 

717 logger.info(f"Actor '{actor.name}' with id '{actid}' finished") 

718 

719 def save_full_mosaic(self, image): 

720 """Saves the full mosaic image 

721 

722 Also creates a thumbnail 

723 

724 Args: 

725 image (PIL.Image): The image to save 

726 

727 Returns 

728 path (str): Path to the newly saved image 

729 """ 

730 directory = self._saving.dirname 

731 if self._config.get("image_subdirectory"): 

732 directory = os.path.join(directory, self._config.get("image_subdirectory")) 

733 if not os.path.exists(directory): 

734 os.makedirs(directory) 

735 

736 filename = os.extsep.join([f"mosaic_full_{time.time()}", "png"]) 

737 path = os.path.join(directory, filename) 

738 image.save(path) 

739 self._generate_thumb(path) 

740 return path 

741 

742 def actor_error(self, actid, exception, actor): 

743 """Callback when an actor fails 

744 

745 For scan actors this will update the datacollection with the end time and 

746 'failed' status 

747 """ 

748 status = "Aborted" if isinstance(exception, ComponentActorKilled) else "Failed" 

749 if actor.name in self._scan_actors: 

750 self.update_datacollection(actor, endtime=datetime.now(), runstatus=status) 

751 self._save_dc_log(actor) 

752 if status == "Failed": 

753 self._save_dc_exception(actor, exception) 

754 if actor.name in ["mosaic", "reference"]: 

755 self._metadata.update_sampleaction( 

756 sampleactionid=actor["sampleactionid"], 

757 no_context=True, 

758 endtimestamp=datetime.now(), 

759 status="error", 

760 message=str(exception), 

761 ) 

762 if status == "Failed": 

763 logger.error(f"Actor '{actor.name}' with id '{actid}' failed") 

764 else: 

765 logger.info(f"Actor '{actor.name}' with id '{actid}' aborted") 

766 

767 def actor_remove(self, actid, actor): 

768 """Callback when an actor is removed from the queue 

769 

770 For scan actors this will remove the item from the database queue 

771 """ 

772 if actor.name in self._scan_actors: 

773 self._metadata.unqueue_subsample( 

774 actor["subsampleid"], 

775 containerqueuesampleid=actor["containerqueuesampleid"], 

776 no_context=True, 

777 ) 

778 

779 def check_running_actors(self): 

780 """Periodicically check for any running actors 

781 

782 This is used to trigger automated downstream procesing, i.e. 

783 map generation so that long scans can be followed 

784 """ 

785 logger.debug("Starting periodic actor checker") 

786 while self._check_running_actors: 

787 if self._generate_during_scan: 

788 running_copy = self._running_actors.copy() 

789 for actid, actall in running_copy.items(): 

790 actor = actall[0] 

791 if actor.name in self._create_maps: 

792 if ( 

793 actor.get("subsampleid") 

794 and actor.get("datacollectionid") 

795 and actor.get("datacollectionnumber") 

796 ): 

797 logger.debug( 

798 f"Re/generating maps for {actor.name} dcid:{actor['datacollectionid']}" 

799 ) 

800 self.generate_maps( 

801 actor["subsampleid"], 

802 actor["datacollectionid"], 

803 auto=True, 

804 ) 

805 try: 

806 time.sleep(self._config["regenerate_interval"]) 

807 except KeyError: 

808 time.sleep(60) 

809 

810 def generate_maps(self, subsampleid, datacollectionid=None, auto=False): 

811 """Launch a series of actors to generate maps for each of the MCA ROIs""" 

812 self._in_generate = True 

813 

814 dcs = self._metadata.get_datacollections( 

815 datacollectionid, subsampleid=subsampleid, no_context=True, ungroup=True 

816 ) 

817 scans = self.get_component("scans") 

818 

819 if datacollectionid: 

820 dcs = [dcs] 

821 else: 

822 dcs = dcs["rows"] 

823 

824 existing = self._metadata.get_xrf_maps( 

825 subsampleid=subsampleid, no_context=True 

826 )["rows"] 

827 

828 if not auto: 

829 self.emit( 

830 "message", 

831 {"type": "generate_maps", "status": "started"}, 

832 ) 

833 log.get("user").info( 

834 "Starting map generation", 

835 type="actor", 

836 ) 

837 

838 count = 0 

839 for dc in dcs: 

840 running = False 

841 for actid in self._map_actors: 

842 actall = self._running_actors.get(actid) 

843 if not actall: 

844 continue 

845 

846 actor = actall[0] 

847 

848 if ( 

849 actor["subsampleid"] == subsampleid 

850 and actor["datacollectionid"] == dc["datacollectionid"] 

851 ): 

852 running = True 

853 break 

854 

855 if running: 

856 logger.info( 

857 f"Generate map actor already running for subsample {subsampleid} datacollection {dc['datacollectionid']}" 

858 ) 

859 continue 

860 

861 rois = self._metadata.get_xrf_map_rois( 

862 sampleid=dc["sampleid"], no_context=True 

863 )["rows"] 

864 spectra = scans.get_scan_spectra(dc["datacollectionnumber"], allpoints=True) 

865 scalars = scans.get_scan_data( 

866 dc["datacollectionnumber"], per_page=1e10, all_scalars=True 

867 ) 

868 if spectra and scalars: 

869 self._map_actors.append( 

870 self.actor( 

871 "createmap", 

872 spawn=True, 

873 success=self._append_map, 

874 error=self._map_failed, 

875 actargs={ 

876 "datacollectionid": dc["datacollectionid"], 

877 "datacollectionnumber": dc["datacollectionnumber"], 

878 "subsampleid": subsampleid, 

879 "rois": rois, 

880 "spectra": spectra, 

881 "scalars": scalars, 

882 }, 

883 ) 

884 ) 

885 count += 1 

886 else: 

887 logger.warning( 

888 f"Generate Map: Cant get spectra for scan {dc['datacollectionnumber']} (datacollectionid: {dc['datacollectionid']})" 

889 ) 

890 log.get("user").warning( 

891 f"Cant get spectra for scan {dc['datacollectionnumber']} (datacollectionid: {dc['datacollectionid']})", 

892 type="queue", 

893 ) 

894 

895 # Update scalar maps 

896 if scalars: 

897 for scalar_name in self._scalar_maps: 

898 for existing_map in existing: 

899 if ( 

900 existing_map["scalar"] == scalar_name 

901 and existing_map["datacollectionid"] 

902 == dc["datacollectionid"] 

903 ): 

904 break 

905 else: 

906 self.generate_additional_map( 

907 datacollectionid=dc["datacollectionid"], 

908 scalars=[scalar_name], 

909 no_context=True, 

910 ) 

911 

912 for m in existing: 

913 if m["scalar"] and m["datacollectionid"] == dc["datacollectionid"]: 

914 new_data = self._get_additional_map( 

915 m["scalar"], scalars=scalars 

916 ) 

917 if new_data: 

918 points = len(new_data) - new_data.count(-1) 

919 self._metadata.update_xrf_map( 

920 mapid=m["mapid"], 

921 data=new_data, 

922 points=points, 

923 no_context=True, 

924 ) 

925 

926 self._in_generate = False 

927 

928 if count == 0 and not auto: 

929 self.emit( 

930 "message", 

931 { 

932 "type": "generate_maps", 

933 "status": "warning", 

934 "message": "No maps to generate, check the log", 

935 }, 

936 ) 

937 log.get("user").info( 

938 "No maps to generation", 

939 type="actor", 

940 ) 

941 

942 return True 

943 

944 def _map_failed(self, actid, exception, actor): 

945 logger.error( 

946 f"Could not generate map for scan {actor['datacollectionnumber']} (datacollectionid: {actor['datacollectionid']}), exception was {exception}" 

947 ) 

948 log.get("user").exception( 

949 f"Could not generate map for scan {actor['datacollectionnumber']} (datacollectionid: {actor['datacollectionid']})", 

950 type="queue", 

951 ) 

952 self._update_map_actor_status(actid) 

953 

954 def _append_map(self, actid, maps, actor): 

955 """Add new map to the maplist 

956 

957 Will try to updating an existing map if it matched dcid and maproiid 

958 """ 

959 dc = self._metadata.get_datacollections( 

960 actor["datacollectionid"], subsampleid=actor["subsampleid"], no_context=True 

961 ) 

962 

963 existing = self._metadata.get_xrf_maps( 

964 subsampleid=dc["subsampleid"], no_context=True 

965 )["rows"] 

966 

967 if not maps: 

968 logger.info("No maps generated to append") 

969 return 

970 

971 # Create / update ROI maps 

972 for m in maps[0]["maps"]: 

973 exists = False 

974 for ex in existing: 

975 if ( 

976 dc["datacollectionid"] == ex["datacollectionid"] 

977 and m["maproiid"] == ex["maproiid"] 

978 ): 

979 mapid = ex["mapid"] 

980 points = len(m["data"]) - m["data"].count(-1) 

981 self._metadata.update_xrf_map( 

982 mapid=ex["mapid"], 

983 data=m["data"], 

984 points=points, 

985 no_context=True, 

986 ) 

987 exists = True 

988 break 

989 

990 if not exists: 

991 newmap = self._metadata.add_xrf_map( 

992 maproiid=m["maproiid"], 

993 datacollectionid=dc["datacollectionid"], 

994 data=m["data"], 

995 no_context=True, 

996 ) 

997 if not newmap: 

998 continue 

999 

1000 mapid = newmap["mapid"] 

1001 

1002 self.emit( 

1003 "message", 

1004 { 

1005 "type": "map", 

1006 "mapid": mapid, 

1007 "sampleid": dc["sampleid"], 

1008 "subsampleid": dc["subsampleid"], 

1009 }, 

1010 ) 

1011 

1012 self._update_map_actor_status(actid) 

1013 

1014 def _update_map_actor_status(self, actid): 

1015 self._map_actors.remove(actid) 

1016 

1017 if self._in_generate: 

1018 return 

1019 

1020 if len(self._map_actors) == 0: 

1021 self.emit( 

1022 "message", 

1023 {"type": "generate_maps", "status": "finished"}, 

1024 ) 

1025 

1026 log.get("user").info( 

1027 "Map generation complete", 

1028 type="actor", 

1029 ) 

1030 else: 

1031 self.emit( 

1032 "message", 

1033 { 

1034 "type": "generate_maps", 

1035 "status": "progress", 

1036 "remaining": len(self._map_actors), 

1037 }, 

1038 ) 

1039 

1040 def _get_additional_map(self, scalar, scalars): 

1041 """Get data for an additional map 

1042 

1043 Args: 

1044 scalar (str): The key for scalar data to use 

1045 scalars (dict): Dict of scan data scalars 

1046 

1047 Returns: 

1048 data (ndarray): The map data 

1049 """ 

1050 if scalar in scalars["data"]: 

1051 data = scalars["data"][scalar]["data"] 

1052 

1053 if not data: 

1054 logger.warning(f"Scalar {scalar} data length is zero") 

1055 return 

1056 

1057 if len(data) < scalars["npoints"]: 

1058 missing = scalars["npoints"] - len(data) 

1059 data.extend([-1 for x in range(missing)]) 

1060 

1061 return data 

1062 

1063 else: 

1064 logger.warning(f"Cannot find scalar {scalar} in scan data") 

1065 

1066 def generate_additional_map(self, **kwargs): 

1067 """Generate additional maps based on scan scalars""" 

1068 dc = self._metadata.get_datacollections( 

1069 datacollectionid=kwargs["datacollectionid"], 

1070 no_context=kwargs.get("no_context"), 

1071 ) 

1072 

1073 if not dc: 

1074 return 

1075 

1076 scans = self.get_component("scans") 

1077 scalars = scans.get_scan_data( 

1078 scanid=dc["datacollectionnumber"], per_page=1e10, scalars=kwargs["scalars"] 

1079 ) 

1080 

1081 if not scalars: 

1082 logger.warning(f"Scan id {dc['datacollectionnumber']} is not available") 

1083 return 

1084 

1085 for scalar in kwargs["scalars"]: 

1086 data = self._get_additional_map(scalar=scalar, scalars=scalars) 

1087 if data: 

1088 roi = self._metadata.add_xrf_map_roi_scalar(scalar=scalar) 

1089 self._metadata.add_xrf_map( 

1090 maproiid=roi["maproiid"], 

1091 datacollectionid=dc["datacollectionid"], 

1092 data=data, 

1093 no_context=True, 

1094 ) 

1095 

1096 return True 

1097 

1098 def get_sources(self): 

1099 """Return list of image sources""" 

1100 sources = [src.info() for src in self._sources] 

1101 return {"total": len(sources), "rows": sources} 

1102 

1103 def move(self, args): 

1104 """Move the source image""" 

1105 absol = self.get_absolute_fp(args) 

1106 self.actor("move", spawn=True, actargs={"absol": absol}) 

1107 return True 

1108 

1109 def _get_matched_positions(self, sampleactionid: int): 

1110 positions = self._metadata.get_sampleaction_positions( 

1111 sampleactionid=sampleactionid 

1112 ) 

1113 

1114 refs = {} 

1115 reals = {} 

1116 for position in positions["rows"]: 

1117 if position["type"] == "reference": 

1118 refs[position["id"]] = position 

1119 if position["type"] == "real": 

1120 reals[position["id"]] = position 

1121 

1122 ref_keys = set(refs.keys()) 

1123 real_keys = set(reals.keys()) 

1124 if ref_keys != real_keys: 

1125 raise RuntimeError( 

1126 f"Real and Reference positions do not match: missing refs: {list(real_keys - ref_keys)}, missing reals: {list(ref_keys - real_keys)}" 

1127 ) 

1128 

1129 # Stolen from: 

1130 # https://gitlab.esrf.fr/id16/LineControl/-/blob/master/src/linecontrol/widget/cool/FluoSampleRegistration.py#L263 

1131 nbref = len(ref_keys) 

1132 pfrom = numpy.ones((nbref, 3), dtype=float) 

1133 pto = numpy.ones((nbref, 3), dtype=float) 

1134 for i, position_id in enumerate(refs.keys()): 

1135 pfrom[i, 0:2] = (refs[position_id]["posx"], refs[position_id]["posy"]) 

1136 pto[i, 0:2] = ( 

1137 reals[position_id]["posx"], 

1138 reals[position_id]["posy"], 

1139 ) 

1140 

1141 return pfrom, pto 

1142 

1143 def select_reference_matrix(self, sampleactionid): 

1144 """Select a sampleaction (reference image) to calculate the transfomation matrix from""" 

1145 pfrom, pto = self._get_matched_positions(sampleactionid) 

1146 self._reference_matrix = calculate_transform_matrix(pfrom, pto) 

1147 

1148 try: 

1149 self._reference_inverse_matrix = numpy.linalg.inv(self._reference_matrix) 

1150 except Exception: 

1151 raise RuntimeError("Could not calculate inverse matrix") 

1152 

1153 with numpy.printoptions(precision=3, suppress=True): 

1154 log.get("user").info( 

1155 f"Transformation matrix calculated:\n{self._reference_matrix}", 

1156 type="actor", 

1157 ) 

1158 

1159 src = self.origin_defining_source 

1160 src.set_reference_inverse_matrix(self._reference_inverse_matrix) 

1161 

1162 def move_to_reference(self, pos, execute): 

1163 """Move the source image to a reference position""" 

1164 if not isinstance(self._reference_matrix, numpy.ndarray): 

1165 raise RuntimeError("No reference matrix computed") 

1166 

1167 transformed_pos = numpy.dot(self._reference_matrix, (pos["x"], pos["y"], 1)) 

1168 absol = self.get_absolute_fp({"x": transformed_pos[0], "y": transformed_pos[1]}) 

1169 if execute: 

1170 self.actor("move", spawn=True, actargs={"absol": absol}) 

1171 

1172 positions = {} 

1173 for motor_id, motor in absol["fixed"].items(): 

1174 positions[motor_id] = { 

1175 "motor": motor["motor"].__repr__(), 

1176 "destination": round(motor["destination"], 3), 

1177 "unit": motor["unit"], 

1178 } 

1179 

1180 if not execute: 

1181 log.get("user").info( 

1182 f"""Move to:\n{positions['x']['motor']}: {positions['x']['destination']}\n{positions['y']['motor']}: {positions['y']['destination']}""", 

1183 type="hardware", 

1184 ) 

1185 

1186 return positions 

1187 

1188 def export_reference_to_sampleimage(self, sampleactionid, crop=None) -> int: 

1189 sampleaction = self._metadata.get_sampleactions(sampleactionid=sampleactionid) 

1190 refs, reals = self._get_matched_positions(sampleactionid) 

1191 

1192 export = export_reference_to_sampleimage( 

1193 original_path=sampleaction["xtalsnapshotbefore"], 

1194 snapshot_path=sampleaction["xtalsnapshotafter"], 

1195 reference_points=refs, 

1196 vlm_points=reals, 

1197 ) 

1198 

1199 sampleimage = self._metadata.add_sampleimage( 

1200 no_context=True, 

1201 sampleid=sampleaction["sampleid"], 

1202 offsetx=int(export.center_x), 

1203 # `add_sampleimage` expects offsety to be negative 

1204 offsety=int(-1 * export.center_y), 

1205 scalex=float(export.scale_factor) * 1e3, 

1206 # Because we inverted offsety, invert scaley to flip back 

1207 scaley=float(-1 * export.scale_factor) * 1e3, 

1208 file=export.image_path, 

1209 ) 

1210 

1211 return sampleimage["sampleimageid"] 

1212 

1213 def _add_exif(self, image_filename: str, metadata: Dict[str, Any]) -> None: 

1214 """Write pixel size and offset to exif""" 

1215 metadata_string = json.dumps(metadata, indent=2) 

1216 

1217 img = Image.open(image_filename) 

1218 exif = img.getexif() 

1219 # TODO: This is Maker, could not get MakerNote to work 

1220 # https://github.com/python-pillow/Pillow/blob/main/src/PIL/ExifTags.py#L155 

1221 exif.update([(271, metadata_string)]) 

1222 img.save(image_filename, exif=exif) 

1223 

1224 def save_image( 

1225 self, 

1226 sessionid=None, 

1227 sampleid=None, 

1228 subsampleid=None, 

1229 savemeta=True, 

1230 annotate=False, 

1231 file_prefix="", 

1232 ): 

1233 """Save an image from the source device, like objects, images are marked 

1234 relative to the origin marking position 

1235 

1236 Args: 

1237 sessionid (int): The session id 

1238 sampleid (int): The sample id 

1239 subsampleid (int): Optionally a subsample id 

1240 savemeta (bool): Whether to save this as a sample image 

1241 annotate (bool): Whether to annotate this image (origin, scalebar, etc) 

1242 

1243 Returns: 

1244 path (str): The path of the saved image 

1245 """ 

1246 directory = self._saving.dirname 

1247 for src in self._sources: 

1248 if not src.origin: 

1249 continue 

1250 

1251 filename = os.extsep.join([f"{file_prefix}{time.time()}", "png"]) 

1252 

1253 if self._config.get("image_subdirectory"): 

1254 directory = os.path.join( 

1255 directory, self._config.get("image_subdirectory") 

1256 ) 

1257 if not os.path.exists(directory): 

1258 os.makedirs(directory) 

1259 

1260 path = os.path.join(directory, filename) 

1261 

1262 if not src.device.online(): 

1263 raise RuntimeError("Cannot save image, camera is offline") 

1264 

1265 src.device.call("save", path) 

1266 

1267 image_info = src.canvas.vlm_image_info 

1268 beam = src.canvas.beam_info 

1269 

1270 subsample = None 

1271 if annotate: 

1272 if subsampleid: 

1273 subsample = self._metadata.get_subsamples( 

1274 subsampleid=subsampleid, no_context=True 

1275 ) 

1276 ann = AnnotateImage(path) 

1277 details = ann.annotate( 

1278 image_info["center"], 

1279 beam["position"], 

1280 image_info["pixelsize"], 

1281 src.unit, 

1282 subsample, 

1283 ) 

1284 subsample = details["subsample"] 

1285 

1286 if sampleid and savemeta: 

1287 self._metadata.add_sampleimage( 

1288 no_context=True, 

1289 sampleid=sampleid, 

1290 offsetx=int(image_info["center"][0]), 

1291 offsety=int(image_info["center"][1]), 

1292 scalex=float(image_info["pixelsize"][0]), 

1293 scaley=float(image_info["pixelsize"][1]), 

1294 file=path, 

1295 positions=src.get_additional(), 

1296 ) 

1297 self._add_exif( 

1298 path, 

1299 { 

1300 "mppx": float(image_info["pixelsize"][0]), 

1301 "mppy": float(image_info["pixelsize"][1]), 

1302 "offsetx": int(image_info["center"][0]), 

1303 "offsety": int(image_info["center"][1]), 

1304 }, 

1305 ) 

1306 

1307 self._generate_thumb(path) 

1308 return { 

1309 "path": path, 

1310 "subsample": subsample, 

1311 "scale": { 

1312 "x": float(image_info["pixelsize"][0]), 

1313 "y": float(image_info["pixelsize"][1]), 

1314 }, 

1315 } 

1316 

1317 def move_to(self, subsampleid): 

1318 """Move to a specific subsample 

1319 

1320 Args: 

1321 subsampleid (int): The subsample id 

1322 

1323 Returns: 

1324 success (bool): Whether the move was successful 

1325 """ 

1326 absol = self.get_absolute(subsampleid) 

1327 self.actor("move", spawn=True, actargs={"absol": absol}) 

1328 return True 

1329 

1330 @property 

1331 def origin_defining_source(self) -> Source: 

1332 for src in self._sources: 

1333 if src.origin is True: 

1334 return src 

1335 

1336 @property 

1337 def beamsize(self): 

1338 src = self.origin_defining_source 

1339 if src: 

1340 return src.beamsize 

1341 

1342 def get_absolute_fp(self, pos, pos2=None): 

1343 """Return absolute motor positions to bring a position to the centre of view 

1344 

1345 Args: 

1346 pos (dict): Dictionary containing 'x' and 'y' positions 

1347 

1348 Returns: 

1349 absolute (dict): Absolute positions and their associated motors 

1350 """ 

1351 src = self.origin_defining_source 

1352 if src is None: 

1353 return None 

1354 

1355 if pos2: 

1356 absol = src.canvas.canvas_to_motor( 

1357 [[pos["x"], -pos["y"]], [pos2["x"], -pos2["y"]]] 

1358 ) 

1359 else: 

1360 absol = src.canvas.canvas_to_motor([pos["x"], -pos["y"]]) 

1361 

1362 absol["axes"] = self.find_axes_from_variable(absol["variable"]) 

1363 absol["move_to"] = self.move_to_absol 

1364 

1365 # print("------ get_absolute_fp ------") 

1366 # pp.pprint(pos) 

1367 # pp.pprint(absol) 

1368 

1369 return absol 

1370 

1371 def get_absolute(self, subsampleid): 

1372 """Return absolute motor positions to bring a subsample id to the origin marking 

1373 

1374 Args: 

1375 subsampleid (int): The subsample to get the position of 

1376 

1377 Returns: 

1378 absolute (dict): A dictionary of the absolute positions for the subsample 

1379 """ 

1380 src = self.origin_defining_source 

1381 if src is None: 

1382 return None 

1383 

1384 obj = self._metadata.get_subsamples(subsampleid) 

1385 if obj is None: 

1386 return 

1387 

1388 if obj["type"] == "loi" or obj["type"] == "roi": 

1389 pos = [[obj["x"], -obj["y"]], [obj["x2"], -obj["y2"]]] 

1390 else: 

1391 pos = [obj["x"], -obj["y"]] 

1392 absol = src.canvas.canvas_to_motor(pos) 

1393 # src.canvas.sampleposition=.... 

1394 

1395 # print("------ get_absolute ------") 

1396 # pp.pprint(obj) 

1397 # pp.pprint(absol) 

1398 

1399 if "z" in absol["fixed"]: 

1400 del absol["fixed"]["z"] 

1401 

1402 absol["axes"] = self.find_axes_from_variable(absol["variable"]) 

1403 absol["move_to_additional"] = src.move_to_additional 

1404 absol["positions"] = obj["positions"] 

1405 absol["move_to"] = self.move_to_absol 

1406 

1407 return absol 

1408 

1409 def find_axes_from_variable(self, variable): 

1410 axes = {} 

1411 for key, obj in variable.items(): 

1412 for axis in ["x", "y", "z"]: 

1413 if key.startswith(axis): 

1414 axes[axis] = obj 

1415 

1416 return axes 

1417 

1418 def move_to_absol(self, absol, sequential=False): 

1419 all_objs = list(absol["fixed"].values()) + list( 

1420 absol.get("variable", {}).values() 

1421 ) 

1422 for obj in all_objs: 

1423 if isinstance(obj["destination"], list): 

1424 obj["motor"].move(obj["destination"][0]) 

1425 else: 

1426 obj["motor"].move(obj["destination"]) 

1427 

1428 if sequential: 

1429 obj["motor"].wait() 

1430 

1431 for obj in all_objs: 

1432 obj["motor"].wait() 

1433 

1434 def _generate_thumb(self, path): 

1435 size = (250, 250) 

1436 thumb = Image.open(path) 

1437 thumb.thumbnail(size, Image.LANCZOS) 

1438 thumb.save(path.replace(".png", "t.png")) 

1439 

1440 def export_subsamples(self, subsampleids): 

1441 subsamples = [] 

1442 sampleid = None 

1443 for subsampleid in subsampleids: 

1444 obj = self._metadata.get_subsamples(subsampleid) 

1445 sampleid = obj["sampleid"] 

1446 sample = self._metadata.get_samples(sampleid) 

1447 abs = self.get_absolute(subsampleid) 

1448 

1449 motor_types = {} 

1450 motor_positions = {} 

1451 for move_type in ("fixed", "variable"): 

1452 motor_positions[move_type] = {} 

1453 

1454 for motor_type, details in abs[move_type].items(): 

1455 motor_types[motor_type] = details["motor"].name() 

1456 motor_positions[move_type][details["motor"].name()] = { 

1457 "destination": details["destination"], 

1458 "unit": details["unit"], 

1459 "unit_exponent": details["unit_exponent"], 

1460 } 

1461 

1462 subsamples.append( 

1463 { 

1464 "subsampleid": subsampleid, 

1465 "type": obj["type"], 

1466 "comments": obj["comments"], 

1467 "extrametadata": { 

1468 **(sample["extrametadata"] if sample["extrametadata"] else {}), 

1469 "subsample": obj["extrametadata"], 

1470 }, 

1471 "additional": abs["positions"], 

1472 "motors": motor_positions, 

1473 } 

1474 ) 

1475 

1476 actor, greenlet = self.actor( 

1477 "export", 

1478 start=self._set_export_path, 

1479 spawn=True, 

1480 return_actor=True, 

1481 actargs={ 

1482 "motor_types": motor_types, 

1483 "subsamples": subsamples, 

1484 "sessionid": g.blsession.get("sessionid"), 

1485 "sampleid": sampleid, 

1486 "time": int(time.time()), 

1487 }, 

1488 ) 

1489 

1490 greenlet.join() 

1491 if actor._failed: 

1492 raise actor._exception 

1493 

1494 return actor["dirname"] 

1495 

1496 def _set_export_path(self, actid, actor): 

1497 self._saving.set_filename( 

1498 set_metadata=False, 

1499 extra_saving_args=actor.saving_args, 

1500 **actor.all_data, 

1501 ) 

1502 self._saving.create_root_path(wait_exists=True) 

1503 actor.update(dirname=self._saving.dirname)