omfpandas 0.1.1__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.
- omfpandas-0.1.1/LICENSE +21 -0
- omfpandas-0.1.1/PKG-INFO +53 -0
- omfpandas-0.1.1/README.md +33 -0
- omfpandas-0.1.1/omfpandas/__init__.py +10 -0
- omfpandas-0.1.1/omfpandas/omfp.py +69 -0
- omfpandas-0.1.1/omfpandas/volume.py +81 -0
- omfpandas-0.1.1/pyproject.toml +38 -0
omfpandas-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Greg Elphick
|
|
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.
|
omfpandas-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: omfpandas
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary:
|
|
5
|
+
Home-page: https://github.com/elphick/omfpandas
|
|
6
|
+
Author: Greg
|
|
7
|
+
Author-email: 11791585+elphick@users.noreply.github.com
|
|
8
|
+
Requires-Python: >=3.9,<3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Provides-Extra: io
|
|
13
|
+
Requires-Dist: omf (>=1.0.1,<2.0.0)
|
|
14
|
+
Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
15
|
+
Requires-Dist: pyarrow (>=16.1.0,<17.0.0) ; extra == "io"
|
|
16
|
+
Project-URL: Documentation, https://elphick.github.io/omfpandas
|
|
17
|
+
Project-URL: Repository, https://github.com/elphick/omfpandas
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# omfpandas
|
|
21
|
+
|
|
22
|
+
[](https://github.com/Elphick/omfpandas/actions/workflows/poetry_build_and_test.yml)
|
|
23
|
+
[](https://github.com/Elphick/omfpandas/actions/workflows/poetry_sphinx_docs_to_gh_pages.yml)
|
|
24
|
+
|
|
25
|
+
A pandas (and parquet) interface for the [Open Mining Format package (omf)](https://omf.readthedocs.io/en/latest/).
|
|
26
|
+
|
|
27
|
+
When working with OMF files, it is often useful to convert the data to a pandas DataFrame.
|
|
28
|
+
This package provides a simple interface to do so.
|
|
29
|
+
|
|
30
|
+
The parquet format is a nice, compact, efficient format to persist pandas DataFrames.
|
|
31
|
+
This package also provides a simple interface to convert an omf element to a parquet file.
|
|
32
|
+
When datasets do not fit into memory, parquet files can be read in chunks or by column.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install omfpandas
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
If you intend to use the parquet functionality, you will need to install the optional dependencies.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install omfpandas[io]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Roadmap
|
|
47
|
+
|
|
48
|
+
- [ ] 0.2.0 - Add support for reading a VolumeElement (Block Model) from an OMF file as a pandas DataFrame.
|
|
49
|
+
Export a VolumeElement as a parquet file.
|
|
50
|
+
- [ ] 0.3.0 - Add support for writing a DataFrame to an OMF VolumeElement
|
|
51
|
+
- [ ] 0.4.0 - Add support for low-memory/out-of-core writing an omf element to parquet
|
|
52
|
+
- [ ] ...
|
|
53
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# omfpandas
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Elphick/omfpandas/actions/workflows/poetry_build_and_test.yml)
|
|
4
|
+
[](https://github.com/Elphick/omfpandas/actions/workflows/poetry_sphinx_docs_to_gh_pages.yml)
|
|
5
|
+
|
|
6
|
+
A pandas (and parquet) interface for the [Open Mining Format package (omf)](https://omf.readthedocs.io/en/latest/).
|
|
7
|
+
|
|
8
|
+
When working with OMF files, it is often useful to convert the data to a pandas DataFrame.
|
|
9
|
+
This package provides a simple interface to do so.
|
|
10
|
+
|
|
11
|
+
The parquet format is a nice, compact, efficient format to persist pandas DataFrames.
|
|
12
|
+
This package also provides a simple interface to convert an omf element to a parquet file.
|
|
13
|
+
When datasets do not fit into memory, parquet files can be read in chunks or by column.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install omfpandas
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If you intend to use the parquet functionality, you will need to install the optional dependencies.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install omfpandas[io]
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Roadmap
|
|
28
|
+
|
|
29
|
+
- [ ] 0.2.0 - Add support for reading a VolumeElement (Block Model) from an OMF file as a pandas DataFrame.
|
|
30
|
+
Export a VolumeElement as a parquet file.
|
|
31
|
+
- [ ] 0.3.0 - Add support for writing a DataFrame to an OMF VolumeElement
|
|
32
|
+
- [ ] 0.4.0 - Add support for low-memory/out-of-core writing an omf element to parquet
|
|
33
|
+
- [ ] ...
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from omf import OMFReader
|
|
6
|
+
|
|
7
|
+
from omfpandas.volume import volume_to_df, volume_to_parquet
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class OMFPandas:
|
|
11
|
+
def __init__(self, omf_filepath: Path):
|
|
12
|
+
"""Instantiate the OMFPandas object.
|
|
13
|
+
|
|
14
|
+
:param omf_filepath: Path to the OMF file.
|
|
15
|
+
"""
|
|
16
|
+
if not omf_filepath.exists():
|
|
17
|
+
raise FileNotFoundError(f'File not found: {omf_filepath}')
|
|
18
|
+
elif not omf_filepath.suffix == '.omf':
|
|
19
|
+
raise ValueError(f'File is not an OMF file: {omf_filepath}')
|
|
20
|
+
self.omf_filepath = omf_filepath
|
|
21
|
+
self._elements = OMFReader(str(omf_filepath)).get_project().elements
|
|
22
|
+
self.elements: dict[str, str] = {e.name: e.subtype for e in self._elements}
|
|
23
|
+
|
|
24
|
+
def get_element_by_name(self, element_name: str):
|
|
25
|
+
"""Get an element by its name.
|
|
26
|
+
|
|
27
|
+
:param element_name: The name of the element to retrieve.
|
|
28
|
+
:return:
|
|
29
|
+
"""
|
|
30
|
+
element = [e for e in self._elements if e.name == element_name]
|
|
31
|
+
if not element:
|
|
32
|
+
raise ValueError(f"Element '{element_name}' not found in the OMF file: {self.omf_filepath.name}. "
|
|
33
|
+
f"Available elements are: {list(self.elements.keys())}")
|
|
34
|
+
elif len(element) > 1:
|
|
35
|
+
raise ValueError(f"Multiple elements with the name '{element_name}' found in the OMF file: "
|
|
36
|
+
f"{self.omf_filepath.name}")
|
|
37
|
+
return element[0]
|
|
38
|
+
|
|
39
|
+
def volume_to_df(self, volume_name: str, variables: Optional[list[str]] = None,
|
|
40
|
+
with_geometry_index: bool = True) -> pd.DataFrame:
|
|
41
|
+
"""Return a DataFrame from a VolumeElement.
|
|
42
|
+
|
|
43
|
+
Only variables assigned to the `cell` (as distinct from the grid `points`) are loaded.
|
|
44
|
+
|
|
45
|
+
:param volume_name: The name of the VolumeElement to convert.
|
|
46
|
+
:param variables: The variables to include in the DataFrame. If None, all variables are included.
|
|
47
|
+
:param with_geometry_index:
|
|
48
|
+
:return: The DataFrame representing the VolumeElement.
|
|
49
|
+
"""
|
|
50
|
+
volume = self.get_element_by_name(volume_name)
|
|
51
|
+
# check the element retrieved is the expected type
|
|
52
|
+
if volume.__class__.__name__ != 'VolumeElement':
|
|
53
|
+
raise ValueError(f"Element '{volume}' is not a VolumeElement in the OMF file: {self.omf_filepath}")
|
|
54
|
+
|
|
55
|
+
return volume_to_df(volume, variables=variables, with_geometry_index=with_geometry_index)
|
|
56
|
+
|
|
57
|
+
def volume_to_parquet(self, volume_name: str, parquet_filepath: Path):
|
|
58
|
+
"""Write a VolumeElement to a Parquet file.
|
|
59
|
+
|
|
60
|
+
:param volume_name: The name of the VolumeElement to convert.
|
|
61
|
+
:param parquet_filepath: The path to the Parquet file to write.
|
|
62
|
+
:return:
|
|
63
|
+
"""
|
|
64
|
+
volume = self.get_element_by_name(volume_name)
|
|
65
|
+
# check the element retrieved is the expected type
|
|
66
|
+
if volume.__class__.__name__ != 'VolumeElement':
|
|
67
|
+
raise ValueError(f"Element '{volume}' is not a VolumeElement in the OMF file: {self.omf_filepath}")
|
|
68
|
+
|
|
69
|
+
volume_to_parquet(volume, parquet_filepath)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from omf import VolumeElement, OMFReader, VolumeGridGeometry
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def volume_to_df(volume: VolumeElement, variables: Optional[list[str]] = None,
|
|
10
|
+
with_geometry_index: bool = True) -> pd.DataFrame:
|
|
11
|
+
"""Convert volume to a DataFrame."""
|
|
12
|
+
# read the data
|
|
13
|
+
df: pd.DataFrame = read_volume_variables(volume, variables=variables)
|
|
14
|
+
if with_geometry_index:
|
|
15
|
+
df.index = get_index(volume)
|
|
16
|
+
return df
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def volume_to_parquet(volume: VolumeElement, out_path: Optional[Path] = None, variables: Optional[list[str]] = None,
|
|
20
|
+
with_geometry_index: bool = True):
|
|
21
|
+
"""Convert volume to a Parquet file."""
|
|
22
|
+
if out_path is None:
|
|
23
|
+
out_path = Path(f"{volume.name}.parquet")
|
|
24
|
+
df: pd.DataFrame = volume_to_df(volume, variables=variables, with_geometry_index=with_geometry_index)
|
|
25
|
+
df.to_parquet(out_path)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def read_volume_variables(volume: VolumeElement, variables: list[str]) -> pd.DataFrame:
|
|
29
|
+
"""Read the variables from the VolumeElement."""
|
|
30
|
+
# identify 'cell' variables in the file
|
|
31
|
+
variables = [v.name for v in volume.data if v.location == 'cells']
|
|
32
|
+
|
|
33
|
+
# Loop over the variables
|
|
34
|
+
chunks: list[np.ndarray] = []
|
|
35
|
+
for variable in variables:
|
|
36
|
+
# Check if the variable exists in the VolumeElement
|
|
37
|
+
if variable not in variables:
|
|
38
|
+
raise ValueError(f"Variable '{variable}' not found in the VolumeElement: {element}")
|
|
39
|
+
chunks.append(_get_variable_data_by_name(volume, variable).ravel())
|
|
40
|
+
|
|
41
|
+
# Concatenate all chunks into a single DataFrame
|
|
42
|
+
return pd.DataFrame(np.vstack(chunks), index=variables).T
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_index(volume: VolumeElement) -> pd.MultiIndex:
|
|
46
|
+
"""Returns a pd.MultiIndex for the volume element."""
|
|
47
|
+
geometry: VolumeGridGeometry = volume.geometry
|
|
48
|
+
ox, oy, oz = geometry.origin
|
|
49
|
+
|
|
50
|
+
# Make coordinates (points) along each axis, i, j, k
|
|
51
|
+
i = ox + np.cumsum(geometry.tensor_u)
|
|
52
|
+
i = np.insert(i, 0, ox)
|
|
53
|
+
j = oy + np.cumsum(geometry.tensor_v)
|
|
54
|
+
j = np.insert(j, 0, oy)
|
|
55
|
+
k = oz + np.cumsum(geometry.tensor_w)
|
|
56
|
+
k = np.insert(k, 0, oz)
|
|
57
|
+
|
|
58
|
+
# convert to centroids
|
|
59
|
+
x, y, z = (i[1:] + i[:-1]) / 2, (j[1:] + j[:-1]) / 2, (k[1:] + k[:-1]) / 2
|
|
60
|
+
xx, yy, zz = np.meshgrid(x, y, z, indexing="ij")
|
|
61
|
+
|
|
62
|
+
# Calculate dx, dy, dz
|
|
63
|
+
dxx, dyy, dzz = np.meshgrid(geometry.tensor_u, geometry.tensor_v, geometry.tensor_w, indexing="ij")
|
|
64
|
+
|
|
65
|
+
# TODO: consider rotation
|
|
66
|
+
|
|
67
|
+
index = pd.MultiIndex.from_arrays([xx.ravel("F"), yy.ravel("F"), zz.ravel("F"),
|
|
68
|
+
dxx.ravel("F"), dyy.ravel("F"), dzz.ravel("F")],
|
|
69
|
+
names=['x', 'y', 'z', 'dx', 'dy', 'dz'])
|
|
70
|
+
|
|
71
|
+
return index
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _get_variable_data_by_name(volume: VolumeElement, variable_name: str) -> np.ndarray:
|
|
75
|
+
# get the index of the variable in order to index into elements
|
|
76
|
+
scalar_data = [sd for sd in volume.data if sd.location == 'cells' and sd.name == variable_name]
|
|
77
|
+
if not scalar_data:
|
|
78
|
+
raise ValueError(f"Variable '{variable_name}' not found as cell data in the VolumeElement: {volume}")
|
|
79
|
+
elif len(scalar_data) > 1:
|
|
80
|
+
raise ValueError(f"Multiple variables with the name '{variable_name}' found in the VolumeElement: {volume}")
|
|
81
|
+
return scalar_data[0].array.array
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "omfpandas"
|
|
3
|
+
packages = [{ include = "omfpandas" }]
|
|
4
|
+
version = "0.1.1"
|
|
5
|
+
description = ""
|
|
6
|
+
authors = ["Greg <11791585+elphick@users.noreply.github.com>"]
|
|
7
|
+
repository = "https://github.com/elphick/omfpandas"
|
|
8
|
+
documentation = "https://elphick.github.io/omfpandas"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
|
|
11
|
+
[[tool.poetry.source]]
|
|
12
|
+
name = "PyPI"
|
|
13
|
+
priority = "primary"
|
|
14
|
+
|
|
15
|
+
[tool.poetry.dependencies]
|
|
16
|
+
python = ">=3.9,<3.11"
|
|
17
|
+
pandas = "^2.2.2"
|
|
18
|
+
pyarrow = { version = "^16.1.0", optional = true }
|
|
19
|
+
omf = "^1.0.1"
|
|
20
|
+
|
|
21
|
+
[tool.poetry.extras]
|
|
22
|
+
io = ["pyarrow"]
|
|
23
|
+
|
|
24
|
+
[tool.poetry.group.dev.dependencies]
|
|
25
|
+
pytest = "^8.2.1"
|
|
26
|
+
sphinx = "^7.3.7"
|
|
27
|
+
sphinx-gallery = "^0.16.0"
|
|
28
|
+
sphinx-rtd-theme = "^2.0.0"
|
|
29
|
+
pyarrow = "^16.1.0"
|
|
30
|
+
ydata-profiling = "^4.8.3"
|
|
31
|
+
coverage = "^7.5.3"
|
|
32
|
+
towncrier = "^23.11.0"
|
|
33
|
+
myst-parser = "^3.0.1"
|
|
34
|
+
sphinx-autodoc-typehints = "^2.1.1"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["poetry-core"]
|
|
38
|
+
build-backend = "poetry.core.masonry.api"
|