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 |
