comparison msnc_tools.py @ 0:cb1b0d757704 draft

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