Skip to content

state_translation Module

Human-readable translation helpers for population states.

Overview

The state_translation module converts PopulationState and DiscretePopulationState into nested dictionaries or JSON payloads, with readable labels and optional zero-value filtering. For age-structured states, translation includes sperm-storage tensors as well.

Observation Output Helpers

Use output_current_state and output_history for observation-centric output. Both helpers support:

  • Building observation rules from groups directly.
  • Reusing a prebuilt observation object via observation=....
  • Optional JSON file output through output_path.

You can build reusable observations from the population API:

observation = pop.create_observation(
  groups={"adult_wt": {"genotype": ["WT|WT"], "age": [1]}},
  collapse_age=False,
)

current_payload = nt.output_current_state(
  population=pop,
  observation=observation,
  output_path="outputs/current.json",
)

history_payload = nt.output_history(
  population=pop,
  observation=observation,
  output_path="outputs/history.json",
)

History Translation Helpers

Use population_history_to_readable_dict and population_history_to_readable_json to convert flattened history snapshots into readable per-tick state payloads.

When to use

  • Inspect historical trajectories without manually reshaping flattened arrays.
  • Export time-series snapshots for logging, debugging, or external tools.

API behavior summary

  • Input history can be omitted; when omitted, the function attempts to call population.get_history().
  • Each row in flattened history is parsed back into either PopulationState or DiscretePopulationState according to the current population.state type.
  • Genotype labels are resolved from population.index_registry when available.
  • Output includes top-level metadata:
  • state_type
  • name
  • n_snapshots
  • snapshots (list of translated state dictionaries)

Example

import natal as nt

hist_payload = nt.population_history_to_readable_dict(
    population=pop,
    include_zero_counts=False,
)

print(hist_payload["n_snapshots"])
print(hist_payload["snapshots"][0]["tick"])

hist_json = nt.population_history_to_readable_json(
    population=pop,
    include_zero_counts=False,
    indent=2,
)
print(hist_json[:200])

Complete Module Reference

natal.state_translation

Human-readable translation helpers for population state objects.

This module converts PopulationState and DiscretePopulationState into structured dictionaries (and JSON strings) that are easier to inspect, log, or serialize for downstream tooling.

population_state_to_dict

population_state_to_dict(state: PopulationState, genotype_labels: Optional[Sequence[str]] = None, sex_labels: Optional[Sequence[str]] = None, include_zero_counts: bool = False) -> Dict[str, Any]

Translate an age-structured PopulationState to a readable dictionary.

Parameters:

Name Type Description Default
state PopulationState

State object to translate.

required
genotype_labels Optional[Sequence[str]]

Optional labels for genotype axis. If omitted, labels are generated as genotype_0, genotype_1, etc.

None
sex_labels Optional[Sequence[str]]

Optional labels for sex axis. If omitted, defaults to female/male (and sex_k for additional axes).

None
include_zero_counts bool

Whether to keep zero-valued entries in output blocks.

False

Returns:

Type Description
Dict[str, Any]

A nested dictionary containing tick, dimensions, individual counts, and

Dict[str, Any]

sperm storage.

Source code in src/natal/state_translation.py
def population_state_to_dict(
    state: PopulationState,
    genotype_labels: Optional[Sequence[str]] = None,
    sex_labels: Optional[Sequence[str]] = None,
    include_zero_counts: bool = False,
) -> Dict[str, Any]:
    """Translate an age-structured ``PopulationState`` to a readable dictionary.

    Args:
        state: State object to translate.
        genotype_labels: Optional labels for genotype axis. If omitted, labels are
            generated as ``genotype_0``, ``genotype_1``, etc.
        sex_labels: Optional labels for sex axis. If omitted, defaults to
            ``female``/``male`` (and ``sex_k`` for additional axes).
        include_zero_counts: Whether to keep zero-valued entries in output blocks.

    Returns:
        A nested dictionary containing tick, dimensions, individual counts, and
        sperm storage.
    """
    n_sexes, n_ages, n_genotypes = state.individual_count.shape
    labels_genotype = _resolve_labels(n_genotypes, genotype_labels, "genotype")
    labels_sex = _resolve_labels(n_sexes, sex_labels or _default_sex_labels(n_sexes), "sex")

    result: Dict[str, Any] = {
        "state_type": "PopulationState",
        "tick": int(state.n_tick),
        "dimensions": {
            "n_sexes": int(n_sexes),
            "n_ages": int(n_ages),
            "n_genotypes": int(n_genotypes),
        },
        "individual_count": _build_individual_count_payload(
            state.individual_count,
            labels_sex,
            labels_genotype,
            include_zero_counts,
        ),
        "sperm_storage": _build_sperm_storage_payload(
            state.sperm_storage,
            labels_genotype,
            include_zero_counts,
        ),
    }
    return result

population_state_to_json

population_state_to_json(state: PopulationState, genotype_labels: Optional[Sequence[str]] = None, sex_labels: Optional[Sequence[str]] = None, include_zero_counts: bool = False, indent: int = 2) -> str

Serialize a PopulationState to a JSON string.

Parameters:

Name Type Description Default
state PopulationState

State object to translate.

required
genotype_labels Optional[Sequence[str]]

Optional labels for genotype axis. Auto-generated when None.

None
sex_labels Optional[Sequence[str]]

Optional labels for sex axis. Defaults to female/male.

None
include_zero_counts bool

Whether to keep zero-valued entries.

False
indent int

JSON indentation level.

2

Returns:

Type Description
str

Indented JSON string of the state dictionary.

Source code in src/natal/state_translation.py
def population_state_to_json(
    state: PopulationState,
    genotype_labels: Optional[Sequence[str]] = None,
    sex_labels: Optional[Sequence[str]] = None,
    include_zero_counts: bool = False,
    indent: int = 2,
) -> str:
    """Serialize a ``PopulationState`` to a JSON string.

    Args:
        state: State object to translate.
        genotype_labels: Optional labels for genotype axis. Auto-generated
            when ``None``.
        sex_labels: Optional labels for sex axis. Defaults to ``female``/``male``.
        include_zero_counts: Whether to keep zero-valued entries.
        indent: JSON indentation level.

    Returns:
        Indented JSON string of the state dictionary.
    """
    payload = population_state_to_dict(
        state=state,
        genotype_labels=genotype_labels,
        sex_labels=sex_labels,
        include_zero_counts=include_zero_counts,
    )
    return json.dumps(payload, ensure_ascii=False, indent=indent)

discrete_population_state_to_dict

discrete_population_state_to_dict(state: DiscretePopulationState, genotype_labels: Optional[Sequence[str]] = None, sex_labels: Optional[Sequence[str]] = None, include_zero_counts: bool = False) -> Dict[str, Any]

Translate a DiscretePopulationState to a readable dictionary.

Parameters:

Name Type Description Default
state DiscretePopulationState

State object to translate.

required
genotype_labels Optional[Sequence[str]]

