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,256 +1,256 @@
|
|
|
1
|
-
from copy import copy
|
|
2
|
-
from enum import Enum
|
|
3
|
-
from functools import reduce
|
|
4
|
-
from typing import Optional, TypeVar, TYPE_CHECKING
|
|
5
|
-
|
|
6
|
-
import numpy as np
|
|
7
|
-
import pandas as pd
|
|
8
|
-
|
|
9
|
-
from elphick.geomet.base import MC
|
|
10
|
-
from elphick.geomet.flowsheet.stream import Stream
|
|
11
|
-
from elphick.geomet.utils.pandas import MeanIntervalIndex
|
|
12
|
-
from elphick.geomet.utils.partition import load_partition_function
|
|
13
|
-
|
|
14
|
-
# generic type variable, used for type hinting that play nicely with subclasses
|
|
15
|
-
OP = TypeVar('OP', bound='Operation')
|
|
16
|
-
|
|
17
|
-
if TYPE_CHECKING:
|
|
18
|
-
from elphick.geomet import IntervalSample
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class NodeType(Enum):
|
|
22
|
-
SOURCE = 'input'
|
|
23
|
-
SINK = 'output'
|
|
24
|
-
BALANCE = 'degree 2+'
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class Operation:
|
|
28
|
-
def __init__(self, name):
|
|
29
|
-
self.name = name
|
|
30
|
-
self._inputs = []
|
|
31
|
-
self._outputs = []
|
|
32
|
-
self._is_balanced: Optional[bool] = None
|
|
33
|
-
self._unbalanced_records: Optional[pd.DataFrame] = None
|
|
34
|
-
|
|
35
|
-
@property
|
|
36
|
-
def has_empty_input(self) -> bool:
|
|
37
|
-
return None in self.inputs
|
|
38
|
-
|
|
39
|
-
@property
|
|
40
|
-
def has_empty_output(self) -> bool:
|
|
41
|
-
return None in self.outputs
|
|
42
|
-
|
|
43
|
-
@property
|
|
44
|
-
def inputs(self):
|
|
45
|
-
return self._inputs
|
|
46
|
-
|
|
47
|
-
@inputs.setter
|
|
48
|
-
def inputs(self, value: list[MC]):
|
|
49
|
-
self._inputs = value
|
|
50
|
-
self.check_balance()
|
|
51
|
-
|
|
52
|
-
@property
|
|
53
|
-
def outputs(self):
|
|
54
|
-
return self._outputs
|
|
55
|
-
|
|
56
|
-
@outputs.setter
|
|
57
|
-
def outputs(self, value: list[MC]):
|
|
58
|
-
self._outputs = value
|
|
59
|
-
self.check_balance()
|
|
60
|
-
|
|
61
|
-
@property
|
|
62
|
-
def node_type(self) -> Optional[NodeType]:
|
|
63
|
-
if self.inputs and not self.outputs:
|
|
64
|
-
res = NodeType.SINK
|
|
65
|
-
elif self.outputs and not self.inputs:
|
|
66
|
-
res = NodeType.SOURCE
|
|
67
|
-
elif self.inputs and self.outputs:
|
|
68
|
-
res = NodeType.BALANCE
|
|
69
|
-
else:
|
|
70
|
-
res = None
|
|
71
|
-
return res
|
|
72
|
-
|
|
73
|
-
def get_input_mass(self) -> pd.DataFrame:
|
|
74
|
-
inputs = [i for i in self.inputs if i is not None]
|
|
75
|
-
|
|
76
|
-
if not inputs:
|
|
77
|
-
try:
|
|
78
|
-
return self._create_zero_mass()
|
|
79
|
-
except:
|
|
80
|
-
return pd.DataFrame()
|
|
81
|
-
elif len(inputs) == 1:
|
|
82
|
-
return inputs[0].mass_data
|
|
83
|
-
else:
|
|
84
|
-
return reduce(lambda a, b: a.add(b, fill_value=0), [stream.mass_data for stream in inputs])
|
|
85
|
-
|
|
86
|
-
def get_output_mass(self) -> pd.DataFrame:
|
|
87
|
-
outputs = [o for o in self.outputs if o is not None]
|
|
88
|
-
|
|
89
|
-
if not outputs:
|
|
90
|
-
try:
|
|
91
|
-
return self._create_zero_mass()
|
|
92
|
-
except:
|
|
93
|
-
return pd.DataFrame()
|
|
94
|
-
elif len(outputs) == 1:
|
|
95
|
-
return outputs[0].mass_data
|
|
96
|
-
else:
|
|
97
|
-
return reduce(lambda a, b: a.add(b, fill_value=0), [output.mass_data for output in outputs])
|
|
98
|
-
|
|
99
|
-
def check_balance(self):
|
|
100
|
-
"""Checks if the mass and chemistry of the input and output are balanced"""
|
|
101
|
-
if not self.inputs or not self.outputs:
|
|
102
|
-
return None
|
|
103
|
-
|
|
104
|
-
input_mass, output_mass = self.get_input_mass(), self.get_output_mass()
|
|
105
|
-
is_balanced = np.all(np.isclose(input_mass.fillna(0.0), output_mass.fillna(0.0)))
|
|
106
|
-
self._unbalanced_records = (input_mass - output_mass).loc[~np.isclose(input_mass, output_mass).any(axis=1)]
|
|
107
|
-
self._is_balanced = is_balanced
|
|
108
|
-
|
|
109
|
-
@property
|
|
110
|
-
def is_balanced(self) -> Optional[bool]:
|
|
111
|
-
return self._is_balanced
|
|
112
|
-
|
|
113
|
-
@property
|
|
114
|
-
def unbalanced_records(self) -> Optional[pd.DataFrame]:
|
|
115
|
-
return self._unbalanced_records
|
|
116
|
-
|
|
117
|
-
def solve(self) -> Optional[MC]:
|
|
118
|
-
"""Solves the operation
|
|
119
|
-
|
|
120
|
-
Missing data is represented by None in the input and output streams.
|
|
121
|
-
Solve will replace None with an object that balances the mass and chemistry of the input and output streams.
|
|
122
|
-
Returns
|
|
123
|
-
The back-calculated mc object
|
|
124
|
-
"""
|
|
125
|
-
|
|
126
|
-
# Check the number of missing inputs and outputs
|
|
127
|
-
missing_count: int = self.inputs.count(None) + self.outputs.count(None)
|
|
128
|
-
if missing_count > 1:
|
|
129
|
-
raise ValueError("The operation cannot be solved - too many degrees of freedom")
|
|
130
|
-
mc = None
|
|
131
|
-
if missing_count == 0 and self.is_balanced:
|
|
132
|
-
return mc
|
|
133
|
-
else:
|
|
134
|
-
if None in self.inputs:
|
|
135
|
-
ref_object = self.outputs[0]
|
|
136
|
-
# Find the index of None in inputs
|
|
137
|
-
none_index = self.inputs.index(None)
|
|
138
|
-
|
|
139
|
-
# Calculate the None object
|
|
140
|
-
new_input_mass: pd.DataFrame = self.get_output_mass() - self.get_input_mass()
|
|
141
|
-
# Create a new object from the mass dataframe
|
|
142
|
-
mc = type(ref_object).from_mass_dataframe(new_input_mass, mass_wet=ref_object.mass_wet_var,
|
|
143
|
-
mass_dry=ref_object.mass_dry_var,
|
|
144
|
-
moisture_column_name=ref_object.moisture_column,
|
|
145
|
-
component_columns=ref_object.composition_columns,
|
|
146
|
-
composition_units=ref_object.composition_units,
|
|
147
|
-
moisture_in_scope=ref_object.moisture_in_scope)
|
|
148
|
-
# Replace None with the new input
|
|
149
|
-
self.inputs[none_index] = mc
|
|
150
|
-
|
|
151
|
-
elif None in self.outputs:
|
|
152
|
-
ref_object = self.inputs[0]
|
|
153
|
-
# Find the index of None in outputs
|
|
154
|
-
none_index = self.outputs.index(None)
|
|
155
|
-
|
|
156
|
-
# Calculate the None object
|
|
157
|
-
if len(self.outputs) == 1 and len(self.inputs) == 1:
|
|
158
|
-
# passthrough, no need to calculate. Shallow copy to minimise memory.
|
|
159
|
-
mc = copy(self.inputs[0])
|
|
160
|
-
mc.name = None
|
|
161
|
-
else:
|
|
162
|
-
new_output_mass: pd.DataFrame = self.get_input_mass() - self.get_output_mass()
|
|
163
|
-
# Create a new object from the mass dataframe
|
|
164
|
-
mc = type(ref_object).from_mass_dataframe(new_output_mass, mass_wet=ref_object.mass_wet_var,
|
|
165
|
-
mass_dry=ref_object.mass_dry_var,
|
|
166
|
-
moisture_column_name=ref_object.moisture_column,
|
|
167
|
-
component_columns=ref_object.composition_columns,
|
|
168
|
-
composition_units=ref_object.composition_units,
|
|
169
|
-
moisture_in_scope=ref_object.moisture_in_scope)
|
|
170
|
-
|
|
171
|
-
# Replace None with the new output
|
|
172
|
-
self.outputs[none_index] = mc
|
|
173
|
-
|
|
174
|
-
# update the balance related attributes
|
|
175
|
-
self.check_balance()
|
|
176
|
-
return mc
|
|
177
|
-
|
|
178
|
-
def _create_zero_mass(self) -> pd.DataFrame:
|
|
179
|
-
"""Creates a zero mass dataframe with the same columns and index as the mass data"""
|
|
180
|
-
# get the firstan object with the mass data
|
|
181
|
-
obj = self._get_object()
|
|
182
|
-
return pd.DataFrame(data=0, columns=obj.mass_data.columns, index=obj.mass_data.index)
|
|
183
|
-
|
|
184
|
-
def _get_object(self, name: Optional[str] = None) -> MC:
|
|
185
|
-
"""Returns an object from inputs or outputs"""
|
|
186
|
-
candidates = [mc for mc in self.outputs + self.inputs if mc is not None]
|
|
187
|
-
if len(candidates) == 0:
|
|
188
|
-
raise ValueError("No object found")
|
|
189
|
-
if name:
|
|
190
|
-
for obj in candidates:
|
|
191
|
-
if obj is not None and obj.name == name:
|
|
192
|
-
return obj
|
|
193
|
-
raise ValueError(f"No object found with name {name}")
|
|
194
|
-
else:
|
|
195
|
-
return candidates[0]
|
|
196
|
-
|
|
197
|
-
@classmethod
|
|
198
|
-
def from_dict(cls, config: dict) -> 'Operation':
|
|
199
|
-
name = config.get('name')
|
|
200
|
-
|
|
201
|
-
return cls(name=name)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
class Input(Operation):
|
|
205
|
-
def __init__(self, name):
|
|
206
|
-
super().__init__(name)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
class Output(Operation):
|
|
210
|
-
def __init__(self, name):
|
|
211
|
-
super().__init__(name)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
class Passthrough(Operation):
|
|
215
|
-
def __init__(self, name):
|
|
216
|
-
super().__init__(name)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
class PartitionOperation(Operation):
|
|
220
|
-
"""An operation that partitions the input stream into multiple output streams based on a partition function
|
|
221
|
-
|
|
222
|
-
The partition input is the mean of the fractions or the geomean if the fractions are in the size dimension
|
|
223
|
-
The partition function is typically a partial function so that the partition is defined for all arguments
|
|
224
|
-
other than the input mean fraction values in one or two dimensions. The argument names must match the
|
|
225
|
-
index names in the IntervalSample.
|
|
226
|
-
|
|
227
|
-
"""
|
|
228
|
-
|
|
229
|
-
def __init__(self, name, partition=None):
|
|
230
|
-
super().__init__(name)
|
|
231
|
-
self.partition = partition
|
|
232
|
-
self.partition_function = None
|
|
233
|
-
if self.partition and 'module' in self.partition and 'function' in self.partition:
|
|
234
|
-
self.partition_function = load_partition_function(self.partition['module'], self.partition['function'])
|
|
235
|
-
|
|
236
|
-
def solve(self) -> [MC, MC]:
|
|
237
|
-
if self.partition_function:
|
|
238
|
-
self.apply_partition()
|
|
239
|
-
# update the balance related attributes
|
|
240
|
-
self.check_balance()
|
|
241
|
-
return self.outputs
|
|
242
|
-
|
|
243
|
-
def apply_partition(self):
|
|
244
|
-
if len(self.inputs) != 1:
|
|
245
|
-
raise ValueError("PartitionOperation must have exactly one input")
|
|
246
|
-
for input_sample in self.inputs:
|
|
247
|
-
input_sample: 'IntervalSample'
|
|
248
|
-
if input_sample is not None:
|
|
249
|
-
output, complement = input_sample.split_by_partition(self.partition_function)
|
|
250
|
-
self.outputs = [output, complement]
|
|
251
|
-
|
|
252
|
-
@classmethod
|
|
253
|
-
def from_dict(cls, config: dict) -> 'PartitionOperation':
|
|
254
|
-
name = config.get('name')
|
|
255
|
-
partition = config.get('partition')
|
|
256
|
-
return cls(name=name, partition=partition)
|
|
1
|
+
from copy import copy
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from functools import reduce
|
|
4
|
+
from typing import Optional, TypeVar, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from elphick.geomet.base import MC
|
|
10
|
+
from elphick.geomet.flowsheet.stream import Stream
|
|
11
|
+
from elphick.geomet.utils.pandas import MeanIntervalIndex
|
|
12
|
+
from elphick.geomet.utils.partition import load_partition_function
|
|
13
|
+
|
|
14
|
+
# generic type variable, used for type hinting that play nicely with subclasses
|
|
15
|
+
OP = TypeVar('OP', bound='Operation')
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from elphick.geomet import IntervalSample
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NodeType(Enum):
|
|
22
|
+
SOURCE = 'input'
|
|
23
|
+
SINK = 'output'
|
|
24
|
+
BALANCE = 'degree 2+'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class Operation:
|
|
28
|
+
def __init__(self, name):
|
|
29
|
+
self.name = name
|
|
30
|
+
self._inputs = []
|
|
31
|
+
self._outputs = []
|
|
32
|
+
self._is_balanced: Optional[bool] = None
|
|
33
|
+
self._unbalanced_records: Optional[pd.DataFrame] = None
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def has_empty_input(self) -> bool:
|
|
37
|
+
return None in self.inputs
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def has_empty_output(self) -> bool:
|
|
41
|
+
return None in self.outputs
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def inputs(self):
|
|
45
|
+
return self._inputs
|
|
46
|
+
|
|
47
|
+
@inputs.setter
|
|
48
|
+
def inputs(self, value: list[MC]):
|
|
49
|
+
self._inputs = value
|
|
50
|
+
self.check_balance()
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def outputs(self):
|
|
54
|
+
return self._outputs
|
|
55
|
+
|
|
56
|
+
@outputs.setter
|
|
57
|
+
def outputs(self, value: list[MC]):
|
|
58
|
+
self._outputs = value
|
|
59
|
+
self.check_balance()
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def node_type(self) -> Optional[NodeType]:
|
|
63
|
+
if self.inputs and not self.outputs:
|
|
64
|
+
res = NodeType.SINK
|
|
65
|
+
elif self.outputs and not self.inputs:
|
|
66
|
+
res = NodeType.SOURCE
|
|
67
|
+
elif self.inputs and self.outputs:
|
|
68
|
+
res = NodeType.BALANCE
|
|
69
|
+
else:
|
|
70
|
+
res = None
|
|
71
|
+
return res
|
|
72
|
+
|
|
73
|
+
def get_input_mass(self) -> pd.DataFrame:
|
|
74
|
+
inputs = [i for i in self.inputs if i is not None]
|
|
75
|
+
|
|
76
|
+
if not inputs:
|
|
77
|
+
try:
|
|
78
|
+
return self._create_zero_mass()
|
|
79
|
+
except:
|
|
80
|
+
return pd.DataFrame()
|
|
81
|
+
elif len(inputs) == 1:
|
|
82
|
+
return inputs[0].mass_data
|
|
83
|
+
else:
|
|
84
|
+
return reduce(lambda a, b: a.add(b, fill_value=0), [stream.mass_data for stream in inputs])
|
|
85
|
+
|
|
86
|
+
def get_output_mass(self) -> pd.DataFrame:
|
|
87
|
+
outputs = [o for o in self.outputs if o is not None]
|
|
88
|
+
|
|
89
|
+
if not outputs:
|
|
90
|
+
try:
|
|
91
|
+
return self._create_zero_mass()
|
|
92
|
+
except:
|
|
93
|
+
return pd.DataFrame()
|
|
94
|
+
elif len(outputs) == 1:
|
|
95
|
+
return outputs[0].mass_data
|
|
96
|
+
else:
|
|
97
|
+
return reduce(lambda a, b: a.add(b, fill_value=0), [output.mass_data for output in outputs])
|
|
98
|
+
|
|
99
|
+
def check_balance(self):
|
|
100
|
+
"""Checks if the mass and chemistry of the input and output are balanced"""
|
|
101
|
+
if not self.inputs or not self.outputs:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
input_mass, output_mass = self.get_input_mass(), self.get_output_mass()
|
|
105
|
+
is_balanced = np.all(np.isclose(input_mass.fillna(0.0), output_mass.fillna(0.0)))
|
|
106
|
+
self._unbalanced_records = (input_mass - output_mass).loc[~np.isclose(input_mass, output_mass).any(axis=1)]
|
|
107
|
+
self._is_balanced = is_balanced
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def is_balanced(self) -> Optional[bool]:
|
|
111
|
+
return self._is_balanced
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def unbalanced_records(self) -> Optional[pd.DataFrame]:
|
|
115
|
+
return self._unbalanced_records
|
|
116
|
+
|
|
117
|
+
def solve(self) -> Optional[MC]:
|
|
118
|
+
"""Solves the operation
|
|
119
|
+
|
|
120
|
+
Missing data is represented by None in the input and output streams.
|
|
121
|
+
Solve will replace None with an object that balances the mass and chemistry of the input and output streams.
|
|
122
|
+
Returns
|
|
123
|
+
The back-calculated mc object
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
# Check the number of missing inputs and outputs
|
|
127
|
+
missing_count: int = self.inputs.count(None) + self.outputs.count(None)
|
|
128
|
+
if missing_count > 1:
|
|
129
|
+
raise ValueError("The operation cannot be solved - too many degrees of freedom")
|
|
130
|
+
mc = None
|
|
131
|
+
if missing_count == 0 and self.is_balanced:
|
|
132
|
+
return mc
|
|
133
|
+
else:
|
|
134
|
+
if None in self.inputs:
|
|
135
|
+
ref_object = self.outputs[0]
|
|
136
|
+
# Find the index of None in inputs
|
|
137
|
+
none_index = self.inputs.index(None)
|
|
138
|
+
|
|
139
|
+
# Calculate the None object
|
|
140
|
+
new_input_mass: pd.DataFrame = self.get_output_mass() - self.get_input_mass()
|
|
141
|
+
# Create a new object from the mass dataframe
|
|
142
|
+
mc = type(ref_object).from_mass_dataframe(new_input_mass, mass_wet=ref_object.mass_wet_var,
|
|
143
|
+
mass_dry=ref_object.mass_dry_var,
|
|
144
|
+
moisture_column_name=ref_object.moisture_column,
|
|
145
|
+
component_columns=ref_object.composition_columns,
|
|
146
|
+
composition_units=ref_object.composition_units,
|
|
147
|
+
moisture_in_scope=ref_object.moisture_in_scope)
|
|
148
|
+
# Replace None with the new input
|
|
149
|
+
self.inputs[none_index] = mc
|
|
150
|
+
|
|
151
|
+
elif None in self.outputs:
|
|
152
|
+
ref_object = self.inputs[0]
|
|
153
|
+
# Find the index of None in outputs
|
|
154
|
+
none_index = self.outputs.index(None)
|
|
155
|
+
|
|
156
|
+
# Calculate the None object
|
|
157
|
+
if len(self.outputs) == 1 and len(self.inputs) == 1:
|
|
158
|
+
# passthrough, no need to calculate. Shallow copy to minimise memory.
|
|
159
|
+
mc = copy(self.inputs[0])
|
|
160
|
+
mc.name = None
|
|
161
|
+
else:
|
|
162
|
+
new_output_mass: pd.DataFrame = self.get_input_mass() - self.get_output_mass()
|
|
163
|
+
# Create a new object from the mass dataframe
|
|
164
|
+
mc = type(ref_object).from_mass_dataframe(new_output_mass, mass_wet=ref_object.mass_wet_var,
|
|
165
|
+
mass_dry=ref_object.mass_dry_var,
|
|
166
|
+
moisture_column_name=ref_object.moisture_column,
|
|
167
|
+
component_columns=ref_object.composition_columns,
|
|
168
|
+
composition_units=ref_object.composition_units,
|
|
169
|
+
moisture_in_scope=ref_object.moisture_in_scope)
|
|
170
|
+
|
|
171
|
+
# Replace None with the new output
|
|
172
|
+
self.outputs[none_index] = mc
|
|
173
|
+
|
|
174
|
+
# update the balance related attributes
|
|
175
|
+
self.check_balance()
|
|
176
|
+
return mc
|
|
177
|
+
|
|
178
|
+
def _create_zero_mass(self) -> pd.DataFrame:
|
|
179
|
+
"""Creates a zero mass dataframe with the same columns and index as the mass data"""
|
|
180
|
+
# get the firstan object with the mass data
|
|
181
|
+
obj = self._get_object()
|
|
182
|
+
return pd.DataFrame(data=0, columns=obj.mass_data.columns, index=obj.mass_data.index)
|
|
183
|
+
|
|
184
|
+
def _get_object(self, name: Optional[str] = None) -> MC:
|
|
185
|
+
"""Returns an object from inputs or outputs"""
|
|
186
|
+
candidates = [mc for mc in self.outputs + self.inputs if mc is not None]
|
|
187
|
+
if len(candidates) == 0:
|
|
188
|
+
raise ValueError("No object found")
|
|
189
|
+
if name:
|
|
190
|
+
for obj in candidates:
|
|
191
|
+
if obj is not None and obj.name == name:
|
|
192
|
+
return obj
|
|
193
|
+
raise ValueError(f"No object found with name {name}")
|
|
194
|
+
else:
|
|
195
|
+
return candidates[0]
|
|
196
|
+
|
|
197
|
+
@classmethod
|
|
198
|
+
def from_dict(cls, config: dict) -> 'Operation':
|
|
199
|
+
name = config.get('name')
|
|
200
|
+
|
|
201
|
+
return cls(name=name)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class Input(Operation):
|
|
205
|
+
def __init__(self, name):
|
|
206
|
+
super().__init__(name)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class Output(Operation):
|
|
210
|
+
def __init__(self, name):
|
|
211
|
+
super().__init__(name)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class Passthrough(Operation):
|
|
215
|
+
def __init__(self, name):
|
|
216
|
+
super().__init__(name)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class PartitionOperation(Operation):
|
|
220
|
+
"""An operation that partitions the input stream into multiple output streams based on a partition function
|
|
221
|
+
|
|
222
|
+
The partition input is the mean of the fractions or the geomean if the fractions are in the size dimension
|
|
223
|
+
The partition function is typically a partial function so that the partition is defined for all arguments
|
|
224
|
+
other than the input mean fraction values in one or two dimensions. The argument names must match the
|
|
225
|
+
index names in the IntervalSample.
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def __init__(self, name, partition=None):
|
|
230
|
+
super().__init__(name)
|
|
231
|
+
self.partition = partition
|
|
232
|
+
self.partition_function = None
|
|
233
|
+
if self.partition and 'module' in self.partition and 'function' in self.partition:
|
|
234
|
+
self.partition_function = load_partition_function(self.partition['module'], self.partition['function'])
|
|
235
|
+
|
|
236
|
+
def solve(self) -> [MC, MC]:
|
|
237
|
+
if self.partition_function:
|
|
238
|
+
self.apply_partition()
|
|
239
|
+
# update the balance related attributes
|
|
240
|
+
self.check_balance()
|
|
241
|
+
return self.outputs
|
|
242
|
+
|
|
243
|
+
def apply_partition(self):
|
|
244
|
+
if len(self.inputs) != 1:
|
|
245
|
+
raise ValueError("PartitionOperation must have exactly one input")
|
|
246
|
+
for input_sample in self.inputs:
|
|
247
|
+
input_sample: 'IntervalSample'
|
|
248
|
+
if input_sample is not None:
|
|
249
|
+
output, complement = input_sample.split_by_partition(self.partition_function)
|
|
250
|
+
self.outputs = [output, complement]
|
|
251
|
+
|
|
252
|
+
@classmethod
|
|
253
|
+
def from_dict(cls, config: dict) -> 'PartitionOperation':
|
|
254
|
+
name = config.get('name')
|
|
255
|
+
partition = config.get('partition')
|
|
256
|
+
return cls(name=name, partition=partition)
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import uuid
|
|
2
|
-
|
|
3
|
-
from elphick.geomet.base import MassComposition
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class Stream(MassComposition):
|
|
7
|
-
def __init__(self, *args, **kwargs):
|
|
8
|
-
super().__init__(*args, **kwargs)
|
|
9
|
-
self.nodes = [uuid.uuid4(), uuid.uuid4()]
|
|
10
|
-
|
|
11
|
-
def set_parent_node(self, parent: 'Stream') -> 'Stream':
|
|
12
|
-
self.nodes = [parent.nodes[1], self.nodes[1]]
|
|
13
|
-
return self
|
|
14
|
-
|
|
15
|
-
def set_child_node(self, child: 'Stream') -> 'Stream':
|
|
16
|
-
self.nodes = [self.nodes[0], child.nodes[0]]
|
|
17
|
-
return self
|
|
18
|
-
|
|
19
|
-
def set_nodes(self, nodes: list) -> 'Stream':
|
|
20
|
-
if len(nodes) != 2:
|
|
21
|
-
raise ValueError('Nodes must be a list of length 2')
|
|
22
|
-
if nodes[0] == nodes[1]:
|
|
23
|
-
raise ValueError('Nodes must be different')
|
|
24
|
-
self.nodes = nodes
|
|
25
|
-
return self
|
|
26
|
-
|
|
27
|
-
# @classmethod
|
|
28
|
-
# def from_mass_composition(cls, obj: MassComposition) -> 'Stream':
|
|
29
|
-
# filtered_kwargs = filter_kwargs(obj, **obj.__dict__)
|
|
30
|
-
# filtered_kwargs['data'] = obj.data
|
|
31
|
-
# stream = cls(**filtered_kwargs)
|
|
32
|
-
# stream.__class__ = type(obj.__class__.__name__, (obj.__class__, cls), {})
|
|
33
|
-
# return stream
|
|
34
|
-
|
|
35
|
-
@classmethod
|
|
36
|
-
def from_dict(cls, config: dict) -> 'Stream':
|
|
37
|
-
name = config.get('name')
|
|
38
|
-
node_in = config.get('node_in')
|
|
39
|
-
node_out = config.get('node_out')
|
|
1
|
+
import uuid
|
|
2
|
+
|
|
3
|
+
from elphick.geomet.base import MassComposition
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Stream(MassComposition):
|
|
7
|
+
def __init__(self, *args, **kwargs):
|
|
8
|
+
super().__init__(*args, **kwargs)
|
|
9
|
+
self.nodes = [uuid.uuid4(), uuid.uuid4()]
|
|
10
|
+
|
|
11
|
+
def set_parent_node(self, parent: 'Stream') -> 'Stream':
|
|
12
|
+
self.nodes = [parent.nodes[1], self.nodes[1]]
|
|
13
|
+
return self
|
|
14
|
+
|
|
15
|
+
def set_child_node(self, child: 'Stream') -> 'Stream':
|
|
16
|
+
self.nodes = [self.nodes[0], child.nodes[0]]
|
|
17
|
+
return self
|
|
18
|
+
|
|
19
|
+
def set_nodes(self, nodes: list) -> 'Stream':
|
|
20
|
+
if len(nodes) != 2:
|
|
21
|
+
raise ValueError('Nodes must be a list of length 2')
|
|
22
|
+
if nodes[0] == nodes[1]:
|
|
23
|
+
raise ValueError('Nodes must be different')
|
|
24
|
+
self.nodes = nodes
|
|
25
|
+
return self
|
|
26
|
+
|
|
27
|
+
# @classmethod
|
|
28
|
+
# def from_mass_composition(cls, obj: MassComposition) -> 'Stream':
|
|
29
|
+
# filtered_kwargs = filter_kwargs(obj, **obj.__dict__)
|
|
30
|
+
# filtered_kwargs['data'] = obj.data
|
|
31
|
+
# stream = cls(**filtered_kwargs)
|
|
32
|
+
# stream.__class__ = type(obj.__class__.__name__, (obj.__class__, cls), {})
|
|
33
|
+
# return stream
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_dict(cls, config: dict) -> 'Stream':
|
|
37
|
+
name = config.get('name')
|
|
38
|
+
node_in = config.get('node_in')
|
|
39
|
+
node_out = config.get('node_out')
|
|
40
40
|
return cls(name=name).set_nodes([node_in, node_out])
|