comparison general.py @ 49:26f99fdd8d61 draft

"planemo upload for repository https://github.com/rolfverberg/galaxytools commit 4f7738d02f4a3fd91373f43937ed311b6fe11a12"
author rv43
date Thu, 28 Jul 2022 16:05:24 +0000
parents
children ca61007a60fa
comparison
equal deleted inserted replaced
48:059819ea1f0e 49:26f99fdd8d61
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 try:
17 import h5py
18 except:
19 pass
20 import numpy as np
21 try:
22 import matplotlib.pyplot as plt
23 from matplotlib.widgets import Button
24 except:
25 pass
26
27 from ast import literal_eval
28 from copy import deepcopy
29 from time import time
30
31
32 def depth_list(L): return isinstance(L, list) and max(map(depth_list, L))+1
33 def depth_tuple(T): return isinstance(T, tuple) and max(map(depth_tuple, T))+1
34 def unwrap_tuple(T):
35 if depth_tuple(T) > 1 and len(T) == 1:
36 T = unwrap_tuple(*T)
37 return T
38
39 def illegal_value(value, name, location=None, exit_flag=False):
40 if not isinstance(location, str):
41 location = ''
42 else:
43 location = f'in {location} '
44 if isinstance(name, str):
45 logging.error(f'Illegal value for {name} {location}({value}, {type(value)})')
46 else:
47 logging.error(f'Illegal value {location}({value}, {type(value)})')
48 if exit_flag:
49 raise ValueError
50
51 def is_int(v, v_min=None, v_max=None):
52 """Value is an integer in range v_min <= v <= v_max.
53 """
54 if not isinstance(v, int):
55 return False
56 if v_min is not None and not isinstance(v_min, int):
57 illegal_value(v_min, 'v_min', 'is_int')
58 return False
59 if v_max is not None and not isinstance(v_max, int):
60 illegal_value(v_max, 'v_max', 'is_int')
61 return False
62 if v_min is not None and v_max is not None and v_min > v_max:
63 logging.error(f'Illegal v_min, v_max combination ({v_min}, {v_max})')
64 return False
65 if (v_min is not None and v < v_min) or (v_max is not None and v > v_max):
66 return False
67 return True
68
69 def is_int_pair(v, v_min=None, v_max=None):
70 """Value is an integer pair, each in range v_min <= v[i] <= v_max or
71 v_min[i] <= v[i] <= v_max[i].
72 """
73 if not (isinstance(v, (tuple, list)) and len(v) == 2 and isinstance(v[0], int) and
74 isinstance(v[1], int)):
75 return False
76 if v_min is not None or v_max is not None:
77 if (v_min is None or isinstance(v_min, int)) and (v_max is None or isinstance(v_max, int)):
78 if True in [True if not is_int(vi, v_min=v_min, v_max=v_max) else False for vi in v]:
79 return False
80 elif is_int_pair(v_min) and is_int_pair(v_max):
81 if True in [True if v_min[i] > v_max[i] else False for i in range(2)]:
82 logging.error(f'Illegal v_min, v_max combination ({v_min}, {v_max})')
83 return False
84 if True in [True if not is_int(v[i], v_min[i], v_max[i]) else False for i in range(2)]:
85 return False
86 elif is_int_pair(v_min) and (v_max is None or isinstance(v_max, int)):
87 if True in [True if not is_int(v[i], v_min=v_min[i], v_max=v_max) else False
88 for i in range(2)]:
89 return False
90 elif (v_min is None or isinstance(v_min, int)) and is_int_pair(v_max):
91 if True in [True if not is_int(v[i], v_min=v_min, v_max=v_max[i]) else False
92 for i in range(2)]:
93 return False
94 else:
95 logging.error(f'Illegal v_min or v_max input ({v_min} {type(v_min)} and '+
96 f'{v_max} {type(v_max)})')
97 return False
98 return True
99
100 def is_int_series(l, v_min=None, v_max=None):
101 """Value is a tuple or list of integers, each in range v_min <= l[i] <= v_max.
102 """
103 if v_min is not None and not isinstance(v_min, int):
104 illegal_value(v_min, 'v_min', 'is_int_series')
105 return False
106 if v_max is not None and not isinstance(v_max, int):
107 illegal_value(v_max, 'v_max', 'is_int_series')
108 return False
109 if not isinstance(l, (tuple, list)):
110 return False
111 if True in [True if not is_int(v, v_min=v_min, v_max=v_max) else False for v in l]:
112 return False
113 return True
114
115 def is_num(v, v_min=None, v_max=None):
116 """Value is a number in range v_min <= v <= v_max.
117 """
118 if not isinstance(v, (int, float)):
119 return False
120 if v_min is not None and not isinstance(v_min, (int, float)):
121 illegal_value(v_min, 'v_min', 'is_num')
122 return False
123 if v_max is not None and not isinstance(v_max, (int, float)):
124 illegal_value(v_max, 'v_max', 'is_num')
125 return False
126 if v_min is not None and v_max is not None and v_min > v_max:
127 logging.error(f'Illegal v_min, v_max combination ({v_min}, {v_max})')
128 return False
129 if (v_min is not None and v < v_min) or (v_max is not None and v > v_max):
130 return False
131 return True
132
133 def is_num_pair(v, v_min=None, v_max=None):
134 """Value is a number pair, each in range v_min <= v[i] <= v_max or
135 v_min[i] <= v[i] <= v_max[i].
136 """
137 if not (isinstance(v, (tuple, list)) and len(v) == 2 and isinstance(v[0], (int, float)) and
138 isinstance(v[1], (int, float))):
139 return False
140 if v_min is not None or v_max is not None:
141 if ((v_min is None or isinstance(v_min, (int, float))) and
142 (v_max is None or isinstance(v_max, (int, float)))):
143 if True in [True if not is_num(vi, v_min=v_min, v_max=v_max) else False for vi in v]:
144 return False
145 elif is_num_pair(v_min) and is_num_pair(v_max):
146 if True in [True if v_min[i] > v_max[i] else False for i in range(2)]:
147 logging.error(f'Illegal v_min, v_max combination ({v_min}, {v_max})')
148 return False
149 if True in [True if not is_num(v[i], v_min[i], v_max[i]) else False for i in range(2)]:
150 return False
151 elif is_num_pair(v_min) and (v_max is None or isinstance(v_max, (int, float))):
152 if True in [True if not is_num(v[i], v_min=v_min[i], v_max=v_max) else False
153 for i in range(2)]:
154 return False
155 elif (v_min is None or isinstance(v_min, (int, float))) and is_num_pair(v_max):
156 if True in [True if not is_num(v[i], v_min=v_min, v_max=v_max[i]) else False
157 for i in range(2)]:
158 return False
159 else:
160 logging.error(f'Illegal v_min or v_max input ({v_min} {type(v_min)} and '+
161 f'{v_max} {type(v_max)})')
162 return False
163 return True
164
165 def is_num_series(l, v_min=None, v_max=None):
166 """Value is a tuple or list of numbers, each in range v_min <= l[i] <= v_max.
167 """
168 if v_min is not None and not isinstance(v_min, (int, float)):
169 illegal_value(v_min, 'v_min', 'is_num_series')
170 return False
171 if v_max is not None and not isinstance(v_max, (int, float)):
172 illegal_value(v_max, 'v_max', 'is_num_series')
173 return False
174 if not isinstance(l, (tuple, list)):
175 return False
176 if True in [True if not is_num(v, v_min=v_min, v_max=v_max) else False for v in l]:
177 return False
178 return True
179
180 def is_index(v, v_min=0, v_max=None):
181 """Value is an array index in range v_min <= v < v_max.
182 NOTE v_max IS NOT included!
183 """
184 if isinstance(v_max, int):
185 if v_max <= v_min:
186 logging.error(f'Illegal v_min, v_max combination ({v_min}, {v_max})')
187 return False
188 v_max -= 1
189 return is_int(v, v_min, v_max)
190
191 def is_index_range(v, v_min=0, v_max=None):
192 """Value is an array index range in range v_min <= v[0] <= v[1] <= v_max.
193 NOTE v_max IS included!
194 """
195 if not is_int_pair(v):
196 return False
197 if not isinstance(v_min, int):
198 illegal_value(v_min, 'v_min', 'is_index_range')
199 return False
200 if v_max is not None:
201 if not isinstance(v_max, int):
202 illegal_value(v_max, 'v_max', 'is_index_range')
203 return False
204 if v_max < v_min:
205 logging.error(f'Illegal v_min, v_max combination ({v_min}, {v_max})')
206 return False
207 if not v_min <= v[0] <= v[1] or (v_max is not None and v[1] > v_max):
208 return False
209 return True
210
211 def index_nearest(a, value):
212 a = np.asarray(a)
213 if a.ndim > 1:
214 logging.warning(f'Illegal input array ({a}, {type(a)})')
215 # Round up for .5
216 value *= 1.0+sys.float_info.epsilon
217 return (int)(np.argmin(np.abs(a-value)))
218
219 def index_nearest_low(a, value):
220 a = np.asarray(a)
221 if a.ndim > 1:
222 logging.warning(f'Illegal input array ({a}, {type(a)})')
223 index = int(np.argmin(np.abs(a-value)))
224 if value < a[index] and index > 0:
225 index -= 1
226 return index
227
228 def index_nearest_upp(a, value):
229 a = np.asarray(a)
230 if a.ndim > 1:
231 logging.warning(f'Illegal input array ({a}, {type(a)})')
232 index = int(np.argmin(np.abs(a-value)))
233 if value > a[index] and index < a.size-1:
234 index += 1
235 return index
236
237 def round_to_n(x, n=1):
238 if x == 0.0:
239 return 0
240 else:
241 return round(x, n-1-int(np.floor(np.log10(abs(x)))))
242
243 def round_up_to_n(x, n=1):
244 xr = round_to_n(x, n)
245 if abs(x/xr) > 1.0:
246 xr += np.sign(x)*10**(np.floor(np.log10(abs(x)))+1-n)
247 return xr
248
249 def trunc_to_n(x, n=1):
250 xr = round_to_n(x, n)
251 if abs(xr/x) > 1.0:
252 xr -= np.sign(x)*10**(np.floor(np.log10(abs(x)))+1-n)
253 return xr
254
255 def string_to_list(s):
256 """Return a list of numbers by splitting/expanding a string on any combination of
257 dashes, commas, and/or whitespaces
258 e.g: '1, 3, 5-8,12 ' -> [1, 3, 5, 6, 7, 8, 12]
259 """
260 if not isinstance(s, str):
261 illegal_value(s, location='string_to_list')
262 return None
263 if not len(s):
264 return []
265 try:
266 list1 = [x for x in re.split('\s+,\s+|\s+,|,\s+|\s+|,', s.strip())]
267 except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
268 return None
269 try:
270 l = []
271 for l1 in list1:
272 l2 = [literal_eval(x) for x in re.split('\s+-\s+|\s+-|-\s+|\s+|-', l1)]
273 if len(l2) == 1:
274 l += l2
275 elif len(l2) == 2 and l2[1] > l2[0]:
276 l += [i for i in range(l2[0], l2[1]+1)]
277 else:
278 raise ValueError
279 except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
280 return None
281 return sorted(set(l))
282
283 def get_trailing_int(string):
284 indexRegex = re.compile(r'\d+$')
285 mo = indexRegex.search(string)
286 if mo is None:
287 return None
288 else:
289 return int(mo.group())
290
291 def input_int(s=None, v_min=None, v_max=None, default=None, inset=None):
292 if default is not None:
293 if not isinstance(default, int):
294 illegal_value(default, 'default', 'input_int')
295 return None
296 default_string = f' [{default}]'
297 else:
298 default_string = ''
299 if v_min is not None:
300 if not isinstance(v_min, int):
301 illegal_value(v_min, 'v_min', 'input_int')
302 return None
303 if default is not None and default < v_min:
304 logging.error('Illegal v_min, default combination ({v_min}, {default})')
305 return None
306 if v_max is not None:
307 if not isinstance(v_max, int):
308 illegal_value(v_max, 'v_max', 'input_int')
309 return None
310 if v_min is not None and v_min > v_max:
311 logging.error(f'Illegal v_min, v_max combination ({v_min}, {v_max})')
312 return None
313 if default is not None and default > v_max:
314 logging.error('Illegal default, v_max combination ({default}, {v_max})')
315 return None
316 if inset is not None:
317 if (not isinstance(inset, (tuple, list)) or False in [True if isinstance(i, int) else
318 False for i in inset]):
319 illegal_value(inset, 'inset', 'input_int')
320 return None
321 if v_min is not None and v_max is not None:
322 v_range = f' ({v_min}, {v_max})'
323 elif v_min is not None:
324 v_range = f' (>= {v_min})'
325 elif v_max is not None:
326 v_range = f' (<= {v_max})'
327 else:
328 v_range = ''
329 if s is None:
330 print(f'Enter an integer{v_range}{default_string}: ')
331 else:
332 print(f'{s}{v_range}{default_string}: ')
333 try:
334 i = input()
335 if isinstance(i, str) and not len(i):
336 v = default
337 else:
338 v = literal_eval(i)
339 if inset and v not in inset:
340 raise ValueError(f'{v} not part of the set {inset}')
341 except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
342 v = None
343 except:
344 print('Unexpected error')
345 raise
346 if not is_int(v, v_min, v_max):
347 print('Illegal input, enter a valid integer')
348 v = input_int(s, v_min, v_max, default)
349 return v
350
351 def input_num(s=None, v_min=None, v_max=None, default=None):
352 if default is not None:
353 if not isinstance(default, (int, float)):
354 illegal_value(default, 'default', 'input_num')
355 return None
356 default_string = f' [{default}]'
357 else:
358 default_string = ''
359 if v_min is not None:
360 if not isinstance(v_min, (int, float)):
361 illegal_value(vmin, 'vmin', 'input_num')
362 return None
363 if default is not None and default < v_min:
364 logging.error('Illegal v_min, default combination ({v_min}, {default})')
365 return None
366 if v_max is not None:
367 if not isinstance(v_max, (int, float)):
368 illegal_value(vmax, 'vmax', 'input_num')
369 return None
370 if v_min is not None and v_max < v_min:
371 logging.error(f'Illegal v_min, v_max combination ({v_min}, {v_max})')
372 return None
373 if default is not None and default > v_max:
374 logging.error('Illegal default, v_max combination ({default}, {v_max})')
375 return None
376 if v_min is not None and v_max is not None:
377 v_range = f' ({v_min}, {v_max})'
378 elif v_min is not None:
379 v_range = f' (>= {v_min})'
380 elif v_max is not None:
381 v_range = f' (<= {v_max})'
382 else:
383 v_range = ''
384 if s is None:
385 print(f'Enter a number{v_range}{default_string}: ')
386 else:
387 print(f'{s}{v_range}{default_string}: ')
388 try:
389 i = input()
390 if isinstance(i, str) and not len(i):
391 v = default
392 else:
393 v = literal_eval(i)
394 except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
395 v = None
396 except:
397 print('Unexpected error')
398 raise
399 if not is_num(v, v_min, v_max):
400 print('Illegal input, enter a valid number')
401 v = input_num(s, v_min, v_max, default)
402 return v
403
404 def input_int_list(s=None, v_min=None, v_max=None):
405 if v_min is not None and not isinstance(v_min, int):
406 illegal_value(vmin, 'vmin', 'input_int_list')
407 return None
408 if v_max is not None:
409 if not isinstance(v_max, int):
410 illegal_value(vmax, 'vmax', 'input_int_list')
411 return None
412 if v_max < v_min:
413 logging.error(f'Illegal v_min, v_max combination ({v_min}, {v_max})')
414 return None
415 if v_min is not None and v_max is not None:
416 v_range = f' (each value in ({v_min}, {v_max}))'
417 elif v_min is not None:
418 v_range = f' (each value >= {v_min})'
419 elif v_max is not None:
420 v_range = f' (each value <= {v_max})'
421 else:
422 v_range = ''
423 if s is None:
424 print(f'Enter a series of integers{v_range}: ')
425 else:
426 print(f'{s}{v_range}: ')
427 try:
428 l = string_to_list(input())
429 except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError):
430 l = None
431 except:
432 print('Unexpected error')
433 raise
434 if (not isinstance(l, list) or
435 True in [True if not is_int(v, v_min, v_max) else False for v in l]):
436 print('Illegal input: enter a valid set of dash/comma/whitespace separated integers '+
437 'e.g. 2,3,5-8,10')
438 l = input_int_list(s, v_min, v_max)
439 return l
440
441 def input_yesno(s=None, default=None):
442 if default is not None:
443 if not isinstance(default, str):
444 illegal_value(default, 'default', 'input_yesno')
445 return None
446 if default.lower() in 'yes':
447 default = 'y'
448 elif default.lower() in 'no':
449 default = 'n'
450 else:
451 illegal_value(default, 'default', 'input_yesno')
452 return None
453 default_string = f' [{default}]'
454 else:
455 default_string = ''
456 if s is None:
457 print(f'Enter yes or no{default_string}: ')
458 else:
459 print(f'{s}{default_string}: ')
460 i = input()
461 if isinstance(i, str) and not len(i):
462 i = default
463 if i is not None and i.lower() in 'yes':
464 v = True
465 elif i is not None and i.lower() in 'no':
466 v = False
467 else:
468 print('Illegal input, enter yes or no')
469 v = input_yesno(s, default)
470 return v
471
472 def input_menu(items, default=None, header=None):
473 if not isinstance(items, (tuple, list)) or False in [True if isinstance(i, str) else False
474 for i in items]:
475 illegal_value(items, 'items', 'input_menu')
476 return None
477 if default is not None:
478 if not (isinstance(default, str) and default in items):
479 logging.error(f'Illegal value for default ({default}), must be in {items}')
480 return None
481 default_string = f' [{items.index(default)+1}]'
482 else:
483 default_string = ''
484 if header is None:
485 print(f'Choose one of the following items (1, {len(items)}){default_string}:')
486 else:
487 print(f'{header} (1, {len(items)}){default_string}:')
488 for i, choice in enumerate(items):
489 print(f' {i+1}: {choice}')
490 choice = input()
491 if isinstance(choice, str) and not len(choice):
492 choice = items.index(default)
493 else:
494 choice = literal_eval(choice)
495 if isinstance(choice, int) and 1 <= choice <= len(items):
496 choice -= 1
497 else:
498 print(f'Illegal choice, enter a number between 1 and {len(items)}')
499 choice = input_menu(items, default)
500 return choice
501
502 def create_mask(x, bounds=None, reverse_mask=False, current_mask=None):
503 # bounds is a pair of number in the same units a x
504 if not isinstance(x, (tuple, list, np.ndarray)) or not len(x):
505 logging.warning(f'Illegal input array ({x}, {type(x)})')
506 return None
507 if bounds is not None and not is_num_pair(bounds):
508 logging.warning(f'Illegal bounds parameter ({bounds} {type(bounds)}, input ignored')
509 bounds = None
510 if bounds is not None:
511 if not reverse_mask:
512 mask = np.logical_and(x > min(bounds), x < max(bounds))
513 else:
514 mask = np.logical_or(x < min(bounds), x > max(bounds))
515 else:
516 mask = np.ones(len(x), dtype=bool)
517 if current_mask is not None:
518 if not isinstance(current_mask, (tuple, list, np.ndarray)) or len(current_mask) != len(x):
519 logging.warning(f'Illegal current_mask ({current_mask}, {type(current_mask)}), '+
520 'input ignored')
521 else:
522 mask = np.logical_and(mask, current_mask)
523 if not True in mask:
524 logging.warning('Entire data array is masked')
525 return mask
526
527 def draw_mask_1d(ydata, xdata=None, current_index_ranges=None, current_mask=None,
528 select_mask=True, num_index_ranges_max=None, title=None, legend=None):
529 def draw_selections(ax):
530 ax.clear()
531 ax.set_title(title)
532 ax.legend([legend])
533 ax.plot(xdata, ydata, 'k')
534 for (low, upp) in current_include:
535 xlow = 0.5*(xdata[max(0, low-1)]+xdata[low])
536 xupp = 0.5*(xdata[upp]+xdata[min(num_data-1, upp+1)])
537 ax.axvspan(xlow, xupp, facecolor='green', alpha=0.5)
538 for (low, upp) in current_exclude:
539 xlow = 0.5*(xdata[max(0, low-1)]+xdata[low])
540 xupp = 0.5*(xdata[upp]+xdata[min(num_data-1, upp+1)])
541 ax.axvspan(xlow, xupp, facecolor='red', alpha=0.5)
542 for (low, upp) in selected_index_ranges:
543 xlow = 0.5*(xdata[max(0, low-1)]+xdata[low])
544 xupp = 0.5*(xdata[upp]+xdata[min(num_data-1, upp+1)])
545 ax.axvspan(xlow, xupp, facecolor=selection_color, alpha=0.5)
546 ax.get_figure().canvas.draw()
547
548 def onclick(event):
549 if event.inaxes in [fig.axes[0]]:
550 selected_index_ranges.append(index_nearest_upp(xdata, event.xdata))
551
552 def onrelease(event):
553 if len(selected_index_ranges) > 0:
554 if isinstance(selected_index_ranges[-1], int):
555 if event.inaxes in [fig.axes[0]]:
556 event.xdata = index_nearest_low(xdata, event.xdata)
557 if selected_index_ranges[-1] <= event.xdata:
558 selected_index_ranges[-1] = (selected_index_ranges[-1], event.xdata)
559 else:
560 selected_index_ranges[-1] = (event.xdata, selected_index_ranges[-1])
561 draw_selections(event.inaxes)
562 else:
563 selected_index_ranges.pop(-1)
564
565 def confirm_selection(event):
566 plt.close()
567
568 def clear_last_selection(event):
569 if len(selected_index_ranges):
570 selected_index_ranges.pop(-1)
571 draw_selections(ax)
572
573 def update_mask(mask):
574 for (low, upp) in selected_index_ranges:
575 selected_mask = np.logical_and(xdata >= xdata[low], xdata <= xdata[upp])
576 mask = np.logical_or(mask, selected_mask)
577 for (low, upp) in unselected_index_ranges:
578 unselected_mask = np.logical_and(xdata >= xdata[low], xdata <= xdata[upp])
579 mask[unselected_mask] = False
580 return mask
581
582 def update_index_ranges(mask):
583 # Update the currently included index ranges (where mask is True)
584 current_include = []
585 for i, m in enumerate(mask):
586 if m == True:
587 if len(current_include) == 0 or type(current_include[-1]) == tuple:
588 current_include.append(i)
589 else:
590 if len(current_include) > 0 and isinstance(current_include[-1], int):
591 current_include[-1] = (current_include[-1], i-1)
592 if len(current_include) > 0 and isinstance(current_include[-1], int):
593 current_include[-1] = (current_include[-1], num_data-1)
594 return current_include
595
596 # Check for valid inputs
597 ydata = np.asarray(ydata)
598 if ydata.ndim > 1:
599 logging.warning(f'Illegal ydata dimension ({ydata.ndim})')
600 return None, None
601 num_data = ydata.size
602 if xdata is None:
603 xdata = np.arange(num_data)
604 else:
605 xdata = np.asarray(xdata, dtype=np.float64)
606 if xdata.ndim > 1 or xdata.size != num_data:
607 logging.warning(f'Illegal xdata shape ({xdata.shape})')
608 return None, None
609 if not np.all(xdata[:-1] < xdata[1:]):
610 logging.warning('Illegal xdata: must be monotonically increasing')
611 return None, None
612 if current_index_ranges is not None:
613 if not isinstance(current_index_ranges, (tuple, list)):
614 logging.warning('Illegal current_index_ranges parameter ({current_index_ranges}, '+
615 f'{type(current_index_ranges)})')
616 return None, None
617 if not isinstance(select_mask, bool):
618 logging.warning('Illegal select_mask parameter ({select_mask}, {type(select_mask)})')
619 return None, None
620 if num_index_ranges_max is not None:
621 logging.warning('num_index_ranges_max input not yet implemented in draw_mask_1d')
622 if title is None:
623 title = 'select ranges of data'
624 elif not isinstance(title, str):
625 illegal(title, 'title')
626 title = ''
627 if legend is None and not isinstance(title, str):
628 illegal(legend, 'legend')
629 legend = None
630
631 if select_mask:
632 title = f'Click and drag to {title} you wish to include'
633 selection_color = 'green'
634 else:
635 title = f'Click and drag to {title} you wish to exclude'
636 selection_color = 'red'
637
638 # Set initial selected mask and the selected/unselected index ranges as needed
639 selected_index_ranges = []
640 unselected_index_ranges = []
641 selected_mask = np.full(xdata.shape, False, dtype=bool)
642 if current_index_ranges is None:
643 if current_mask is None:
644 if not select_mask:
645 selected_index_ranges = [(0, num_data-1)]
646 selected_mask = np.full(xdata.shape, True, dtype=bool)
647 else:
648 selected_mask = np.copy(np.asarray(current_mask, dtype=bool))
649 if current_index_ranges is not None and len(current_index_ranges):
650 current_index_ranges = sorted([(low, upp) for (low, upp) in current_index_ranges])
651 for (low, upp) in current_index_ranges:
652 if low > upp or low >= num_data or upp < 0:
653 continue
654 if low < 0:
655 low = 0
656 if upp >= num_data:
657 upp = num_data-1
658 selected_index_ranges.append((low, upp))
659 selected_mask = update_mask(selected_mask)
660 if current_index_ranges is not None and current_mask is not None:
661 selected_mask = np.logical_and(current_mask, selected_mask)
662 if current_mask is not None:
663 selected_index_ranges = update_index_ranges(selected_mask)
664
665 # Set up range selections for display
666 current_include = selected_index_ranges
667 current_exclude = []
668 selected_index_ranges = []
669 if not len(current_include):
670 if select_mask:
671 current_exclude = [(0, num_data-1)]
672 else:
673 current_include = [(0, num_data-1)]
674 else:
675 if current_include[0][0] > 0:
676 current_exclude.append((0, current_include[0][0]-1))
677 for i in range(1, len(current_include)):
678 current_exclude.append((current_include[i-1][1]+1, current_include[i][0]-1))
679 if current_include[-1][1] < num_data-1:
680 current_exclude.append((current_include[-1][1]+1, num_data-1))
681
682 # Set up matplotlib figure
683 plt.close('all')
684 fig, ax = plt.subplots()
685 plt.subplots_adjust(bottom=0.2)
686 draw_selections(ax)
687
688 # Set up event handling for click-and-drag range selection
689 cid_click = fig.canvas.mpl_connect('button_press_event', onclick)
690 cid_release = fig.canvas.mpl_connect('button_release_event', onrelease)
691
692 # Set up confirm / clear range selection buttons
693 confirm_b = Button(plt.axes([0.75, 0.05, 0.15, 0.075]), 'Confirm')
694 clear_b = Button(plt.axes([0.59, 0.05, 0.15, 0.075]), 'Clear')
695 cid_confirm = confirm_b.on_clicked(confirm_selection)
696 cid_clear = clear_b.on_clicked(clear_last_selection)
697
698 # Show figure
699 plt.show(block=True)
700
701 # Disconnect callbacks when figure is closed
702 fig.canvas.mpl_disconnect(cid_click)
703 fig.canvas.mpl_disconnect(cid_release)
704 confirm_b.disconnect(cid_confirm)
705 clear_b.disconnect(cid_clear)
706
707 # Swap selection depending on select_mask
708 if not select_mask:
709 selected_index_ranges, unselected_index_ranges = unselected_index_ranges, \
710 selected_index_ranges
711
712 # Update the mask with the currently selected/unselected x-ranges
713 selected_mask = update_mask(selected_mask)
714
715 # Update the currently included index ranges (where mask is True)
716 current_include = update_index_ranges(selected_mask)
717
718 return selected_mask, current_include
719
720 def findImageFiles(path, filetype, name=None):
721 if isinstance(name, str):
722 name = f' {name} '
723 else:
724 name = ' '
725 # Find available index range
726 if filetype == 'tif':
727 if not isinstance(path, str) or not os.path.isdir(path):
728 illegal_value(path, 'path', 'findImageRange')
729 return -1, 0, []
730 indexRegex = re.compile(r'\d+')
731 # At this point only tiffs
732 files = sorted([f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f)) and
733 f.endswith('.tif') and indexRegex.search(f)])
734 num_imgs = len(files)
735 if num_imgs < 1:
736 logging.warning('No available'+name+'files')
737 return -1, 0, []
738 first_index = indexRegex.search(files[0]).group()
739 last_index = indexRegex.search(files[-1]).group()
740 if first_index is None or last_index is None:
741 logging.error('Unable to find correctly indexed'+name+'images')
742 return -1, 0, []
743 first_index = int(first_index)
744 last_index = int(last_index)
745 if num_imgs != last_index-first_index+1:
746 logging.error('Non-consecutive set of indices for'+name+'images')
747 return -1, 0, []
748 paths = [os.path.join(path, f) for f in files]
749 elif filetype == 'h5':
750 if not isinstance(path, str) or not os.path.isfile(path):
751 illegal_value(path, 'path', 'findImageRange')
752 return -1, 0, []
753 # At this point only h5 in alamo2 detector style
754 first_index = 0
755 with h5py.File(path, 'r') as f:
756 num_imgs = f['entry/instrument/detector/data'].shape[0]
757 last_index = num_imgs-1
758 paths = [path]
759 else:
760 illegal_value(filetype, 'filetype', 'findImageRange')
761 return -1, 0, []
762 logging.debug('\nNumber of available'+name+f'images: {num_imgs}')
763 logging.debug('Index range of available'+name+f'images: [{first_index}, '+
764 f'{last_index}]')
765
766 return first_index, num_imgs, paths
767
768 def selectImageRange(first_index, offset, num_imgs, name=None, num_required=None):
769 if isinstance(name, str):
770 name = f' {name} '
771 else:
772 name = ' '
773 # Check existing values
774 use_input = False
775 if (is_int(first_index, 0) and is_int(offset, 0) and is_int(num_imgs, 1)):
776 if offset < 0:
777 use_input = input_yesno(f'\nCurrent{name}first index = {first_index}, '+
778 'use this value (y/n)?', 'y')
779 else:
780 use_input = input_yesno(f'\nCurrent{name}first index/offset = '+
781 f'{first_index}/{offset}, use these values (y/n)?', 'y')
782 if num_required is None:
783 if use_input:
784 use_input = input_yesno(f'Current number of{name}images = '+
785 f'{num_imgs}, use this value (y/n)? ', 'y')
786 if use_input:
787 return first_index, offset, num_imgs
788
789 # Check range against requirements
790 if num_imgs < 1:
791 logging.warning('No available'+name+'images')
792 return -1, -1, 0
793 if num_required is None:
794 if num_imgs == 1:
795 return first_index, 0, 1
796 else:
797 if not is_int(num_required, 1):
798 illegal_value(num_required, 'num_required', 'selectImageRange')
799 return -1, -1, 0
800 if num_imgs < num_required:
801 logging.error('Unable to find the required'+name+
802 f'images ({num_imgs} out of {num_required})')
803 return -1, -1, 0
804
805 # Select index range
806 print('\nThe number of available'+name+f'images is {num_imgs}')
807 if num_required is None:
808 last_index = first_index+num_imgs
809 use_all = f'Use all ([{first_index}, {last_index}])'
810 pick_offset = 'Pick a first index offset and a number of images'
811 pick_bounds = 'Pick the first and last index'
812 choice = input_menu([use_all, pick_offset, pick_bounds], default=pick_offset)
813 if not choice:
814 offset = 0
815 elif choice == 1:
816 offset = input_int('Enter the first index offset', 0, last_index-first_index)
817 first_index += offset
818 if first_index == last_index:
819 num_imgs = 1
820 else:
821 num_imgs = input_int('Enter the number of images', 1, num_imgs-offset)
822 else:
823 offset = input_int('Enter the first index', first_index, last_index)
824 first_index += offset
825 num_imgs = input_int('Enter the last index', first_index, last_index)-first_index+1
826 else:
827 use_all = f'Use ([{first_index}, {first_index+num_required-1}])'
828 pick_offset = 'Pick the first index offset'
829 choice = input_menu([use_all, pick_offset], pick_offset)
830 offset = 0
831 if choice == 1:
832 offset = input_int('Enter the first index offset', 0, num_imgs-num_required)
833 first_index += offset
834 num_imgs = num_required
835
836 return first_index, offset, num_imgs
837
838 def loadImage(f, img_x_bounds=None, img_y_bounds=None):
839 """Load a single image from file.
840 """
841 if not os.path.isfile(f):
842 logging.error(f'Unable to load {f}')
843 return None
844 img_read = plt.imread(f)
845 if not img_x_bounds:
846 img_x_bounds = (0, img_read.shape[0])
847 else:
848 if (not isinstance(img_x_bounds, (tuple, list)) or len(img_x_bounds) != 2 or
849 not (0 <= img_x_bounds[0] < img_x_bounds[1] <= img_read.shape[0])):
850 logging.error(f'inconsistent row dimension in {f}')
851 return None
852 if not img_y_bounds:
853 img_y_bounds = (0, img_read.shape[1])
854 else:
855 if (not isinstance(img_y_bounds, list) or len(img_y_bounds) != 2 or
856 not (0 <= img_y_bounds[0] < img_y_bounds[1] <= img_read.shape[1])):
857 logging.error(f'inconsistent column dimension in {f}')
858 return None
859 return img_read[img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]]
860
861 def loadImageStack(files, filetype, img_offset, num_imgs, num_img_skip=0,
862 img_x_bounds=None, img_y_bounds=None):
863 """Load a set of images and return them as a stack.
864 """
865 logging.debug(f'img_offset = {img_offset}')
866 logging.debug(f'num_imgs = {num_imgs}')
867 logging.debug(f'num_img_skip = {num_img_skip}')
868 logging.debug(f'\nfiles:\n{files}\n')
869 img_stack = np.array([])
870 if filetype == 'tif':
871 img_read_stack = []
872 i = 1
873 t0 = time()
874 for f in files[img_offset:img_offset+num_imgs:num_img_skip+1]:
875 if not i%20:
876 logging.info(f' loading {i}/{num_imgs}: {f}')
877 else:
878 logging.debug(f' loading {i}/{num_imgs}: {f}')
879 img_read = loadImage(f, img_x_bounds, img_y_bounds)
880 img_read_stack.append(img_read)
881 i += num_img_skip+1
882 img_stack = np.stack([img_read for img_read in img_read_stack])
883 logging.info(f'... done in {time()-t0:.2f} seconds!')
884 logging.debug(f'img_stack shape = {np.shape(img_stack)}')
885 del img_read_stack, img_read
886 elif filetype == 'h5':
887 if not isinstance(files[0], str) and not os.path.isfile(files[0]):
888 illegal_value(files[0], 'files[0]', 'loadImageStack')
889 return img_stack
890 t0 = time()
891 logging.info(f'Loading {files[0]}')
892 with h5py.File(files[0], 'r') as f:
893 shape = f['entry/instrument/detector/data'].shape
894 if len(shape) != 3:
895 logging.error(f'inconsistent dimensions in {files[0]}')
896 if not img_x_bounds:
897 img_x_bounds = (0, shape[1])
898 else:
899 if (not isinstance(img_x_bounds, (tuple, list)) or len(img_x_bounds) != 2 or
900 not (0 <= img_x_bounds[0] < img_x_bounds[1] <= shape[1])):
901 logging.error(f'inconsistent row dimension in {files[0]} {img_x_bounds} '+
902 f'{shape[1]}')
903 if not img_y_bounds:
904 img_y_bounds = (0, shape[2])
905 else:
906 if (not isinstance(img_y_bounds, list) or len(img_y_bounds) != 2 or
907 not (0 <= img_y_bounds[0] < img_y_bounds[1] <= shape[2])):
908 logging.error(f'inconsistent column dimension in {files[0]}')
909 img_stack = f.get('entry/instrument/detector/data')[
910 img_offset:img_offset+num_imgs:num_img_skip+1,
911 img_x_bounds[0]:img_x_bounds[1],img_y_bounds[0]:img_y_bounds[1]]
912 logging.info(f'... done in {time()-t0:.2f} seconds!')
913 else:
914 illegal_value(filetype, 'filetype', 'loadImageStack')
915 return img_stack
916
917 def combine_tiffs_in_h5(files, num_imgs, h5_filename):
918 img_stack = loadImageStack(files, 'tif', 0, num_imgs)
919 with h5py.File(h5_filename, 'w') as f:
920 f.create_dataset('entry/instrument/detector/data', data=img_stack)
921 del img_stack
922 return [h5_filename]
923
924 def clearImshow(title=None):
925 plt.ioff()
926 if title is None:
927 title = 'quick imshow'
928 elif not isinstance(title, str):
929 illegal_value(title, 'title', 'clearImshow')
930 return
931 plt.close(fig=title)
932
933 def clearPlot(title=None):
934 plt.ioff()
935 if title is None:
936 title = 'quick plot'
937 elif not isinstance(title, str):
938 illegal_value(title, 'title', 'clearPlot')
939 return
940 plt.close(fig=title)
941
942 def quickImshow(a, title=None, path=None, name=None, save_fig=False, save_only=False,
943 clear=True, extent=None, show_grid=False, grid_color='w', grid_linewidth=1, **kwargs):
944 if title is not None and not isinstance(title, str):
945 illegal_value(title, 'title', 'quickImshow')
946 return
947 if path is not None and not isinstance(path, str):
948 illegal_value(path, 'path', 'quickImshow')
949 return
950 if not isinstance(save_fig, bool):
951 illegal_value(save_fig, 'save_fig', 'quickImshow')
952 return
953 if not isinstance(save_only, bool):
954 illegal_value(save_only, 'save_only', 'quickImshow')
955 return
956 if not isinstance(clear, bool):
957 illegal_value(clear, 'clear', 'quickImshow')
958 return
959 if not title:
960 title='quick imshow'
961 # else:
962 # title = re.sub(r"\s+", '_', title)
963 if name is None:
964 ttitle = re.sub(r"\s+", '_', title)
965 if path is None:
966 path = f'{ttitle}.png'
967 else:
968 path = f'{path}/{ttitle}.png'
969 else:
970 if path is None:
971 path = name
972 else:
973 path = f'{path}/{name}'
974 if extent is None:
975 extent = (0, a.shape[1], a.shape[0], 0)
976 if clear:
977 plt.close(fig=title)
978 if not save_only:
979 plt.ion()
980 plt.figure(title)
981 plt.imshow(a, extent=extent, **kwargs)
982 if show_grid:
983 ax = plt.gca()
984 ax.grid(color=grid_color, linewidth=grid_linewidth)
985 # if title != 'quick imshow':
986 # plt.title = title
987 if save_only:
988 plt.savefig(path)
989 plt.close(fig=title)
990 else:
991 if save_fig:
992 plt.savefig(path)
993
994 def quickPlot(*args, xerr=None, yerr=None, vlines=None, title=None, xlim=None, ylim=None,
995 xlabel=None, ylabel=None, legend=None, path=None, name=None, show_grid=False,
996 save_fig=False, save_only=False, clear=True, block=False, **kwargs):
997 if title is not None and not isinstance(title, str):
998 illegal_value(title, 'title', 'quickPlot')
999 title = None
1000 if xlim is not None and not isinstance(xlim, (tuple, list)) and len(xlim) != 2:
1001 illegal_value(xlim, 'xlim', 'quickPlot')
1002 xlim = None
1003 if ylim is not None and not isinstance(ylim, (tuple, list)) and len(ylim) != 2:
1004 illegal_value(ylim, 'ylim', 'quickPlot')
1005 ylim = None
1006 if xlabel is not None and not isinstance(xlabel, str):
1007 illegal_value(xlabel, 'xlabel', 'quickPlot')
1008 xlabel = None
1009 if ylabel is not None and not isinstance(ylabel, str):
1010 illegal_value(ylabel, 'ylabel', 'quickPlot')
1011 ylabel = None
1012 if legend is not None and not isinstance(legend, (tuple, list)):
1013 illegal_value(legend, 'legend', 'quickPlot')
1014 legend = None
1015 if path is not None and not isinstance(path, str):
1016 illegal_value(path, 'path', 'quickPlot')
1017 return
1018 if not isinstance(show_grid, bool):
1019 illegal_value(show_grid, 'show_grid', 'quickPlot')
1020 return
1021 if not isinstance(save_fig, bool):
1022 illegal_value(save_fig, 'save_fig', 'quickPlot')
1023 return
1024 if not isinstance(save_only, bool):
1025 illegal_value(save_only, 'save_only', 'quickPlot')
1026 return
1027 if not isinstance(clear, bool):
1028 illegal_value(clear, 'clear', 'quickPlot')
1029 return
1030 if not isinstance(block, bool):
1031 illegal_value(block, 'block', 'quickPlot')
1032 return
1033 if title is None:
1034 title = 'quick plot'
1035 # else:
1036 # title = re.sub(r"\s+", '_', title)
1037 if name is None:
1038 ttitle = re.sub(r"\s+", '_', title)
1039 if path is None:
1040 path = f'{ttitle}.png'
1041 else:
1042 path = f'{path}/{ttitle}.png'
1043 else:
1044 if path is None:
1045 path = name
1046 else:
1047 path = f'{path}/{name}'
1048 if clear:
1049 plt.close(fig=title)
1050 args = unwrap_tuple(args)
1051 if depth_tuple(args) > 1 and (xerr is not None or yerr is not None):
1052 logging.warning('Error bars ignored form multiple curves')
1053 if not save_only:
1054 if block:
1055 plt.ioff()
1056 else:
1057 plt.ion()
1058 plt.figure(title)
1059 if depth_tuple(args) > 1:
1060 for y in args:
1061 plt.plot(*y, **kwargs)
1062 else:
1063 if xerr is None and yerr is None:
1064 plt.plot(*args, **kwargs)
1065 else:
1066 plt.errorbar(*args, xerr=xerr, yerr=yerr, **kwargs)
1067 if vlines is not None:
1068 for v in vlines:
1069 plt.axvline(v, color='r', linestyle='--', **kwargs)
1070 # if vlines is not None:
1071 # for s in tuple(([x, x], list(plt.gca().get_ylim())) for x in vlines):
1072 # plt.plot(*s, color='red', **kwargs)
1073 if xlim is not None:
1074 plt.xlim(xlim)
1075 if ylim is not None:
1076 plt.ylim(ylim)
1077 if xlabel is not None:
1078 plt.xlabel(xlabel)
1079 if ylabel is not None:
1080 plt.ylabel(ylabel)
1081 if show_grid:
1082 ax = plt.gca()
1083 ax.grid(color='k')#, linewidth=1)
1084 if legend is not None:
1085 plt.legend(legend)
1086 if save_only:
1087 plt.savefig(path)
1088 plt.close(fig=title)
1089 else:
1090 if save_fig:
1091 plt.savefig(path)
1092 if block:
1093 plt.show(block=block)
1094
1095 def selectArrayBounds(a, x_low=None, x_upp=None, num_x_min=None, ask_bounds=False,
1096 title='select array bounds'):
1097 """Interactively select the lower and upper data bounds for a numpy array.
1098 """
1099 if isinstance(a, (tuple, list)):
1100 a = np.array(a)
1101 if not isinstance(a, np.ndarray) or a.ndim != 1:
1102 illegal_value(a.ndim, 'array type or dimension', 'selectArrayBounds')
1103 return None
1104 len_a = len(a)
1105 if num_x_min is None:
1106 num_x_min = 1
1107 else:
1108 if num_x_min < 2 or num_x_min > len_a:
1109 logging.warning('Illegal value for num_x_min in selectArrayBounds, input ignored')
1110 num_x_min = 1
1111
1112 # Ask to use current bounds
1113 if ask_bounds and (x_low is not None or x_upp is not None):
1114 if x_low is None:
1115 x_low = 0
1116 if not is_int(x_low, 0, len_a-num_x_min):
1117 illegal_value(x_low, 'x_low', 'selectArrayBounds')
1118 return None
1119 if x_upp is None:
1120 x_upp = len_a
1121 if not is_int(x_upp, x_low+num_x_min, len_a):
1122 illegal_value(x_upp, 'x_upp', 'selectArrayBounds')
1123 return None
1124 quickPlot((range(len_a), a), vlines=(x_low,x_upp), title=title)
1125 if not input_yesno(f'\nCurrent array bounds: [{x_low}, {x_upp}] '+
1126 'use these values (y/n)?', 'y'):
1127 x_low = None
1128 x_upp = None
1129 else:
1130 clearPlot(title)
1131 return x_low, x_upp
1132
1133 if x_low is None:
1134 x_min = 0
1135 x_max = len_a
1136 x_low_max = len_a-num_x_min
1137 while True:
1138 quickPlot(range(x_min, x_max), a[x_min:x_max], title=title)
1139 zoom_flag = input_yesno('Set lower data bound (y) or zoom in (n)?', 'y')
1140 if zoom_flag:
1141 x_low = input_int(' Set lower data bound', 0, x_low_max)
1142 break
1143 else:
1144 x_min = input_int(' Set lower zoom index', 0, x_low_max)
1145 x_max = input_int(' Set upper zoom index', x_min+1, x_low_max+1)
1146 else:
1147 if not is_int(x_low, 0, len_a-num_x_min):
1148 illegal_value(x_low, 'x_low', 'selectArrayBounds')
1149 return None
1150 if x_upp is None:
1151 x_min = x_low+num_x_min
1152 x_max = len_a
1153 x_upp_min = x_min
1154 while True:
1155 quickPlot(range(x_min, x_max), a[x_min:x_max], title=title)
1156 zoom_flag = input_yesno('Set upper data bound (y) or zoom in (n)?', 'y')
1157 if zoom_flag:
1158 x_upp = input_int(' Set upper data bound', x_upp_min, len_a)
1159 break
1160 else:
1161 x_min = input_int(' Set upper zoom index', x_upp_min, len_a-1)
1162 x_max = input_int(' Set upper zoom index', x_min+1, len_a)
1163 else:
1164 if not is_int(x_upp, x_low+num_x_min, len_a):
1165 illegal_value(x_upp, 'x_upp', 'selectArrayBounds')
1166 return None
1167 print(f'lower bound = {x_low} (inclusive)\nupper bound = {x_upp} (exclusive)]')
1168 quickPlot((range(len_a), a), vlines=(x_low,x_upp), title=title)
1169 if not input_yesno('Accept these bounds (y/n)?', 'y'):
1170 x_low, x_upp = selectArrayBounds(a, None, None, num_x_min, title=title)
1171 clearPlot(title)
1172 return x_low, x_upp
1173
1174 def selectImageBounds(a, axis, low=None, upp=None, num_min=None,
1175 title='select array bounds'):
1176 """Interactively select the lower and upper data bounds for a 2D numpy array.
1177 """
1178 if isinstance(a, np.ndarray):
1179 if a.ndim != 2:
1180 illegal_value(a.ndim, 'array dimension', 'selectImageBounds')
1181 return None
1182 elif isinstance(a, (tuple, list)):
1183 if len(a) != 2:
1184 illegal_value(len(a), 'array dimension', 'selectImageBounds')
1185 return None
1186 if len(a[0]) != len(a[1]) or not (isinstance(a[0], (tuple, list, np.ndarray)) and
1187 isinstance(a[1], (tuple, list, np.ndarray))):
1188 logging.error(f'Illegal array type in selectImageBounds ({type(a[0])} {type(a[1])})')
1189 return None
1190 a = np.array(a)
1191 else:
1192 illegal_value(a, 'array type', 'selectImageBounds')
1193 return None
1194 if axis < 0 or axis >= a.ndim:
1195 illegal_value(axis, 'axis', 'selectImageBounds')
1196 return None
1197 low_save = low
1198 upp_save = upp
1199 num_min_save = num_min
1200 if num_min is None:
1201 num_min = 1
1202 else:
1203 if num_min < 2 or num_min > a.shape[axis]:
1204 logging.warning('Illegal input for num_min in selectImageBounds, input ignored')
1205 num_min = 1
1206 if low is None:
1207 min_ = 0
1208 max_ = a.shape[axis]
1209 low_max = a.shape[axis]-num_min
1210 while True:
1211 if axis:
1212 quickImshow(a[:,min_:max_], title=title, aspect='auto',
1213 extent=[min_,max_,a.shape[0],0])
1214 else:
1215 quickImshow(a[min_:max_,:], title=title, aspect='auto',
1216 extent=[0,a.shape[1], max_,min_])
1217 zoom_flag = input_yesno('Set lower data bound (y) or zoom in (n)?', 'y')
1218 if zoom_flag:
1219 low = input_int(' Set lower data bound', 0, low_max)
1220 break
1221 else:
1222 min_ = input_int(' Set lower zoom index', 0, low_max)
1223 max_ = input_int(' Set upper zoom index', min_+1, low_max+1)
1224 else:
1225 if not is_int(low, 0, a.shape[axis]-num_min):
1226 illegal_value(low, 'low', 'selectImageBounds')
1227 return None
1228 if upp is None:
1229 min_ = low+num_min
1230 max_ = a.shape[axis]
1231 upp_min = min_
1232 while True:
1233 if axis:
1234 quickImshow(a[:,min_:max_], title=title, aspect='auto',
1235 extent=[min_,max_,a.shape[0],0])
1236 else:
1237 quickImshow(a[min_:max_,:], title=title, aspect='auto',
1238 extent=[0,a.shape[1], max_,min_])
1239 zoom_flag = input_yesno('Set upper data bound (y) or zoom in (n)?', 'y')
1240 if zoom_flag:
1241 upp = input_int(' Set upper data bound', upp_min, a.shape[axis])
1242 break
1243 else:
1244 min_ = input_int(' Set upper zoom index', upp_min, a.shape[axis]-1)
1245 max_ = input_int(' Set upper zoom index', min_+1, a.shape[axis])
1246 else:
1247 if not is_int(upp, low+num_min, a.shape[axis]):
1248 illegal_value(upp, 'upp', 'selectImageBounds')
1249 return None
1250 bounds = (low, upp)
1251 a_tmp = np.copy(a)
1252 a_tmp_max = a.max()
1253 if axis:
1254 a_tmp[:,bounds[0]] = a_tmp_max
1255 a_tmp[:,bounds[1]-1] = a_tmp_max
1256 else:
1257 a_tmp[bounds[0],:] = a_tmp_max
1258 a_tmp[bounds[1]-1,:] = a_tmp_max
1259 print(f'lower bound = {low} (inclusive)\nupper bound = {upp} (exclusive)')
1260 quickImshow(a_tmp, title=title)
1261 del a_tmp
1262 if not input_yesno('Accept these bounds (y/n)?', 'y'):
1263 bounds = selectImageBounds(a, axis, low=low_save, upp=upp_save, num_min=num_min_save,
1264 title=title)
1265 return bounds
1266
1267
1268 class Config:
1269 """Base class for processing a config file or dictionary.
1270 """
1271 def __init__(self, config_file=None, config_dict=None):
1272 self.config = {}
1273 self.load_flag = False
1274 self.suffix = None
1275
1276 # Load config file
1277 if config_file is not None and config_dict is not None:
1278 logging.warning('Ignoring config_dict (both config_file and config_dict are specified)')
1279 if config_file is not None:
1280 self.loadFile(config_file)
1281 elif config_dict is not None:
1282 self.loadDict(config_dict)
1283
1284 def loadFile(self, config_file):
1285 """Load a config file.
1286 """
1287 if self.load_flag:
1288 logging.warning('Overwriting any previously loaded config file')
1289 self.config = {}
1290
1291 # Ensure config file exists
1292 if not os.path.isfile(config_file):
1293 logging.error(f'Unable to load {config_file}')
1294 return
1295
1296 # Load config file (for now for Galaxy, allow .dat extension)
1297 self.suffix = os.path.splitext(config_file)[1]
1298 if self.suffix == '.yml' or self.suffix == '.yaml' or self.suffix == '.dat':
1299 with open(config_file, 'r') as f:
1300 self.config = yaml.safe_load(f)
1301 elif self.suffix == '.txt':
1302 with open(config_file, 'r') as f:
1303 lines = f.read().splitlines()
1304 self.config = {item[0].strip():literal_eval(item[1].strip()) for item in
1305 [line.split('#')[0].split('=') for line in lines if '=' in line.split('#')[0]]}
1306 else:
1307 illegal_value(self.suffix, 'config file extension', 'Config.loadFile')
1308
1309 # Make sure config file was correctly loaded
1310 if isinstance(self.config, dict):
1311 self.load_flag = True
1312 else:
1313 logging.error(f'Unable to load dictionary from config file: {config_file}')
1314 self.config = {}
1315
1316 def loadDict(self, config_dict):
1317 """Takes a dictionary and places it into self.config.
1318 """
1319 if self.load_flag:
1320 logging.warning('Overwriting the previously loaded config file')
1321
1322 if isinstance(config_dict, dict):
1323 self.config = config_dict
1324 self.load_flag = True
1325 else:
1326 illegal_value(config_dict, 'dictionary config object', 'Config.loadDict')
1327 self.config = {}
1328
1329 def saveFile(self, config_file):
1330 """Save the config file (as a yaml file only right now).
1331 """
1332 suffix = os.path.splitext(config_file)[1]
1333 if suffix != '.yml' and suffix != '.yaml':
1334 illegal_value(suffix, 'config file extension', 'Config.saveFile')
1335
1336 # Check if config file exists
1337 if os.path.isfile(config_file):
1338 logging.info(f'Updating {config_file}')
1339 else:
1340 logging.info(f'Saving {config_file}')
1341
1342 # Save config file
1343 with open(config_file, 'w') as f:
1344 yaml.safe_dump(self.config, f)
1345
1346 def validate(self, pars_required, pars_missing=None):
1347 """Returns False if any required keys are missing.
1348 """
1349 if not self.load_flag:
1350 logging.error('Load a config file prior to calling Config.validate')
1351
1352 def validate_nested_pars(config, par):
1353 par_levels = par.split(':')
1354 first_level_par = par_levels[0]
1355 try:
1356 first_level_par = int(first_level_par)
1357 except:
1358 pass
1359 try:
1360 next_level_config = config[first_level_par]
1361 if len(par_levels) > 1:
1362 next_level_par = ':'.join(par_levels[1:])
1363 return validate_nested_pars(next_level_config, next_level_par)
1364 else:
1365 return True
1366 except:
1367 return False
1368
1369 pars_missing = [p for p in pars_required if not validate_nested_pars(self.config, p)]
1370 if len(pars_missing) > 0:
1371 logging.error(f'Missing item(s) in configuration: {", ".join(pars_missing)}')
1372 return False
1373 else:
1374 return True