"""
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",
]