Source code for gain.genomic_resources.testing.statistics

"""Run a fixture resource's REAL statistics build, the way grr_manage does.

A test about what a statistics build leaves behind -- a histogram it drew,
one it nullified, a manifest that lists the result -- wants the build
itself, not a hand-written ``statistics/`` directory that the build might
never produce.  :func:`build_statistics` runs the implementation's own
tasks to completion; :func:`refresh_manifest` rewrites the stored manifest
afterwards, which is what ``repo-stats`` does after its build and without
which the resource's loaded manifest still describes the tree from
before.  :func:`publish_statistics` is the two in the order the CLI runs
them.  Tests that want the build without the refresh -- to show a manifest
going stale -- call the two apart.

Beside them live the score recipes a build nullifies, one per way it can:
a histogram the min/max pass finds no range for, and one the accumulation
overflows.  What a nullification leaves on disk is a build-internals fact,
so the shapes that provoke it are stated here once.
"""
from __future__ import annotations

from typing import Any

from gain.genomic_resources.histogram import CategoricalHistogram
from gain.genomic_resources.repository import (
    GenomicResource,
    ReadWriteRepositoryProtocol,
)
from gain.genomic_resources.repository_factory import (
    build_resource_implementation,
)
from gain.genomic_resources.testing.builders import (
    GeneScoreBuilder,
    PositionScoreBuilder,
    a_gene_score,
    a_position_score,
)
from gain.task_graph.cli_tools import task_graph_run
from gain.task_graph.executor import TaskGraphExecutor
from gain.task_graph.graph import TaskGraph
from gain.task_graph.sequential_executor import SequentialExecutor


[docs] def build_statistics( resource: GenomicResource, *, executor: TaskGraphExecutor | None = None, **build_kwargs: Any, ) -> None: """Run ``resource``'s statistics build tasks to completion. ``build_kwargs`` go to the implementation's ``create_statistics_build_tasks`` as given (``region_size=`` for a genomic score); its defaults stand otherwise. ``executor`` defaults to a plain sequential one. """ impl = build_resource_implementation(resource) graph = TaskGraph() graph.add_tasks(impl.create_statistics_build_tasks(**build_kwargs)) task_graph_run(graph, executor or SequentialExecutor())
[docs] def refresh_manifest(resource: GenomicResource) -> None: """Rewrite the stored manifest, as grr_manage does after a build. ``save_manifest`` drops the resource's memoised manifest itself, so the next ``get_loaded_manifest`` reads the rewritten one. """ proto = resource.proto assert isinstance(proto, ReadWriteRepositoryProtocol), ( "a manifest is written into the resource; the protocol " f"<{proto.proto_id}> cannot") proto.save_manifest(resource, proto.build_manifest(resource))
[docs] def publish_statistics(resource: GenomicResource, **build_kwargs: Any) -> None: """Build ``resource``'s statistics and refresh its stored manifest.""" build_statistics(resource, **build_kwargs) refresh_manifest(resource)
[docs] def a_score_with_no_values() -> PositionScoreBuilder: """A configured number histogram the build nullifies in its min/max pass. Every value is NA, so no range is found and nothing is drawn; a null histogram file carrying that reason is written instead. """ return ( a_position_score() .with_score("score", "float") .with_histogram({"type": "number", "number_of_bins": 10}) .with_na_values("NA") .with_data(""" chrom pos_begin score 1 10 NA 1 20 NA """) )
def _too_many_labels() -> list[str]: return [ f"label{i}" for i in range(CategoricalHistogram.UNIQUE_VALUES_LIMIT + 1) ]
[docs] def a_score_with_too_many_categories() -> PositionScoreBuilder: """A default categorical histogram the build nullifies accumulating. No ``histogram:`` block, so the ``str`` score gets the default categorical config, which enforces the unique-values limit -- and the values exceed it. A null histogram file is written for this one too; still, nothing is drawn. """ rows = "\n".join( f"1 {10 * (i + 1)} {label}" for i, label in enumerate(_too_many_labels())) return ( a_position_score() .with_score("score", "str") .with_data("chrom pos_begin score\n" + rows) )
[docs] def a_gene_score_with_too_many_categories() -> GeneScoreBuilder: """The gene-score twin of :func:`a_score_with_too_many_categories`. ``pli`` has values and gets its histogram drawn; ``label`` overflows the default categorical histogram and gets none. """ rows = "\n".join( f"G{i} {i / 1000} {label}" for i, label in enumerate(_too_many_labels())) return ( a_gene_score() .with_score("pli", "float") .with_score("label", "str") .with_data("gene pli label\n" + rows) )