geometallurgy 0.4.12__py3-none-any.whl → 0.4.13__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 -358
- 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/output.html +617 -0
- 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.12.dist-info → geometallurgy-0.4.13.dist-info}/LICENSE +21 -21
- {geometallurgy-0.4.12.dist-info → geometallurgy-0.4.13.dist-info}/METADATA +7 -5
- geometallurgy-0.4.13.dist-info/RECORD +49 -0
- {geometallurgy-0.4.12.dist-info → geometallurgy-0.4.13.dist-info}/WHEEL +1 -1
- geometallurgy-0.4.12.dist-info/RECORD +0 -48
- {geometallurgy-0.4.12.dist-info → geometallurgy-0.4.13.dist-info}/entry_points.txt +0 -0
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Managing components/composition
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
from typing import List, Dict, Union
|
|
6
|
-
|
|
7
|
-
import periodictable as pt
|
|
8
|
-
from periodictable.formulas import Formula
|
|
9
|
-
|
|
10
|
-
custom_components: List[str] = ['LOI', 'Ash']
|
|
11
|
-
|
|
12
|
-
# Kudos: pyrolite
|
|
13
|
-
DEFAULT_CHARGES: Dict = dict(
|
|
14
|
-
H=1,
|
|
15
|
-
Li=1,
|
|
16
|
-
Be=1,
|
|
17
|
-
B=3,
|
|
18
|
-
C=4,
|
|
19
|
-
O=-2,
|
|
20
|
-
F=-1,
|
|
21
|
-
Na=1,
|
|
22
|
-
Mg=2,
|
|
23
|
-
Al=3,
|
|
24
|
-
Si=4,
|
|
25
|
-
P=3,
|
|
26
|
-
Cl=-1,
|
|
27
|
-
K=1,
|
|
28
|
-
Ca=2,
|
|
29
|
-
Sc=3,
|
|
30
|
-
Ti=4,
|
|
31
|
-
V=3,
|
|
32
|
-
Cr=3,
|
|
33
|
-
Mn=2,
|
|
34
|
-
Fe=2,
|
|
35
|
-
Co=2,
|
|
36
|
-
Ni=2,
|
|
37
|
-
Cu=2,
|
|
38
|
-
Zn=2,
|
|
39
|
-
Br=-1,
|
|
40
|
-
Rb=1,
|
|
41
|
-
Sr=2,
|
|
42
|
-
Y=3,
|
|
43
|
-
Zr=4,
|
|
44
|
-
Nb=5,
|
|
45
|
-
Sn=4,
|
|
46
|
-
I=-1,
|
|
47
|
-
Cs=1,
|
|
48
|
-
Ba=2,
|
|
49
|
-
La=3,
|
|
50
|
-
Ce=3,
|
|
51
|
-
Pr=3,
|
|
52
|
-
Nd=3,
|
|
53
|
-
Sm=3,
|
|
54
|
-
Eu=3,
|
|
55
|
-
Gd=3,
|
|
56
|
-
Tb=3,
|
|
57
|
-
Dy=3,
|
|
58
|
-
Ho=3,
|
|
59
|
-
Er=3,
|
|
60
|
-
Tm=3,
|
|
61
|
-
Yb=3,
|
|
62
|
-
Lu=3,
|
|
63
|
-
Hf=4,
|
|
64
|
-
Pb=2,
|
|
65
|
-
Th=4,
|
|
66
|
-
U=4,
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def elements() -> List[str]:
|
|
71
|
-
res: List[str] = [el.symbol for el in pt.elements]
|
|
72
|
-
return res
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def is_element(candidates: List[str], strict: bool = True) -> Union[List[str], Dict[str, str]]:
|
|
76
|
-
if strict:
|
|
77
|
-
matches: list = list(set(candidates).intersection(elements()))
|
|
78
|
-
else:
|
|
79
|
-
e_map: Dict[str, str] = {e.symbol.lower(): e.symbol for e in pt.elements}
|
|
80
|
-
matches: Dict[str, str] = {c: e_map[c.lower()] for c in candidates if c.lower() in e_map.keys()}
|
|
81
|
-
|
|
82
|
-
return matches
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def oxides() -> List[Formula]:
|
|
86
|
-
# cats = {e for e in [el for el in pt.elements if str(el) in DEFAULT_CHARGES.keys()] if DEFAULT_CHARGES[str(e)] > 0}
|
|
87
|
-
cats = {el for el in pt.elements if (str(el) in DEFAULT_CHARGES.keys()) and (DEFAULT_CHARGES[str(el)] > 0)}
|
|
88
|
-
|
|
89
|
-
res: List[Formula] = []
|
|
90
|
-
for c in cats:
|
|
91
|
-
charge = DEFAULT_CHARGES[str(c)]
|
|
92
|
-
if charge % 2 == 0:
|
|
93
|
-
res.append(pt.formula(str(c) + str(1) + 'O' + str(charge // 2)))
|
|
94
|
-
else:
|
|
95
|
-
res.append(pt.formula(str(c) + str(2) + 'O' + str(charge)))
|
|
96
|
-
|
|
97
|
-
return res
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def is_oxide(candidates: List[str], strict: bool = True) -> Union[List[str], Dict[str, str]]:
|
|
101
|
-
if strict:
|
|
102
|
-
oxs = {str(o) for o in oxides()}
|
|
103
|
-
matches: list = list(set(candidates).intersection(oxs))
|
|
104
|
-
else:
|
|
105
|
-
o_map: Dict[str, str] = {str(o).lower(): str(o) for o in oxides()}
|
|
106
|
-
matches: Dict[str, str] = {c: o_map[c.lower()] for c in candidates if c.lower() in o_map.keys()}
|
|
107
|
-
|
|
108
|
-
return matches
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def is_compositional(candidates: List[str], strict: bool = True) -> Union[List[str], Dict[str, str]]:
|
|
112
|
-
"""
|
|
113
|
-
Check if a list of candidates are compositional components (elements or oxides)
|
|
114
|
-
Args:
|
|
115
|
-
candidates: list of string candidates
|
|
116
|
-
strict: If True, the candidates must be in the list of known compositional components (elements or oxides)
|
|
117
|
-
as chemical symbols.
|
|
118
|
-
|
|
119
|
-
Returns:
|
|
120
|
-
If strict, a list of compositional components, otherwise a dict of the original candidates (keys) and
|
|
121
|
-
their compositional component symbols (values)
|
|
122
|
-
"""
|
|
123
|
-
if strict:
|
|
124
|
-
comps = {str(o) for o in oxides()}.union(set(elements())).union(set(custom_components))
|
|
125
|
-
matches: list = list(set(candidates).intersection(comps))
|
|
126
|
-
else:
|
|
127
|
-
comp_map: Dict[str, str] = {**{str(o).lower(): str(o) for o in oxides()},
|
|
128
|
-
**{a.lower(): a for a in elements()},
|
|
129
|
-
**{c.lower(): c for c in custom_components}}
|
|
130
|
-
matches: Dict[str, str] = {c: comp_map[c.lower()] for c in candidates if c.lower() in comp_map.keys()}
|
|
131
|
-
|
|
132
|
-
return matches
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def get_components(candidates: List[str], strict: bool = True) -> list[str]:
|
|
136
|
-
return list(is_compositional(candidates, strict=strict).keys())
|
|
1
|
+
"""
|
|
2
|
+
Managing components/composition
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import List, Dict, Union
|
|
6
|
+
|
|
7
|
+
import periodictable as pt
|
|
8
|
+
from periodictable.formulas import Formula
|
|
9
|
+
|
|
10
|
+
custom_components: List[str] = ['LOI', 'Ash']
|
|
11
|
+
|
|
12
|
+
# Kudos: pyrolite
|
|
13
|
+
DEFAULT_CHARGES: Dict = dict(
|
|
14
|
+
H=1,
|
|
15
|
+
Li=1,
|
|
16
|
+
Be=1,
|
|
17
|
+
B=3,
|
|
18
|
+
C=4,
|
|
19
|
+
O=-2,
|
|
20
|
+
F=-1,
|
|
21
|
+
Na=1,
|
|
22
|
+
Mg=2,
|
|
23
|
+
Al=3,
|
|
24
|
+
Si=4,
|
|
25
|
+
P=3,
|
|
26
|
+
Cl=-1,
|
|
27
|
+
K=1,
|
|
28
|
+
Ca=2,
|
|
29
|
+
Sc=3,
|
|
30
|
+
Ti=4,
|
|
31
|
+
V=3,
|
|
32
|
+
Cr=3,
|
|
33
|
+
Mn=2,
|
|
34
|
+
Fe=2,
|
|
35
|
+
Co=2,
|
|
36
|
+
Ni=2,
|
|
37
|
+
Cu=2,
|
|
38
|
+
Zn=2,
|
|
39
|
+
Br=-1,
|
|
40
|
+
Rb=1,
|
|
41
|
+
Sr=2,
|
|
42
|
+
Y=3,
|
|
43
|
+
Zr=4,
|
|
44
|
+
Nb=5,
|
|
45
|
+
Sn=4,
|
|
46
|
+
I=-1,
|
|
47
|
+
Cs=1,
|
|
48
|
+
Ba=2,
|
|
49
|
+
La=3,
|
|
50
|
+
Ce=3,
|
|
51
|
+
Pr=3,
|
|
52
|
+
Nd=3,
|
|
53
|
+
Sm=3,
|
|
54
|
+
Eu=3,
|
|
55
|
+
Gd=3,
|
|
56
|
+
Tb=3,
|
|
57
|
+
Dy=3,
|
|
58
|
+
Ho=3,
|
|
59
|
+
Er=3,
|
|
60
|
+
Tm=3,
|
|
61
|
+
Yb=3,
|
|
62
|
+
Lu=3,
|
|
63
|
+
Hf=4,
|
|
64
|
+
Pb=2,
|
|
65
|
+
Th=4,
|
|
66
|
+
U=4,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def elements() -> List[str]:
|
|
71
|
+
res: List[str] = [el.symbol for el in pt.elements]
|
|
72
|
+
return res
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_element(candidates: List[str], strict: bool = True) -> Union[List[str], Dict[str, str]]:
|
|
76
|
+
if strict:
|
|
77
|
+
matches: list = list(set(candidates).intersection(elements()))
|
|
78
|
+
else:
|
|
79
|
+
e_map: Dict[str, str] = {e.symbol.lower(): e.symbol for e in pt.elements}
|
|
80
|
+
matches: Dict[str, str] = {c: e_map[c.lower()] for c in candidates if c.lower() in e_map.keys()}
|
|
81
|
+
|
|
82
|
+
return matches
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def oxides() -> List[Formula]:
|
|
86
|
+
# cats = {e for e in [el for el in pt.elements if str(el) in DEFAULT_CHARGES.keys()] if DEFAULT_CHARGES[str(e)] > 0}
|
|
87
|
+
cats = {el for el in pt.elements if (str(el) in DEFAULT_CHARGES.keys()) and (DEFAULT_CHARGES[str(el)] > 0)}
|
|
88
|
+
|
|
89
|
+
res: List[Formula] = []
|
|
90
|
+
for c in cats:
|
|
91
|
+
charge = DEFAULT_CHARGES[str(c)]
|
|
92
|
+
if charge % 2 == 0:
|
|
93
|
+
res.append(pt.formula(str(c) + str(1) + 'O' + str(charge // 2)))
|
|
94
|
+
else:
|
|
95
|
+
res.append(pt.formula(str(c) + str(2) + 'O' + str(charge)))
|
|
96
|
+
|
|
97
|
+
return res
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def is_oxide(candidates: List[str], strict: bool = True) -> Union[List[str], Dict[str, str]]:
|
|
101
|
+
if strict:
|
|
102
|
+
oxs = {str(o) for o in oxides()}
|
|
103
|
+
matches: list = list(set(candidates).intersection(oxs))
|
|
104
|
+
else:
|
|
105
|
+
o_map: Dict[str, str] = {str(o).lower(): str(o) for o in oxides()}
|
|
106
|
+
matches: Dict[str, str] = {c: o_map[c.lower()] for c in candidates if c.lower() in o_map.keys()}
|
|
107
|
+
|
|
108
|
+
return matches
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def is_compositional(candidates: List[str], strict: bool = True) -> Union[List[str], Dict[str, str]]:
|
|
112
|
+
"""
|
|
113
|
+
Check if a list of candidates are compositional components (elements or oxides)
|
|
114
|
+
Args:
|
|
115
|
+
candidates: list of string candidates
|
|
116
|
+
strict: If True, the candidates must be in the list of known compositional components (elements or oxides)
|
|
117
|
+
as chemical symbols.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
If strict, a list of compositional components, otherwise a dict of the original candidates (keys) and
|
|
121
|
+
their compositional component symbols (values)
|
|
122
|
+
"""
|
|
123
|
+
if strict:
|
|
124
|
+
comps = {str(o) for o in oxides()}.union(set(elements())).union(set(custom_components))
|
|
125
|
+
matches: list = list(set(candidates).intersection(comps))
|
|
126
|
+
else:
|
|
127
|
+
comp_map: Dict[str, str] = {**{str(o).lower(): str(o) for o in oxides()},
|
|
128
|
+
**{a.lower(): a for a in elements()},
|
|
129
|
+
**{c.lower(): c for c in custom_components}}
|
|
130
|
+
matches: Dict[str, str] = {c: comp_map[c.lower()] for c in candidates if c.lower() in comp_map.keys()}
|
|
131
|
+
|
|
132
|
+
return matches
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_components(candidates: List[str], strict: bool = True) -> list[str]:
|
|
136
|
+
return list(is_compositional(candidates, strict=strict).keys())
|
elphick/geomet/utils/data.py
CHANGED
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
import pandas as pd
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def sample_data(include_wet_mass: bool = True, include_dry_mass: bool = True,
|
|
5
|
-
include_moisture: bool = False, include_chem_vars: bool = True) -> pd.DataFrame:
|
|
6
|
-
"""Creates synthetic data for testing
|
|
7
|
-
|
|
8
|
-
Args:
|
|
9
|
-
include_wet_mass: If True, wet mass is included.
|
|
10
|
-
include_dry_mass: If True, dry mass is included.
|
|
11
|
-
include_moisture: If True, moisture (H2O) is included.
|
|
12
|
-
include_chem_vars: If True, chemical variables are included.
|
|
13
|
-
|
|
14
|
-
Returns:
|
|
15
|
-
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
# mass_wet: pd.Series = pd.Series([100, 90, 110], name='wet_mass')
|
|
19
|
-
# mass_dry: pd.Series = pd.Series([90, 80, 100], name='dry_mass')
|
|
20
|
-
mass_wet: pd.Series = pd.Series([100., 90., 110.], name='wet_mass')
|
|
21
|
-
mass_dry: pd.Series = pd.Series([90., 80., 90.], name='mass_dry')
|
|
22
|
-
chem: pd.DataFrame = pd.DataFrame.from_dict({'FE': [57., 59., 61.],
|
|
23
|
-
'SIO2': [5.2, 3.1, 2.2],
|
|
24
|
-
'al2o3': [3.0, 1.7, 0.9],
|
|
25
|
-
'LOI': [5.0, 4.0, 3.0]})
|
|
26
|
-
attrs: pd.Series = pd.Series(['grp_1', 'grp_1', 'grp_2'], name='group')
|
|
27
|
-
|
|
28
|
-
if include_wet_mass and not include_dry_mass:
|
|
29
|
-
mass = pd.DataFrame(mass_wet)
|
|
30
|
-
elif not include_wet_mass and include_dry_mass:
|
|
31
|
-
mass = pd.DataFrame(mass_dry)
|
|
32
|
-
elif include_wet_mass and include_dry_mass:
|
|
33
|
-
mass = pd.concat([mass_wet, mass_dry], axis='columns')
|
|
34
|
-
else:
|
|
35
|
-
raise AssertionError('Arguments provided result in no mass column')
|
|
36
|
-
|
|
37
|
-
if include_moisture is True:
|
|
38
|
-
moisture: pd.DataFrame = (mass_wet - mass_dry) / mass_wet * 100
|
|
39
|
-
moisture.name = 'H2O'
|
|
40
|
-
res: pd.DataFrame = pd.concat([mass, moisture, chem, attrs], axis='columns')
|
|
41
|
-
else:
|
|
42
|
-
res: pd.DataFrame = pd.concat([mass, chem, attrs], axis='columns')
|
|
43
|
-
|
|
44
|
-
if include_chem_vars is False:
|
|
45
|
-
res = res.drop(columns=chem.columns)
|
|
46
|
-
|
|
47
|
-
res.index.name = 'index'
|
|
48
|
-
|
|
49
|
-
return res
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def sample_data(include_wet_mass: bool = True, include_dry_mass: bool = True,
|
|
5
|
+
include_moisture: bool = False, include_chem_vars: bool = True) -> pd.DataFrame:
|
|
6
|
+
"""Creates synthetic data for testing
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
include_wet_mass: If True, wet mass is included.
|
|
10
|
+
include_dry_mass: If True, dry mass is included.
|
|
11
|
+
include_moisture: If True, moisture (H2O) is included.
|
|
12
|
+
include_chem_vars: If True, chemical variables are included.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
# mass_wet: pd.Series = pd.Series([100, 90, 110], name='wet_mass')
|
|
19
|
+
# mass_dry: pd.Series = pd.Series([90, 80, 100], name='dry_mass')
|
|
20
|
+
mass_wet: pd.Series = pd.Series([100., 90., 110.], name='wet_mass')
|
|
21
|
+
mass_dry: pd.Series = pd.Series([90., 80., 90.], name='mass_dry')
|
|
22
|
+
chem: pd.DataFrame = pd.DataFrame.from_dict({'FE': [57., 59., 61.],
|
|
23
|
+
'SIO2': [5.2, 3.1, 2.2],
|
|
24
|
+
'al2o3': [3.0, 1.7, 0.9],
|
|
25
|
+
'LOI': [5.0, 4.0, 3.0]})
|
|
26
|
+
attrs: pd.Series = pd.Series(['grp_1', 'grp_1', 'grp_2'], name='group')
|
|
27
|
+
|
|
28
|
+
if include_wet_mass and not include_dry_mass:
|
|
29
|
+
mass = pd.DataFrame(mass_wet)
|
|
30
|
+
elif not include_wet_mass and include_dry_mass:
|
|
31
|
+
mass = pd.DataFrame(mass_dry)
|
|
32
|
+
elif include_wet_mass and include_dry_mass:
|
|
33
|
+
mass = pd.concat([mass_wet, mass_dry], axis='columns')
|
|
34
|
+
else:
|
|
35
|
+
raise AssertionError('Arguments provided result in no mass column')
|
|
36
|
+
|
|
37
|
+
if include_moisture is True:
|
|
38
|
+
moisture: pd.DataFrame = (mass_wet - mass_dry) / mass_wet * 100
|
|
39
|
+
moisture.name = 'H2O'
|
|
40
|
+
res: pd.DataFrame = pd.concat([mass, moisture, chem, attrs], axis='columns')
|
|
41
|
+
else:
|
|
42
|
+
res: pd.DataFrame = pd.concat([mass, chem, attrs], axis='columns')
|
|
43
|
+
|
|
44
|
+
if include_chem_vars is False:
|
|
45
|
+
res = res.drop(columns=chem.columns)
|
|
46
|
+
|
|
47
|
+
res.index.name = 'index'
|
|
48
|
+
|
|
49
|
+
return res
|
|
@@ -1,108 +1,108 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
|
|
3
|
-
import pandas as pd
|
|
4
|
-
|
|
5
|
-
from elphick.geomet.base import MassComposition
|
|
6
|
-
from elphick.geomet.flowsheet import Flowsheet
|
|
7
|
-
from elphick.geomet.flowsheet.stream import Stream
|
|
8
|
-
from elphick.geomet.utils.moisture import solve_mass_moisture
|
|
9
|
-
from elphick.geomet.utils.pandas import composition_to_mass
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def coerce_estimates(estimate_stream: Stream, input_stream: Stream,
|
|
13
|
-
recovery_bounds: tuple[float, float] = (0.01, 0.99),
|
|
14
|
-
complement_name: str = 'complement',
|
|
15
|
-
fs_name: str = 'Flowsheet',
|
|
16
|
-
show_plot: bool = False) -> Stream:
|
|
17
|
-
"""Coerce output estimates within recovery and the component range.
|
|
18
|
-
|
|
19
|
-
Estimates contain error and at times can exceed the specified component range, or can consume more component
|
|
20
|
-
mass than is available in the feed. This function modifies (coerces) only the non-compliant estimate records
|
|
21
|
-
in order to balance the node and keep all dry components within range. Moisture is not modified.
|
|
22
|
-
|
|
23
|
-
estimate_stream (supplied)
|
|
24
|
-
/
|
|
25
|
-
input_stream (supplied)
|
|
26
|
-
\
|
|
27
|
-
complement_stream
|
|
28
|
-
|
|
29
|
-
1. limits the estimate to within the recovery bounds,
|
|
30
|
-
2. ensures the estimate is within the component range,
|
|
31
|
-
3. solves the complement, and ensures it is in range,
|
|
32
|
-
4. if the complement is out of range, it is adjusted and the estimate adjusted to maintain the balance.
|
|
33
|
-
|
|
34
|
-
Args:
|
|
35
|
-
estimate_stream: The estimated object, which is a node output
|
|
36
|
-
input_stream: The input object, which is a node input
|
|
37
|
-
recovery_bounds: The bounds for the recovery, default is 0.01 to 0.99
|
|
38
|
-
complement_name: The name of the complement , for plots.
|
|
39
|
-
fs_name: The name of the flowsheet, for plots.
|
|
40
|
-
show_plot: If True, show the network plot
|
|
41
|
-
|
|
42
|
-
Returns:
|
|
43
|
-
The coerced estimate stream
|
|
44
|
-
"""
|
|
45
|
-
|
|
46
|
-
if show_plot:
|
|
47
|
-
complement_stream: MassComposition = input_stream.sub(estimate_stream, name=complement_name)
|
|
48
|
-
fs: Flowsheet = Flowsheet.from_objects([input_stream, estimate_stream, complement_stream],
|
|
49
|
-
name=f"{fs_name}: Balance prior to coercion")
|
|
50
|
-
fs.table_plot(plot_type='network', table_area=0.2, table_pos='top').show()
|
|
51
|
-
|
|
52
|
-
# # debugging snippet to show a failing record
|
|
53
|
-
# qry: str = 'index==1000'
|
|
54
|
-
# fs_debug: Flowsheet = fs.query(qry)
|
|
55
|
-
# fs_debug.name = f"{fs_name}: Balance prior to coercion: [{qry}]"
|
|
56
|
-
# fs_debug.table_plot(plot_type='network', table_area=0.2, table_pos='top').show()
|
|
57
|
-
|
|
58
|
-
if input_stream.status.ok is False:
|
|
59
|
-
raise ValueError('Input stream is not OK')
|
|
60
|
-
|
|
61
|
-
# clip the composition to the bounds
|
|
62
|
-
estimate_stream = estimate_stream.clip_composition()
|
|
63
|
-
|
|
64
|
-
# coerce the estimate component mass to within the total dry mass
|
|
65
|
-
estimate_stream = estimate_stream.balance_composition()
|
|
66
|
-
|
|
67
|
-
# clip the recovery
|
|
68
|
-
estimate_stream = estimate_stream.clip_recovery(other=input_stream, recovery_bounds=(0.01, 0.99))
|
|
69
|
-
|
|
70
|
-
if estimate_stream.status.ok is False:
|
|
71
|
-
raise ValueError('Estimate stream is not OK - it should be after bounding recovery')
|
|
72
|
-
|
|
73
|
-
# solve the complement
|
|
74
|
-
complement_stream: Stream = input_stream.sub(estimate_stream, name=complement_name)
|
|
75
|
-
|
|
76
|
-
# clip the composition to the bounds
|
|
77
|
-
complement_stream = complement_stream.clip_composition()
|
|
78
|
-
|
|
79
|
-
# coerce the estimate component mass to within the total dry mass
|
|
80
|
-
complement_stream = complement_stream.balance_composition()
|
|
81
|
-
|
|
82
|
-
# adjust the estimate to maintain the balance
|
|
83
|
-
estimate_stream = input_stream.sub(complement_stream, name=estimate_stream.name,
|
|
84
|
-
include_supplementary_data=True)
|
|
85
|
-
|
|
86
|
-
if estimate_stream.status.ok is False:
|
|
87
|
-
# This can occur in cases where the complement grade has been reduced (by balance_composition) to a point
|
|
88
|
-
# where the resultant estimate grade is out of range. In this case, we need to adjust the complement grade.
|
|
89
|
-
|
|
90
|
-
estimate_stream = estimate_stream.clip_composition()
|
|
91
|
-
complement_stream = input_stream.sub(estimate_stream, name=complement_name)
|
|
92
|
-
|
|
93
|
-
if estimate_stream.status.ok is False:
|
|
94
|
-
raise ValueError('Estimate stream is not OK after adjustment')
|
|
95
|
-
|
|
96
|
-
fs2: Flowsheet = Flowsheet.from_objects([input_stream, estimate_stream, complement_stream],
|
|
97
|
-
name=f"{fs_name}: Coerced Estimates")
|
|
98
|
-
|
|
99
|
-
if show_plot:
|
|
100
|
-
fs2.table_plot(plot_type='network', table_area=0.2, table_pos='top').show()
|
|
101
|
-
|
|
102
|
-
if fs2.all_nodes_healthy is False:
|
|
103
|
-
if fs2.all_streams_healthy and not fs2.all_nodes_healthy:
|
|
104
|
-
logging.warning('All streams are healthy but not all nodes are healthy. Consider the water balance.')
|
|
105
|
-
else:
|
|
106
|
-
raise ValueError('Flowsheet is not balanced after adjustment')
|
|
107
|
-
|
|
108
|
-
return estimate_stream
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from elphick.geomet.base import MassComposition
|
|
6
|
+
from elphick.geomet.flowsheet import Flowsheet
|
|
7
|
+
from elphick.geomet.flowsheet.stream import Stream
|
|
8
|
+
from elphick.geomet.utils.moisture import solve_mass_moisture
|
|
9
|
+
from elphick.geomet.utils.pandas import composition_to_mass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def coerce_estimates(estimate_stream: Stream, input_stream: Stream,
|
|
13
|
+
recovery_bounds: tuple[float, float] = (0.01, 0.99),
|
|
14
|
+
complement_name: str = 'complement',
|
|
15
|
+
fs_name: str = 'Flowsheet',
|
|
16
|
+
show_plot: bool = False) -> Stream:
|
|
17
|
+
"""Coerce output estimates within recovery and the component range.
|
|
18
|
+
|
|
19
|
+
Estimates contain error and at times can exceed the specified component range, or can consume more component
|
|
20
|
+
mass than is available in the feed. This function modifies (coerces) only the non-compliant estimate records
|
|
21
|
+
in order to balance the node and keep all dry components within range. Moisture is not modified.
|
|
22
|
+
|
|
23
|
+
estimate_stream (supplied)
|
|
24
|
+
/
|
|
25
|
+
input_stream (supplied)
|
|
26
|
+
\
|
|
27
|
+
complement_stream
|
|
28
|
+
|
|
29
|
+
1. limits the estimate to within the recovery bounds,
|
|
30
|
+
2. ensures the estimate is within the component range,
|
|
31
|
+
3. solves the complement, and ensures it is in range,
|
|
32
|
+
4. if the complement is out of range, it is adjusted and the estimate adjusted to maintain the balance.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
estimate_stream: The estimated object, which is a node output
|
|
36
|
+
input_stream: The input object, which is a node input
|
|
37
|
+
recovery_bounds: The bounds for the recovery, default is 0.01 to 0.99
|
|
38
|
+
complement_name: The name of the complement , for plots.
|
|
39
|
+
fs_name: The name of the flowsheet, for plots.
|
|
40
|
+
show_plot: If True, show the network plot
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The coerced estimate stream
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
if show_plot:
|
|
47
|
+
complement_stream: MassComposition = input_stream.sub(estimate_stream, name=complement_name)
|
|
48
|
+
fs: Flowsheet = Flowsheet.from_objects([input_stream, estimate_stream, complement_stream],
|
|
49
|
+
name=f"{fs_name}: Balance prior to coercion")
|
|
50
|
+
fs.table_plot(plot_type='network', table_area=0.2, table_pos='top').show()
|
|
51
|
+
|
|
52
|
+
# # debugging snippet to show a failing record
|
|
53
|
+
# qry: str = 'index==1000'
|
|
54
|
+
# fs_debug: Flowsheet = fs.query(qry)
|
|
55
|
+
# fs_debug.name = f"{fs_name}: Balance prior to coercion: [{qry}]"
|
|
56
|
+
# fs_debug.table_plot(plot_type='network', table_area=0.2, table_pos='top').show()
|
|
57
|
+
|
|
58
|
+
if input_stream.status.ok is False:
|
|
59
|
+
raise ValueError('Input stream is not OK')
|
|
60
|
+
|
|
61
|
+
# clip the composition to the bounds
|
|
62
|
+
estimate_stream = estimate_stream.clip_composition()
|
|
63
|
+
|
|
64
|
+
# coerce the estimate component mass to within the total dry mass
|
|
65
|
+
estimate_stream = estimate_stream.balance_composition()
|
|
66
|
+
|
|
67
|
+
# clip the recovery
|
|
68
|
+
estimate_stream = estimate_stream.clip_recovery(other=input_stream, recovery_bounds=(0.01, 0.99))
|
|
69
|
+
|
|
70
|
+
if estimate_stream.status.ok is False:
|
|
71
|
+
raise ValueError('Estimate stream is not OK - it should be after bounding recovery')
|
|
72
|
+
|
|
73
|
+
# solve the complement
|
|
74
|
+
complement_stream: Stream = input_stream.sub(estimate_stream, name=complement_name)
|
|
75
|
+
|
|
76
|
+
# clip the composition to the bounds
|
|
77
|
+
complement_stream = complement_stream.clip_composition()
|
|
78
|
+
|
|
79
|
+
# coerce the estimate component mass to within the total dry mass
|
|
80
|
+
complement_stream = complement_stream.balance_composition()
|
|
81
|
+
|
|
82
|
+
# adjust the estimate to maintain the balance
|
|
83
|
+
estimate_stream = input_stream.sub(complement_stream, name=estimate_stream.name,
|
|
84
|
+
include_supplementary_data=True)
|
|
85
|
+
|
|
86
|
+
if estimate_stream.status.ok is False:
|
|
87
|
+
# This can occur in cases where the complement grade has been reduced (by balance_composition) to a point
|
|
88
|
+
# where the resultant estimate grade is out of range. In this case, we need to adjust the complement grade.
|
|
89
|
+
|
|
90
|
+
estimate_stream = estimate_stream.clip_composition()
|
|
91
|
+
complement_stream = input_stream.sub(estimate_stream, name=complement_name)
|
|
92
|
+
|
|
93
|
+
if estimate_stream.status.ok is False:
|
|
94
|
+
raise ValueError('Estimate stream is not OK after adjustment')
|
|
95
|
+
|
|
96
|
+
fs2: Flowsheet = Flowsheet.from_objects([input_stream, estimate_stream, complement_stream],
|
|
97
|
+
name=f"{fs_name}: Coerced Estimates")
|
|
98
|
+
|
|
99
|
+
if show_plot:
|
|
100
|
+
fs2.table_plot(plot_type='network', table_area=0.2, table_pos='top').show()
|
|
101
|
+
|
|
102
|
+
if fs2.all_nodes_healthy is False:
|
|
103
|
+
if fs2.all_streams_healthy and not fs2.all_nodes_healthy:
|
|
104
|
+
logging.warning('All streams are healthy but not all nodes are healthy. Consider the water balance.')
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError('Flowsheet is not balanced after adjustment')
|
|
107
|
+
|
|
108
|
+
return estimate_stream
|