assaycalc 0.1.0__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.
- assaycalc-0.1.0/.gitignore +8 -0
- assaycalc-0.1.0/LICENSE +21 -0
- assaycalc-0.1.0/PKG-INFO +73 -0
- assaycalc-0.1.0/README.md +57 -0
- assaycalc-0.1.0/assay_compuesto.csv +6 -0
- assaycalc-0.1.0/assay_original.csv +6 -0
- assaycalc-0.1.0/pyproject.toml +29 -0
- assaycalc-0.1.0/src/mining_assay_toolkit/__init__.py +17 -0
- assaycalc-0.1.0/src/mining_assay_toolkit/core.py +81 -0
- assaycalc-0.1.0/tests/test_core.py +133 -0
assaycalc-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rodrigo Hurtado Quispe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
assaycalc-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: assaycalc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Librería para leer, validar y procesar datos de ensayos geológicos (assay) de sondajes mineros.
|
|
5
|
+
Author-email: Rodrigo <martinhq30@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: pandas>=2.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
14
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# mining-assay-toolkit
|
|
18
|
+
|
|
19
|
+
Herramientas en Python para el procesamiento y validación de datos de assay geológico (sondajes de exploración minera).
|
|
20
|
+
|
|
21
|
+
## Instalación
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install mining-assay-toolkit
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Uso rápido
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from mining_assay_toolkit import leer_csv_assay, validar_assay, componer_assay, guardar_csv
|
|
31
|
+
|
|
32
|
+
# 1. Leer datos de assay desde un CSV
|
|
33
|
+
df = leer_csv_assay("assay_original.csv")
|
|
34
|
+
|
|
35
|
+
# 2. Validar integridad de los datos
|
|
36
|
+
validar_assay(df)
|
|
37
|
+
|
|
38
|
+
# 3. Generar compósitos de 2 metros ponderados por ley de Cu
|
|
39
|
+
df_compositos = componer_assay(df, longitud_composito=2.0)
|
|
40
|
+
|
|
41
|
+
# 4. Guardar el resultado
|
|
42
|
+
guardar_csv(df_compositos, "assay_compuesto.csv")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Formato de entrada esperado
|
|
46
|
+
|
|
47
|
+
El CSV debe contener, como mínimo, estas columnas:
|
|
48
|
+
|
|
49
|
+
| Columna | Descripción |
|
|
50
|
+
|------------|---------------------------------------|
|
|
51
|
+
| `hole_id` | Identificador del sondaje |
|
|
52
|
+
| `from` | Profundidad inicial del intervalo (m) |
|
|
53
|
+
| `to` | Profundidad final del intervalo (m) |
|
|
54
|
+
| `Cu_pct` | Ley de cobre (%) en el intervalo |
|
|
55
|
+
|
|
56
|
+
## Validaciones incluidas
|
|
57
|
+
|
|
58
|
+
`validar_assay()` verifica automáticamente:
|
|
59
|
+
|
|
60
|
+
- Que no existan `hole_id` vacíos
|
|
61
|
+
- Que todos los intervalos tengan `from < to`
|
|
62
|
+
- Que no existan leyes de `Cu_pct` negativas
|
|
63
|
+
- Que no existan intervalos solapados dentro de un mismo sondaje
|
|
64
|
+
|
|
65
|
+
Si alguna validación falla, se lanza un `ValueError` con el detalle del problema.
|
|
66
|
+
|
|
67
|
+
## Compositado
|
|
68
|
+
|
|
69
|
+
`componer_assay()` divide cada sondaje en intervalos de longitud fija (por ejemplo, cada 2 metros) y calcula la ley promedio ponderada por la longitud de cada intervalo original que cae dentro del compósito.
|
|
70
|
+
|
|
71
|
+
## Licencia
|
|
72
|
+
|
|
73
|
+
MIT — ver [LICENSE](LICENSE) para más detalles.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# mining-assay-toolkit
|
|
2
|
+
|
|
3
|
+
Herramientas en Python para el procesamiento y validación de datos de assay geológico (sondajes de exploración minera).
|
|
4
|
+
|
|
5
|
+
## Instalación
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install mining-assay-toolkit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Uso rápido
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from mining_assay_toolkit import leer_csv_assay, validar_assay, componer_assay, guardar_csv
|
|
15
|
+
|
|
16
|
+
# 1. Leer datos de assay desde un CSV
|
|
17
|
+
df = leer_csv_assay("assay_original.csv")
|
|
18
|
+
|
|
19
|
+
# 2. Validar integridad de los datos
|
|
20
|
+
validar_assay(df)
|
|
21
|
+
|
|
22
|
+
# 3. Generar compósitos de 2 metros ponderados por ley de Cu
|
|
23
|
+
df_compositos = componer_assay(df, longitud_composito=2.0)
|
|
24
|
+
|
|
25
|
+
# 4. Guardar el resultado
|
|
26
|
+
guardar_csv(df_compositos, "assay_compuesto.csv")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Formato de entrada esperado
|
|
30
|
+
|
|
31
|
+
El CSV debe contener, como mínimo, estas columnas:
|
|
32
|
+
|
|
33
|
+
| Columna | Descripción |
|
|
34
|
+
|------------|---------------------------------------|
|
|
35
|
+
| `hole_id` | Identificador del sondaje |
|
|
36
|
+
| `from` | Profundidad inicial del intervalo (m) |
|
|
37
|
+
| `to` | Profundidad final del intervalo (m) |
|
|
38
|
+
| `Cu_pct` | Ley de cobre (%) en el intervalo |
|
|
39
|
+
|
|
40
|
+
## Validaciones incluidas
|
|
41
|
+
|
|
42
|
+
`validar_assay()` verifica automáticamente:
|
|
43
|
+
|
|
44
|
+
- Que no existan `hole_id` vacíos
|
|
45
|
+
- Que todos los intervalos tengan `from < to`
|
|
46
|
+
- Que no existan leyes de `Cu_pct` negativas
|
|
47
|
+
- Que no existan intervalos solapados dentro de un mismo sondaje
|
|
48
|
+
|
|
49
|
+
Si alguna validación falla, se lanza un `ValueError` con el detalle del problema.
|
|
50
|
+
|
|
51
|
+
## Compositado
|
|
52
|
+
|
|
53
|
+
`componer_assay()` divide cada sondaje en intervalos de longitud fija (por ejemplo, cada 2 metros) y calcula la ley promedio ponderada por la longitud de cada intervalo original que cae dentro del compósito.
|
|
54
|
+
|
|
55
|
+
## Licencia
|
|
56
|
+
|
|
57
|
+
MIT — ver [LICENSE](LICENSE) para más detalles.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "assaycalc"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Librería para leer, validar y procesar datos de ensayos geológicos (assay) de sondajes mineros."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Rodrigo", email = "martinhq30@gmail.com" }
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.9"
|
|
15
|
+
|
|
16
|
+
dependencies = [
|
|
17
|
+
"pandas>=2.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=8.0",
|
|
23
|
+
"pytest-cov>=5.0",
|
|
24
|
+
"mypy>=1.10",
|
|
25
|
+
"ruff>=0.5",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/mining_assay_toolkit"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Mining Assay Toolkit: procesamiento de datos de ensayos geológicos."""
|
|
2
|
+
|
|
3
|
+
from mining_assay_toolkit.core import (
|
|
4
|
+
componer_assay,
|
|
5
|
+
guardar_csv,
|
|
6
|
+
leer_csv_assay,
|
|
7
|
+
validar_assay,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"componer_assay",
|
|
12
|
+
"guardar_csv",
|
|
13
|
+
"leer_csv_assay",
|
|
14
|
+
"validar_assay",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Procesamiento básico de datos de assay geológico (v0)."""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
REQUIRED_COLUMNS = {"hole_id", "from", "to", "Cu_pct"}
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def leer_csv_assay(ruta: str) -> pd.DataFrame:
|
|
10
|
+
"""Lee un archivo CSV de assay y devuelve un DataFrame."""
|
|
11
|
+
df = pd.read_csv(
|
|
12
|
+
ruta,
|
|
13
|
+
sep=None,
|
|
14
|
+
engine="python",
|
|
15
|
+
encoding="utf-8-sig"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
columnas_faltantes = REQUIRED_COLUMNS - set(df.columns)
|
|
19
|
+
|
|
20
|
+
if columnas_faltantes:
|
|
21
|
+
raise ValueError(f"Faltan columnas requeridas: {columnas_faltantes}")
|
|
22
|
+
|
|
23
|
+
return df
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def validar_assay(df: pd.DataFrame) -> None:
|
|
27
|
+
"""Valida reglas mínimas de integridad sobre los datos de assay."""
|
|
28
|
+
if df["hole_id"].isna().any():
|
|
29
|
+
raise ValueError("Existen filas con hole_id vacío.")
|
|
30
|
+
|
|
31
|
+
if (df["from"] >= df["to"]).any():
|
|
32
|
+
raise ValueError("Existen intervalos donde 'from' >= 'to'.")
|
|
33
|
+
|
|
34
|
+
if (df["Cu_pct"] < 0).any():
|
|
35
|
+
raise ValueError("Existen leyes de Cu_pct negativas.")
|
|
36
|
+
|
|
37
|
+
for hole_id, grupo in df.groupby("hole_id"):
|
|
38
|
+
grupo_ordenado = grupo.sort_values("from")
|
|
39
|
+
solapes = (
|
|
40
|
+
grupo_ordenado["from"].iloc[1:].to_numpy()
|
|
41
|
+
< grupo_ordenado["to"].iloc[:-1].to_numpy()
|
|
42
|
+
)
|
|
43
|
+
if solapes.any():
|
|
44
|
+
raise ValueError(f"Intervalos solapados en el sondaje {hole_id}.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def componer_assay(df: pd.DataFrame, longitud_composito: float) -> pd.DataFrame:
|
|
48
|
+
"""Genera compósitos de longitud fija ponderando la ley por longitud del intervalo."""
|
|
49
|
+
resultados = []
|
|
50
|
+
|
|
51
|
+
for hole_id, grupo in df.groupby("hole_id"):
|
|
52
|
+
grupo = grupo.sort_values("from").reset_index(drop=True)
|
|
53
|
+
profundidad_max = grupo["to"].max()
|
|
54
|
+
inicio = 0.0
|
|
55
|
+
|
|
56
|
+
while inicio < profundidad_max:
|
|
57
|
+
fin = inicio + longitud_composito
|
|
58
|
+
interseccion = grupo[(grupo["from"] < fin) & (grupo["to"] > inicio)]
|
|
59
|
+
|
|
60
|
+
if not interseccion.empty:
|
|
61
|
+
largos = (
|
|
62
|
+
interseccion["to"].clip(upper=fin)
|
|
63
|
+
- interseccion["from"].clip(lower=inicio)
|
|
64
|
+
)
|
|
65
|
+
ley_ponderada = (interseccion["Cu_pct"] * largos).sum() / largos.sum()
|
|
66
|
+
|
|
67
|
+
resultados.append({
|
|
68
|
+
"hole_id": hole_id,
|
|
69
|
+
"from": inicio,
|
|
70
|
+
"to": fin,
|
|
71
|
+
"Cu_pct": round(ley_ponderada, 3),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
inicio = fin
|
|
75
|
+
|
|
76
|
+
return pd.DataFrame(resultados)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def guardar_csv(df: pd.DataFrame, ruta: str) -> None:
|
|
80
|
+
"""Guarda un DataFrame como archivo CSV."""
|
|
81
|
+
df.to_csv(ruta, index=False)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Tests para mining_assay_toolkit.core"""
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from mining_assay_toolkit.core import (
|
|
7
|
+
leer_csv_assay,
|
|
8
|
+
validar_assay,
|
|
9
|
+
componer_assay,
|
|
10
|
+
guardar_csv,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------- Fixtures ----------
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def df_valido():
|
|
18
|
+
return pd.DataFrame({
|
|
19
|
+
"hole_id": ["DDH-01", "DDH-01", "DDH-01"],
|
|
20
|
+
"from": [0.0, 2.0, 4.0],
|
|
21
|
+
"to": [2.0, 4.0, 6.0],
|
|
22
|
+
"Cu_pct": [0.5, 1.0, 0.3],
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------- Tests: leer_csv_assay ----------
|
|
27
|
+
|
|
28
|
+
def test_leer_csv_assay_lee_correctamente(tmp_path, df_valido):
|
|
29
|
+
ruta = tmp_path / "assay.csv"
|
|
30
|
+
df_valido.to_csv(ruta, index=False)
|
|
31
|
+
|
|
32
|
+
resultado = leer_csv_assay(str(ruta))
|
|
33
|
+
|
|
34
|
+
assert list(resultado.columns) == list(df_valido.columns)
|
|
35
|
+
assert len(resultado) == 3
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_leer_csv_assay_falla_si_faltan_columnas(tmp_path):
|
|
39
|
+
df_incompleto = pd.DataFrame({"hole_id": ["DDH-01"], "from": [0.0]})
|
|
40
|
+
ruta = tmp_path / "assay_incompleto.csv"
|
|
41
|
+
df_incompleto.to_csv(ruta, index=False)
|
|
42
|
+
|
|
43
|
+
with pytest.raises(ValueError, match="Faltan columnas requeridas"):
|
|
44
|
+
leer_csv_assay(str(ruta))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ---------- Tests: validar_assay ----------
|
|
48
|
+
|
|
49
|
+
def test_validar_assay_pasa_con_datos_correctos(df_valido):
|
|
50
|
+
# No debe lanzar ninguna excepción
|
|
51
|
+
validar_assay(df_valido)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_validar_assay_falla_con_hole_id_vacio(df_valido):
|
|
55
|
+
df_valido.loc[0, "hole_id"] = None
|
|
56
|
+
|
|
57
|
+
with pytest.raises(ValueError, match="hole_id vacío"):
|
|
58
|
+
validar_assay(df_valido)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_validar_assay_falla_con_from_mayor_igual_to(df_valido):
|
|
62
|
+
df_valido.loc[0, "from"] = 5.0
|
|
63
|
+
df_valido.loc[0, "to"] = 2.0
|
|
64
|
+
|
|
65
|
+
with pytest.raises(ValueError, match="'from' >= 'to'"):
|
|
66
|
+
validar_assay(df_valido)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_validar_assay_falla_con_ley_negativa(df_valido):
|
|
70
|
+
df_valido.loc[0, "Cu_pct"] = -0.1
|
|
71
|
+
|
|
72
|
+
with pytest.raises(ValueError, match="Cu_pct negativas"):
|
|
73
|
+
validar_assay(df_valido)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_validar_assay_falla_con_intervalos_solapados():
|
|
77
|
+
df_solapado = pd.DataFrame({
|
|
78
|
+
"hole_id": ["DDH-01", "DDH-01"],
|
|
79
|
+
"from": [0.0, 1.0],
|
|
80
|
+
"to": [2.0, 3.0],
|
|
81
|
+
"Cu_pct": [0.5, 0.8],
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
with pytest.raises(ValueError, match="solapados"):
|
|
85
|
+
validar_assay(df_solapado)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------- Tests: componer_assay ----------
|
|
89
|
+
|
|
90
|
+
def test_componer_assay_genera_compositos_correctos(df_valido):
|
|
91
|
+
resultado = componer_assay(df_valido, longitud_composito=2.0)
|
|
92
|
+
|
|
93
|
+
assert len(resultado) == 3
|
|
94
|
+
assert list(resultado["from"]) == [0.0, 2.0, 4.0]
|
|
95
|
+
assert list(resultado["to"]) == [2.0, 4.0, 6.0]
|
|
96
|
+
# El primer intervalo coincide exactamente con un compósito de 2m
|
|
97
|
+
assert resultado.loc[0, "Cu_pct"] == 0.5
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_componer_assay_pondera_correctamente_intervalo_parcial():
|
|
101
|
+
# Un intervalo de 0 a 3m con ley 1.0, compositado cada 2m
|
|
102
|
+
df = pd.DataFrame({
|
|
103
|
+
"hole_id": ["DDH-01"],
|
|
104
|
+
"from": [0.0],
|
|
105
|
+
"to": [3.0],
|
|
106
|
+
"Cu_pct": [1.0],
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
resultado = componer_assay(df, longitud_composito=2.0)
|
|
110
|
+
|
|
111
|
+
# Ambos compósitos (0-2 y 2-4) deben tener ley 1.0,
|
|
112
|
+
# ya que todo el intervalo original tiene la misma ley
|
|
113
|
+
assert resultado["Cu_pct"].tolist() == [1.0, 1.0]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_componer_assay_devuelve_dataframe_vacio_si_no_hay_datos():
|
|
117
|
+
df_vacio = pd.DataFrame(columns=["hole_id", "from", "to", "Cu_pct"])
|
|
118
|
+
|
|
119
|
+
resultado = componer_assay(df_vacio, longitud_composito=2.0)
|
|
120
|
+
|
|
121
|
+
assert resultado.empty
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------- Tests: guardar_csv ----------
|
|
125
|
+
|
|
126
|
+
def test_guardar_csv_crea_archivo_correctamente(tmp_path, df_valido):
|
|
127
|
+
ruta = tmp_path / "salida.csv"
|
|
128
|
+
|
|
129
|
+
guardar_csv(df_valido, str(ruta))
|
|
130
|
+
|
|
131
|
+
assert ruta.exists()
|
|
132
|
+
df_leido = pd.read_csv(ruta)
|
|
133
|
+
assert len(df_leido) == len(df_valido)
|