Optional labels for genotype axis. If omitted, labels are generated as genotype_0, genotype_1, etc.

None
sex_labels Optional[Sequence[str]]

Optional labels for sex axis. If omitted, defaults to female/male (and sex_k for additional axes).

None
include_zero_counts bool

Whether to keep zero-valued entries in output blocks.

False

Returns:

Type Description
Dict[str, Any]

A nested dictionary containing tick, dimensions, and individual counts.

Source code in src/natal/state_translation.py
def discrete_population_state_to_dict(
    state: DiscretePopulationState,
    genotype_labels: Optional[Sequence[str]] = None,
    sex_labels: Optional[Sequence[str]] = None,
    include_zero_counts: bool = False,
) -> Dict[str, Any]:
    """Translate a ``DiscretePopulationState`` to a readable dictionary.

    Args:
        state: State object to translate.
        genotype_labels: Optional labels for genotype axis. If omitted, labels are
            generated as ``genotype_0``, ``genotype_1``, etc.
        sex_labels: Optional labels for sex axis. If omitted, defaults to
            ``female``/``male`` (and ``sex_k`` for additional axes).
        include_zero_counts: Whether to keep zero-valued entries in output blocks.

    Returns:
        A nested dictionary containing tick, dimensions, and individual counts.
    """
    n_sexes, n_ages, n_genotypes = state.individual_count.shape
    labels_genotype = _resolve_labels(n_genotypes, genotype_labels, "genotype")
    labels_sex = _resolve_labels(n_sexes, sex_labels or _default_sex_labels(n_sexes), "sex")

    result: Dict[str, Any] = {
        "state_type": "DiscretePopulationState",
        "tick": int(state.n_tick),
        "dimensions": {
            "n_sexes": int(n_sexes),
            "n_ages": int(n_ages),
            "n_genotypes": int(n_genotypes),
        },
        "individual_count": _build_individual_count_payload(
            state.individual_count,
            labels_sex,
            labels_genotype,
            include_zero_counts,
        ),
    }
    return result

discrete_population_state_to_json

discrete_population_state_to_json(state: DiscretePopulationState, genotype_labels: Optional[Sequence[str]] = None, sex_labels: Optional[Sequence[str]] = None, include_zero_counts: bool = False, indent: int = 2) -> str

Serialize a DiscretePopulationState to a JSON string.

Parameters:

Name Type Description Default
state DiscretePopulationState

Discrete state object to translate.

required
genotype_labels Optional[Sequence[str]]

Optional labels for genotype axis. Auto-generated when None.

None
sex_labels Optional[Sequence[str]]

Optional labels for sex axis. Defaults to female/male.

None
include_zero_counts bool

Whether to keep zero-valued entries.

False
indent int

JSON indentation level.

2

Returns:

Type Description
str

Indented JSON string of the discrete state dictionary.

Source code in src/natal/state_translation.py
def discrete_population_state_to_json(
    state: DiscretePopulationState,
    genotype_labels: Optional[Sequence[str]] = None,
    sex_labels: Optional[Sequence[str]] = None,
    include_zero_counts: bool = False,
    indent: int = 2,
) -> str:
    """Serialize a ``DiscretePopulationState`` to a JSON string.

    Args:
        state: Discrete state object to translate.
        genotype_labels: Optional labels for genotype axis. Auto-generated
            when ``None``.
        sex_labels: Optional labels for sex axis. Defaults to ``female``/``male``.
        include_zero_counts: Whether to keep zero-valued entries.
        indent: JSON indentation level.

    Returns:
        Indented JSON string of the discrete state dictionary.
    """
    payload = discrete_population_state_to_dict(
        state=state,
        genotype_labels=genotype_labels,
        sex_labels=sex_labels,
        include_zero_counts=include_zero_counts,
    )
    return json.dumps(payload, ensure_ascii=False, indent=indent)

population_to_readable_dict

population_to_readable_dict(population: BasePopulation[Any], include_zero_counts: bool = False) -> Dict[str, Any]

Translate a population's current state to a readable dictionary.

This helper automatically pulls genotype labels from population.index_registry so output keys use genotype strings.

Parameters:

Name Type Description Default
population BasePopulation[Any]

Population instance containing either an age-structured or discrete state object.

required
include_zero_counts bool

Whether to keep zero-valued entries in output blocks.

False

Returns:

Type Description
Dict[str, Any]

A nested dictionary describing the current state.

Raises:

Type Description
TypeError

If the population state type is unsupported.

Source code in src/natal/state_translation.py
def population_to_readable_dict(
    population: BasePopulation[Any],
    include_zero_counts: bool = False,
) -> Dict[str, Any]:
    """Translate a population's current state to a readable dictionary.

    This helper automatically pulls genotype labels from
    ``population.index_registry`` so output keys use genotype strings.

    Args:
        population: Population instance containing either an age-structured or
            discrete state object.
        include_zero_counts: Whether to keep zero-valued entries in output blocks.

    Returns:
        A nested dictionary describing the current state.

    Raises:
        TypeError: If the population state type is unsupported.
    """
    state = population.state
    n_genotypes = int(state.individual_count.shape[2])
    genotype_labels = _genotype_labels_from_registry(population.index_registry, n_genotypes)

    if isinstance(state, PopulationState):
        return population_state_to_dict(
            state=state,
            genotype_labels=genotype_labels,
            include_zero_counts=include_zero_counts,
        )
    if isinstance(state, DiscretePopulationState):
        return discrete_population_state_to_dict(
            state=state,
            genotype_labels=genotype_labels,
            include_zero_counts=include_zero_counts,
        )

    raise TypeError(f"Unsupported state type: {type(state).__name__}")

population_to_readable_json

population_to_readable_json(population: BasePopulation[Any], include_zero_counts: bool = False, indent: int = 2) -> str

Serialize a population's current state to a JSON string.

Parameters:

Name Type Description Default
population BasePopulation[Any]

Population instance containing an age-structured or discrete state object.

required
include_zero_counts bool

Whether to keep zero-valued entries.

False
indent int

JSON indentation level.

2

Returns:

Type Description
str

Indented JSON string of the readable state dictionary.

Source code in src/natal/state_translation.py
def population_to_readable_json(
    population: BasePopulation[Any],
    include_zero_counts: bool = False,
    indent: int = 2,
) -> str:
    """Serialize a population's current state to a JSON string.

    Args:
        population: Population instance containing an age-structured or
            discrete state object.
        include_zero_counts: Whether to keep zero-valued entries.
        indent: JSON indentation level.

    Returns:
        Indented JSON string of the readable state dictionary.
    """
    payload = population_to_readable_dict(
        population=population,
        include_zero_counts=include_zero_counts,
    )
    return json.dumps(payload, ensure_ascii=False, indent=indent)

population_history_to_readable_dict

population_history_to_readable_dict(population: BasePopulation[Any], history: Optional[ndarray] = None, include_zero_counts: bool = False) -> Dict[str, Any]

Translate flattened history records into readable snapshot dictionaries.

