comparison msnc_tools.py @ 0:f35c772fed27 draft

Uploaded
author rv43
date Thu, 24 Mar 2022 16:57:09 +0000
parents
children
comparison
equal deleted inserted replaced
-1:000000000000 0:f35c772fed27
1 #!/usr/bin/env python3
2
3 # -*- coding: utf-8 -*-
4 """
5 Created on Mon Dec 6 15:36:22 2021
6
7 @author: rv43
8 """
9
10 import logging
11
12 import os
13 import sys
14 import re
15 import yaml
16 import h5py
17 import pyinputplus as pyip
18 import numpy as np
19 import imageio as img
20 import matplotlib.pyplot as plt
21 from time import time
22 from ast import literal_eval
23 from lmfit.models import StepModel, RectangleModel
24
25 def depth_list(L): return isinstance(L, list) and max(map(depth_list, L))+1
26 def depth_tuple(T): return isinstance(T, tuple) and max(map(depth_tuple, T))+1
27
28 def is_int(v, v_min=None, v_max=None):
29 """Value is an integer in range v_min <= v <= v_max.
30 """
31 if not isinstance(v, int):
32 return False
33 if (v_min != None and v < v_min) or (v_max != None and v > v_max):
34 return False
35 return True
36
37 def is_num(v, v_min=None, v_max=None):
38 """Value is a number in range v_min <= v <= v_max.
39 """
40 if not isinstance(v, (int,float)):
41 return False
42 if (v_min != None and v < v_min) or (v_max != None and v > v_max):
43 return False
44 return True
45
46 def is_index(v, v_min=0, v_max=None):
47 """Value is an array index in range v_min <= v < v_max.
48 """
49 if not isinstance(v, int):
50 return False
51 if v < v_min or (v_max != None and v >= v_max):
52 return False
53 return True
54
55 def is_index_range(v, v_min=0, v_max=None):
56 """Value is an array index range in range v_min <= v[0] <= v[1] < v_max.
57 """
58 if not (isinstance(v, list) and len(v) == 2 and isinstance(v[0], int) and
59 isinstance(v[1], int)):
60 return False
61 if not 0 <= v[0] < v[1] or (v_max != None and v[1] >= v_max):
62 return False
63 return True
64
65 def illegal_value(name, value, location=None, exit_flag=False):
66 if not isinstance(location, str):
67 location = ''
68 else:
69 location = f'in {location} '
70 if isinstance(name, str):
71 logging.error(f'Illegal value for {name} {location}({value}, {type(value)})')
72 else:
73 logging.error(f'Illegal value {location}({value}, {type(value)})')
74 if exit_flag:
75 exit(1)
76
77 def get_trailing_int(string):
78 indexRegex = re.compile(r'\d+$')
79 mo = indexRegex.search(string)
80 if mo == None:
81 return None
82 else:
83 return int(mo.group())
84
85 def findImageFiles(path, filetype, name=None, select_range=False, num_required=None):
86 if isinstance(name, str):
87 name = f' {name} '
88 else:
89 name = ' '
90 # Find available index range
91 if filetype == 'tif':
92 if not isinstance(path, str) and not os.path.isdir(path):
93 illegal_value('path', path, 'findImageRange')
94 return -1, 0, []
95 indexRegex = re.compile(r'\d+')
96 # At this point only tiffs
97 files = sorted([f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) and
98 f.endswith('.tif') and indexRegex.search(f)])
99 num_imgs = len(files)
100 if num_imgs < 1:
101 logging.warning('No available'+name+'files')
102 return -1, 0, []
103 first_index = indexRegex.search(files[0]).group()
104 last_index = indexRegex.search(files[-1]).group()
105 if first_index == None or last_index == None:
106 logging.error('Unable to find correctly indexed'+name+'images')
107 return -1, 0, []
108 first_index = int(first_index)
109 last_index = int(last_index)
110 if num_imgs != last_index-first_index+1:
111 logging.error('Non-consecutive set of indices for'+name+'images')
112 return -1, 0, []
113 paths = [os.path.join(path, f) for f in files]
114 elif filetype == 'h5':
115 if not isinstance(path, str) or not os.path.isfile(path):
116 illegal_value('path', path, 'findImageRange')
117 return -1, 0, []
118 # At this point only h5 in alamo2 detector style
119 first_index = 0
120 with h5py.File(path, 'r') as f:
121 num_imgs = f['entry/instrument/detector/data'].shape[0]
122 last_index = num_imgs-1
123 paths = [path]
124 else:
125 illegal_value('filetype', filetype, 'findImageRange')
126 return -1, 0, []
127 logging.debug('\nNumber of available'+name+f'images: {num_imgs}')
128 logging.debug('Index range of available'+name+f'images: [{first_index}, '+
129 f'{last_index}]')
130
131 return first_index, num_imgs, paths
132
133 def selectImageRange(first_index, offset, num_imgs, name=None, num_required=None):
134 if isinstance(name, str):
135 name = f' {name} '
136 else:
137 name = ' '
138 # Check existing values
139 use_input = 'no'
140 if (is_int(first_index, 0) and is_int(offset, 0) and is_int(num_imgs, 1)):
141 if offset < 0:
142 use_input = pyip.inputYesNo('\nCurrent'+name+f'first index = {first_index}, '+
143 'use this value ([y]/n)? ', blank=True)
144 else:
145 use_input = pyip.inputYesNo('\nCurrent'+name+'first index/offset = '+
146 f'{first_index}/{offset}, use these values ([y]/n)? ',
147 blank=True)
148 if use_input != 'no':
149 use_input = pyip.inputYesNo('Current number of'+name+'images = '+
150 f'{num_imgs}, use this value ([y]/n)? ',
151 blank=True)
152 if use_input == 'yes':
153 return first_index, offset, num_imgs
154
155 # Check range against requirements
156 if num_imgs < 1:
157 logging.warning('No available'+name+'images')
158 return -1, -1, 0
159 if num_required == None:
160 if num_imgs == 1:
161 return first_index, 0, 1
162 else:
163 if not is_int(num_required, 1):
164 illegal_value('num_required', num_required, 'selectImageRange')
165 return -1, -1, 0
166 if num_imgs < num_required:
167 logging.error('Unable to find the required'+name+
168 f'images ({num_imgs} out of {num_required})')
169 return -1, -1, 0
170
171 # Select index range
172 if num_required == None:
173 last_index = first_index+num_imgs
174 use_all = f'Use all ([{first_index}, {last_index}])'
175 pick_offset = 'Pick a first index offset and a number of images'
176 pick_bounds = 'Pick the first and last index'
177 menuchoice = pyip.inputMenu([use_all, pick_offset, pick_bounds], numbered=True)
178 if menuchoice == use_all:
179 offset = 0
180 elif menuchoice == pick_offset:
181 offset = pyip.inputInt('Enter the first index offset'+
182 f' [0, {last_index-first_index}]: ', min=0, max=last_index-first_index)
183 first_index += offset
184 if first_index == last_index:
185 num_imgs = 1
186 else:
187 num_imgs = pyip.inputInt(f'Enter the number of images [1, {num_imgs}]: ',
188 min=1, max=num_imgs)
189 else:
190 offset = pyip.inputInt(f'Enter the first index [{first_index}, {last_index}]: ',
191 min=first_index, max=last_index)-first_index
192 first_index += offset
193 num_imgs = pyip.inputInt(f'Enter the last index [{first_index}, {last_index}]: ',
194 min=first_index, max=last_index)-first_index+1
195 else:
196 use_all = f'Use ([{first_index}, {first_index+num_required-1}])'
197 pick_offset = 'Pick the first index offset'
198 menuchoice = pyip.inputMenu([use_all, pick_offset], numbered=True)
199 offset = 0
200 if menuchoice == pick_offset:
201 offset = pyip.inputInt('Enter the first index offset'+
202 f'[0, {num_imgs-num_required}]: ', min=0, max=num_imgs-num_required)
203 first_index += offset
204 num_imgs = num_required
205
206 return first_index, offset, num_imgs
207
208 def loadImage(f, img_x_bounds=None, img_y_bounds=None):
209 """Load a single image from file.
210 """
211 if not os.path.isfile(f):
212 logging.error(f'Unable to load {f}')
213 return None
214 img_read = img.imread(f)
215 if not img_x_bounds:
216 img_x_bounds = [0, img_read.shape[0]]
217 else:
218 if (not isinstance(img_x_bounds, list) or len(img_x_bounds) != 2 or
219 not (0 <= img_x_bounds[0] < img_x_bounds[1] <= img_read.shape[0])):
220 logging.error(f'inconsistent row dimension in {f}')
221 return None
222 if not img_y_bounds:
223 img_y_bounds = [0, img_read.shape[1]]
224 else:
225 if (not isinstance(img_y_bounds, list) or len(img_y_bounds) != 2 or
226 not (0 <= img_y_bounds[0] < img_y_bounds[1] <= img_read.shape[0])):
227 logging.error(f'inconsistent column dimension in {f}')
228 return None
229 return img_read[img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]]
230
231 def loadImageStack(files, filetype, img_offset, num_imgs, num_img_skip=0,
232 img_x_bounds=None, img_y_bounds=None):
233 """Load a set of images and return them as a stack.
234 """
235 logging.debug(f'img_offset = {img_offset}')
236 logging.debug(f'num_imgs = {num_imgs}')
237 logging.debug(f'num_img_skip = {num_img_skip}')
238 logging.debug(f'\nfiles:\n{files}\n')
239 img_stack = np.array([])
240 if filetype == 'tif':
241 img_read_stack = []
242 i = 1
243 t0 = time()
244 for f in files[img_offset:img_offset+num_imgs:num_img_skip+1]:
245 if not i%20:
246 logging.info(f' loading {i}/{num_imgs}: {f}')
247 else:
248 logging.debug(f' loading {i}/{num_imgs}: {f}')
249 img_read = loadImage(f, img_x_bounds, img_y_bounds)
250 img_read_stack.append(img_read)
251 i += num_img_skip+1
252 img_stack = np.stack([img_read for img_read in img_read_stack])
253 logging.info(f'... done in {time()-t0:.2f} seconds!')
254 logging.debug(f'img_stack shape = {np.shape(img_stack)}')
255 del img_read_stack, img_read
256 elif filetype == 'h5':
257 if not isinstance(files[0], str) and not os.path.isfile(files[0]):
258 illegal_value('files[0]', files[0], 'loadImageStack')
259 return img_stack
260 t0 = time()
261 with h5py.File(files[0], 'r') as f:
262 shape = f['entry/instrument/detector/data'].shape
263 if len(shape) != 3:
264 logging.error(f'inconsistent dimensions in {files[0]}')
265 if not img_x_bounds:
266 img_x_bounds = [0, shape[1]]
267 else:
268 if (not isinstance(img_x_bounds, list) or len(img_x_bounds) != 2 or
269 not (0 <= img_x_bounds[0] < img_x_bounds[1] <= shape[1])):
270 logging.error(f'inconsistent row dimension in {files[0]}')
271 if not img_y_bounds:
272 img_y_bounds = [0, shape[2]]
273 else:
274 if (not isinstance(img_y_bounds, list) or len(img_y_bounds) != 2 or
275 not (0 <= img_y_bounds[0] < img_y_bounds[1] <= shape[2])):
276 logging.error(f'inconsistent column dimension in {files[0]}')
277 img_stack = f.get('entry/instrument/detector/data')[
278 img_offset:img_offset+num_imgs:num_img_skip+1,
279 img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]]
280 logging.info(f'... done in {time()-t0:.2f} seconds!')
281 else:
282 illegal_value('filetype', filetype, 'findImageRange')
283 return img_stack
284
285 def clearFig(title):
286 if not isinstance(title, str):
287 illegal_value('title', title, 'clearFig')
288 return
289 plt.close(fig=re.sub(r"\s+", '_', title))
290
291 def quickImshow(a, title=None, path=None, name=None, save_fig=False, save_only=False,
292 clear=True, **kwargs):
293 if title != None and not isinstance(title, str):
294 illegal_value('title', title, 'quickImshow')
295 return
296 if path is not None and not isinstance(path, str):
297 illegal_value('path', path, 'quickImshow')
298 return
299 if not isinstance(save_fig, bool):
300 illegal_value('save_fig', save_fig, 'quickImshow')
301 return
302 if not isinstance(save_only, bool):
303 illegal_value('save_only', save_only, 'quickImshow')
304 return
305 if not isinstance(clear, bool):
306 illegal_value('clear', clear, 'quickImshow')
307 return
308 if not title:
309 title='quick_imshow'
310 else:
311 title = re.sub(r"\s+", '_', title)
312 if name is None:
313 if path is None:
314 path = f'{title}.png'
315 else:
316 path = f'{path}/{title}.png'
317 else:
318 if path is None:
319 path = name
320 else:
321 path = f'{path}/{name}'
322 if clear:
323 plt.close(fig=title)
324 if save_only:
325 plt.figure(title)
326 plt.imshow(a, **kwargs)
327 plt.savefig(path)
328 plt.close(fig=title)
329 #plt.imsave(f'{title}.png', a, **kwargs)
330 else:
331 plt.ion()
332 plt.figure(title)
333 plt.imshow(a, **kwargs)
334 if save_fig:
335 plt.savefig(path)
336 plt.pause(1)
337
338 def quickPlot(*args, title=None, path=None, name=None, save_fig=False, save_only=False,
339 clear=True, **kwargs):
340 if title != None and not isinstance(title, str):
341 illegal_value('title', title, 'quickPlot')
342 return
343 if path is not None and not isinstance(path, str):
344 illegal_value('path', path, 'quickPlot')
345 return
346 if not isinstance(save_fig, bool):
347 illegal_value('save_fig', save_fig, 'quickPlot')
348 return
349 if not isinstance(save_only, bool):
350 illegal_value('save_only', save_only, 'quickPlot')
351 return
352 if not isinstance(clear, bool):
353 illegal_value('clear', clear, 'quickPlot')
354 return
355 if not title:
356 title = 'quick_plot'
357 else:
358 title = re.sub(r"\s+", '_', title)
359 if name is None:
360 if path is None:
361 path = f'{title}.png'
362 else:
363 path = f'{path}/{title}.png'
364 else:
365 if path is None:
366 path = name
367 else:
368 path = f'{path}/{name}'
369 if clear:
370 plt.close(fig=title)
371 if save_only:
372 plt.figure(title)
373 if depth_tuple(args) > 1:
374 for y in args:
375 plt.plot(*y, **kwargs)
376 else:
377 plt.plot(*args, **kwargs)
378 plt.savefig(path)
379 plt.close(fig=title)
380 else:
381 plt.ion()
382 plt.figure(title)
383 if depth_tuple(args) > 1:
384 for y in args:
385 plt.plot(*y, **kwargs)
386 else:
387 plt.plot(*args, **kwargs)
388 if save_fig:
389 plt.savefig(path)
390 plt.pause(1)
391
392 def selectArrayBounds(a, x_low=None, x_upp=None, num_x_min=None,
393 title='select array bounds'):
394 """Interactively select the lower and upper data bounds for a numpy array.
395 """
396 if not isinstance(a, np.ndarray) or a.ndim != 1:
397 logging.error('Illegal array type or dimension in selectArrayBounds')
398 return None
399 if num_x_min == None:
400 num_x_min = 1
401 else:
402 if num_x_min < 2 or num_x_min > a.size:
403 logging.warning('Illegal input for num_x_min in selectArrayBounds, input ignored')
404 num_x_min = 1
405 if x_low == None:
406 x_min = 0
407 x_max = a.size
408 x_low_max = a.size-num_x_min
409 while True:
410 quickPlot(range(x_min, x_max), a[x_min:x_max], title=title)
411 zoom_flag = pyip.inputInt('Set lower data bound (0) or zoom in (1)?: ',
412 min=0, max=1)
413 if zoom_flag:
414 x_min = pyip.inputInt(f' Set lower zoom index [0, {x_low_max}]: ',
415 min=0, max=x_low_max)
416 x_max = pyip.inputInt(f' Set upper zoom index [{x_min+1}, {x_low_max+1}]: ',
417 min=x_min+1, max=x_low_max+1)
418 else:
419 x_low = pyip.inputInt(f' Set lower data bound [0, {x_low_max}]: ',
420 min=0, max=x_low_max)
421 break
422 else:
423 if not is_int(x_low, 0, a.size-num_x_min):
424 illegal_value('x_low', x_low, 'selectArrayBounds')
425 return None
426 if x_upp == None:
427 x_min = x_low+num_x_min
428 x_max = a.size
429 x_upp_min = x_min
430 while True:
431 quickPlot(range(x_min, x_max), a[x_min:x_max], title=title)
432 zoom_flag = pyip.inputInt('Set upper data bound (0) or zoom in (1)?: ',
433 min=0, max=1)
434 if zoom_flag:
435 x_min = pyip.inputInt(f' Set upper zoom index [{x_upp_min}, {a.size-1}]: ',
436 min=x_upp_min, max=a.size-1)
437 x_max = pyip.inputInt(f' Set upper zoom index [{x_min+1}, {a.size}]: ',
438 min=x_min+1, max=a.size)
439 else:
440 x_upp = pyip.inputInt(f' Set upper data bound [{x_upp_min}, {a.size}]: ',
441 min=x_upp_min, max=a.size)
442 break
443 else:
444 if not is_int(x_upp, x_low+num_x_min, a.size):
445 illegal_value('x_upp', x_upp, 'selectArrayBounds')
446 return None
447 print(f'lower bound = {x_low} (inclusive)\nupper bound = {x_upp} (exclusive)]')
448 bounds = [x_low, x_upp]
449 #quickPlot(range(bounds[0], bounds[1]), a[bounds[0]:bounds[1]], title=title)
450 quickPlot((range(a.size), a), ([bounds[0], bounds[0]], [a.min(), a.max()], 'r-'),
451 ([bounds[1], bounds[1]], [a.min(), a.max()], 'r-'), title=title)
452 if pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True) == 'no':
453 bounds = selectArrayBounds(a, title=title)
454 return bounds
455
456 def selectImageBounds(a, axis, low=None, upp=None, num_min=None,
457 title='select array bounds'):
458 """Interactively select the lower and upper data bounds for a 2D numpy array.
459 """
460 if not isinstance(a, np.ndarray) or a.ndim != 2:
461 logging.error('Illegal array type or dimension in selectImageBounds')
462 return None
463 if axis < 0 or axis >= a.ndim:
464 illegal_value('axis', axis, 'selectImageBounds')
465 return None
466 if num_min == None:
467 num_min = 1
468 else:
469 if num_min < 2 or num_min > a.shape[axis]:
470 logging.warning('Illegal input for num_min in selectImageBounds, input ignored')
471 num_min = 1
472 if low == None:
473 min_ = 0
474 max_ = a.shape[axis]
475 low_max = a.shape[axis]-num_min
476 while True:
477 if axis:
478 quickImshow(a[:,min_:max_], title=title, aspect='auto',
479 extent=[min_,max_,a.shape[0],0])
480 else:
481 quickImshow(a[min_:max_,:], title=title, aspect='auto',
482 extent=[0,a.shape[1], max_,min_])
483 zoom_flag = pyip.inputInt('Set lower data bound (0) or zoom in (1)?: ',
484 min=0, max=1)
485 if zoom_flag:
486 min_ = pyip.inputInt(f' Set lower zoom index [0, {low_max}]: ',
487 min=0, max=low_max)
488 max_ = pyip.inputInt(f' Set upper zoom index [{min_+1}, {low_max+1}]: ',
489 min=min_+1, max=low_max+1)
490 else:
491 low = pyip.inputInt(f' Set lower data bound [0, {low_max}]: ',
492 min=0, max=low_max)
493 break
494 else:
495 if not is_int(low, 0, a.shape[axis]-num_min):
496 illegal_value('low', low, 'selectImageBounds')
497 return None
498 if upp == None:
499 min_ = low+num_min
500 max_ = a.shape[axis]
501 upp_min = min_
502 while True:
503 if axis:
504 quickImshow(a[:,min_:max_], title=title, aspect='auto',
505 extent=[min_,max_,a.shape[0],0])
506 else:
507 quickImshow(a[min_:max_,:], title=title, aspect='auto',
508 extent=[0,a.shape[1], max_,min_])
509 zoom_flag = pyip.inputInt('Set upper data bound (0) or zoom in (1)?: ',
510 min=0, max=1)
511 if zoom_flag:
512 min_ = pyip.inputInt(f' Set upper zoom index [{upp_min}, {a.shape[axis]-1}]: ',
513 min=upp_min, max=a.shape[axis]-1)
514 max_ = pyip.inputInt(f' Set upper zoom index [{min_+1}, {a.shape[axis]}]: ',
515 min=min_+1, max=a.shape[axis])
516 else:
517 upp = pyip.inputInt(f' Set upper data bound [{upp_min}, {a.shape[axis]}]: ',
518 min=upp_min, max=a.shape[axis])
519 break
520 else:
521 if not is_int(upp, low+num_min, a.shape[axis]):
522 illegal_value('upp', upp, 'selectImageBounds')
523 return None
524 print(f'lower bound = {low} (inclusive)\nupper bound = {upp} (exclusive)')
525 bounds = [low, upp]
526 a_tmp = a
527 if axis:
528 a_tmp[:,bounds[0]] = a.max()
529 a_tmp[:,bounds[1]] = a.max()
530 else:
531 a_tmp[bounds[0],:] = a.max()
532 a_tmp[bounds[1],:] = a.max()
533 quickImshow(a_tmp, title=title)
534 if pyip.inputYesNo('Accept these bounds ([y]/n)?: ', blank=True) == 'no':
535 bounds = selectImageBounds(a, title=title)
536 return bounds
537
538 def fitStep(x=None, y=None, model='step', form='arctan'):
539 if not isinstance(y, np.ndarray) or y.ndim != 1:
540 logging.error('Illegal array type or dimension for y in fitStep')
541 return
542 if isinstance(x, type(None)):
543 x = np.array(range(y.size))
544 elif not isinstance(x, np.ndarray) or x.ndim != 1 or x.size != y.size:
545 logging.error('Illegal array type or dimension for x in fitStep')
546 return
547 if not isinstance(model, str) or not model in ('step', 'rectangle'):
548 illegal_value('model', model, 'fitStepModel')
549 return
550 if not isinstance(form, str) or not form in ('linear', 'atan', 'arctan', 'erf', 'logistic'):
551 illegal_value('form', form, 'fitStepModel')
552 return
553
554 if model == 'step':
555 mod = StepModel(form=form)
556 else:
557 mod = RectangleModel(form=form)
558 pars = mod.guess(y, x=x)
559 out = mod.fit(y, pars, x=x)
560 #print(out.fit_report())
561 #quickPlot((x,y),(x,out.best_fit))
562 return out.best_values
563
564 class Config:
565 """Base class for processing a config file or dictionary.
566 """
567 def __init__(self, config_file=None, config_dict=None):
568 self.config = {}
569 self.load_flag = False
570 self.suffix = None
571
572 # Load config file
573 if config_file is not None and config_dict is not None:
574 logging.warning('Ignoring config_dict (both config_file and config_dict are specified)')
575 if config_file is not None:
576 self.loadFile(config_file)
577 elif config_dict is not None:
578 self.loadDict(config_dict)
579
580 def loadFile(self, config_file):
581 """Load a config file.
582 """
583 if self.load_flag:
584 logging.warning('Overwriting any previously loaded config file')
585 self.config = {}
586
587 # Ensure config file exists
588 if not os.path.isfile(config_file):
589 logging.error(f'Unable to load {config_file}')
590 return
591
592 # Load config file
593 self.suffix = os.path.splitext(config_file)[1]
594 if self.suffix == '.yml' or self.suffix == '.yaml':
595 with open(config_file, 'r') as f:
596 self.config = yaml.safe_load(f)
597 elif self.suffix == '.txt':
598 with open(config_file, 'r') as f:
599 lines = f.read().splitlines()
600 self.config = {item[0].strip():literal_eval(item[1].strip()) for item in
601 [line.split('#')[0].split('=') for line in lines if '=' in line.split('#')[0]]}
602 else:
603 logging.error(f'Illegal config file extension: {self.suffix}')
604
605 # Make sure config file was correctly loaded
606 if isinstance(self.config, dict):
607 self.load_flag = True
608 else:
609 logging.error(f'Unable to load dictionary from config file: {config_file}')
610 self.config = {}
611
612 def loadDict(self, config_dict):
613 """Takes a dictionary and places it into self.config.
614 """
615 exit('loadDict not tested yet, what format do we follow: txt or yaml?')
616 if self.load_flag:
617 logging.warning('Overwriting the previously loaded config file')
618
619 if isinstance(config_dict, dict):
620 self.config = config_dict
621 self.load_flag = True
622 else:
623 logging.error(f'Illegal dictionary config object: {config_dict}')
624 self.config = {}
625
626 def saveFile(self, config_file):
627 """Save the config file (as a yaml file only right now).
628 """
629 suffix = os.path.splitext(config_file)[1]
630 if suffix != '.yml' and suffix != '.yaml':
631 logging.error(f'Illegal config file extension: {suffix}')
632
633 # Check if config file exists
634 if os.path.isfile(config_file):
635 logging.info(f'Updating {config_file}')
636 else:
637 logging.info(f'Saving {config_file}')
638
639 # Save config file
640 with open(config_file, 'w') as f:
641 yaml.dump(self.config, f)
642
643 def validate(self, pars_required, pars_missing=None):
644 """Returns False if any required first level keys are missing.
645 """
646 if not self.load_flag:
647 logging.error('Load a config file prior to calling Config.validate')
648 pars = [p for p in pars_required if p not in self.config]
649 if isinstance(pars_missing, list):
650 pars_missing.extend(pars)
651 elif pars_missing is not None:
652 illegal_value('pars_missing', pars_missing, 'Config.validate')
653 if len(pars) > 0:
654 return False
655 return True
656
657 #RV FIX this is for a txt file, obsolete?
658 # def update_txt(self, config_file, key, value, search_string=None, header=None):
659 # if not self.load_flag:
660 # logging.error('Load a config file prior to calling Config.update')
661 #
662 # if not os.path.isfile(config_file):
663 # logging.error(f'Unable to load {config_file}')
664 # lines = []
665 # else:
666 # with open(config_file, 'r') as f:
667 # lines = f.read().splitlines()
668 # config = {item[0].strip():literal_eval(item[1].strip()) for item in
669 # [line.split('#')[0].split('=') for line in lines if '=' in line.split('#')[0]]}
670 # if not isinstance(key, str):
671 # illegal_value('key', key, 'Config.update')
672 # return config
673 # if isinstance(value, str):
674 # newline = f"{key} = '{value}'"
675 # else:
676 # newline = f'{key} = {value}'
677 # if key in config.keys():
678 # # Update key with value
679 # for index,line in enumerate(lines):
680 # if '=' in line:
681 # item = line.split('#')[0].split('=')
682 # if item[0].strip() == key:
683 # lines[index] = newline
684 # break
685 # else:
686 # # Insert new key/value pair
687 # if search_string != None:
688 # if isinstance(search_string, str):
689 # search_string = [search_string]
690 # elif not isinstance(search_string, (tuple, list)):
691 # illegal_value('search_string', search_string, 'Config.update')
692 # search_string = None
693 # update_flag = False
694 # if search_string != None:
695 # indices = [[index for index,line in enumerate(lines) if item in line]
696 # for item in search_string]
697 # for i,index in enumerate(indices):
698 # if index:
699 # if len(search_string) > 1 and key < search_string[i]:
700 # lines.insert(index[0], newline)
701 # else:
702 # lines.insert(index[0]+1, newline)
703 # update_flag = True
704 # break
705 # if not update_flag:
706 # if isinstance(header, str):
707 # lines += ['', header, newline]
708 # else:
709 # lines += ['', newline]
710 # # Write updated config file
711 # with open(config_file, 'w') as f:
712 # for line in lines:
713 # f.write(f'{line}\n')
714 # # Update loaded config
715 # config['key'] = value
716 #
717 #RV update and bring into Config if needed again
718 #def search(config_file, search_string):
719 # if not os.path.isfile(config_file):
720 # logging.error(f'Unable to load {config_file}')
721 # return False
722 # with open(config_file, 'r') as f:
723 # lines = f.read()
724 # if search_string in lines:
725 # return True
726 # return False
727
728 class Detector:
729 """Class for processing a detector info file or dictionary.
730 """
731 def __init__(self, detector_id):
732 self.detector = {}
733 self.load_flag = False
734 self.validate_flag = False
735
736 # Load detector file
737 self.loadFile(detector_id)
738
739 def loadFile(self, detector_id):
740 """Load a detector file.
741 """
742 if self.load_flag:
743 logging.warning('Overwriting the previously loaded detector file')
744 self.detector = {}
745
746 # Ensure detector file exists
747 if not isinstance(detector_id, str):
748 illegal_value('detector_id', detector_id, 'Detector.loadFile')
749 return
750 detector_file = f'{detector_id}.yaml'
751 if not os.path.isfile(detector_file):
752 detector_file = self.config['detector_id']+'.yaml'
753 if not os.path.isfile(detector_file):
754 logging.error(f'Unable to load detector info file for {detector_id}')
755 return
756
757 # Load detector file
758 with open(detector_file, 'r') as f:
759 self.detector = yaml.safe_load(f)
760
761 # Make sure detector file was correctly loaded
762 if isinstance(self.detector, dict):
763 self.load_flag = True
764 else:
765 logging.error(f'Unable to load dictionary from detector file: {detector_file}')
766 self.detector = {}
767
768 def validate(self):
769 """Returns False if any config parameters is illegal or missing.
770 """
771 if not self.load_flag:
772 logging.error('Load a detector file prior to calling Detector.validate')
773
774 # Check for required first-level keys
775 pars_required = ['detector', 'lens_magnification']
776 pars_missing = [p for p in pars_required if p not in self.detector]
777 if len(pars_missing) > 0:
778 logging.error(f'Missing item(s) in detector file: {", ".join(pars_missing)}')
779 return False
780
781 is_valid = True
782
783 # Check detector pixel config keys
784 pixels = self.detector['detector'].get('pixels')
785 if not pixels:
786 pars_missing.append('detector:pixels')
787 else:
788 rows = pixels.get('rows')
789 if not rows:
790 pars_missing.append('detector:pixels:rows')
791 columns = pixels.get('columns')
792 if not columns:
793 pars_missing.append('detector:pixels:columns')
794 size = pixels.get('size')
795 if not size:
796 pars_missing.append('detector:pixels:size')
797
798 if not len(pars_missing):
799 self.validate_flag = True
800 else:
801 is_valid = False
802
803 return is_valid
804
805 def getPixelSize(self):
806 """Returns the detector pixel size.
807 """
808 if not self.validate_flag:
809 logging.error('Validate detector file info prior to calling Detector.getPixelSize')
810
811 lens_magnification = self.detector.get('lens_magnification')
812 if not isinstance(lens_magnification, (int,float)) or lens_magnification <= 0.:
813 illegal_value('lens_magnification', lens_magnification, 'detector file')
814 return 0
815 pixel_size = self.detector['detector'].get('pixels').get('size')
816 if isinstance(pixel_size, (int,float)):
817 if pixel_size <= 0.:
818 illegal_value('pixel_size', pixel_size, 'detector file')
819 return 0
820 pixel_size /= lens_magnification
821 elif isinstance(pixel_size, list):
822 if ((len(pixel_size) > 2) or
823 (len(pixel_size) == 2 and pixel_size[0] != pixel_size[1])):
824 illegal_value('pixel size', pixel_size, 'detector file')
825 return 0
826 elif not is_num(pixel_size[0], 0.):
827 illegal_value('pixel size', pixel_size, 'detector file')
828 return 0
829 else:
830 pixel_size = pixel_size[0]/lens_magnification
831 else:
832 illegal_value('pixel size', pixel_size, 'detector file')
833 return 0
834
835 return pixel_size
836
837 def getDimensions(self):
838 """Returns the detector pixel dimensions.
839 """
840 if not self.validate_flag:
841 logging.error('Validate detector file info prior to calling Detector.getDimensions')
842
843 pixels = self.detector['detector'].get('pixels')
844 num_rows = pixels.get('rows')
845 if not is_int(num_rows, 1):
846 illegal_value('rows', num_rows, 'detector file')
847 return (0, 0)
848 num_columns = pixels.get('columns')
849 if not is_int(num_columns, 1):
850 illegal_value('columns', num_columns, 'detector file')
851 return (0, 0)
852
853 return num_rows, num_columns