geometallurgy 0.4.6__tar.gz → 0.4.8__tar.gz

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.
Files changed (45) hide show
  1. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/PKG-INFO +1 -1
  2. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/base.py +115 -3
  3. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/config/mc_config.yml +1 -0
  4. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/pyproject.toml +1 -1
  5. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/LICENSE +0 -0
  6. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/README.md +0 -0
  7. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/__init__.py +0 -0
  8. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/block_model.py +0 -0
  9. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/config/__init__.py +0 -0
  10. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/config/config_read.py +0 -0
  11. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/config/flowsheet_example.yaml +0 -0
  12. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/data/downloader.py +0 -0
  13. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/data/register.csv +0 -0
  14. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/datasets/__init__.py +0 -0
  15. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/datasets/datasets.py +0 -0
  16. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/datasets/downloader.py +0 -0
  17. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/datasets/register.csv +0 -0
  18. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/datasets/sample_data.py +0 -0
  19. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/extras.py +0 -0
  20. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/flowsheet/__init__.py +0 -0
  21. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/flowsheet/flowsheet.py +0 -0
  22. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/flowsheet/loader.py +0 -0
  23. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/flowsheet/operation.py +0 -0
  24. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/flowsheet/stream.py +0 -0
  25. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/interval_sample.py +0 -0
  26. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/io.py +0 -0
  27. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/plot.py +0 -0
  28. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/profile.py +0 -0
  29. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/sample.py +0 -0
  30. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/__init__.py +0 -0
  31. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/amenability.py +0 -0
  32. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/block_model_converter.py +0 -0
  33. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/components.py +0 -0
  34. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/data.py +0 -0
  35. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/interp.py +0 -0
  36. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/layout.py +0 -0
  37. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/moisture.py +0 -0
  38. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/pandas.py +0 -0
  39. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/parallel.py +0 -0
  40. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/partition.py +0 -0
  41. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/sampling.py +0 -0
  42. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/size.py +0 -0
  43. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/timer.py +0 -0
  44. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/utils/viz.py +0 -0
  45. {geometallurgy-0.4.6 → geometallurgy-0.4.8}/elphick/geomet/validate.py.hide +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geometallurgy
3
- Version: 0.4.6
3
+ Version: 0.4.8
4
4
  Summary: Tools for the geometallurgist
5
5
  Home-page: https://github.com/elphick/geometallurgy
6
6
  Author: Greg
@@ -4,10 +4,12 @@ import logging
4
4
  import re
5
5
  from abc import ABC
6
6
  from pathlib import Path
7
- from typing import Optional, Union, Literal, TypeVar, TYPE_CHECKING, Any
7
+ from typing import Optional, Union, Literal, TypeVar, TYPE_CHECKING
8
8
 
9
9
  import numpy as np
10
10
  import pandas as pd
11
+ import plotly.express as px
12
+ import plotly.graph_objects as go
11
13
 
12
14
  from elphick.geomet.config import read_yaml
13
15
  from elphick.geomet.utils.components import get_components, is_compositional
@@ -17,8 +19,6 @@ from elphick.geomet.utils.sampling import random_int
17
19
  from elphick.geomet.utils.timer import log_timer
18
20
  from .config.config_read import get_column_config
19
21
  from .plot import parallel_plot, comparison_plot
20
- import plotly.express as px
21
- import plotly.graph_objects as go
22
22
 
23
23
  if TYPE_CHECKING:
24
24
  from elphick.geomet.flowsheet.stream import Stream
@@ -138,6 +138,22 @@ class MassComposition(ABC):
138
138
  # Recalculate the aggregate whenever the data changes
139
139
  self.aggregate = self.weight_average()
140
140
 
141
+ def get_mass_data(self, include_moisture: bool = True) -> pd.DataFrame:
142
+ """Get the mass data
143
+
144
+ Args:
145
+ include_moisture: If True (and moisture is in scope), include the moisture mass column
146
+
147
+ Returns:
148
+
149
+ """
150
+ if include_moisture and self.moisture_in_scope:
151
+ moisture_mass = self._mass_data[self.mass_wet_var] - self._mass_data[self.mass_dry_var]
152
+ mass_data: pd.DataFrame = self._mass_data.copy()
153
+ mass_data.insert(loc=2, column=self.moisture_column, value=moisture_mass)
154
+ return mass_data
155
+ return self._mass_data
156
+
141
157
  @property
142
158
  def aggregate(self) -> pd.DataFrame:
143
159
  if self._aggregate is None and self._mass_data is not None:
@@ -235,6 +251,49 @@ class MassComposition(ABC):
235
251
 
236
252
  return self
237
253
 
