Source code for gunz_cm.structs.loader_config

"""
Loader configuration data structure (DOP Phase 6, v2.23.0).

This module introduces :class:`LoaderConfig`, a frozen Pydantic v2
``BaseModel`` that captures the canonical set of inputs to every
format-specific loader (``load_cooler``, ``load_hic``, ``load_csv``,
etc.). It is the **typed boundary** between public dispatch in
:func:`gunz_cm.loaders.load_cm_data` and the per-format loaders.

DOP alignment
-------------
- **Principle 1 (separate code from data):** the configuration of a load
  is data; the act of loading is code. Prior to Phase 6 the two were
  tangled across 17+ keyword arguments on ``_load_cooler_data`` and
  ``_load_hic_data``. Phase 6 makes the configuration explicit so the
  loading code can consume one canonical shape.
- **Principle 2 (immutable data):** ``LoaderConfig`` is a frozen
  ``BaseModel``. Once constructed, it cannot be mutated. This makes the
  data safe to pass across function boundaries without defensive copies.
- **Principle 3 (validate at the boundary, trust internally):** the
  ``@validate_call`` decorator on ``load_cm_data`` would not catch
  negative ``bin_size_bp`` because the parameter was typed
  ``int | None``. With ``LoaderConfig``, the ``Annotated[int, Field(gt=0)]``
  alias from :mod:`gunz_cm.structs._types` rejects illegal values at the
  boundary in a single place.
- **Principle 4 (make illegal states unrepresentable):** the
  ``backend: Backend | None`` field collapses the previous
  ``backend`` + ``use_fast_hic`` overlap into a single, validated
  representation. ``use_fast_hic`` is preserved as a back-compat input
  on ``load_cm_data`` but the **resolved** config exposes only
  ``backend``.

Pattern C — use_fast_hic/backend overlap
----------------------------------------
Historical callers pass ``use_fast_hic=True`` to opt into the STRAW
backend. New callers pass ``backend=Backend.STRAW`` directly. Phase 6
keeps ``use_fast_hic`` as a back-compat input parameter on
``load_cm_data`` for one release (v2.23.0). Inside ``load_cm_data`` it
is translated to ``backend=Backend.STRAW`` *before* the ``LoaderConfig``
is constructed, so ``LoaderConfig`` itself only carries the resolved
``backend`` field.

Phase 8 (v2.25.0) will remove the ``use_fast_hic`` parameter from
``load_cm_data`` entirely.

Examples
--------
>>> from gunz_cm.structs.loader_config import LoaderConfig
>>> cfg = LoaderConfig(bin_size_bp=5000, region1="chr1")
>>> cfg.bin_size_bp
5000
>>> cfg.backend is None
True
>>> cfg.output_format.value
'df'

Notes
-----
``LoaderConfig`` is constructed internally by ``load_cm_data`` from
its keyword arguments. Public callers usually do not instantiate it
directly — it is exposed as part of the public API so advanced users
can pre-build a configuration and pass it to the dispatcher in a
future minor release.
"""
from __future__ import annotations

import pathlib
import typing as t

from pydantic import BaseModel, ConfigDict, Field

from gunz_cm.consts import Backend, Balancing, DataStructure, Format
from gunz_cm.structs._types import BinSizeBP


# RegionSpec: a broader type alias for genomic regions. LoaderConfig
# accepts chromosome names ("chr1"), chromosome + range ("chr1:1-10000"),
# or normalised whole-chromosome aliases ("ALL", "all"). The strict
# ChromName regex is too narrow for the loader use case, so region
# fields use Annotated[str, Field(min_length=1, max_length=128)] which
# rejects only the empty string and unreasonably long inputs.
RegionSpec = t.Annotated[
    str,
    Field(
        min_length=1,
        max_length=128,
        description=(
            "Genomic region specifier. Accepts chromosome names "
            "('chr1'), chromosome+range ('chr1:1-10000'), and "
            "whole-chromosome aliases ('ALL'). Empty strings are "
            "rejected; call sites must normalise 'ALL'/'all'/'' to "
            "None before constructing LoaderConfig."
        ),
    ),
]