Parameters:

Name Type Description Default
population BasePopulation[Any]

Population instance that defines history shape and labels.

required
history Optional[ndarray]

Optional flattened history array. When None, uses population.get_history().

None
include_zero_counts bool

Whether to keep zero-valued entries in each snapshot payload.

False

Returns:

Type Description
Dict[str, Any]

A dictionary containing metadata and translated snapshot entries.

Raises:

Type Description
TypeError

If the population state type is unsupported.

ValueError

If history is not a 2D array.

Source code in src/natal/state_translation.py
def population_history_to_readable_dict(
    population: BasePopulation[Any],
    history: Optional[np.ndarray] = None,
    include_zero_counts: bool = False,
) -> Dict[str, Any]:
    """Translate flattened history records into readable snapshot dictionaries.

    Args:
        population: Population instance that defines history shape and labels.
        history: Optional flattened history array. When ``None``, uses
            ``population.get_history()``.
        include_zero_counts: Whether to keep zero-valued entries in each
            snapshot payload.

    Returns:
        A dictionary containing metadata and translated snapshot entries.

    Raises:
        TypeError: If the population state type is unsupported.
        ValueError: If ``history`` is not a 2D array.
    """
    state = population.state
    n_sexes, n_ages, n_genotypes = state.individual_count.shape
    genotype_labels = _genotype_labels_from_registry(population.index_registry, int(n_genotypes))

    history_array: np.ndarray
    if history is None:
        get_history = getattr(population, "get_history", None)
        if callable(get_history):
            try:
                history_array = cast(np.ndarray, get_history())
            except ValueError:
                history_array = np.zeros((0, 0), dtype=np.float64)
        else:
            history_array = np.zeros((0, 0), dtype=np.float64)
    else:
        history_array = cast(np.ndarray, np.asarray(history, dtype=np.float64))

    if history_array.ndim != 2:
        raise ValueError(
            f"history must be a 2D array, got shape {history_array.shape}"
        )

    snapshots: List[Dict[str, Any]] = []
    if isinstance(state, PopulationState):
        for idx in range(int(history_array.shape[0])):
            row = history_array[idx, :]
            parsed_state = parse_flattened_state(
                row,
                n_sexes=n_sexes,
                n_ages=n_ages,
                n_genotypes=n_genotypes,
                copy=True,
            )
            snapshots.append(
                population_state_to_dict(
                    state=parsed_state,
                    genotype_labels=genotype_labels,
                    include_zero_counts=include_zero_counts,
                )
            )
    elif isinstance(state, DiscretePopulationState):
        for idx in range(int(history_array.shape[0])):
            row = history_array[idx, :]
            parsed_state = parse_flattened_discrete_state(
                row,
                n_sexes=n_sexes,
                n_ages=n_ages,
                n_genotypes=n_genotypes,
                copy=True,
            )
            snapshots.append(
                discrete_population_state_to_dict(
                    state=parsed_state,
                    genotype_labels=genotype_labels,
                    include_zero_counts=include_zero_counts,
                )
            )
    else:
        raise TypeError(f"Unsupported state type: {type(state).__name__}")

    return {
        "state_type": type(state).__name__,
        "name": str(population.name),
        "n_snapshots": int(history_array.shape[0]),
        "snapshots": snapshots,
    }

population_history_to_readable_json

population_history_to_readable_json(population: BasePopulation[Any], history: Optional[ndarray] = None, include_zero_counts: bool = False, indent: int = 2) -> str

Translate flattened history records to a readable JSON string.

Parameters:

Name Type Description Default
population BasePopulation[Any]

Population instance that defines history shape and labels.

required
history Optional[ndarray]

Optional flattened history array. When None, uses population.get_history().

None
include_zero_counts bool

Whether to keep zero-valued entries.

False
indent int

Indentation level used by json.dumps.

2

Returns:

Type Description
str

JSON string containing history metadata and translated snapshots.

Source code in src/natal/state_translation.py
def population_history_to_readable_json(
    population: BasePopulation[Any],
    history: Optional[np.ndarray] = None,
    include_zero_counts: bool = False,
    indent: int = 2,
) -> str:
    """Translate flattened history records to a readable JSON string.

    Args:
        population: Population instance that defines history shape and labels.
        history: Optional flattened history array. When ``None``, uses
            ``population.get_history()``.
        include_zero_counts: Whether to keep zero-valued entries.
        indent: Indentation level used by ``json.dumps``.

    Returns:
        JSON string containing history metadata and translated snapshots.
    """
    payload = population_history_to_readable_dict(
        population=population,
        history=history,
        include_zero_counts=include_zero_counts,
    )
    return json.dumps(payload, ensure_ascii=False, indent=indent)

population_observation_history_to_readable_dict

population_observation_history_to_readable_dict(population: BasePopulation[Any], *, history: Optional[ndarray] = None, include_zero_counts: bool = False) -> Dict[str, Any]

Translate pre-recorded observation history into readable snapshot dicts.

This is used when the population was run with record_observation set and the kernel recorded compressed observation snapshots. Each row is [tick, observed.ravel()] where observed has shape (n_groups, n_sexes, n_ages), so no per-genotype data is stored.

Falls back to population_history_to_readable_dict (raw state decoding) if population.record_observation is None.

Parameters:

Name Type Description Default
population BasePopulation[Any]

Population instance with record_observation set.

required
history Optional[ndarray]

Optional flattened history array. When None, uses population.get_history().

None
include_zero_counts bool

Whether to keep zero-valued entries.

False

Returns:

Type Description
Dict[str, Any]

A dictionary with observation metadata and per-snapshot entries.

Source code in src/natal/state_translation.py
def population_observation_history_to_readable_dict(
    population: BasePopulation[Any],
    *,
    history: Optional[np.ndarray] = None,
    include_zero_counts: bool = False,
) -> Dict[str, Any]:
    """Translate pre-recorded observation history into readable snapshot dicts.

    This is used when the population was run with ``record_observation`` set
    and the kernel recorded compressed observation snapshots. Each row is
    ``[tick, observed.ravel()]`` where ``observed`` has shape
    ``(n_groups, n_sexes, n_ages)``, so no per-genotype data is stored.

    Falls back to ``population_history_to_readable_dict`` (raw state decoding)
    if ``population.record_observation`` is ``None``.

    Args:
        population: Population instance with ``record_observation`` set.
        history: Optional flattened history array. When ``None``, uses
            ``population.get_history()``.
        include_zero_counts: Whether to keep zero-valued entries.

    Returns:
        A dictionary with observation metadata and per-snapshot entries.
    """
    obs = getattr(population, "record_observation", None)
    if obs is None:
        return _build_history_observation_payload(
            population, history=history, observation=None,
            groups=None, collapse_age=False,
            include_zero_counts=include_zero_counts,
        )

    obs = cast(Observation, obs)
    state = cast(PopulationState | DiscretePopulationState, population.state)

    n_sexes = int(state.individual_count.shape[0])
    n_ages = int(state.individual_count.shape[1])
    sex_labels = _default_sex_labels(n_sexes)
    n_groups = len(obs.labels)
    labels = list(obs.labels)

    history_array = _get_history_array(population, history)
    if history_array.ndim != 2:
        raise ValueError(f"history must be a 2D array, got shape {history_array.shape}")

    snapshots: List[Dict[str, Any]] = []
    for idx in range(int(history_array.shape[0])):
        row = history_array[idx, :]
        tick = int(row[0])
        observed = row[1:].reshape(n_groups, n_sexes, n_ages)
        snapshots.append({
            "tick": tick,
            "state_type": type(state).__name__,
            "labels": labels,
            "collapse_age": bool(obs.collapse_age),
            "observed": _build_observation_payload(
                observed=observed,
                labels=labels,
                sex_labels=sex_labels,
                include_zero_counts=include_zero_counts,
            ),
        })

    return {
        "state_type": type(state).__name__,
        "name": str(population.name),
        "n_snapshots": int(history_array.shape[0]),
        "collapse_age": bool(obs.collapse_age),
        "labels": labels,
        "snapshots": snapshots,
    }

