Mercurial > repos > rv43 > tomo_setup
comparison msnc_tools.py @ 4:7405057bcb29 draft default tip
Uploaded
author | rv43 |
---|---|
date | Thu, 24 Mar 2022 17:02:54 +0000 |
parents | f35c772fed27 |
children |
comparison
equal
deleted
inserted
replaced
3:e14621486c18 | 4:7405057bcb29 |
---|---|
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 |