Mercurial > repos > rv43 > tomo
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 |