"""
BSC Decoder wrapper for GZCM v3 compression.
Uses BSC CLI subprocess with LD_LIBRARY_PATH for libomp.
Examples
--------
"""
__author__ = "Yeremia Gunawan Adhisantoso"
__email__ = "adhisant@tnt.uni-hannover.de"
__license__ = "Clear BSD"
import subprocess
import tempfile
import pathlib
import numpy as np
from ._paths import resolve_bsc_binary, make_bsc_env
[docs]class BscDecoder:
"""BSC decoder for contact matrix tiles.
Uses bsc CLI subprocess for true BSC (Block Sorting Compression) decompression.
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,
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._bsc_path = resolve_bsc_binary(bsc_bin)
self._env = make_bsc_env(ld_library_path)
[docs] def decode_tile(
self,
payload: bytes,
shape: tuple[int, int] | None = None,
) -> np.ndarray:
"""Decode a single BSC-compressed tile.
Parameters
----------
payload : bytes
BSC-compressed bitstream. When ``shape`` is provided (v3 GZCM
path), the first 8 bytes encode ``(rows, cols)`` as ``np.int32``
and are stripped before decompression. When ``shape`` is
``None`` the payload is treated as raw compressed bytes
(legacy / codec tests).
shape : tuple of (int, int), optional
Actual tile shape ``(rows, cols)``. Required for edge tiles
where ``rows != self.tile_size`` or ``cols != self.tile_size``.
If ``None``, falls back to ``(self.tile_size, self.tile_size)``
and assumes no 8-byte header is present.
Returns
-------
np.ndarray
Decoded contact matrix tile with the requested shape.
Examples
--------
"""
if shape is not None:
# v3 GZCM path: payload = 8-byte (rows, cols) int32 header + body
rows, cols = shape
body = payload[8:]
else:
# Legacy / direct codec API: raw compressed bytes, square tile.
rows = cols = self.tile_size
body = payload
with tempfile.NamedTemporaryFile(suffix=".dat", delete=False) as f_in:
f_in.write(body)
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:
subprocess.run([str(self._bsc_path), "d", str(in_path), str(out_path)], check=True, capture_output=True, env=self._env)
with open(out_path, "rb") as f:
data = f.read()
return np.frombuffer(data, dtype=self.dtype).reshape(rows, cols)
finally:
in_path.unlink(missing_ok=True)
out_path.unlink(missing_ok=True)
[docs] def decode_tiles(
self,
payloads: list[bytes],
shapes: list[tuple[int, int]] | None = None,
) -> np.ndarray:
"""Decode multiple tiles into a 4D array.
Parameters
----------
payloads : list[bytes]
List of encoded bitstreams.
shapes : list of tuple of (int, int), optional
Per-tile shapes matching ``payloads``. When provided, each
payload is assumed to carry the 8-byte header. Defaults to
``None`` (legacy: square tiles, no header).
Returns
-------
np.ndarray
4D array of decoded tiles (n_tile_rows, n_tile_cols, tile_rows, tile_cols).
Examples
--------
"""
n_tiles = len(payloads)
if shapes is None:
decoded = [self.decode_tile(p) for p in payloads]
else:
decoded = [self.decode_tile(p, shape=s) for p, s in zip(payloads, shapes, strict=True)]
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