Source code for parq_blockmodel.reblocking.conversion

import warnings

import numpy as np
import pandas as pd

from parq_blockmodel.utils.geometry_utils import dense_ijk_multiindex


[docs] def tabular_to_3d_dict(df: pd.DataFrame) -> tuple[ dict[str, np.ndarray], dict[str, pd.Index]]: """ Convert a DataFrame indexed by i, j, k into a dict of 3D arrays. Object/categorical columns are converted to codes for efficient storage/interpolation. Parameters: - df: DataFrame indexed by i, j, k Returns: - dict of 3D arrays (C-order) - dict of pd.Indexes (categories) """ if df.index.names != ['i', 'j', 'k']: raise ValueError("DataFrame index must be a MultiIndex with levels ['i', 'j', 'k']") shape = tuple(len(df.index.levels[i]) for i in range(3)) dense_ijk_index = dense_ijk_multiindex(shape) if not df.index.equals(dense_ijk_index): df = df.reindex(dense_ijk_index) arrays = {} categories = {} for col in df.columns: col_data = df[col] if pd.api.types.is_object_dtype(col_data) or isinstance(col_data.dtype, pd.CategoricalDtype): cat = pd.Categorical(col_data) arrays[col] = cat.codes.reshape(shape, order='C') categories[col] = cat.categories else: arrays[col] = np.asarray(col_data).reshape(shape, order='C') return arrays, categories
[docs] def dict_3d_to_tabular( arrays: dict[str, np.ndarray], categories: dict[str, pd.Index] = None ) -> pd.DataFrame: """ Convert a dict of dense 3D arrays (C-order) to a DataFrame indexed by i, j, k. Optionally reconstruct categorical columns using provided categories. Parameters: - arrays: dict of 3D arrays (C-order) - categories: dict mapping column name to pd.Index of categories (optional) Returns: - DataFrame indexed by i, j, k """ data = {} shape = None for col, arr in arrays.items(): if not shape: shape = arr.shape if arr.shape != shape: raise ValueError(f"Shape mismatch for column {col}: {arr.shape} != {shape}") flat = arr.ravel(order='C') if categories and col in categories: data[col] = pd.Categorical.from_codes( np.asarray(flat, dtype=flat.dtype if np.issubdtype(flat.dtype, np.integer) else np.int64), categories[col] ) else: data[col] = flat dense_ijk_index = dense_ijk_multiindex(shape) df = pd.DataFrame(data, index=dense_ijk_index) return df
def _warn_dtype_promotion(original_dtype, operation: str, attribute: str | None, reason: str) -> None: attr_text = f" for attribute '{attribute}'" if attribute else "" warnings.warn( f"{operation}{attr_text}: keeping promoted dtype because {reason} (original dtype: {original_dtype}).", RuntimeWarning, stacklevel=3, )
[docs] def to_numeric(arr, categories=None, *, operation: str = "reblocking", attribute: str | None = None): """ Convert array to numeric for interpolation. Returns numeric array and a restoration function. """ if categories is not None: arr_numeric = np.asarray(arr, dtype=np.float64) def restore_fn(x): x = np.round(x).astype(int) return pd.Categorical.from_codes( np.where(np.isnan(x), -1, x), categories ) return arr_numeric, restore_fn if pd.api.types.is_integer_dtype(arr): arr_numeric = np.asarray(arr, dtype=np.float64) is_extension_dtype = pd.api.types.is_extension_array_dtype(arr.dtype) target_numpy_dtype = np.dtype(getattr(arr.dtype, "numpy_dtype", arr.dtype)) def restore_fn(x): values = np.asarray(x, dtype=np.float64) rounded = np.rint(values) finite_mask = np.isfinite(rounded) if finite_mask.any(): info = np.iinfo(target_numpy_dtype) finite_values = rounded[finite_mask] if finite_values.min() < info.min or finite_values.max() > info.max: _warn_dtype_promotion( arr.dtype, operation=operation, attribute=attribute, reason=( "integer result exceeds representable range " f"[{info.min}, {info.max}]" ), ) return values if is_extension_dtype: rounded_with_na = rounded.copy() rounded_with_na[~finite_mask] = np.nan return pd.array(rounded_with_na, dtype=arr.dtype) if not finite_mask.all(): _warn_dtype_promotion( arr.dtype, operation=operation, attribute=attribute, reason="integer result contains NaN/Inf", ) return values return rounded.astype(target_numpy_dtype, copy=False) return arr_numeric, restore_fn if pd.api.types.is_float_dtype(arr): arr_numeric = np.asarray(arr, dtype=np.float64) target_dtype = np.dtype(arr.dtype) def restore_fn(x): return np.asarray(x, dtype=target_dtype) return arr_numeric, restore_fn arr_numeric = np.asarray(arr, dtype=np.float64) def restore_fn(x): return x return arr_numeric, restore_fn