[docs]class LoaderConfig(BaseModel): """Canonical loader configuration consumed by every format-specific loader. Frozen, immutable, and validated at construction. The fields mirror the union of all keyword arguments accepted by ``_load_cooler_data``, ``_load_hic_data``, ``_load_csv_data``, ``_load_memmap_data``, and ``_load_gzcm_data``. Attributes ---------- fpath Path to the contact-matrix file. Required. bin_size_bp Bin size in base pairs. Uses :data:`BinSizeBP` (positive int). ``None`` is allowed because some loaders (CSV, GZCM, PICKLE) do not require an explicit bin size — the value is inferred from the file header. region1, region2 Genomic regions to load. ``region2=None`` means "intra-chromosomal of region1". Both default to ``None`` so format-specific loaders that don't support regions (NPY, PICKLE) can still construct a valid ``LoaderConfig``. Accepts chromosome names ("chr1"), region ranges ("chr1:1-10000"), or the whole-chromosome alias ("ALL"). The strict ``ChromName`` regex from ``_types`` is intentionally not used here because loaders accept a broader range of region formats. balancing Balancing (normalization) method(s). ``None`` means "no balancing"; some formats also accept a list of methods to return multiple normalised columns in one call. output_format In-memory representation of the loaded data. Defaults to :attr:`DataStructure.DF` (DataFrame). fformat Explicit file format. ``None`` triggers extension-based inference. backend Loader backend. ``None`` selects the format's default backend. This is the *resolved* representation — the legacy ``use_fast_hic`` parameter is translated to ``backend=Backend.STRAW`` upstream. return_raw_counts If True, the loader returns raw counts alongside balanced counts. Not supported by every format; loaders that don't support it raise ``NotImplementedError`` at runtime. chunksize Optional chunked-streaming parameter. ``None`` means "load the full region in one go". Used by COOLER and HIC loaders. Examples -------- >>> cfg = LoaderConfig( ... fpath=pathlib.Path("data.hic"), ... bin_size_bp=50_000, ... region1="chr1", ... backend=Backend.STRAW, ... ) >>> cfg.model_dump() {'fpath': PosixPath('data.hic'), 'bin_size_bp': 50000, ...} """ model_config = ConfigDict( frozen=True, arbitrary_types_allowed=True, extra="forbid", ) # --- Required --- fpath: pathlib.Path = Field( ..., description="Path to the contact-matrix file.", ) # --- Genomic --- bin_size_bp: t.Optional[BinSizeBP] = Field( default=None, description=( "Bin size in base pairs. Positive integer when set. " "Optional for formats that encode the bin size in the file " "header (CSV, GZCM, PICKLE)." ), ) region1: t.Optional[RegionSpec] = Field( default=None, description=( "First genomic region. None for full-chromosome or " "file-wide loads. Accepts chromosome names or chr:start-end." ), ) region2: t.Optional[RegionSpec] = Field( default=None, description=( "Second genomic region. None means intra-chromosomal of " "region1. Only valid when region1 is set." ), ) # --- Balancing --- balancing: t.Optional[t.Union[Balancing, t.List[Balancing]]] = Field( default=None, description=( "Balancing (normalization) method(s). None means no balancing; " "a list returns multiple normalised columns in one call." ), ) # --- Output / Format --- output_format: DataStructure = Field( default=DataStructure.DF, description="In-memory representation of the loaded data.", ) fformat: t.Optional[Format] = Field( default=None, description="Explicit file format. None triggers extension-based inference.", ) # --- Backend --- # Pattern C: this is the *resolved* backend. The legacy use_fast_hic # parameter on load_cm_data is translated to Backend.STRAW before # LoaderConfig is constructed, so LoaderConfig itself never carries # the redundant flag. backend: t.Optional[Backend] = Field( default=None, description=( "Loader backend. None selects the format's default backend. " "For COOLER: 'cooler' or 'hictk'. For HIC: 'hicstraw', " "'hictk', or 'straw'." ), ) # --- Toggles --- return_raw_counts: bool = Field( default=False, description=( "If True, return raw counts alongside the primary (balanced) counts. " "Not supported by every format." ), ) chunksize: t.Optional[int] = Field( default=None, ge=1, description=( "Chunk size for streaming loads. None means load the full " "region in one call. Only COOLER and HIC loaders use this." ), )
__all__ = [ "LoaderConfig", "RegionSpec", ]