ialdev-core 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- iad/core/__init__.py +9 -0
- iad/core/array.py +1961 -0
- iad/core/binary.py +377 -0
- iad/core/cache.py +903 -0
- iad/core/codetools.py +203 -0
- iad/core/datatools.py +671 -0
- iad/core/docs/locators.ipynb +754 -0
- iad/core/dotstyle.py +99 -0
- iad/core/env.py +271 -0
- iad/core/events.py +650 -0
- iad/core/filesproc.py +1046 -0
- iad/core/fnctools.py +390 -0
- iad/core/label.py +240 -0
- iad/core/logs.py +182 -0
- iad/core/nptools.py +449 -0
- iad/core/one_dark.puml +881 -0
- iad/core/param/__init__.py +17 -0
- iad/core/param/confargparse.py +55 -0
- iad/core/param/paramaze.py +339 -0
- iad/core/param/tbox.py +277 -0
- iad/core/paths.py +563 -0
- iad/core/pdtools.py +2570 -0
- iad/core/pydantools/__init__.py +5 -0
- iad/core/pydantools/fixed_pydantic_yaml/__init__.py +32 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/__init__.py +0 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/hacks.py +76 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/old_enums.py +37 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/representers.py +92 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/types.py +122 -0
- iad/core/pydantools/fixed_pydantic_yaml/compat/yaml_lib.py +104 -0
- iad/core/pydantools/fixed_pydantic_yaml/ext/__init__.py +1 -0
- iad/core/pydantools/fixed_pydantic_yaml/ext/semver.py +152 -0
- iad/core/pydantools/fixed_pydantic_yaml/ext/versioned_model.py +113 -0
- iad/core/pydantools/fixed_pydantic_yaml/main.py +30 -0
- iad/core/pydantools/fixed_pydantic_yaml/mixin.py +281 -0
- iad/core/pydantools/fixed_pydantic_yaml/model.py +20 -0
- iad/core/pydantools/fixed_pydantic_yaml/py.typed +1 -0
- iad/core/pydantools/fixed_pydantic_yaml/version.py +1 -0
- iad/core/pydantools/models.py +560 -0
- iad/core/regexp.py +348 -0
- iad/core/short.py +308 -0
- iad/core/strings.py +635 -0
- iad/core/unc_panda.py +270 -0
- iad/core/units.py +58 -0
- iad/core/wrap.py +420 -0
- ialdev_core-0.1.0.dist-info/METADATA +73 -0
- ialdev_core-0.1.0.dist-info/RECORD +48 -0
- ialdev_core-0.1.0.dist-info/WHEEL +4 -0
iad/core/logs.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from logging import (getLogger, Formatter, FileHandler, StreamHandler, _nameToLevel as nameToLevel,
|
|
5
|
+
DEBUG, INFO, FATAL, ERROR, WARNING, WARN)
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
# noinspection PyUnresolvedReferences
|
|
9
|
+
__all__ = ['getLogger', 'Formatter', 'FileHandler', 'StreamHandler', 'logging', 'logger',
|
|
10
|
+
'nameToLevel', 'DEBUG', 'INFO', 'FATAL', 'ERROR', 'WARNING', 'WARN']
|
|
11
|
+
|
|
12
|
+
from typing import TypeVar
|
|
13
|
+
|
|
14
|
+
prf_fmt = '%(relativeCreated)5d|%(name)13s.%(funcName)-14s|%(levelname)7s|%(message)s'
|
|
15
|
+
reg_fmt = '▷%(relativeCreated)5d|%(name)-14s|%(levelname)7s🚦%(message)s'
|
|
16
|
+
def_fmt = reg_fmt
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def set_format(profile=False):
|
|
20
|
+
global def_fmt
|
|
21
|
+
fmt = prf_fmt if profile else reg_fmt
|
|
22
|
+
def_fmt = fmt
|
|
23
|
+
log = getLogger('root')
|
|
24
|
+
if not log.hasHandlers():
|
|
25
|
+
handler = StreamHandler()
|
|
26
|
+
log.addHandler(handler)
|
|
27
|
+
log.handlers[0].setFormatter(Formatter(fmt))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def_datefmt = '%H:%M:%S'
|
|
31
|
+
T = TypeVar('T')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _add_levels_attrs(obj: T) -> T:
|
|
35
|
+
if not hasattr(obj, 'DEBUG'): # already assigned
|
|
36
|
+
for name, val in nameToLevel.items():
|
|
37
|
+
setattr(obj, name, val)
|
|
38
|
+
return obj
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def module_log_file(file: str | Path):
|
|
42
|
+
"""Constructs default log file name from module file"""
|
|
43
|
+
from .filesproc import Locator
|
|
44
|
+
out_folder = Locator('/tmp/ramdisk', '/tmp', envar='TEMP').first_existing()
|
|
45
|
+
if not out_folder:
|
|
46
|
+
from tempfile import mkdtemp
|
|
47
|
+
out_folder = Path(mkdtemp())
|
|
48
|
+
|
|
49
|
+
path = out_folder / 'logs' / f"{Path(file).name.split('.')[0]}.log"
|
|
50
|
+
msg = f"Log file: {str(path)}"
|
|
51
|
+
print(f"(!) ---> {msg}")
|
|
52
|
+
logging.getLogger().info(f'Log file: {msg}')
|
|
53
|
+
return path
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def set_levels(logs_levels: dict[str, str | int] = None, *,
|
|
57
|
+
debug=None, error=None, info=None, warn=None, **levels_logs: str):
|
|
58
|
+
"""
|
|
59
|
+
Set levels for multiple logs:
|
|
60
|
+
|
|
61
|
+
>>> set_levels(
|
|
62
|
+
... {'deep_debug': 2},
|
|
63
|
+
... debug = 'general',
|
|
64
|
+
... info = ['scan', 'post'],
|
|
65
|
+
... error ='root',
|
|
66
|
+
... critical='another'
|
|
67
|
+
... )
|
|
68
|
+
|
|
69
|
+
:param logs_levels:
|
|
70
|
+
:param levels_logs: {level: log(s)}
|
|
71
|
+
:param info: log or logs name(s)
|
|
72
|
+
:param debug: log or logs name(s)
|
|
73
|
+
:param warn: log or logs name(s)
|
|
74
|
+
:param error: log or logs name(s)
|
|
75
|
+
"""
|
|
76
|
+
from .short import drop_undef
|
|
77
|
+
|
|
78
|
+
to_num = lambda lvl: nameToLevel[lvl.upper()] if isinstance(lvl, str) else lvl
|
|
79
|
+
levels_logs |= drop_undef('debug', 'error', 'info', 'warn', ns=locals())
|
|
80
|
+
levels_logs = {to_num(lvl): logs for lvl, logs in levels_logs.items()
|
|
81
|
+
} | {lvl: logs for logs, lvl in (logs_levels or {}).items()}
|
|
82
|
+
|
|
83
|
+
from . import as_iter
|
|
84
|
+
for level, logs in levels_logs.items():
|
|
85
|
+
for log in as_iter(logs):
|
|
86
|
+
getLogger(log).setLevel(level)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def logger(name: str = None, *, fmt=None, datefmt=None, level=None,
|
|
90
|
+
add_handler: str | Path | bool | None = False):
|
|
91
|
+
"""
|
|
92
|
+
Create stream logger with given settings, or return an existing one
|
|
93
|
+
without altering.
|
|
94
|
+
|
|
95
|
+
:param name: name to by accessed by
|
|
96
|
+
:param fmt: handler formatting string
|
|
97
|
+
:param level: logger level
|
|
98
|
+
:param level: handler level
|
|
99
|
+
:param add_handler: if True - adds default stream handler
|
|
100
|
+
if str or Path - add
|
|
101
|
+
:return: logger
|
|
102
|
+
"""
|
|
103
|
+
log = getLogger(name)
|
|
104
|
+
|
|
105
|
+
add_formatter = fmt or datefmt
|
|
106
|
+
|
|
107
|
+
if isinstance(add_handler, (str, Path)):
|
|
108
|
+
file = Path(add_handler).absolute()
|
|
109
|
+
for h in log.handlers:
|
|
110
|
+
if isinstance(h, FileHandler) and str(file) == h.baseFilename:
|
|
111
|
+
break
|
|
112
|
+
else:
|
|
113
|
+
file.parent.mkdir(exist_ok=True)
|
|
114
|
+
handler = FileHandler(file, mode='wt', encoding='utf-8')
|
|
115
|
+
log.addHandler(handler)
|
|
116
|
+
add_formatter = True
|
|
117
|
+
elif add_handler is True or add_handler is None and not log.hasHandlers():
|
|
118
|
+
handler = StreamHandler()
|
|
119
|
+
log.addHandler(handler)
|
|
120
|
+
add_formatter = True
|
|
121
|
+
elif add_handler is not False:
|
|
122
|
+
raise TypeError(f"Invalid {add_handler=}")
|
|
123
|
+
|
|
124
|
+
if add_formatter:
|
|
125
|
+
formatter = Formatter(fmt or def_fmt, datefmt=datefmt or def_datefmt)
|
|
126
|
+
for handler in log.handlers:
|
|
127
|
+
handler.setFormatter(formatter)
|
|
128
|
+
|
|
129
|
+
if level:
|
|
130
|
+
log.setLevel(level)
|
|
131
|
+
|
|
132
|
+
return _add_levels_attrs(log)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def setup_logs(*, file=None, name_from=None, profile=False, **levels_logs):
|
|
136
|
+
"""
|
|
137
|
+
High level function to quickly set basic logs config
|
|
138
|
+
:param file: full path to the log file
|
|
139
|
+
:param name_from: <name> from this path is used: ../logs/<name>.log
|
|
140
|
+
:param levels_logs: {log_levels: loggers_names}
|
|
141
|
+
:param profile: if True set profiling friendly format
|
|
142
|
+
"""
|
|
143
|
+
# TODO: what if all three arguments not given ?
|
|
144
|
+
if name_from:
|
|
145
|
+
if file: raise ValueError("Use either `file` OR `name_from` argument!")
|
|
146
|
+
file = module_log_file(name_from)
|
|
147
|
+
|
|
148
|
+
if file:
|
|
149
|
+
logger(None, add_handler=file)
|
|
150
|
+
|
|
151
|
+
if levels_logs:
|
|
152
|
+
set_levels(**levels_logs)
|
|
153
|
+
|
|
154
|
+
set_format(profile)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def error(err: BaseException | type[BaseException], msg: str | None = None,
|
|
158
|
+
fail: bool = True, level: int | str = 'ERROR',
|
|
159
|
+
logger: logging.Logger | str | None = None):
|
|
160
|
+
"""
|
|
161
|
+
Routes errors into logger and optionally raise Exception.
|
|
162
|
+
|
|
163
|
+
:param err: Exception object ot type
|
|
164
|
+
:param msg: message to log / throw
|
|
165
|
+
:param fail: if True raise requested Exception
|
|
166
|
+
:param level: logging level
|
|
167
|
+
:param logger: logger or its name or None for root
|
|
168
|
+
"""
|
|
169
|
+
if isinstance(err, type) and issubclass(err, BaseException):
|
|
170
|
+
err = err(msg) # create exception object given class, message
|
|
171
|
+
elif msg: # replace message in given exception object
|
|
172
|
+
err = type(err)(msg)
|
|
173
|
+
else: # extract message from exception object
|
|
174
|
+
msg = err.args
|
|
175
|
+
|
|
176
|
+
if logger is None or isinstance(logger, str):
|
|
177
|
+
logger = getLogger(logger)
|
|
178
|
+
if not isinstance(level, int):
|
|
179
|
+
level = nameToLevel[level]
|
|
180
|
+
logger.log(level, msg)
|
|
181
|
+
if fail:
|
|
182
|
+
raise err
|
iad/core/nptools.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
""" Convenience tools for NumPy package """
|
|
2
|
+
|
|
3
|
+
__all__ = ['var_filter', 'lindex', 'xy2i', 'Array', 'min_ids',
|
|
4
|
+
'valid_in_range']
|
|
5
|
+
|
|
6
|
+
from typing import Union, Any
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from scipy import signal
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Array(np.ndarray):
|
|
13
|
+
_default_options = dict(
|
|
14
|
+
rows=16,
|
|
15
|
+
cols=12,
|
|
16
|
+
precision=3,
|
|
17
|
+
stats=1e6,
|
|
18
|
+
info=True
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_print_options = _default_options.copy()
|
|
22
|
+
|
|
23
|
+
UNDEF = object()
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def set_printoptions(self, rows=UNDEF, cols=UNDEF, stats=UNDEF, info=UNDEF,
|
|
27
|
+
precision=UNDEF, **options):
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
:param cols: maximal columns to show content
|
|
31
|
+
:param rows: maximal rows to show content
|
|
32
|
+
:param stats: maximal number of elements to calc stats for info
|
|
33
|
+
:param info: bool - show info summary, (None - automatic)
|
|
34
|
+
:param precision (one of np.setprintoptions)
|
|
35
|
+
:param options: other options for `np.set_printoptions`
|
|
36
|
+
:return:
|
|
37
|
+
"""
|
|
38
|
+
options = {k: v for k, v in options.items() if k in np.get_printoptions()}
|
|
39
|
+
|
|
40
|
+
loc = locals()
|
|
41
|
+
defined = {k: v for k, v in loc.items() if k in self._default_options and v is not self.UNDEF}
|
|
42
|
+
|
|
43
|
+
self._print_options.update(**defined, **options)
|
|
44
|
+
|
|
45
|
+
def __repr__(self):
|
|
46
|
+
return array_info_func(**self._print_options)(self)
|
|
47
|
+
|
|
48
|
+
def __str__(self):
|
|
49
|
+
return self.__repr__()
|
|
50
|
+
|
|
51
|
+
def __new__(cls, obj, *, dtype=None, **kwargs):
|
|
52
|
+
return np.asarray(obj, dtype).view(cls)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def valid_in_range(m, min_v, max_v):
|
|
56
|
+
"""Returns boolean mask
|
|
57
|
+
with True where m is not nan and in the given range.
|
|
58
|
+
Avoids Runtime warning on nan comparisons!
|
|
59
|
+
:param m: array
|
|
60
|
+
:param min_v: m >= min_v
|
|
61
|
+
:param max_v: m <= max_v
|
|
62
|
+
:return:
|
|
63
|
+
"""
|
|
64
|
+
np.warnings.filterwarnings('ignore')
|
|
65
|
+
valid = m >= min_v
|
|
66
|
+
np.logical_and(valid, m < max_v, out=valid)
|
|
67
|
+
np.warnings.resetwarnings()
|
|
68
|
+
return valid
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def var_filter(im, ker_sz):
|
|
72
|
+
""" Fast variance in a window filter (normalized by n*n-1)
|
|
73
|
+
|
|
74
|
+
:param im: 2D array
|
|
75
|
+
:param ker_sz: windows 1D size (will create window sz x sz)
|
|
76
|
+
:return: 2D array os same size as im
|
|
77
|
+
"""
|
|
78
|
+
if hasattr(ker_sz, '__len__'):
|
|
79
|
+
assert len(ker_sz) == 2
|
|
80
|
+
var_win = ker_sz
|
|
81
|
+
else:
|
|
82
|
+
var_win = (ker_sz, ker_sz)
|
|
83
|
+
assert var_win[0] % 2 == 1 and var_win[1] % 2 == 1
|
|
84
|
+
|
|
85
|
+
flt = np.ones(var_win, dtype='int')
|
|
86
|
+
im = im.astype(float)
|
|
87
|
+
sum_x2 = signal.convolve2d(im ** 2, flt, mode='same', boundary='symm')
|
|
88
|
+
sum_x = signal.convolve2d(im, flt, mode='same', boundary='symm')
|
|
89
|
+
num = var_win[0] * var_win[1]
|
|
90
|
+
return (sum_x2 * num - sum_x ** 2) / (num ** 2)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def has_nans(a) -> bool:
|
|
94
|
+
# Checks if an array has any nans (fast implementation)
|
|
95
|
+
a = a.reshape(a.size)
|
|
96
|
+
return np.isnan(np.dot(a, a))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def lindex(coords):
|
|
100
|
+
"""
|
|
101
|
+
Create index for n-dimensional array from array of N n-dimensional coordinates.
|
|
102
|
+
Coordinates may not be integer - they are converted to int inside
|
|
103
|
+
:param coords: (N x n) N - number of coordinates, n -dimension of array to index
|
|
104
|
+
:return: numpy nd array compatible index : tuple of tuples
|
|
105
|
+
"""
|
|
106
|
+
return tuple(zip(*coords.astype(int)))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def xy2i(xy, y=None):
|
|
110
|
+
"""
|
|
111
|
+
Translate into ij 2d-indexes various representation of set of x-y coordinates.
|
|
112
|
+
|
|
113
|
+
:param xy: x or array[N x (x,y)] or obj with attrs or items 'x', 'y'
|
|
114
|
+
:param y: if xy is x, then y - otherwise None!
|
|
115
|
+
:return: index for 2d arrays addressing tuple(is, js)
|
|
116
|
+
|
|
117
|
+
Examples:
|
|
118
|
+
xy2i(x, y) # numerical arrays of x, y coordinates
|
|
119
|
+
|
|
120
|
+
xy2i(xy_array) # convertible to Nx2 array of pairs [[x0, y0], ..., ]
|
|
121
|
+
|
|
122
|
+
xy2i(dict_xy) # object with items ['x'] and ['y']
|
|
123
|
+
|
|
124
|
+
xy2i(obj_xy) # object with attributes .x and .y
|
|
125
|
+
|
|
126
|
+
array [Nx2] of N 2-dimensional coordinates (x, y)
|
|
127
|
+
|
|
128
|
+
Create tuple(is, js) index selecting corresponding elements from a 2D array.
|
|
129
|
+
- values casted to int!
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
"""
|
|
133
|
+
if y is None:
|
|
134
|
+
if hasattr(xy, 'x') and hasattr(xy, 'y'):
|
|
135
|
+
xy = xy.x, xy.y
|
|
136
|
+
elif hasattr(xy, '__getitem__') and 'x' in xy and 'y' in xy:
|
|
137
|
+
xy = xy['x'], xy['y']
|
|
138
|
+
else:
|
|
139
|
+
xy = np.array(xy, dtype=int).T
|
|
140
|
+
else:
|
|
141
|
+
xy = (xy, y)
|
|
142
|
+
return tuple(np.round(np.array(v)).astype(dtype=int) for v in xy[::-1])
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def slice_1_axis(ndim, n, sl):
|
|
146
|
+
""" Create n-dim slice with given 1-d slice for given dimension
|
|
147
|
+
and rest of dimensions not sliced
|
|
148
|
+
:param ndim: number of dimensions
|
|
149
|
+
:param n: location of the slice dimension
|
|
150
|
+
:param sl: the slice to insert
|
|
151
|
+
"""
|
|
152
|
+
assert 0 <= n < ndim
|
|
153
|
+
if ndim == 1:
|
|
154
|
+
return sl
|
|
155
|
+
ll = [slice(None), ] * (ndim - 1)
|
|
156
|
+
ll.insert(n, sl)
|
|
157
|
+
return tuple(ll)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def min_ids(a, n=1, *, axis=-1):
|
|
161
|
+
"""
|
|
162
|
+
Find indices of n smallest elements along given axis.
|
|
163
|
+
Args:
|
|
164
|
+
a - array
|
|
165
|
+
n - number of best elements
|
|
166
|
+
axis - direction to sort by (default -1 to flatten)
|
|
167
|
+
Return:
|
|
168
|
+
For a.shape == N_0 ... N_axis ... N_dim, returns
|
|
169
|
+
array.shape == N_0 ... n ... N_dim
|
|
170
|
+
"""
|
|
171
|
+
return np.argpartition(a, n - 1, axis=axis).take(range(n), axis=axis)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def abool(value: Union[Any, np.ndarray]) -> bool:
|
|
175
|
+
""" A convenience utility: boolean conversion which works on both scalars and numpy arrays.
|
|
176
|
+
|
|
177
|
+
For arrays returns:
|
|
178
|
+
Args:
|
|
179
|
+
value: any type supporting bool() or numpy array
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True | False
|
|
183
|
+
|
|
184
|
+
"""
|
|
185
|
+
return value.all() if isinstance(value, np.ndarray) else bool(value)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def array_info_str(a, stats=1e7):
|
|
189
|
+
"""
|
|
190
|
+
Short description of array, like:
|
|
191
|
+
[4×816×1200×7]f4 ⌊-30.53|454.01⌋ 0.2%NA
|
|
192
|
+
|
|
193
|
+
To be used with or formatters:
|
|
194
|
+
np.set_string_function(array_info_str)
|
|
195
|
+
|
|
196
|
+
:param a: array-like object
|
|
197
|
+
:param stats: maximal number of elements to add calculate statistics
|
|
198
|
+
"""
|
|
199
|
+
sz = 1
|
|
200
|
+
for _ in a.shape: sz *= _ # to make it work for both numpy and torch tensors
|
|
201
|
+
s = '×'.join(map(str, a.shape)) or (f"val={a.item()}" if sz == 1 else '×')
|
|
202
|
+
|
|
203
|
+
if isinstance(a, np.ndarray):
|
|
204
|
+
kind = a.dtype.kind
|
|
205
|
+
s = f"[{s}]{kind}{a.dtype.itemsize}"
|
|
206
|
+
else: # torch! - No stats and fancy type info implemented yet
|
|
207
|
+
dtype = str(a.dtype)[6:].replace('float', 'f') # remove 'torch.'
|
|
208
|
+
return f"[{s}]{dtype}{(':c' if a.is_cuda else '')}"
|
|
209
|
+
|
|
210
|
+
if not 1 < sz < stats: return s
|
|
211
|
+
|
|
212
|
+
if kind == 'f':
|
|
213
|
+
_a = a[np.isfinite(a)]
|
|
214
|
+
mn, mx = np.min(_a), np.max(_a)
|
|
215
|
+
s = f"{s} ⌊{mn:.3g}|{mx:.3g}⌉"
|
|
216
|
+
|
|
217
|
+
def occurrence_info(name, num, num_lim=1000):
|
|
218
|
+
if not (num := int(num)):
|
|
219
|
+
return ''
|
|
220
|
+
ss = str(num) if num < num_lim else f"{num / a.size}%"
|
|
221
|
+
return f"{ss}{name}"
|
|
222
|
+
|
|
223
|
+
if occ := ', '.join(filter(None, [
|
|
224
|
+
occurrence_info('∅', nans := np.isnan(a).sum()),
|
|
225
|
+
occurrence_info('∞', a.size - _a.size - nans) # _a excluded both inf and nan!
|
|
226
|
+
])): s += f"({occ})"
|
|
227
|
+
|
|
228
|
+
elif kind != 'O':
|
|
229
|
+
s += f" ⌊{a.min():.3g}|{a.max():.3g}⌉"
|
|
230
|
+
|
|
231
|
+
return s
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def ascii_map(a, levels=None, show=False):
|
|
235
|
+
"""
|
|
236
|
+
Unicode representation of array as map of shaded blocks.
|
|
237
|
+
:param a: array
|
|
238
|
+
:param levels: number of levels
|
|
239
|
+
:param show: if True - print debug info
|
|
240
|
+
:return: string (for printing)
|
|
241
|
+
"""
|
|
242
|
+
import re
|
|
243
|
+
shades = '·⬚░▒▓█'
|
|
244
|
+
full = lambda smb: re.sub(r'[0\[\]]', ' ', str(np.full_like(a, 1, dtype=int)).replace('1', smb))
|
|
245
|
+
|
|
246
|
+
mx = a.max()
|
|
247
|
+
mn = a.min()
|
|
248
|
+
|
|
249
|
+
if not (np.isfinite(mn) and np.isfinite(mx)):
|
|
250
|
+
return full('⊗')
|
|
251
|
+
if mn == mx:
|
|
252
|
+
return full(shades[0] if mn == 0 else shades[-1])
|
|
253
|
+
|
|
254
|
+
ua = np.unique(a)
|
|
255
|
+
if levels is None:
|
|
256
|
+
levels = min(len(shades), len(ua))
|
|
257
|
+
|
|
258
|
+
if levels < 2:
|
|
259
|
+
return full()
|
|
260
|
+
|
|
261
|
+
max_level = levels - 1
|
|
262
|
+
assert max_level < len(shades)
|
|
263
|
+
idx = np.linspace(0, len(shades) - 1, max_level + 1, dtype=int)
|
|
264
|
+
|
|
265
|
+
levels = np.linspace(0, max_level, levels, dtype=int)
|
|
266
|
+
if len(ua) == max_level + 1:
|
|
267
|
+
la = np.empty_like(a, dtype=int)
|
|
268
|
+
for l, v in zip(levels, ua):
|
|
269
|
+
la[a == v] = l
|
|
270
|
+
# print(la)
|
|
271
|
+
s = str(la)
|
|
272
|
+
else:
|
|
273
|
+
s = str(((max_level / (mx - mn)) * (a - mn)).astype(int))
|
|
274
|
+
|
|
275
|
+
if show:
|
|
276
|
+
print('unique:', ua)
|
|
277
|
+
print('idx: ', idx)
|
|
278
|
+
print('levels:', levels)
|
|
279
|
+
|
|
280
|
+
for i, l in zip(idx, levels):
|
|
281
|
+
s = s.replace(str(l), shades[i])
|
|
282
|
+
return re.sub(r'[0\[\]]', ' ', s)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def pprint_arrays(do=True, **kws):
|
|
286
|
+
"""
|
|
287
|
+
Set representation function of nd arrays and torch tensors
|
|
288
|
+
to ``array_info_func(**kws)``.
|
|
289
|
+
|
|
290
|
+
:param do: set to False to reset
|
|
291
|
+
:param kws: kwargs for array_info_func
|
|
292
|
+
"""
|
|
293
|
+
if do:
|
|
294
|
+
func = array_info_func(**kws)
|
|
295
|
+
np.set_string_function(func)
|
|
296
|
+
pprint_tensors(do)
|
|
297
|
+
else:
|
|
298
|
+
np.set_string_function()
|
|
299
|
+
pprint_tensors(False)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def pprint_tensors(do=True):
|
|
303
|
+
"""
|
|
304
|
+
Session level hack to replace (or restore)
|
|
305
|
+
``Tensor.__repr__`` with ``array_info_func()``
|
|
306
|
+
"""
|
|
307
|
+
try:
|
|
308
|
+
import torch
|
|
309
|
+
except ModuleNotFoundError:
|
|
310
|
+
from warnings import warn
|
|
311
|
+
do and warn("torch not found Can't hack Tensor repr")
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
if do:
|
|
315
|
+
torch.Tensor._original_repr = torch.Tensor.__repr__
|
|
316
|
+
torch.Tensor.__repr__ = array_info_func()
|
|
317
|
+
else:
|
|
318
|
+
torch.Tensor.__repr__ = torch.Tensor._original_repr
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def array_info_func(cols=12, rows=8, *, stats=1e7, info=None, **options):
|
|
322
|
+
""" Return regular numpy array smart compact representation,
|
|
323
|
+
optionally including info summary (`array_info_str`).
|
|
324
|
+
|
|
325
|
+
:param cols: maximal columns to show content
|
|
326
|
+
:param rows: maximal rows to show content
|
|
327
|
+
:param stats: maximal number of elements to calc stats for info
|
|
328
|
+
:param info: bool - show info summary, (None - automatic)
|
|
329
|
+
:param options: for `set_printoptions`, like:
|
|
330
|
+
edgeitems=3, threshold=1000, floatmode=maxprec, precision=2,
|
|
331
|
+
suppress=False, linewidth=75
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
def content_repr(a):
|
|
335
|
+
if options:
|
|
336
|
+
prev = np.get_printoptions()
|
|
337
|
+
np.set_printoptions(**options)
|
|
338
|
+
s = np.ndarray.__str__(a)
|
|
339
|
+
np.set_printoptions(**prev)
|
|
340
|
+
return s
|
|
341
|
+
return np.ndarray.__str__(a)
|
|
342
|
+
|
|
343
|
+
def is_compact(last_dim, size):
|
|
344
|
+
return last_dim == 0 or (
|
|
345
|
+
0 < last_dim <= cols and size / last_dim <= rows)
|
|
346
|
+
|
|
347
|
+
def full_repr(a):
|
|
348
|
+
show_content = a.ndim and (info is False or is_compact(
|
|
349
|
+
a.shape[-1], a.size if isinstance(a, np.ndarray) else a.numel()
|
|
350
|
+
))
|
|
351
|
+
content = show_content and content_repr(a)
|
|
352
|
+
if content and not info: # return only content unless info is requested
|
|
353
|
+
return content
|
|
354
|
+
|
|
355
|
+
inf = array_info_str(a, stats=stats)
|
|
356
|
+
return f"{inf}:\n{content}" if content else inf
|
|
357
|
+
|
|
358
|
+
return full_repr
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def stats(data, measure, out=str, prec=3, sep=', '):
|
|
362
|
+
"""Calculate multiple metrics on data
|
|
363
|
+
:param data: array
|
|
364
|
+
:param measure: list of functions names from `numpy` module
|
|
365
|
+
first try to prepend `nan` to the name.
|
|
366
|
+
:param out: if `str` return result as string otherwise dict
|
|
367
|
+
":param prec: precision of the fixed-point representation
|
|
368
|
+
"""
|
|
369
|
+
import numpy as np
|
|
370
|
+
itr = [(msr, getattr(np, 'nan' + msr, getattr(np, msr))(data)) for msr in measure]
|
|
371
|
+
if out is str:
|
|
372
|
+
fmt = f"{{}}: {{:.{prec}f}}"
|
|
373
|
+
return sep.join(map(lambda mv: fmt.format(*mv), itr))
|
|
374
|
+
return dict(itr)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def cast_float(a, dtype=int, *, nan=0, inf='nan'):
|
|
378
|
+
"""
|
|
379
|
+
From floating point array create integer typed and set nans and inf to specific values
|
|
380
|
+
|
|
381
|
+
:param a: array
|
|
382
|
+
:param nan: value to replace nans
|
|
383
|
+
:param inf: value to replace inf or
|
|
384
|
+
'nan' - same as nan
|
|
385
|
+
'clip' - clip by maximal positive or negative int
|
|
386
|
+
:param dtype: int type to cast into
|
|
387
|
+
:return: int array
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
res = a.astype(dtype)
|
|
391
|
+
|
|
392
|
+
if inf == 'nan':
|
|
393
|
+
res[~np.isfinite(a)] = nan
|
|
394
|
+
return res
|
|
395
|
+
|
|
396
|
+
res[np.isnan(a)] = nan
|
|
397
|
+
if inf == 'clip':
|
|
398
|
+
res[a == np.inf] = np.iinfo(dtype).max
|
|
399
|
+
res[a == -np.inf] = np.iinfo(dtype).min
|
|
400
|
+
else:
|
|
401
|
+
res[np.abs(a) == np.inf] = inf
|
|
402
|
+
|
|
403
|
+
return res
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def fill_mask(a, mask, *, val=np.nan, inplace=False):
|
|
407
|
+
"""
|
|
408
|
+
Fill array with elements where `~mask` set to `nan`
|
|
409
|
+
:param a:
|
|
410
|
+
:param mask: binary array describing (True) area to fill
|
|
411
|
+
:param val: value to fill the mask with (default - nan)
|
|
412
|
+
:param inplace: change and return the input array
|
|
413
|
+
:return:
|
|
414
|
+
"""
|
|
415
|
+
na = a if inplace else a.copy()
|
|
416
|
+
na[mask] = val
|
|
417
|
+
return na
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def full_indices(shape, *, base=1, dtype=int):
|
|
421
|
+
"""
|
|
422
|
+
Create array of given shape full of values composed of its indices.
|
|
423
|
+
|
|
424
|
+
Example:
|
|
425
|
+
|
|
426
|
+
>>> full_indices([2,3])
|
|
427
|
+
array([[11, 21, 31],
|
|
428
|
+
[12, 22, 32]])
|
|
429
|
+
|
|
430
|
+
:param shape: shape to create
|
|
431
|
+
:param base: number to start the indexing from (usually 0 or 1)
|
|
432
|
+
:param dtype: of resulting array.
|
|
433
|
+
:return: array filled with index values
|
|
434
|
+
"""
|
|
435
|
+
a = np.empty(shape, dtype=dtype)
|
|
436
|
+
p10 = np.power(10, np.arange(len(shape))).astype(int)
|
|
437
|
+
for idx, _ in np.ndenumerate(a):
|
|
438
|
+
a[idx] = ((np.array(idx) + base).astype(int) * p10).sum()
|
|
439
|
+
return a
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def copy_nans(src, dst):
|
|
443
|
+
"""Set nans found in source into same locations in dst """
|
|
444
|
+
if src.dtype.kind == 'f':
|
|
445
|
+
nans = np.isnan(src)
|
|
446
|
+
if np.count_nonzero(nans):
|
|
447
|
+
dst = dst.copy()
|
|
448
|
+
dst[nans] = np.nan
|
|
449
|
+
return dst
|