population_observation_history_to_readable_json

population_observation_history_to_readable_json(population: BasePopulation[Any], *, history: Optional[ndarray] = None, include_zero_counts: bool = False, indent: int = 2) -> str

JSON version of :func:population_observation_history_to_readable_dict.

Source code in src/natal/state_translation.py
def population_observation_history_to_readable_json(
    population: BasePopulation[Any],
    *,
    history: Optional[np.ndarray] = None,
    include_zero_counts: bool = False,
    indent: int = 2,
) -> str:
    """JSON version of :func:`population_observation_history_to_readable_dict`."""
    payload = population_observation_history_to_readable_dict(
        population=population,
        history=history,
        include_zero_counts=include_zero_counts,
    )
    return json.dumps(payload, ensure_ascii=False, indent=indent)

output_current_state

output_current_state(population: BasePopulation[Any], *, observation: Optional[Observation] = None, groups: Optional[GroupsInput] = None, collapse_age: bool = False, include_zero_counts: bool = False, output_path: Optional[Union[str, Path]] = None, indent: int = 2) -> Dict[str, Any]

Export the current population state with observation rules applied.

This function integrates natal.observation with state translation and can optionally write the JSON payload to output_path.

Parameters:

Name Type Description Default
population BasePopulation[Any]

Population instance to observe.

required
observation Optional[Observation]

Optional prebuilt observation object. When provided, groups and collapse_age are ignored.

None
groups Optional[GroupsInput]

Observation groups passed to ObservationFilter.build_filter. When None, one group per genotype index is used.

None
collapse_age bool

Whether observation rule generation collapses age axis.

False
include_zero_counts bool

Whether to keep zero-valued entries.

False
output_path Optional[Union[str, Path]]

Optional JSON file path. When provided, the payload is written to this file as UTF-8 JSON.

None
indent int

Indentation used when writing JSON.

2

Returns:

Type Description
Dict[str, Any]

A dictionary with observation metadata and observed counts.

Source code in src/natal/state_translation.py
def output_current_state(
    population: BasePopulation[Any],
    *,
    observation: Optional[Observation] = None,
    groups: Optional[GroupsInput] = None,
    collapse_age: bool = False,
    include_zero_counts: bool = False,
    output_path: Optional[Union[str, Path]] = None,
    indent: int = 2,
) -> Dict[str, Any]:
    """Export the current population state with observation rules applied.

    This function integrates ``natal.observation`` with state translation and
    can optionally write the JSON payload to ``output_path``.

    Args:
        population: Population instance to observe.
        observation: Optional prebuilt observation object. When provided,
            ``groups`` and ``collapse_age`` are ignored.
        groups: Observation groups passed to ``ObservationFilter.build_filter``.
            When ``None``, one group per genotype index is used.
        collapse_age: Whether observation rule generation collapses age axis.
        include_zero_counts: Whether to keep zero-valued entries.
        output_path: Optional JSON file path. When provided, the payload is
            written to this file as UTF-8 JSON.
        indent: Indentation used when writing JSON.

    Returns:
        A dictionary with observation metadata and observed counts.
    """
    payload = _get_population_observation_payload(
        population,
        observation=observation,
        groups=groups,
        collapse_age=collapse_age,
        include_zero_counts=include_zero_counts,
    )
    _write_json_payload(payload, output_path, indent)
    return payload

output_history

output_history(population: BasePopulation[Any], *, observation: Optional[Observation] = None, groups: Optional[GroupsInput] = None, collapse_age: bool = False, include_zero_counts: bool = False, history: Optional[ndarray] = None, output_path: Optional[Union[str, Path]] = None, indent: int = 2) -> Dict[str, Any]

Export the observation history for a population.

Parameters:

Name Type Description Default
population BasePopulation[Any]

Population instance to observe.

required
observation Optional[Observation]

Optional prebuilt observation object. When provided, groups and collapse_age are ignored.

None
groups Optional[GroupsInput]

Observation groups passed to ObservationFilter.build_filter. When None, one group per genotype index is used.

None
collapse_age bool

Whether observation rule generation collapses age axis.

False
include_zero_counts bool

Whether to keep zero-valued entries.

False
history Optional[ndarray]

Optional flattened history array. When omitted, the population history is fetched from population.get_history().

None
output_path Optional[Union[str, Path]]

Optional JSON file path. When provided, the payload is written to this file as UTF-8 JSON.

None
indent int

Indentation used when writing JSON.

2

Returns:

Type Description
Dict[str, Any]

A dictionary containing observation metadata and per-snapshot outputs.

Source code in src/natal/state_translation.py
def output_history(
    population: BasePopulation[Any],
    *,
    observation: Optional[Observation] = None,
    groups: Optional[GroupsInput] = None,
    collapse_age: bool = False,
    include_zero_counts: bool = False,
    history: Optional[np.ndarray] = None,
    output_path: Optional[Union[str, Path]] = None,
    indent: int = 2,
) -> Dict[str, Any]:
    """Export the observation history for a population.

    Args:
        population: Population instance to observe.
        observation: Optional prebuilt observation object. When provided,
            ``groups`` and ``collapse_age`` are ignored.
        groups: Observation groups passed to ``ObservationFilter.build_filter``.
            When ``None``, one group per genotype index is used.
        collapse_age: Whether observation rule generation collapses age axis.
        include_zero_counts: Whether to keep zero-valued entries.
        history: Optional flattened history array. When omitted, the population
            history is fetched from ``population.get_history()``.
        output_path: Optional JSON file path. When provided, the payload is
            written to this file as UTF-8 JSON.
        indent: Indentation used when writing JSON.

    Returns:
        A dictionary containing observation metadata and per-snapshot outputs.
    """
    # Auto-detect: prefer pre-recorded observation history when available and
    # no explicit observation override is provided.  This avoids re-parsing
    # the full raw state per snapshot.
    pop_obs = getattr(population, "record_observation", None)
    if pop_obs is not None and observation is None and groups is None:
        payload = population_observation_history_to_readable_dict(
            population=population,
            history=history,
            include_zero_counts=include_zero_counts,
        )
    else:
        payload = _build_history_observation_payload(
            population,
            history=history,
            observation=observation,
            groups=groups,
            collapse_age=collapse_age,
            include_zero_counts=include_zero_counts,
        )
    _write_json_payload(payload, output_path, indent)
    return payload

