geometallurgy 0.4.13__py3-none-any.whl → 0.4.15__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.
- elphick/geomet/__init__.py +11 -11
- elphick/geomet/base.py +1133 -1133
- elphick/geomet/block_model.py +319 -319
- elphick/geomet/config/__init__.py +1 -1
- elphick/geomet/config/config_read.py +39 -39
- elphick/geomet/config/flowsheet_example_partition.yaml +31 -31
- elphick/geomet/config/flowsheet_example_simple.yaml +25 -25
- elphick/geomet/config/mc_config.yml +35 -35
- elphick/geomet/data/downloader.py +39 -39
- elphick/geomet/data/register.csv +12 -12
- elphick/geomet/datasets/__init__.py +2 -2
- elphick/geomet/datasets/datasets.py +47 -47
- elphick/geomet/datasets/downloader.py +40 -40
- elphick/geomet/datasets/register.csv +12 -12
- elphick/geomet/datasets/sample_data.py +196 -196
- elphick/geomet/extras.py +35 -35
- elphick/geomet/flowsheet/__init__.py +1 -1
- elphick/geomet/flowsheet/flowsheet.py +1216 -1216
- elphick/geomet/flowsheet/loader.py +99 -99
- elphick/geomet/flowsheet/operation.py +256 -256
- elphick/geomet/flowsheet/stream.py +39 -39
- elphick/geomet/interval_sample.py +641 -641
- elphick/geomet/io.py +379 -379
- elphick/geomet/plot.py +147 -147
- elphick/geomet/sample.py +28 -28
- elphick/geomet/utils/amenability.py +49 -49
- elphick/geomet/utils/block_model_converter.py +93 -93
- elphick/geomet/utils/components.py +136 -136
- elphick/geomet/utils/data.py +49 -49
- elphick/geomet/utils/estimates.py +108 -108
- elphick/geomet/utils/interp.py +193 -193
- elphick/geomet/utils/interp2.py +134 -134
- elphick/geomet/utils/layout.py +72 -72
- elphick/geomet/utils/moisture.py +61 -61
- elphick/geomet/utils/pandas.py +378 -378
- elphick/geomet/utils/parallel.py +29 -29
- elphick/geomet/utils/partition.py +63 -63
- elphick/geomet/utils/size.py +51 -51
- elphick/geomet/utils/timer.py +80 -80
- elphick/geomet/utils/viz.py +56 -56
- elphick/geomet/validate.py.hide +176 -176
- {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/LICENSE +21 -21
- {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/METADATA +2 -3
- geometallurgy-0.4.15.dist-info/RECORD +48 -0
- {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/WHEEL +1 -1
- elphick/geomet/utils/output.html +0 -617
- geometallurgy-0.4.13.dist-info/RECORD +0 -49
- {geometallurgy-0.4.13.dist-info → geometallurgy-0.4.15.dist-info}/entry_points.txt +0 -0
elphick/geomet/utils/parallel.py
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
from joblib import Parallel
|
|
2
|
-
from tqdm import tqdm
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
class TqdmParallel(Parallel):
|
|
6
|
-
def __init__(self, *args, **kwargs):
|
|
7
|
-
self._desc = kwargs.pop('desc', None) # Get the description from kwargs
|
|
8
|
-
self._tqdm = tqdm(total=kwargs.pop('total', None), desc=self._desc) # Pass the description to tqdm
|
|
9
|
-
super().__init__(*args, **kwargs)
|
|
10
|
-
|
|
11
|
-
def __call__(self, iterable):
|
|
12
|
-
iterable = list(iterable)
|
|
13
|
-
self._tqdm.total = len(iterable)
|
|
14
|
-
result = super().__call__(iterable)
|
|
15
|
-
self._tqdm.close()
|
|
16
|
-
return result
|
|
17
|
-
|
|
18
|
-
def _print(self, msg, *msg_args):
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
def print_progress(self):
|
|
22
|
-
self._tqdm.update()
|
|
23
|
-
|
|
24
|
-
def _dispatch(self, batch):
|
|
25
|
-
job_idx = super()._dispatch(batch)
|
|
26
|
-
return job_idx
|
|
27
|
-
|
|
28
|
-
def _collect(self, output):
|
|
29
|
-
return super()._collect(output)
|
|
1
|
+
from joblib import Parallel
|
|
2
|
+
from tqdm import tqdm
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TqdmParallel(Parallel):
|
|
6
|
+
def __init__(self, *args, **kwargs):
|
|
7
|
+
self._desc = kwargs.pop('desc', None) # Get the description from kwargs
|
|
8
|
+
self._tqdm = tqdm(total=kwargs.pop('total', None), desc=self._desc) # Pass the description to tqdm
|
|
9
|
+
super().__init__(*args, **kwargs)
|
|
10
|
+
|
|
11
|
+
def __call__(self, iterable):
|
|
12
|
+
iterable = list(iterable)
|
|
13
|
+
self._tqdm.total = len(iterable)
|
|
14
|
+
result = super().__call__(iterable)
|
|
15
|
+
self._tqdm.close()
|
|
16
|
+
return result
|
|
17
|
+
|
|
18
|
+
def _print(self, msg, *msg_args):
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
def print_progress(self):
|
|
22
|
+
self._tqdm.update()
|
|
23
|
+
|
|
24
|
+
def _dispatch(self, batch):
|
|
25
|
+
job_idx = super()._dispatch(batch)
|
|
26
|
+
return job_idx
|
|
27
|
+
|
|
28
|
+
def _collect(self, output):
|
|
29
|
+
return super()._collect(output)
|
|
@@ -1,63 +1,63 @@
|
|
|
1
|
-
import importlib
|
|
2
|
-
from functools import partial
|
|
3
|
-
|
|
4
|
-
import numpy as np
|
|
5
|
-
import pandas as pd
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def perfect(x: np.ndarray, d50: float) -> np.ndarray:
|
|
9
|
-
"""A perfect partition
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
x: The input dimension, e.g. size or density
|
|
13
|
-
d50: The cut-point
|
|
14
|
-
|
|
15
|
-
Returns:
|
|
16
|
-
|
|
17
|
-
"""
|
|
18
|
-
pn: np.ndarray = np.where(x >= d50, 1.0, 0.0)
|
|
19
|
-
return pn
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def napier_munn(x: np.ndarray, d50: float, ep: float) -> np.ndarray:
|
|
23
|
-
"""The Napier-Munn partition (1998)
|
|
24
|
-
|
|
25
|
-
REF: https://www.sciencedirect.com/science/article/pii/S1474667016453036
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
x: The input dimension, e.g. size or density
|
|
29
|
-
d50: The cut-point
|
|
30
|
-
ep: The Escarte Probable
|
|
31
|
-
|
|
32
|
-
Returns:
|
|
33
|
-
|
|
34
|
-
"""
|
|
35
|
-
pn: np.ndarray = 1 / (1 + np.exp(1.099 * (d50 - x) / ep))
|
|
36
|
-
return pn
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def napier_munn_size(size: np.ndarray, d50: float, ep: float) -> np.ndarray:
|
|
40
|
-
return napier_munn(size, d50, ep)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def napier_munn_density(density: np.ndarray, d50: float, ep: float) -> np.ndarray:
|
|
44
|
-
return napier_munn(density, d50, ep)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
napier_munn_size_1mm = partial(napier_munn_size, d50=1.0, ep=0.1)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def load_partition_function(module_name, function_name):
|
|
51
|
-
module = importlib.import_module(module_name)
|
|
52
|
-
return getattr(module, function_name)
|
|
53
|
-
|
|
54
|
-
# if __name__ == '__main__':
|
|
55
|
-
# da = np.arange(0, 10)
|
|
56
|
-
# PN = perfect(da, d50=6.3)
|
|
57
|
-
# df = pd.DataFrame([da, PN], index=['da', 'pn']).T
|
|
58
|
-
# print(df)
|
|
59
|
-
#
|
|
60
|
-
# da = np.arange(0, 10)
|
|
61
|
-
# PN = napier_munn(da, d50=6.3, ep=0.1)
|
|
62
|
-
# df = pd.DataFrame([da, PN], index=['da', 'pn']).T
|
|
63
|
-
# print(df)
|
|
1
|
+
import importlib
|
|
2
|
+
from functools import partial
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def perfect(x: np.ndarray, d50: float) -> np.ndarray:
|
|
9
|
+
"""A perfect partition
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
x: The input dimension, e.g. size or density
|
|
13
|
+
d50: The cut-point
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
pn: np.ndarray = np.where(x >= d50, 1.0, 0.0)
|
|
19
|
+
return pn
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def napier_munn(x: np.ndarray, d50: float, ep: float) -> np.ndarray:
|
|
23
|
+
"""The Napier-Munn partition (1998)
|
|
24
|
+
|
|
25
|
+
REF: https://www.sciencedirect.com/science/article/pii/S1474667016453036
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
x: The input dimension, e.g. size or density
|
|
29
|
+
d50: The cut-point
|
|
30
|
+
ep: The Escarte Probable
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
|
|
34
|
+
"""
|
|
35
|
+
pn: np.ndarray = 1 / (1 + np.exp(1.099 * (d50 - x) / ep))
|
|
36
|
+
return pn
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def napier_munn_size(size: np.ndarray, d50: float, ep: float) -> np.ndarray:
|
|
40
|
+
return napier_munn(size, d50, ep)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def napier_munn_density(density: np.ndarray, d50: float, ep: float) -> np.ndarray:
|
|
44
|
+
return napier_munn(density, d50, ep)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
napier_munn_size_1mm = partial(napier_munn_size, d50=1.0, ep=0.1)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def load_partition_function(module_name, function_name):
|
|
51
|
+
module = importlib.import_module(module_name)
|
|
52
|
+
return getattr(module, function_name)
|
|
53
|
+
|
|
54
|
+
# if __name__ == '__main__':
|
|
55
|
+
# da = np.arange(0, 10)
|
|
56
|
+
# PN = perfect(da, d50=6.3)
|
|
57
|
+
# df = pd.DataFrame([da, PN], index=['da', 'pn']).T
|
|
58
|
+
# print(df)
|
|
59
|
+
#
|
|
60
|
+
# da = np.arange(0, 10)
|
|
61
|
+
# PN = napier_munn(da, d50=6.3, ep=0.1)
|
|
62
|
+
# df = pd.DataFrame([da, PN], index=['da', 'pn']).T
|
|
63
|
+
# print(df)
|
elphick/geomet/utils/size.py
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
from typing import Union
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
from pandas import IntervalIndex
|
|
5
|
-
from pandas.arrays import IntervalArray
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def mean_size(size_intervals: Union[IntervalArray, IntervalIndex]) -> np.ndarray:
|
|
9
|
-
"""Geometric mean size
|
|
10
|
-
|
|
11
|
-
Size calculations are performed using the geometric mean, not the arithmetic mean
|
|
12
|
-
|
|
13
|
-
NOTE: If geometric mean is used for the pan fraction (0.0mm retained) it will return zero, which is an
|
|
14
|
-
edge size not mean size. So the mean ratio of the geometric mean to the arithmetic mean for all other
|
|
15
|
-
fractions is used for the bottom fraction.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Args:
|
|
19
|
-
size_intervals: A pandas IntervalArray or IntervalIndex
|
|
20
|
-
|
|
21
|
-
Returns:
|
|
22
|
-
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
intervals = size_intervals.copy()
|
|
26
|
-
res = np.array((intervals.left * intervals.right) ** 0.5)
|
|
27
|
-
|
|
28
|
-
geomean_mean_ratio: float = float(np.mean((res[0:-1] / intervals.mid[0:-1])))
|
|
29
|
-
|
|
30
|
-
if np.isclose(size_intervals.min().left, 0.0):
|
|
31
|
-
res[np.isclose(size_intervals.left, 0.0)] = size_intervals.min().mid * geomean_mean_ratio
|
|
32
|
-
|
|
33
|
-
return res
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# REF: https://www.globalgilson.com/blog/sieve-sizes
|
|
37
|
-
|
|
38
|
-
sizes_iso_565 = [63.0, 56.0, 53.0, 50.0, 45.0, 40.0, 37.5, 35.5, 31.5, 28.0, 26.5, 25.0, 22.4, 20.0,
|
|
39
|
-
19.0, 18.0, 16.0, 14.0, 13.2, 12.5, 11.2, 10.0, 9.5, 9.0, 8.0, 7.1, 6.7, 6.3, 5.6,
|
|
40
|
-
5.0, 4.75, 4.5, 4.0, 3.55, 3.35, 3.15, 2.8, 2.5, 2.36, 2.0, 1.8, 1.7, 1.6, 1.4, 1.25,
|
|
41
|
-
1.18, 1.12, 1.0, 0.900, 0.850, 0.800, 0.710, 0.630, 0.600, 0.560, 0.500, 0.450, 0.425,
|
|
42
|
-
0.400, 0.355, 0.315, 0.300, 0.280, 0.250, 0.224, 0.212, 0.200, 0.180, 0.160, 0.150, 0.140,
|
|
43
|
-
0.125, 0.112, 0.106, 0.100, 0.090, 0.080, 0.075, 0.071, 0.063, 0.056, 0.053, 0.050, 0.045,
|
|
44
|
-
0.040, 0.038, 0.036, 0.032, 0.025, 0.020, 0.0]
|
|
45
|
-
|
|
46
|
-
sizes_astm_e11 = [100.0, 90.0, 75.0, 63.0, 53.0, 50.0, 45.0, 37.5, 31.5, 26.5, 25.0, 22.4, 19.0, 16.0,
|
|
47
|
-
13.2, 12.5, 11.2, 9.5, 8.0, 6.7, 6.3, 5.6, 4.75, 4.0, 3.35, 2.8, 2.36, 2.0, 1.7, 1.4,
|
|
48
|
-
1.18, 1.0, 0.850, 0.710, 0.600, 0.500, 0.425, 0.355, 0.300, 0.250, 0.212, 0.180, 0.150,
|
|
49
|
-
0.125, 0.106, 0.090, 0.075, 0.063, 0.053, 0.045, 0.038, 0.032, 0.025, 0.020, 0.0]
|
|
50
|
-
|
|
51
|
-
sizes_all = sorted(list(set(sizes_astm_e11).union(set(sizes_iso_565))), reverse=True)
|
|
1
|
+
from typing import Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pandas import IntervalIndex
|
|
5
|
+
from pandas.arrays import IntervalArray
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def mean_size(size_intervals: Union[IntervalArray, IntervalIndex]) -> np.ndarray:
|
|
9
|
+
"""Geometric mean size
|
|
10
|
+
|
|
11
|
+
Size calculations are performed using the geometric mean, not the arithmetic mean
|
|
12
|
+
|
|
13
|
+
NOTE: If geometric mean is used for the pan fraction (0.0mm retained) it will return zero, which is an
|
|
14
|
+
edge size not mean size. So the mean ratio of the geometric mean to the arithmetic mean for all other
|
|
15
|
+
fractions is used for the bottom fraction.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
size_intervals: A pandas IntervalArray or IntervalIndex
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
intervals = size_intervals.copy()
|
|
26
|
+
res = np.array((intervals.left * intervals.right) ** 0.5)
|
|
27
|
+
|
|
28
|
+
geomean_mean_ratio: float = float(np.mean((res[0:-1] / intervals.mid[0:-1])))
|
|
29
|
+
|
|
30
|
+
if np.isclose(size_intervals.min().left, 0.0):
|
|
31
|
+
res[np.isclose(size_intervals.left, 0.0)] = size_intervals.min().mid * geomean_mean_ratio
|
|
32
|
+
|
|
33
|
+
return res
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# REF: https://www.globalgilson.com/blog/sieve-sizes
|
|
37
|
+
|
|
38
|
+
sizes_iso_565 = [63.0, 56.0, 53.0, 50.0, 45.0, 40.0, 37.5, 35.5, 31.5, 28.0, 26.5, 25.0, 22.4, 20.0,
|
|
39
|
+
19.0, 18.0, 16.0, 14.0, 13.2, 12.5, 11.2, 10.0, 9.5, 9.0, 8.0, 7.1, 6.7, 6.3, 5.6,
|
|
40
|
+
5.0, 4.75, 4.5, 4.0, 3.55, 3.35, 3.15, 2.8, 2.5, 2.36, 2.0, 1.8, 1.7, 1.6, 1.4, 1.25,
|
|
41
|
+
1.18, 1.12, 1.0, 0.900, 0.850, 0.800, 0.710, 0.630, 0.600, 0.560, 0.500, 0.450, 0.425,
|
|
42
|
+
0.400, 0.355, 0.315, 0.300, 0.280, 0.250, 0.224, 0.212, 0.200, 0.180, 0.160, 0.150, 0.140,
|
|
43
|
+
0.125, 0.112, 0.106, 0.100, 0.090, 0.080, 0.075, 0.071, 0.063, 0.056, 0.053, 0.050, 0.045,
|
|
44
|
+
0.040, 0.038, 0.036, 0.032, 0.025, 0.020, 0.0]
|
|
45
|
+
|
|
46
|
+
sizes_astm_e11 = [100.0, 90.0, 75.0, 63.0, 53.0, 50.0, 45.0, 37.5, 31.5, 26.5, 25.0, 22.4, 19.0, 16.0,
|
|
47
|
+
13.2, 12.5, 11.2, 9.5, 8.0, 6.7, 6.3, 5.6, 4.75, 4.0, 3.35, 2.8, 2.36, 2.0, 1.7, 1.4,
|
|
48
|
+
1.18, 1.0, 0.850, 0.710, 0.600, 0.500, 0.425, 0.355, 0.300, 0.250, 0.212, 0.180, 0.150,
|
|
49
|
+
0.125, 0.106, 0.090, 0.075, 0.063, 0.053, 0.045, 0.038, 0.032, 0.025, 0.020, 0.0]
|
|
50
|
+
|
|
51
|
+
sizes_all = sorted(list(set(sizes_astm_e11).union(set(sizes_iso_565))), reverse=True)
|
elphick/geomet/utils/timer.py
CHANGED
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
"""
|
|
2
|
-
REF: https://ankitbko.github.io/blog/2021/04/logging-in-python/
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import functools
|
|
6
|
-
import logging
|
|
7
|
-
from datetime import datetime
|
|
8
|
-
from typing import Union
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class MyLogger:
|
|
12
|
-
def __init__(self):
|
|
13
|
-
logging.basicConfig(level=logging.DEBUG,
|
|
14
|
-
format=' %(asctime)s - %(levelname)s - %(message)s')
|
|
15
|
-
|
|
16
|
-
def get_logger(self, name=None):
|
|
17
|
-
return logging.getLogger(name)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def get_default_logger():
|
|
21
|
-
return MyLogger().get_logger()
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def log_timer(_func=None, *, my_logger: Union[MyLogger, logging.Logger] = None):
|
|
25
|
-
def decorator_log(func):
|
|
26
|
-
@functools.wraps(func)
|
|
27
|
-
def wrapper(*args, **kwargs):
|
|
28
|
-
logger = get_default_logger()
|
|
29
|
-
try:
|
|
30
|
-
if my_logger is None:
|
|
31
|
-
first_args = next(iter(args), None) # capture first arg to check for `self`
|
|
32
|
-
logger_params = [ # does kwargs have any logger
|
|
33
|
-
x
|
|
34
|
-
for x in kwargs.values()
|
|
35
|
-
if isinstance(x, logging.Logger) or isinstance(x, MyLogger)
|
|
36
|
-
] + [ # # does args have any logger
|
|
37
|
-
x
|
|
38
|
-
for x in args
|
|
39
|
-
if isinstance(x, logging.Logger) or isinstance(x, MyLogger)
|
|
40
|
-
]
|
|
41
|
-
if hasattr(first_args, "__dict__"): # is first argument `self`
|
|
42
|
-
logger_params = logger_params + [
|
|
43
|
-
x
|
|
44
|
-
for x in first_args.__dict__.values() # does class (dict) members have any logger
|
|
45
|
-
if isinstance(x, logging.Logger)
|
|
46
|
-
or isinstance(x, MyLogger)
|
|
47
|
-
]
|
|
48
|
-
h_logger = next(iter(logger_params), MyLogger()) # get the next/first/default logger
|
|
49
|
-
else:
|
|
50
|
-
h_logger = my_logger # logger is passed explicitly to the decorator
|
|
51
|
-
|
|
52
|
-
if isinstance(h_logger, MyLogger):
|
|
53
|
-
logger = h_logger.get_logger(func.__name__)
|
|
54
|
-
else:
|
|
55
|
-
logger = h_logger
|
|
56
|
-
|
|
57
|
-
# args_repr = [repr(a) for a in args]
|
|
58
|
-
# kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
|
|
59
|
-
# signature = ", ".join(args_repr + kwargs_repr)
|
|
60
|
-
# logger.debug(f"function {func.__name__} called with args {signature}")
|
|
61
|
-
|
|
62
|
-
except Exception:
|
|
63
|
-
pass
|
|
64
|
-
|
|
65
|
-
try:
|
|
66
|
-
_tic = datetime.now()
|
|
67
|
-
result = func(*args, **kwargs)
|
|
68
|
-
_toc = datetime.now()
|
|
69
|
-
logger.info(f"Elapsed time for {func.__name__}: {_toc - _tic}")
|
|
70
|
-
return result
|
|
71
|
-
except Exception as e:
|
|
72
|
-
logger.exception(f"Exception raised in {func.__name__}. exception: {str(e)}")
|
|
73
|
-
raise e
|
|
74
|
-
|
|
75
|
-
return wrapper
|
|
76
|
-
|
|
77
|
-
if _func is None:
|
|
78
|
-
return decorator_log
|
|
79
|
-
else:
|
|
80
|
-
return decorator_log(_func)
|
|
1
|
+
"""
|
|
2
|
+
REF: https://ankitbko.github.io/blog/2021/04/logging-in-python/
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import functools
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Union
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class MyLogger:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
logging.basicConfig(level=logging.DEBUG,
|
|
14
|
+
format=' %(asctime)s - %(levelname)s - %(message)s')
|
|
15
|
+
|
|
16
|
+
def get_logger(self, name=None):
|
|
17
|
+
return logging.getLogger(name)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_default_logger():
|
|
21
|
+
return MyLogger().get_logger()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def log_timer(_func=None, *, my_logger: Union[MyLogger, logging.Logger] = None):
|
|
25
|
+
def decorator_log(func):
|
|
26
|
+
@functools.wraps(func)
|
|
27
|
+
def wrapper(*args, **kwargs):
|
|
28
|
+
logger = get_default_logger()
|
|
29
|
+
try:
|
|
30
|
+
if my_logger is None:
|
|
31
|
+
first_args = next(iter(args), None) # capture first arg to check for `self`
|
|
32
|
+
logger_params = [ # does kwargs have any logger
|
|
33
|
+
x
|
|
34
|
+
for x in kwargs.values()
|
|
35
|
+
if isinstance(x, logging.Logger) or isinstance(x, MyLogger)
|
|
36
|
+
] + [ # # does args have any logger
|
|
37
|
+
x
|
|
38
|
+
for x in args
|
|
39
|
+
if isinstance(x, logging.Logger) or isinstance(x, MyLogger)
|
|
40
|
+
]
|
|
41
|
+
if hasattr(first_args, "__dict__"): # is first argument `self`
|
|
42
|
+
logger_params = logger_params + [
|
|
43
|
+
x
|
|
44
|
+
for x in first_args.__dict__.values() # does class (dict) members have any logger
|
|
45
|
+
if isinstance(x, logging.Logger)
|
|
46
|
+
or isinstance(x, MyLogger)
|
|
47
|
+
]
|
|
48
|
+
h_logger = next(iter(logger_params), MyLogger()) # get the next/first/default logger
|
|
49
|
+
else:
|
|
50
|
+
h_logger = my_logger # logger is passed explicitly to the decorator
|
|
51
|
+
|
|
52
|
+
if isinstance(h_logger, MyLogger):
|
|
53
|
+
logger = h_logger.get_logger(func.__name__)
|
|
54
|
+
else:
|
|
55
|
+
logger = h_logger
|
|
56
|
+
|
|
57
|
+
# args_repr = [repr(a) for a in args]
|
|
58
|
+
# kwargs_repr = [f"{k}={v!r}" for k, v in kwargs.items()]
|
|
59
|
+
# signature = ", ".join(args_repr + kwargs_repr)
|
|
60
|
+
# logger.debug(f"function {func.__name__} called with args {signature}")
|
|
61
|
+
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
_tic = datetime.now()
|
|
67
|
+
result = func(*args, **kwargs)
|
|
68
|
+
_toc = datetime.now()
|
|
69
|
+
logger.info(f"Elapsed time for {func.__name__}: {_toc - _tic}")
|
|
70
|
+
return result
|
|
71
|
+
except Exception as e:
|
|
72
|
+
logger.exception(f"Exception raised in {func.__name__}. exception: {str(e)}")
|
|
73
|
+
raise e
|
|
74
|
+
|
|
75
|
+
return wrapper
|
|
76
|
+
|
|
77
|
+
if _func is None:
|
|
78
|
+
return decorator_log
|
|
79
|
+
else:
|
|
80
|
+
return decorator_log(_func)
|
elphick/geomet/utils/viz.py
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
|
|
3
|
-
import pandas as pd
|
|
4
|
-
|
|
5
|
-
import plotly.graph_objects as go
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def plot_parallel(data: pd.DataFrame, color: Optional[str] = None, title: Optional[str] = None) -> go.Figure:
|
|
9
|
-
"""Create an interactive parallel plot
|
|
10
|
-
|
|
11
|
-
Useful to explore multi-dimensional data like mass-composition data
|
|
12
|
-
|
|
13
|
-
Args:
|
|
14
|
-
data: Dataframe to plot
|
|
15
|
-
color: Optional color variable
|
|
16
|
-
title: Optional plot title
|
|
17
|
-
|
|
18
|
-
Returns:
|
|
19
|
-
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
# Kudos: https://stackoverflow.com/questions/72125802/parallel-coordinate-plot-in-plotly-with-continuous-
|
|
23
|
-
# and-categorical-data
|
|
24
|
-
|
|
25
|
-
categorical_columns = data.select_dtypes(include=['category', 'object'])
|
|
26
|
-
col_list = []
|
|
27
|
-
|
|
28
|
-
for col in data.columns:
|
|
29
|
-
if col in categorical_columns: # categorical columns
|
|
30
|
-
values = data[col].unique()
|
|
31
|
-
value2dummy = dict(zip(values, range(
|
|
32
|
-
len(values)))) # works if values are strings, otherwise we probably need to convert them
|
|
33
|
-
data[col] = [value2dummy[v] for v in data[col]]
|
|
34
|
-
col_dict = dict(
|
|
35
|
-
label=col,
|
|
36
|
-
tickvals=list(value2dummy.values()),
|
|
37
|
-
ticktext=list(value2dummy.keys()),
|
|
38
|
-
values=data[col],
|
|
39
|
-
)
|
|
40
|
-
else: # continuous columns
|
|
41
|
-
col_dict = dict(
|
|
42
|
-
range=(data[col].min(), data[col].max()),
|
|
43
|
-
label=col,
|
|
44
|
-
values=data[col],
|
|
45
|
-
)
|
|
46
|
-
col_list.append(col_dict)
|
|
47
|
-
|
|
48
|
-
if color is None:
|
|
49
|
-
fig = go.Figure(data=go.Parcoords(dimensions=col_list))
|
|
50
|
-
else:
|
|
51
|
-
fig = go.Figure(data=go.Parcoords(dimensions=col_list, line=dict(color=data[color])))
|
|
52
|
-
|
|
53
|
-
fig.update_layout(title=title)
|
|
54
|
-
|
|
55
|
-
return fig
|
|
56
|
-
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
import plotly.graph_objects as go
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def plot_parallel(data: pd.DataFrame, color: Optional[str] = None, title: Optional[str] = None) -> go.Figure:
|
|
9
|
+
"""Create an interactive parallel plot
|
|
10
|
+
|
|
11
|
+
Useful to explore multi-dimensional data like mass-composition data
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
data: Dataframe to plot
|
|
15
|
+
color: Optional color variable
|
|
16
|
+
title: Optional plot title
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Kudos: https://stackoverflow.com/questions/72125802/parallel-coordinate-plot-in-plotly-with-continuous-
|
|
23
|
+
# and-categorical-data
|
|
24
|
+
|
|
25
|
+
categorical_columns = data.select_dtypes(include=['category', 'object'])
|
|
26
|
+
col_list = []
|
|
27
|
+
|
|
28
|
+
for col in data.columns:
|
|
29
|
+
if col in categorical_columns: # categorical columns
|
|
30
|
+
values = data[col].unique()
|
|
31
|
+
value2dummy = dict(zip(values, range(
|
|
32
|
+
len(values)))) # works if values are strings, otherwise we probably need to convert them
|
|
33
|
+
data[col] = [value2dummy[v] for v in data[col]]
|
|
34
|
+
col_dict = dict(
|
|
35
|
+
label=col,
|
|
36
|
+
tickvals=list(value2dummy.values()),
|
|
37
|
+
ticktext=list(value2dummy.keys()),
|
|
38
|
+
values=data[col],
|
|
39
|
+
)
|
|
40
|
+
else: # continuous columns
|
|
41
|
+
col_dict = dict(
|
|
42
|
+
range=(data[col].min(), data[col].max()),
|
|
43
|
+
label=col,
|
|
44
|
+
values=data[col],
|
|
45
|
+
)
|
|
46
|
+
col_list.append(col_dict)
|
|
47
|
+
|
|
48
|
+
if color is None:
|
|
49
|
+
fig = go.Figure(data=go.Parcoords(dimensions=col_list))
|
|
50
|
+
else:
|
|
51
|
+
fig = go.Figure(data=go.Parcoords(dimensions=col_list, line=dict(color=data[color])))
|
|
52
|
+
|
|
53
|
+
fig.update_layout(title=title)
|
|
54
|
+
|
|
55
|
+
return fig
|
|
56
|
+
|