Source code for mtlearn.layers.cfp.preparation.prepared_batch

"""Dense batches of immutable, model-independent CPU morphology."""
from __future__ import annotations
from dataclasses import dataclass
from types import MappingProxyType

import numpy as np
import torch

from .prepared_morphology import PreparedMorphology


[docs] @dataclass(frozen=True, eq=False) class PreparedBatch: """Morphology grouped as ``samples[batch][channel][tree_key]``. All channels share a spatial shape. Logical sample IDs affect CFPContext, never cache identity. Tensors are shared and read-only by contract. A batch owns its active data even after the corresponding store entries are evicted. """ samples: tuple sample_ids: tuple | None = None def __post_init__(self): samples = tuple(tuple(MappingProxyType(dict(channel)) for channel in sample) for sample in self.samples) object.__setattr__(self, "samples", samples) ids = tuple(str(i) for i in range(len(samples))) if self.sample_ids is None else tuple(self.sample_ids) if len(ids) != len(samples) or any(not isinstance(i, str) for i in ids): raise ValueError("sample_ids must contain one string per sample.") object.__setattr__(self, "sample_ids", ids) if not samples or not samples[0]: raise ValueError("PreparedBatch requires at least one sample and channel.") shape = None for sample in samples: if len(sample) != len(samples[0]): raise ValueError("All prepared samples must have the same channel count.") for channel in sample: if not channel: raise ValueError("Each prepared channel requires at least one tree.") for key, prepared in channel.items(): if not isinstance(prepared, PreparedMorphology) or key != prepared.tree_key: raise ValueError("Prepared channel keys must match their morphology trees.") prepared.validate() if shape is not None and prepared.image_shape != shape: raise ValueError("All prepared channels must share a spatial shape.") shape = prepared.image_shape @property def shape(self): first = next(iter(self.samples[0][0].values())) return len(self.samples), len(self.samples[0]), *first.image_shape def sample_key(self, batch_index, channel_index): return f"{self.sample_ids[batch_index]}_{channel_index}"
[docs] def validate_for(self, layer): """Check compatibility before transferring tensors or scoring any sample. Extra trees/attributes are allowed, enabling preparation shared by models with different feature subsets. Required raw dtypes must match the layer. """ if self.shape[1] != layer.in_channels: raise ValueError(f"Prepared channels do not match in_channels={layer.in_channels}.") dtype = torch.float64 if np.dtype(layer.attribute_dtype) == np.dtype(np.float64) else torch.float32 for sample in self.samples: for channel in sample: for key, attrs in layer._scoring_attrs_by_tree_key.items(): if key not in channel: raise ValueError(f"Prepared batch is missing tree configuration {key}.") prepared = channel[key] prepared.validate() if not set(attrs).issubset(prepared.raw_attributes): raise ValueError(f"Prepared batch is missing attributes for {key}.") if any(prepared.raw_attributes[attr].dtype != dtype for attr in attrs): raise ValueError("Prepared attribute_dtype does not match the layer.")