population_to_observation_dict

population_to_observation_dict(population: BasePopulation[Any], *, observation: Optional[Observation] = None, groups: Optional[GroupsInput] = None, collapse_age: bool = False, include_zero_counts: bool = False) -> Dict[str, Any]

Post-hoc observation on a panmictic population's current state.

Delegates to :func:output_current_state with the same arguments. Provided as a convenience wrapper for observation-specific workflows.

Parameters:

Name Type Description Default
population BasePopulation[Any]

Panmictic population instance.

required
observation Optional[Observation]

Pre-built observation filter. When None, one is constructed from groups and collapse_age.

None
groups Optional[GroupsInput]

Observation group specs used when observation is None.

None
collapse_age bool

Whether the observation collapses the age axis.

False
include_zero_counts bool

Whether to keep zero-valued entries.

False

Returns:

Type Description
Dict[str, Any]

Dictionary with observed state payload grouped by observation labels.

Source code in src/natal/state_translation.py
def population_to_observation_dict(
    population: BasePopulation[Any],
    *,
    observation: Optional[Observation] = None,
    groups: Optional[GroupsInput] = None,
    collapse_age: bool = False,
    include_zero_counts: bool = False,
) -> Dict[str, Any]:
    """Post-hoc observation on a panmictic population's current state.

    Delegates to :func:`output_current_state` with the same arguments.
    Provided as a convenience wrapper for observation-specific workflows.

    Args:
        population: Panmictic population instance.
        observation: Pre-built observation filter. When ``None``, one is
            constructed from *groups* and *collapse_age*.
        groups: Observation group specs used when *observation* is ``None``.
        collapse_age: Whether the observation collapses the age axis.
        include_zero_counts: Whether to keep zero-valued entries.

    Returns:
        Dictionary with observed state payload grouped by observation labels.
    """
    return output_current_state(
        population,
        observation=observation,
        groups=groups,
        collapse_age=collapse_age,
        include_zero_counts=include_zero_counts,
    )

population_to_observation_json

population_to_observation_json(population: BasePopulation[Any], *, observation: Optional[Observation] = None, groups: Optional[GroupsInput] = None, collapse_age: bool = False, include_zero_counts: bool = False, indent: int = 2) -> str

Post-hoc observation on a panmictic population, serialized to JSON.

Delegates to :func:output_current_state and serializes the result.

Parameters:

Name Type Description Default
population BasePopulation[Any]

Panmictic population instance.

required
observation Optional[Observation]

Pre-built observation filter. When None, one is constructed from groups and collapse_age.

None
groups Optional[GroupsInput]

Observation group specs used when observation is None.

None
collapse_age bool

Whether the observation collapses the age axis.

False
include_zero_counts bool

Whether to keep zero-valued entries.

False
indent int

JSON indentation level.

2

Returns:

Type Description
str

JSON string of the observed state payload.

Source code in src/natal/state_translation.py
def population_to_observation_json(
    population: BasePopulation[Any],
    *,
    observation: Optional[Observation] = None,
    groups: Optional[GroupsInput] = None,
    collapse_age: bool = False,
    include_zero_counts: bool = False,
    indent: int = 2,
) -> str:
    """Post-hoc observation on a panmictic population, serialized to JSON.

    Delegates to :func:`output_current_state` and serializes the result.

    Args:
        population: Panmictic population instance.
        observation: Pre-built observation filter. When ``None``, one is
            constructed from *groups* and *collapse_age*.
        groups: Observation group specs used when *observation* is ``None``.
        collapse_age: Whether the observation collapses the age axis.
        include_zero_counts: Whether to keep zero-valued entries.
        indent: JSON indentation level.

    Returns:
        JSON string of the observed state payload.
    """
    payload = output_current_state(
        population,
        observation=observation,
        groups=groups,
        collapse_age=collapse_age,
        include_zero_counts=include_zero_counts,
    )
    return json.dumps(payload, ensure_ascii=False, indent=indent)

spatial_population_to_readable_dict

spatial_population_to_readable_dict(spatial_population: SpatialPopulation, include_zero_counts: bool = False) -> Dict[str, Any]

Translate a SpatialPopulation into readable per-deme dictionaries.

Parameters:

Name Type Description Default
spatial_population SpatialPopulation

Spatial population container.

required
include_zero_counts bool

Whether to keep zero-valued entries.

False

Returns:

Type Description
Dict[str, Any]

Dictionary containing per-deme readable payloads and one aggregate state.

Source code in src/natal/state_translation.py
def spatial_population_to_readable_dict(
    spatial_population: SpatialPopulation,
    include_zero_counts: bool = False,
) -> Dict[str, Any]:
    """Translate a ``SpatialPopulation`` into readable per-deme dictionaries.

    Args:
        spatial_population: Spatial population container.
        include_zero_counts: Whether to keep zero-valued entries.

    Returns:
        Dictionary containing per-deme readable payloads and one aggregate state.
    """
    aggregate_state = spatial_population.aggregate_state()
    n_genotypes = int(aggregate_state.individual_count.shape[2])
    genotype_labels = _genotype_labels_from_registry(
        spatial_population.deme(0).index_registry,
        n_genotypes,
    )

    demes_payload: Dict[str, Any] = {}
    for deme_idx, deme in enumerate(spatial_population.demes):
        demes_payload[f"deme_{deme_idx}"] = population_to_readable_dict(
            population=deme,
            include_zero_counts=include_zero_counts,
        )

    aggregate_payload = population_state_to_dict(
        state=aggregate_state,
        genotype_labels=genotype_labels,
        include_zero_counts=include_zero_counts,
    )

    return {
        "state_type": "SpatialPopulation",
        "name": str(spatial_population.name),
        "tick": int(spatial_population.tick),
        "n_demes": int(spatial_population.n_demes),
        "demes": demes_payload,
        "aggregate": aggregate_payload,
    }

spatial_population_to_readable_json

spatial_population_to_readable_json(spatial_population: SpatialPopulation, include_zero_counts: bool = False, indent: int = 2) -> str

Translate a SpatialPopulation into a readable JSON string.

Source code in src/natal/state_translation.py
def spatial_population_to_readable_json(
    spatial_population: SpatialPopulation,
    include_zero_counts: bool = False,
    indent: int = 2,
) -> str:
    """Translate a ``SpatialPopulation`` into a readable JSON string."""
    payload = spatial_population_to_readable_dict(
        spatial_population=spatial_population,
        include_zero_counts=include_zero_counts,
    )
    return json.dumps(payload, ensure_ascii=False, indent=indent)

