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.
@@ -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.
@@ -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
+ [![Run Tests](https://github.com/Elphick/omfpandas/actions/workflows/poetry_build_and_test.yml/badge.svg?branch=main)](https://github.com/Elphick/omfpandas/actions/workflows/poetry_build_and_test.yml)
23
+ [![Publish Docs](https://github.com/Elphick/omfpandas/actions/workflows/poetry_sphinx_docs_to_gh_pages.yml/badge.svg?branch=main)](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
+ [![Run Tests](https://github.com/Elphick/omfpandas/actions/workflows/poetry_build_and_test.yml/badge.svg?branch=main)](https://github.com/Elphick/omfpandas/actions/workflows/poetry_build_and_test.yml)
4
+ [![Publish Docs](https://github.com/Elphick/omfpandas/actions/workflows/poetry_sphinx_docs_to_gh_pages.yml/badge.svg?branch=main)](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,10 @@
1
+ from omfpandas.omfp import OMFPandas
2
+ import omfpandas.volume
3
+
4
+ from importlib import metadata
5
+
6
+ try:
7
+ __version__ = metadata.version('omfpandas')
8
+ except metadata.PackageNotFoundError:
9
+ # Package is not installed
10
+ pass
@@ -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"