254
+ def clip_recovery(self, other: MC, recovery_bounds: tuple[float, float] = (0.01, 0.99),
255
+ allow_moisture_coercion: bool = True) -> MC:
256
+ """Clip the recovery to the specified bounds and recalculate the estimate.
257
+
258
+ Args:
259
+ other: The other MassComposition object, from which the recovery of self is calculated.
260
+ recovery_bounds: The bounds for the recovery between 0.0 and 1.0
261
+ allow_moisture_coercion: if True, allow the wet mass to be modified to maintain the moisture (in the
262
+ case that dry mass is clipped to manage recovery)
263
+
264
+ Returns:
265
+ The MassComposition object with the recovery clipped to the bounds.
266
+ """
267
+ recovery: pd.DataFrame = (self.get_mass_data(include_moisture=False) /
268
+ other.get_mass_data(include_moisture=False))
269
+
270
+ # Limit the recovery to the bounds
271
+ before_clip = recovery.copy()
272
+ recovery = recovery.clip(lower=recovery_bounds[0], upper=recovery_bounds[1]).fillna(0.0)
273
+
274
+ # Check if any records were affected
275
+ affected_indexes = set(recovery.index[np.any(before_clip != recovery, axis=1)])
276
+ if affected_indexes:
277
+ # Recalculate the estimate from the bound recovery
278
+ new_mass: pd.DataFrame = recovery * other.get_mass_data(include_moisture=False)[recovery.columns]
279
+
280
+ if self.moisture_in_scope and allow_moisture_coercion:
281
+ # Calculate the moisture from the new mass
282
+ new_mass[self.mass_wet_var] = solve_mass_moisture(mass_dry=new_mass[self.mass_dry_var],
283
+ moisture=self.data[self.moisture_column])
284
+
285
+ # Log the top 50 records affected by the recovery coercion
286
+ affected_indexes_list = sorted(affected_indexes)[:50]
287
+ self._logger.info(f"Recovery coercion affected {len(affected_indexes)} records. "
288
+ f"Affected indexes (first 50): {affected_indexes_list}")
289
+
290
+ # Update the mass data of self
291
+ self.update_mass_data(new_mass)
292
+ else:
293
+ self._logger.info("Recovery coercion did not affect any records.")
294
+
295
+ return self
296
+
238
297
  def set_moisture(self, moisture: Union[pd.Series, float, int], mass_to_adjust: Literal['wet', 'dry'] = 'wet') -> MC:
239
298
  """Set the moisture to the specified value
240
299
 
@@ -268,6 +327,45 @@ class MassComposition(ABC):
268
327
 
269
328
  return self
270
329
 
330
+ def clip_composition(self, ranges: Optional[dict[str, list[float]]] = None) -> MC:
331
+ """Clip the components
332
+
333
+ Clip to the components to within the range provided or the default range for each component.
334
+ This method does not clip moisture - see set_moisture and solve_moisture for that.
335
+
336
+ Args:
337
+ ranges: An optional dict defining a list of [lo, hi] floats for each component. If not provided,
338
+ the default range from the config file will be used.
339
+
340
+ Returns:
341
+ The object with clipped composition.
342
+ """
343
+
344
+ # load the default ranges from the config file
345
+ component_ranges: dict = self._get_component_ranges(ranges)
346
+
347
+ # define a small value to ensure the clipped values lie marginally inside the specified range.
348
+ epsilon: float = 0.0 # 1.0e-05
349
+ # clip the components
350
+ affected_indexes = set()
351
+ for component, component_range in component_ranges.items():
352
+ before_clip = self._mass_data[component].copy()
353
+ # define the component mass that aligns with the lower and upper bounds
354
+ component_mass_limits = self._mass_data[self.mass_dry_var].values[:, np.newaxis] * np.array(
355
+ component_range) / self.composition_factor
356
+ # apply the clip to the mass data
357
+ self._mass_data[component] = self._mass_data[component].clip(lower=component_mass_limits[:, 0] + epsilon,
358
+ upper=component_mass_limits[:, 1] - epsilon)
359
+ affected_indexes.update(self._mass_data.index[before_clip != self._mass_data[component]])
360
+
361
+ # log the action, including the first 50 indexes affected
362
+ affected_indexes_list = sorted(affected_indexes)[:50]
363
+ self._logger.info(
364
+ f"{len(affected_indexes)} records where composition has been clipped to the range: {component_ranges}."
365
+ f" Affected indexes (first 50): {affected_indexes_list}")
366
+
367
+ return self
368
+
271
369
  def plot_parallel(self, color: Optional[str] = None,
272
370
  vars_include: Optional[list[str]] = None,
273
371
  vars_exclude: Optional[list[str]] = None,
@@ -804,6 +902,20 @@ class MassComposition(ABC):
804
902
 
805
903
  return res
806
904
 
905
+ def _get_component_ranges(self, ranges: dict[str, list]) -> dict[str, list]:
906
+
907
+ d_ranges: dict = get_column_config(config_dict=self.config, var_map=self.variable_map,
908
+ config_key='range')
909
+ # filter to include only components
910
+ d_ranges = {k: v for k, v in d_ranges.items() if k in self.composition_columns}
911
+
912
+ # modify the default dict based on any user passed constraints
913
+ if ranges:
914
+ for k, v in ranges.items():
915
+ d_ranges[k] = v
916
+
917
+ return d_ranges
918
+
807
919
 
808
920
  class OutOfRangeStatus:
809
921
  """A class to check and report out-of-range records in an MC object."""
@@ -27,6 +27,7 @@ MC:
27
27
  - [from, to]
28
28
  - [retained, passing]
29
29
  - [sink, float]
30
+ - [lo, hi]
30
31
  comparisons:
31
32
  recovery: 'rec'
32
33
  difference: 'diff'
@@ -1,7 +1,7 @@
1
1
  [tool.poetry]
2
2
  name = "geometallurgy"
3
3
  packages = [{ include = "elphick/geomet" }]
4
- version = "0.4.6"
4
+ version = "0.4.8"
5
5
  description = "Tools for the geometallurgist"
6
6
  authors = ["Greg <11791585+elphick@users.noreply.github.com>"]
7
7
  repository = "https://github.com/elphick/geometallurgy"
File without changes
File without changes