spatial_population_to_observation_dict

spatial_population_to_observation_dict(spatial_population: SpatialPopulation, *, groups: Optional[GroupsInput] = None, collapse_age: bool = False, include_zero_counts: bool = False) -> Dict[str, Any]

Post-hoc observation on the current spatial state (all demes + aggregate).

Builds an Observation from the first deme's registry, applies it to each deme individually, then aggregates by summing across demes.

Parameters:

Name Type Description Default
spatial_population SpatialPopulation

Spatial population container.

required
groups Optional[GroupsInput]

Observation groups passed to underlying observation filter.

None
collapse_age bool

Whether observation collapses age axis.

False
include_zero_counts bool

Whether to keep zero-valued entries.

False

Returns:

Type Description
Dict[str, Any]

Dictionary with per-deme and aggregate observation payloads.

Source code in src/natal/state_translation.py
def spatial_population_to_observation_dict(
    spatial_population: SpatialPopulation,
    *,
    groups: Optional[GroupsInput] = None,
    collapse_age: bool = False,
    include_zero_counts: bool = False,
) -> Dict[str, Any]:
    """Post-hoc observation on the current spatial state (all demes + aggregate).

    Builds an ``Observation`` from the first deme's registry, applies it to
    each deme individually, then aggregates by summing across demes.

    Args:
        spatial_population: Spatial population container.
        groups: Observation groups passed to underlying observation filter.
        collapse_age: Whether observation collapses age axis.
        include_zero_counts: Whether to keep zero-valued entries.

    Returns:
        Dictionary with per-deme and aggregate observation payloads.
    """
    from natal.observation import ObservationFilter

    per_deme_payload: Dict[str, Any] = {}
    for deme_idx, deme in enumerate(spatial_population.demes):
        per_deme_payload[f"deme_{deme_idx}"] = population_to_observation_dict(
            population=deme,
            groups=groups,
            collapse_age=collapse_age,
            include_zero_counts=include_zero_counts,
        )

    aggregate_state = spatial_population.aggregate_state()
    n_sexes = int(aggregate_state.individual_count.shape[0])
    sex_labels = _default_sex_labels(n_sexes)

    obs_filter = ObservationFilter(spatial_population.deme(0).index_registry)
    observation = obs_filter.create_observation(
        diploid_genotypes=spatial_population.species,
        groups=groups,
        collapse_age=collapse_age,
    )
    observed = observation.apply(aggregate_state.individual_count)

    aggregate_payload: Dict[str, Any] = {
        "state_type": "PopulationState",
        "tick": int(aggregate_state.n_tick),
        "collapse_age": bool(observation.collapse_age),
        "labels": list(observation.labels),
        "observed": _build_observation_payload(
            observed=observed,
            labels=observation.labels,
            sex_labels=sex_labels,
            include_zero_counts=include_zero_counts,
        ),
    }

    return {
        "state_type": "SpatialPopulationObservation",
        "name": str(spatial_population.name),
        "tick": int(spatial_population.tick),
        "n_demes": int(spatial_population.n_demes),
        "demes": per_deme_payload,
        "aggregate": aggregate_payload,
    }

spatial_population_to_observation_json

spatial_population_to_observation_json(spatial_population: SpatialPopulation, *, groups: Optional[GroupsInput] = None, collapse_age: bool = False, include_zero_counts: bool = False, indent: int = 2) -> str

Post-hoc observation on a spatial population, serialized to JSON.

Delegates to :func:spatial_population_to_observation_dict and serializes the result.

Parameters:

Name Type Description Default
spatial_population SpatialPopulation

Spatial population container.

required
groups Optional[GroupsInput]

Observation group specs passed to each deme's observation.

None
collapse_age bool

Whether observation collapses age axis.

False
include_zero_counts bool

Whether to keep zero-valued entries.

False
indent int

JSON indentation level.

2

Returns:

Type Description
str

JSON string with per-deme and aggregate observation payloads.

Source code in src/natal/state_translation.py
def spatial_population_to_observation_json(
    spatial_population: SpatialPopulation,
    *,
    groups: Optional[GroupsInput] = None,
    collapse_age: bool = False,
    include_zero_counts: bool = False,
    indent: int = 2,
) -> str:
    """Post-hoc observation on a spatial population, serialized to JSON.

    Delegates to :func:`spatial_population_to_observation_dict` and serializes
    the result.

    Args:
        spatial_population: Spatial population container.
        groups: Observation group specs passed to each deme's observation.
        collapse_age: Whether observation collapses age axis.
        include_zero_counts: Whether to keep zero-valued entries.
        indent: JSON indentation level.

    Returns:
        JSON string with per-deme and aggregate observation payloads.
    """
    payload = spatial_population_to_observation_dict(
        spatial_population=spatial_population,
        groups=groups,
        collapse_age=collapse_age,
        include_zero_counts=include_zero_counts,
    )
    return json.dumps(payload, ensure_ascii=False, indent=indent)

spatial_population_history_to_readable_dict

spatial_population_history_to_readable_dict(spatial_population: SpatialPopulation, history: Optional[ndarray] = None, include_zero_counts: bool = False) -> Dict[str, Any]

Translate recorded spatial history into per-deme readable snapshots.

Each history snapshot contains one entry per deme with the same format as population_to_readable_dict.

Parameters:

Name Type Description Default
spatial_population SpatialPopulation

Spatial population container.

required
history Optional[ndarray]

Optional stacked history array. When None, uses spatial_population.get_history().

None
include_zero_counts bool

Whether to keep zero-valued entries.

False

Returns:

Type Description
Dict[str, Any]

Dictionary containing per-tick, per-deme readable payloads.

