"""
BSC + CMC Transforms Decoder for GZCM v3 compression.
Decodes BSC-compressed data that was encoded with CMC transforms.
Reverses BSC entropy coding then CMC's domain-specific transforms.
Examples
--------
"""
__author__ = "Yeremia Gunawan Adhisantoso"
__email__ = "adhisant@tnt.uni-hannover.de"
__license__ = "Clear BSD"
import pathlib
import tempfile
import typing as t
import numpy as np
from . import _paths
from ._paths import resolve_bsc_binary, make_bsc_env
# Module-level registry of CMC inverse-transform functions. See
# ``cmc_encoder.py`` for the rationale (DOP Phase 3, v2.20.0).
_FUNCS: dict[str, t.Any] = {}
_LOADED: bool = False
def _ensure_loaded() -> None:
"""Lazily resolve CMC inverse-transform functions on first access.
See ``cmc_encoder.py`` for the full rationale.
"""
global _LOADED
if _LOADED:
return
_cmc = _paths.load_cmc_inverse_transform()
_FUNCS["debinarize_rc_bin_split_v2"] = _cmc["debinarize_rc_bin_split_v2"]
_FUNCS["reverse_diag_transform_mode0"] = _cmc["reverse_diag_transform_mode0"]
_FUNCS["reverse_diag_transform"] = _cmc["reverse_diag_transform"]
_LOADED = True
def __getattr__(name: str) -> t.Any:
"""PEP 562 module-level lazy attribute access for CMC inverse-transform functions."""
if name in {"debinarize_rc_bin_split_v2", "reverse_diag_transform_mode0", "reverse_diag_transform"}:
_ensure_loaded()
return _FUNCS[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
[docs]class BscCmcDecoder:
"""BSC + CMC Transforms decoder for contact matrix tiles.
Decodes BSC-compressed data that was encoded with CMC transforms.
Reverses BSC entropy coding then CMC's domain-specific transforms.
Parameters
----------
tile_size : int, default=512
Tile size for block processing.
resolution : int, default=50000
Hi-C resolution in bp.
dtype : np.dtype, default=np.uint32
Data type for decoded tiles.
bsc_bin : str or pathlib.Path, optional
Explicit path to the `bsc` binary. If None, resolved via
GUNZ_CM_BSC_BIN env var or system PATH.
ld_library_path : str or pathlib.Path, optional
Explicit LD_LIBRARY_PATH for the bsc subprocess. If None, resolved via
GUNZ_CM_BSC_LD_LIBRARY_PATH env var.
Examples
--------
"""
def __init__(
self,
tile_size: int = 512,
resolution: int = 50000,
dtype: np.dtype = np.uint32,
diag_mode: int = 0,
bsc_bin: str | pathlib.Path | None = None,
ld_library_path: str | pathlib.Path | None = None,
):
"""
Examples
--------
"""
self.tile_size = tile_size
self.resolution = resolution
self.dtype = np.dtype(dtype)
self.diag_mode = diag_mode
self._bsc_path = resolve_bsc_binary(bsc_bin)
self._env = make_bsc_env(ld_library_path)
[docs] def decode_tile(self, payload: bytes) -> np.ndarray:
"""Decode a single compressed tile.
Parameters
----------
payload : bytes
Compressed bitstream (shape info + encoded data).
Returns
-------
np.ndarray
Decoded contact matrix tile.
Examples
--------
"""
shape = np.frombuffer(payload[:8], dtype=np.int32)
encoded_data = payload[8:]
with tempfile.NamedTemporaryFile(suffix=".dat", delete=False) as f_in:
f_in.write(encoded_data)
f_in.flush()
in_path = pathlib.Path(f_in.name)
with tempfile.NamedTemporaryFile(suffix=".dat", delete=False) as f_out:
out_path = pathlib.Path(f_out.name)
try:
import subprocess
result = subprocess.run(
[
str(self._bsc_path),
"d",
str(in_path),
str(out_path),
],
env=self._env,
capture_output=True,
timeout=30,
)
if result.returncode != 0:
raise RuntimeError(f"BSC decode failed: {result.stderr.decode()}")
finally:
in_path.unlink(missing_ok=True)
data = out_path.read_bytes()
out_path.unlink(missing_ok=True)
_ensure_loaded()
bin_mat = np.frombuffer(data, dtype=np.bool_).reshape(shape)
debinarized = _FUNCS["debinarize_rc_bin_split_v2"](bin_mat, axis=0)
if self.diag_mode == 0:
return _FUNCS["reverse_diag_transform_mode0"](debinarized)
return _FUNCS["reverse_diag_transform"](debinarized, mode=self.diag_mode)
[docs] def decode_tiles(self, payloads: list[bytes]) -> np.ndarray:
"""Decode multiple tiles into a 4D array.
Parameters
----------
payloads : list[bytes]
List of encoded bitstreams.
Returns
-------
np.ndarray
4D array of decoded tiles (n_tile_rows, n_tile_cols, tile_size, tile_size).
Examples
--------
"""
n_tiles = len(payloads)
decoded = [self.decode_tile(p) for p in payloads]
tile_shape = decoded[0].shape
tile_rows = int(np.sqrt(n_tiles))
tile_cols = n_tiles // tile_rows if tile_rows > 0 else 1
result = np.empty((tile_rows, tile_cols, *tile_shape), dtype=self.dtype)
idx = 0
for i in range(tile_rows):
for j in range(tile_cols):
result[i, j] = decoded[idx]
idx += 1
return result