Source code in src/natal/state_translation.py
def spatial_population_history_to_readable_dict(
    spatial_population: SpatialPopulation,
    history: Optional[np.ndarray] = None,
    include_zero_counts: bool = False,
) -> Dict[str, Any]:
    """Translate recorded spatial history into per-deme readable snapshots.

    Each history snapshot contains one entry per deme with the same format
    as ``population_to_readable_dict``.

    Args:
        spatial_population: Spatial population container.
        history: Optional stacked history array. When ``None``, uses
            ``spatial_population.get_history()``.
        include_zero_counts: Whether to keep zero-valued entries.

    Returns:
        Dictionary containing per-tick, per-deme readable payloads.
    """
    n_demes = spatial_population.n_demes
    first_deme = spatial_population.deme(0)
    state = first_deme.state
    n_sexes, n_ages, n_genotypes = state.individual_count.shape
    genotype_labels = _genotype_labels_from_registry(
        first_deme.index_registry,
        n_genotypes,
    )

    # Resolve history array
    history_array: np.ndarray
    if history is None:
        history_array = spatial_population.get_history()
    else:
        history_array = cast(np.ndarray, np.asarray(history, dtype=np.float64))

    if history_array.ndim != 2:
        raise ValueError(
            f"history must be a 2D array, got shape {history_array.shape}"
        )

    # Row layout: [tick, ind_d0..., ind_d1..., ..., sperm_d0..., sperm_d1..., ...]
    ind_per_deme = n_sexes * n_ages * n_genotypes
    sperm_per_deme = n_ages * n_genotypes * n_genotypes

    is_discrete = isinstance(state, DiscretePopulationState)

    snapshots: List[Dict[str, Any]] = []
    for row_idx in range(int(history_array.shape[0])):
        row = history_array[row_idx, :]
        tick = int(row[0])

        per_deme: Dict[str, Any] = {}
        for d in range(n_demes):
            d_ind_start = 1 + d * ind_per_deme
            d_ind_end = 1 + (d + 1) * ind_per_deme
            deme_ind = row[d_ind_start:d_ind_end].reshape(
                n_sexes, n_ages, n_genotypes
            )

            if is_discrete:
                deme_state = DiscretePopulationState(
                    n_tick=tick,
                    individual_count=deme_ind,
                )
                per_deme[f"deme_{d}"] = discrete_population_state_to_dict(
                    state=deme_state,
                    genotype_labels=genotype_labels,
                    include_zero_counts=include_zero_counts,
                )
            else:
                d_sp_start = 1 + n_demes * ind_per_deme + d * sperm_per_deme
                d_sp_end = 1 + n_demes * ind_per_deme + (d + 1) * sperm_per_deme
                deme_sperm = row[d_sp_start:d_sp_end].reshape(
                    n_ages, n_genotypes, n_genotypes
                )
                deme_state = PopulationState(
                    n_tick=tick,
                    individual_count=deme_ind,
                    sperm_storage=deme_sperm,
                )
                per_deme[f"deme_{d}"] = population_state_to_dict(
                    state=deme_state,
                    genotype_labels=genotype_labels,
                    include_zero_counts=include_zero_counts,
                )

        snapshots.append({
            "tick": tick,
            "demes": per_deme,
        })

    return {
        "state_type": "SpatialPopulationHistory",
        "name": str(spatial_population.name),
        "n_demes": int(n_demes),
        "n_snapshots": int(history_array.shape[0]),
        "snapshots": snapshots,
    }

spatial_population_observation_history_to_readable_dict

spatial_population_observation_history_to_readable_dict(spatial_population: SpatialPopulation, *, history: Optional[ndarray] = None, include_zero_counts: bool = False) -> Dict[str, Any]

Translate pre-recorded spatial observation history into readable dict.

Each snapshot row is [tick, observed.ravel()] where observed has shape (n_demes, n_groups, n_sexes, n_ages). For each tick the function expands per-deme observation payloads and a cross-deme aggregate (sum over all demes).

Falls back to spatial_population_history_to_readable_dict (raw state decoding) if spatial_population.record_observation is None.

Parameters:

Name Type Description Default
spatial_population SpatialPopulation

Spatial population with record_observation set.

required
history Optional[ndarray]

Optional history array. When None, uses spatial_population.get_history().

None
include_zero_counts bool

Whether to keep zero-valued entries.

False

Returns:

Type Description
Dict[str, Any]

A dictionary with observation metadata and per-tick, per-deme entries.

Source code in src/natal/state_translation.py
def spatial_population_observation_history_to_readable_dict(
    spatial_population: SpatialPopulation,
    *,
    history: Optional[np.ndarray] = None,
    include_zero_counts: bool = False,
) -> Dict[str, Any]:
    """Translate pre-recorded spatial observation history into readable dict.

    Each snapshot row is ``[tick, observed.ravel()]`` where ``observed`` has
    shape ``(n_demes, n_groups, n_sexes, n_ages)``.  For each tick the
    function expands per-deme observation payloads and a cross-deme aggregate
    (sum over all demes).

    Falls back to ``spatial_population_history_to_readable_dict`` (raw state
    decoding) if ``spatial_population.record_observation`` is ``None``.

    Args:
        spatial_population: Spatial population with ``record_observation`` set.
        history: Optional history array. When ``None``, uses
            ``spatial_population.get_history()``.
        include_zero_counts: Whether to keep zero-valued entries.

    Returns:
        A dictionary with observation metadata and per-tick, per-deme entries.
    """
    obs = getattr(spatial_population, "record_observation", None)
    if obs is None:
        return spatial_population_history_to_readable_dict(
            spatial_population=spatial_population,
            history=history,
            include_zero_counts=include_zero_counts,
        )

    n_demes = spatial_population.n_demes
    first_deme = spatial_population.deme(0)
    state = first_deme.state
    n_sexes = int(state.individual_count.shape[0])
    n_ages = int(state.individual_count.shape[1])
    sex_labels = _default_sex_labels(n_sexes)
    n_groups = len(obs.labels)
    labels = list(obs.labels)

    history_array: np.ndarray
    if history is None:
        history_array = spatial_population.get_history()
    else:
        history_array = cast(np.ndarray, np.asarray(history, dtype=np.float64))

    if history_array.ndim != 2:
        raise ValueError(f"history must be a 2D array, got shape {history_array.shape}")

    compact = getattr(spatial_population, "_compact_meta", None)
    sex_ages = n_sexes * n_ages
    has_compact = compact is not None

    snapshots: List[Dict[str, Any]] = []
    for idx in range(int(history_array.shape[0])):
        row = history_array[idx, :]
        tick = int(row[0])

        collapse = bool(obs.collapse_age)
        if collapse:
            agg_arr: np.ndarray = np.zeros((n_groups, n_sexes), dtype=np.float64)
        else:
            agg_arr = np.zeros((n_groups, n_sexes, n_ages), dtype=np.float64)

        per_deme: Dict[str, Any] = {}

        if has_compact:
            meta = cast(CompactMeta, compact)

            for gi in range(n_groups):
                label = labels[gi]
                off = int(meta.offsets[gi])
                nd = int(meta.n_demes_per_group[gi])
                sn = int(meta.selected_n[gi])

                if meta.mode_aggregate[gi]:
                    chunk = row[1 + off : 1 + off + sex_ages]
                    obs_chunk = chunk.reshape(1, n_sexes, n_ages)
                    entry = _build_observation_payload(
                        observed=obs_chunk, labels=[label],
                        sex_labels=sex_labels, include_zero_counts=include_zero_counts,
                    )
                    per_deme.setdefault("aggregate", {}).update(entry)
                    chunk_2d = chunk.reshape(n_sexes, n_ages)
                    if collapse:
                        agg_arr[gi] += chunk_2d.sum(axis=1)
                    else:
                        agg_arr[gi] += chunk_2d
                else:
                    for di in range(nd):
                        d = int(meta.deme_map[gi, di])
                        chunk = row[1 + off + di * sex_ages : 1 + off + (di + 1) * sex_ages]
                        if di >= sn:
                            per_deme.setdefault(f"deme_{d}", {})[label] = "masked"
                        else:
                            obs_chunk = chunk.reshape(1, n_sexes, n_ages)
                            entry = _build_observation_payload(
                                observed=obs_chunk, labels=[label],
                                sex_labels=sex_labels, include_zero_counts=include_zero_counts,
                            )
                            per_deme.setdefault(f"deme_{d}", {}).update(entry)
                            chunk_2d = chunk.reshape(n_sexes, n_ages)
                            if collapse:
                                agg_arr[gi] += chunk_2d.sum(axis=1)
                            else:
                                agg_arr[gi] += chunk_2d
        else:
            observed = row[1:].reshape(n_demes, n_groups, n_sexes, n_ages)
            for d in range(n_demes):
                deme_obs = observed[d]
                per_deme[f"deme_{d}"] = _build_observation_payload(
                    observed=deme_obs, labels=labels,
                    sex_labels=sex_labels, include_zero_counts=include_zero_counts,
                )
            agg_arr = observed.sum(axis=0)

        aggregate_payload = _build_observation_payload(
            observed=agg_arr, labels=labels,
            sex_labels=sex_labels, include_zero_counts=include_zero_counts,
        )

        snapshots.append({
            "tick": tick,
            "labels": labels,
            "collapse_age": bool(obs.collapse_age),
            "demes": per_deme,
            "aggregate": aggregate_payload,
        })

    return {
        "state_type": "SpatialPopulationObservationHistory",
        "name": str(spatial_population.name),
        "n_demes": int(n_demes),
        "n_snapshots": int(history_array.shape[0]),
        "collapse_age": bool(obs.collapse_age),
        "labels": labels,
        "snapshots": snapshots,
    }

spatial_population_observation_history_to_readable_json

spatial_population_observation_history_to_readable_json(spatial_population: SpatialPopulation, *, history: Optional[ndarray] = None, include_zero_counts: bool = False, indent: int = 2) -> str

Observation-based spatial history translated to readable JSON.

Delegates to :func:spatial_population_observation_history_to_readable_dict and serializes the result.

Parameters:

Name Type Description Default
spatial_population SpatialPopulation

Spatial population container.

required
history Optional[ndarray]

Optional stacked history array. When None, uses spatial_population.get_history().

None
include_zero_counts bool

Whether to keep zero-valued entries.

False
indent int

JSON indentation level.

2

Returns:

Type Description
str

JSON string of the observation-based history payload.

Source code in src/natal/state_translation.py
def spatial_population_observation_history_to_readable_json(
    spatial_population: SpatialPopulation,
    *,
    history: Optional[np.ndarray] = None,
    include_zero_counts: bool = False,
    indent: int = 2,
) -> str:
    """Observation-based spatial history translated to readable JSON.

    Delegates to :func:`spatial_population_observation_history_to_readable_dict`
    and serializes the result.

    Args:
        spatial_population: Spatial population container.
        history: Optional stacked history array. When ``None``, uses
            ``spatial_population.get_history()``.
        include_zero_counts: Whether to keep zero-valued entries.
        indent: JSON indentation level.

    Returns:
        JSON string of the observation-based history payload.
    """
    payload = spatial_population_observation_history_to_readable_dict(
        spatial_population=spatial_population,
        history=history,
        include_zero_counts=include_zero_counts,
    )
    return json.dumps(payload, ensure_ascii=False, indent=indent)

spatial_population_history_to_readable_json

spatial_population_history_to_readable_json(spatial_population: SpatialPopulation, history: Optional[ndarray] = None, include_zero_counts: bool = False, indent: int = 2) -> str

Spatial raw-count history translated to readable JSON.

Delegates to :func:spatial_population_history_to_readable_dict and serializes the result.

Parameters:

Name Type Description Default
spatial_population SpatialPopulation

Spatial population container.

required
history Optional[ndarray]

Optional stacked history array. When None, uses spatial_population.get_history().

None
include_zero_counts bool

Whether to keep zero-valued entries.

False
indent int

JSON indentation level.

2

Returns:

Type Description
str

JSON string of the raw-count history payload.

Source code in src/natal/state_translation.py
def spatial_population_history_to_readable_json(
    spatial_population: SpatialPopulation,
    history: Optional[np.ndarray] = None,
    include_zero_counts: bool = False,
    indent: int = 2,
) -> str:
    """Spatial raw-count history translated to readable JSON.

    Delegates to :func:`spatial_population_history_to_readable_dict` and
    serializes the result.

    Args:
        spatial_population: Spatial population container.
        history: Optional stacked history array. When ``None``, uses
            ``spatial_population.get_history()``.
        include_zero_counts: Whether to keep zero-valued entries.
        indent: JSON indentation level.

    Returns:
        JSON string of the raw-count history payload.
    """
    payload = spatial_population_history_to_readable_dict(
        spatial_population=spatial_population,
        history=history,
        include_zero_counts=include_zero_counts,
    )
    return json.dumps(payload, ensure_ascii=False, indent=indent)

spatial_population_output_history

spatial_population_output_history(spatial_population: SpatialPopulation, *, history: Optional[ndarray] = None, output_path: Optional[Union[str, Path]] = None, indent: int = 2) -> Dict[str, Any]

Export spatial history as a readable dictionary.

When spatial_population.record_observation is set, the observation history path is used automatically.

Parameters:

Name Type Description Default
spatial_population SpatialPopulation

Spatial population container.

required
history Optional[ndarray]

Optional stacked history array. When None, uses spatial_population.get_history().

None
output_path Optional[Union[str, Path]]

Optional JSON file path. When provided, the payload is written to this file as UTF-8 JSON.

None
indent int

Indentation used when writing JSON.

2

Returns:

Type Description
Dict[str, Any]

Dictionary containing per-tick, per-deme readable payloads.

Source code in src/natal/state_translation.py
def spatial_population_output_history(
    spatial_population: SpatialPopulation,
    *,
    history: Optional[np.ndarray] = None,
    output_path: Optional[Union[str, Path]] = None,
    indent: int = 2,
) -> Dict[str, Any]:
    """Export spatial history as a readable dictionary.

    When ``spatial_population.record_observation`` is set, the observation
    history path is used automatically.

    Args:
        spatial_population: Spatial population container.
        history: Optional stacked history array. When ``None``, uses
            ``spatial_population.get_history()``.
        output_path: Optional JSON file path. When provided, the payload is
            written to this file as UTF-8 JSON.
        indent: Indentation used when writing JSON.

    Returns:
        Dictionary containing per-tick, per-deme readable payloads.
    """
    # Auto-detect pre-recorded observation history (same logic as output_history).
    obs = getattr(spatial_population, "record_observation", None)
    if obs is not None:
        payload = spatial_population_observation_history_to_readable_dict(
            spatial_population=spatial_population,
            history=history,
            include_zero_counts=False,
        )
    else:
        payload = spatial_population_history_to_readable_dict(
            spatial_population=spatial_population,
            history=history,
            include_zero_counts=False,
        )
    _write_json_payload(payload, output_path, indent)
    return payload