Skip to content

replicate_generator

tissue_simulator.replicate_generator

Replicate tissue generator based on spatial statistics.

This module allows generation of random tissue replicates that match specified spatial interaction statistics. It uses an optimization-based approach to tune tissue parameters to achieve desired cell-cell interaction patterns.

TargetStatistics dataclass

TargetStatistics(interaction_stats: List[InteractionStatistics], cell_type_proportions: Optional[Dict[str, float]] = None, target_cell_count: Optional[int] = None, target_density: Optional[float] = None)

Target spatial statistics for replicate generation.

Attributes:

Name Type Description
interaction_stats List[InteractionStatistics]

List of InteractionStatistics defining target patterns

cell_type_proportions Optional[Dict[str, float]]

Dict mapping cell types to target proportions (0-1)

target_cell_count Optional[int]

Optional target for total cell count

target_density Optional[float]

Optional target for packing fraction

validate

validate()

Validate that statistics are consistent.

Source code in tissue_simulator/replicate_generator.py
def validate(self):
    """Validate that statistics are consistent."""
    if self.cell_type_proportions:
        total_prop = sum(self.cell_type_proportions.values())
        if not (0.99 <= total_prop <= 1.01):
            raise ValueError(f"Cell type proportions must sum to 1.0, got {total_prop}")

    if self.target_density and not (0 < self.target_density < 1):
        raise ValueError(f"Target density must be between 0 and 1, got {self.target_density}")

ReplicateStatistics dataclass

ReplicateStatistics(replicate_id: int, num_cells: int, cell_type_counts: Dict[str, int], packing_fraction: float, interaction_stats: List[InteractionStatistics], divergence_score: float, packing_report: Optional[Dict] = None, composition_error: Optional[float] = None, layout_flags: Optional[List[str]] = None)

Statistics for a generated replicate.

Attributes:

Name Type Description
replicate_id int

Unique identifier

num_cells int

Total cell count

cell_type_counts Dict[str, int]

Dict of counts per cell type

packing_fraction float

Volume fraction occupied by cells

interaction_stats List[InteractionStatistics]

Measured interaction statistics

divergence_score float

Overall divergence from target statistics

to_dict

to_dict() -> Dict

Convert to dictionary.

Source code in tissue_simulator/replicate_generator.py
def to_dict(self) -> Dict:
    """Convert to dictionary."""
    result = asdict(self)
    result['interaction_stats'] = [s.to_dict() for s in self.interaction_stats]
    return result

ReplicateGenerator

ReplicateGenerator(target_stats: TargetStatistics, tissue_dimensions: Tuple[float, float, float], base_cell_radii: Dict[str, Tuple[float, float]], network_mode: str = 'contact', network_radius: Optional[float] = None, seed: Optional[int] = None, method: str = 'radius_tuning', coloring_params: Optional[Dict] = None, n_restarts: int = 1, radius_optimizer: str = 'heuristic', de_params: Optional[Dict] = None, density_model: Optional[DensityModel] = None, layout: str = 'resample', composition_weight: float = 4.0, composition_bin: float = 40.0, packing_params: Optional[Dict] = None)

Generate tissue replicates matching target spatial statistics.

This class uses an iterative approach to generate tissue samples that match specified spatial interaction patterns. It adjusts tissue parameters to achieve the desired statistics.

Initialize replicate generator.

Parameters:

Name Type Description Default
target_stats TargetStatistics

Target spatial statistics to match

required
tissue_dimensions Tuple[float, float, float]

(height, width, thickness) in micrometers

required
base_cell_radii Dict[str, Tuple[float, float]]

Dict mapping cell types to (min_radius, max_radius)

required
network_mode str

"contact" or "radius" for spatial analysis

'contact'
network_radius Optional[float]

Distance threshold if using "radius" mode

None
seed Optional[int]

Random seed for reproducibility

None
method str

Replicate strategy. "radius_tuning" (default, unchanged behavior) iteratively repacks and nudges per-type radii to match proportions. "graph_coloring" packs geometry once per replicate and assigns cell types via simulated-annealing graph coloring to match the target interaction statistics — far more consistent and faster-converging for interaction targets.

'radius_tuning'
coloring_params Optional[Dict]

Optional overrides for the "graph_coloring" SA schedule (keys: initial_temp, final_temp, cooling_rate, max_iterations, and optionally patience for adaptive stopping).

None
n_restarts int

For "graph_coloring", number of independent SA runs per replicate; the lowest-cost coloring is kept. >1 hardens against bad local minima at a linear cost. Default 1.

1
radius_optimizer str

For method="radius_tuning", the proportion tuner. "heuristic" (default, unchanged) uses the one-shot sqrt-ratio radius adjustment. "differential_evolution" runs a gradient-free scipy optimizer over per-type radius multipliers against a fixed-seed (deterministic) proportion objective. Gradient methods are deliberately not offered: the radius->cell-count map is integer-valued and stochastic, so finite-difference gradients are mostly zero. DE is more robust but slower; for matching interaction patterns prefer method="graph_coloring".

'heuristic'
de_params Optional[Dict]

Optional overrides for differential_evolution (e.g. maxiter, popsize, tol).

None
density_model Optional[DensityModel]

Optional :class:~tissue_simulator.density.DensityModel fitted to the source region. When given (method must be "graph_coloring"), every replicate is packed on a density-aware layout sampled from it, and the annealer also matches the layout's expected composition in square bins of composition_bin µm. max_attempts and min_spacing are then unused.

None
layout str

"resample" (a new arrangement of dense and sparse compartments per replicate) or "copy" (the region's own maps). Used only with density_model.

'resample'
composition_weight float

Weight of the spatial-composition term. It is multiplied by the squared mean degree of each replicate graph, which keeps its pull comparable to the edge-count term across graph sizes. The default 4.0 was chosen by ablation on synthetic nest processes (larger values trade pair-fraction accuracy for composition accuracy).

4.0
composition_bin float

Side in µm of the composition bins.

40.0
packing_params Optional[Dict]

Extra keyword arguments for :class:~tissue_simulator.packing.InhomogeneousPacker.

None
Source code in tissue_simulator/replicate_generator.py
def __init__(self, 
             target_stats: TargetStatistics,
             tissue_dimensions: Tuple[float, float, float],
             base_cell_radii: Dict[str, Tuple[float, float]],
             network_mode: str = "contact",
             network_radius: Optional[float] = None,
             seed: Optional[int] = None,
             method: str = "radius_tuning",
             coloring_params: Optional[Dict] = None,
             n_restarts: int = 1,
             radius_optimizer: str = "heuristic",
             de_params: Optional[Dict] = None,
             density_model: Optional[DensityModel] = None,
             layout: str = "resample",
             composition_weight: float = 4.0,
             composition_bin: float = 40.0,
             packing_params: Optional[Dict] = None):
    """
    Initialize replicate generator.

    Args:
        target_stats: Target spatial statistics to match
        tissue_dimensions: (height, width, thickness) in micrometers
        base_cell_radii: Dict mapping cell types to (min_radius, max_radius)
        network_mode: "contact" or "radius" for spatial analysis
        network_radius: Distance threshold if using "radius" mode
        seed: Random seed for reproducibility
        method: Replicate strategy. ``"radius_tuning"`` (default, unchanged
            behavior) iteratively repacks and nudges per-type radii to match
            proportions. ``"graph_coloring"`` packs geometry once per
            replicate and assigns cell types via simulated-annealing graph
            coloring to match the target interaction statistics — far more
            consistent and faster-converging for interaction targets.
        coloring_params: Optional overrides for the ``"graph_coloring"`` SA
            schedule (keys: ``initial_temp``, ``final_temp``,
            ``cooling_rate``, ``max_iterations``, and optionally
            ``patience`` for adaptive stopping).
        n_restarts: For ``"graph_coloring"``, number of independent SA runs
            per replicate; the lowest-cost coloring is kept. >1 hardens
            against bad local minima at a linear cost. Default 1.
        radius_optimizer: For ``method="radius_tuning"``, the proportion
            tuner. ``"heuristic"`` (default, unchanged) uses the one-shot
            sqrt-ratio radius adjustment. ``"differential_evolution"`` runs
            a gradient-free scipy optimizer over per-type radius multipliers
            against a fixed-seed (deterministic) proportion objective.
            Gradient methods are deliberately not offered: the
            radius->cell-count map is integer-valued and stochastic, so
            finite-difference gradients are mostly zero. DE is more robust
            but slower; for matching interaction patterns prefer
            ``method="graph_coloring"``.
        de_params: Optional overrides for ``differential_evolution`` (e.g.
            ``maxiter``, ``popsize``, ``tol``).
        density_model: Optional :class:`~tissue_simulator.density.DensityModel`
            fitted to the source region. When given (``method`` must be
            ``"graph_coloring"``), every replicate is packed on a
            density-aware layout sampled from it, and the annealer also
            matches the layout's expected composition in square bins of
            ``composition_bin`` µm. ``max_attempts`` and ``min_spacing``
            are then unused.
        layout: ``"resample"`` (a new arrangement of dense and sparse
            compartments per replicate) or ``"copy"`` (the region's own
            maps). Used only with ``density_model``.
        composition_weight: Weight of the spatial-composition term. It is
            multiplied by the squared mean degree of each replicate graph,
            which keeps its pull comparable to the edge-count term across
            graph sizes. The default 4.0 was chosen by ablation on
            synthetic nest processes (larger values trade pair-fraction
            accuracy for composition accuracy).
        composition_bin: Side in µm of the composition bins.
        packing_params: Extra keyword arguments for
            :class:`~tissue_simulator.packing.InhomogeneousPacker`.
    """
    if not NETWORKX_AVAILABLE:
        raise ImportError("NetworkX required for replicate generation")

    target_stats.validate()

    if method not in ("radius_tuning", "graph_coloring"):
        raise ValueError(
            f"method must be 'radius_tuning' or 'graph_coloring', got {method!r}."
        )
    if radius_optimizer not in ("heuristic", "differential_evolution"):
        raise ValueError(
            "radius_optimizer must be 'heuristic' or 'differential_evolution', "
            f"got {radius_optimizer!r}."
        )
    if density_model is not None and method != "graph_coloring":
        raise ValueError("density_model requires method='graph_coloring'.")
    if layout not in ("resample", "copy"):
        raise ValueError(f"layout must be 'resample' or 'copy', got {layout!r}.")

    self.target_stats = target_stats
    self.tissue_dimensions = tissue_dimensions
    self.base_cell_radii = base_cell_radii
    self.network_mode = network_mode
    self.network_radius = network_radius
    self.seed = seed
    self.method = method
    self.n_restarts = max(1, int(n_restarts))
    self.radius_optimizer = radius_optimizer
    self.density_model = density_model
    self.layout = layout
    self.composition_weight = float(composition_weight)
    self.composition_bin = float(composition_bin)
    self.packing_params = dict(packing_params or {})
    self.de_params = {'maxiter': 15, 'popsize': 10, 'tol': 0.01, 'polish': False}
    if de_params:
        self.de_params.update(de_params)

    # SA schedule for the graph-coloring path (overridable).
    self.coloring_params = {
        'initial_temp': 100.0,
        'final_temp': 0.1,
        'cooling_rate': 0.995,
        'max_iterations': 20000,
    }
    if coloring_params:
        self.coloring_params.update(coloring_params)

    # NOTE: we deliberately do NOT seed the global ``np.random`` module
    # RNG here. Doing so leaks state into the rest of the process and
    # makes reproducibility "best-effort" rather than guaranteed. Instead,
    # each replicate gets a deterministic per-replicate seed derived from
    # ``self.seed`` in ``generate_single_replicate``, and that seed is
    # threaded explicitly through ``TissueSection`` / ``SpherePacker``.

    # Extract cell types from target stats. Stored as a sorted tuple
    # rather than a set so iteration order is bit-stable across Python
    # processes (a plain set's order depends on PYTHONHASHSEED, and
    # downstream code feeds this iteration order into rng.choice via
    # dict construction — non-determinism there silently breaks the
    # reproducibility guarantee that ``seed`` is meant to provide).
    types_seen = set()
    for stat in target_stats.interaction_stats:
        types_seen.add(stat.type_a)
        types_seen.add(stat.type_b)
    self.cell_types = tuple(sorted(types_seen))

    # Set default proportions if not provided
    if target_stats.cell_type_proportions is None:
        n_types = len(self.cell_types)
        self.target_stats.cell_type_proportions = {
            ct: 1.0 / n_types for ct in self.cell_types
        }

    # Validate cell types match
    config_types = set(base_cell_radii.keys())
    missing = set(self.cell_types) - config_types
    if missing:
        raise ValueError(f"Cell types in target stats not in radii config: {missing}")
    if density_model is not None and not set(density_model.cell_types) & set(self.cell_types):
        raise ValueError("density_model shares no cell types with the target statistics.")

from_coordinates classmethod

from_coordinates(filepath: str, network_mode: str = 'radius', network_radius: Optional[float] = 20.0, tissue_dimensions: Optional[Tuple[float, float, float]] = None, layout: str = 'resample', density_kwargs: Optional[Dict] = None, **kwargs) -> ReplicateGenerator

Replicate generator fitted to a coordinate CSV (the recommended path).

Reads the source region with :func:~tissue_simulator.tissue.load_tissue_from_csv, extracts its target statistics, fits a :class:~tissue_simulator.density.DensityModel to it, and returns a method="graph_coloring" generator that packs every replicate on a density-aware layout.

Parameters:

Name Type Description Default
filepath str

Coordinate CSV of the source region.

required
network_mode str

Neighbor graph mode for targets and replicates.

'radius'
network_radius Optional[float]

Graph radius in µm for "radius" mode.

20.0
tissue_dimensions Optional[Tuple[float, float, float]]

(height, width, thickness) of the replicates; defaults to the source region's.

None
layout str

"resample" (default) or "copy".

'resample'
density_kwargs Optional[Dict]

Keyword arguments for :meth:DensityModel.fit.

None
**kwargs

Other :class:ReplicateGenerator arguments (seed, coloring_params, composition_weight, ...).

{}
Source code in tissue_simulator/replicate_generator.py
@classmethod
def from_coordinates(cls, filepath: str,
                     network_mode: str = "radius",
                     network_radius: Optional[float] = 20.0,
                     tissue_dimensions: Optional[Tuple[float, float, float]] = None,
                     layout: str = "resample",
                     density_kwargs: Optional[Dict] = None,
                     **kwargs) -> 'ReplicateGenerator':
    """Replicate generator fitted to a coordinate CSV (the recommended path).

    Reads the source region with
    :func:`~tissue_simulator.tissue.load_tissue_from_csv`, extracts its
    target statistics, fits a :class:`~tissue_simulator.density.DensityModel`
    to it, and returns a ``method="graph_coloring"`` generator that packs
    every replicate on a density-aware layout.

    Args:
        filepath: Coordinate CSV of the source region.
        network_mode: Neighbor graph mode for targets and replicates.
        network_radius: Graph radius in µm for ``"radius"`` mode.
        tissue_dimensions: (height, width, thickness) of the replicates;
            defaults to the source region's.
        layout: ``"resample"`` (default) or ``"copy"``.
        density_kwargs: Keyword arguments for :meth:`DensityModel.fit`.
        **kwargs: Other :class:`ReplicateGenerator` arguments (``seed``,
            ``coloring_params``, ``composition_weight``, ...).
    """
    tissue = load_tissue_from_csv(filepath)
    target_stats = load_target_statistics_from_tissue(
        tissue, network_mode=network_mode, network_radius=network_radius)
    if target_stats.target_density is not None and not 0 < target_stats.target_density < 1:
        # A thin slab around a 2D section has no meaningful 3D packing
        # fraction, and density-aware replicates do not use it.
        target_stats.target_density = None
    density_model = DensityModel.from_tissue(tissue, **(density_kwargs or {}))
    radii: Dict[str, Tuple[float, float]] = {}
    for cell in tissue.cells:
        lo, hi = radii.get(cell.cell_type, (cell.radius, cell.radius))
        radii[cell.cell_type] = (min(lo, cell.radius), max(hi, cell.radius))
    if tissue_dimensions is None:
        tissue_dimensions = (tissue.height, tissue.width, tissue.thickness)
    kwargs.setdefault("method", "graph_coloring")
    return cls(target_stats, tissue_dimensions, radii,
               network_mode=network_mode, network_radius=network_radius,
               density_model=density_model, layout=layout, **kwargs)

generate_single_replicate

generate_single_replicate(replicate_id: int, max_attempts: int = 1000, min_spacing: float = 0.5, allow_boundary: bool = True, max_iterations: int = 5, tolerance: float = 0.15, patience: Optional[int] = None, method: Optional[str] = None) -> Tuple[TissueSection, ReplicateStatistics]

Generate a single tissue replicate.

For method="radius_tuning" (default), iteratively generates tissues and adjusts parameters to approach target statistics. For method="graph_coloring", packs geometry once and assigns cell types via simulated-annealing graph coloring (max_iterations / tolerance apply only to the radius-tuning loop).

Parameters:

Name Type Description Default
replicate_id int

Unique identifier for this replicate

required
max_attempts int

Max attempts for cell packing

1000
min_spacing float

Minimum spacing between cells

0.5
allow_boundary bool

Allow cells extending beyond bounds

True
max_iterations int

Max parameter adjustment iterations (radius-tuning only)

5
tolerance float

Acceptable divergence threshold (radius-tuning only)

0.15
patience Optional[int]

Optional adaptive-stopping budget for the radius-tuning loop. When set, stop early once the best divergence has not improved for patience consecutive iterations. Lets you raise max_iterations without always paying for it.

None
method Optional[str]

Override the instance method for this call.

None

Returns:

Type Description
Tuple[TissueSection, ReplicateStatistics]

Tuple of (TissueSection, ReplicateStatistics)

Source code in tissue_simulator/replicate_generator.py
def generate_single_replicate(self,
                             replicate_id: int,
                             max_attempts: int = 1000,
                             min_spacing: float = 0.5,
                             allow_boundary: bool = True,
                             max_iterations: int = 5,
                             tolerance: float = 0.15,
                             patience: Optional[int] = None,
                             method: Optional[str] = None) -> Tuple[TissueSection, ReplicateStatistics]:
    """
    Generate a single tissue replicate.

    For ``method="radius_tuning"`` (default), iteratively generates tissues
    and adjusts parameters to approach target statistics. For
    ``method="graph_coloring"``, packs geometry once and assigns cell types
    via simulated-annealing graph coloring (``max_iterations`` / ``tolerance``
    apply only to the radius-tuning loop).

    Args:
        replicate_id: Unique identifier for this replicate
        max_attempts: Max attempts for cell packing
        min_spacing: Minimum spacing between cells
        allow_boundary: Allow cells extending beyond bounds
        max_iterations: Max parameter adjustment iterations (radius-tuning only)
        tolerance: Acceptable divergence threshold (radius-tuning only)
        patience: Optional adaptive-stopping budget for the radius-tuning
            loop. When set, stop early once the best divergence has not
            improved for ``patience`` consecutive iterations. Lets you raise
            ``max_iterations`` without always paying for it.
        method: Override the instance ``method`` for this call.

    Returns:
        Tuple of (TissueSection, ReplicateStatistics)
    """
    if (method or self.method) == "graph_coloring":
        return self._generate_single_replicate_colored(
            replicate_id,
            max_attempts=max_attempts,
            min_spacing=min_spacing,
            allow_boundary=allow_boundary,
        )

    if self.radius_optimizer == "differential_evolution":
        return self._generate_single_replicate_de(
            replicate_id,
            max_attempts=max_attempts,
            min_spacing=min_spacing,
            allow_boundary=allow_boundary,
        )

    best_tissue = None
    best_divergence = float('inf')
    best_stats = None
    iters_since_improvement = 0

    current_radii = self.base_cell_radii.copy()

    # Derive a deterministic per-replicate seed from (self.seed, replicate_id)
    # using SeedSequence, which mixes the entropy in a stable, well-defined
    # way. When self.seed is None we fall through to None and tissue
    # generation remains unseeded (backward-compatible).
    if self.seed is not None:
        replicate_seed = int(
            np.random.SeedSequence([self.seed, replicate_id]).generate_state(1)[0]
        )
    else:
        replicate_seed = None

    for iteration in range(max_iterations):
        # Each iteration within a replicate gets its own derived seed so
        # the parameter-adjustment loop is also deterministic.
        if replicate_seed is not None:
            iter_seed = int(
                np.random.SeedSequence([replicate_seed, iteration]).generate_state(1)[0]
            )
        else:
            iter_seed = None

        # Generate tissue
        tissue = TissueSection(
            height=self.tissue_dimensions[0],
            width=self.tissue_dimensions[1],
            thickness=self.tissue_dimensions[2],
            cell_radii=current_radii,
            seed=iter_seed,
        )

        num_cells = tissue.generate_cells(
            max_attempts=max_attempts,
            min_spacing=min_spacing,
            allow_boundary_cells=allow_boundary,
        )

        if num_cells == 0:
            warnings.warn(f"Replicate {replicate_id}, iteration {iteration}: No cells generated")
            continue

        # Analyze spatial interactions
        analyzer = SpatialNetworkAnalyzer()
        analyzer.build_network_from_tissue(
            tissue,
            mode=self.network_mode,
            radius=self.network_radius
        )

        measured_interactions = analyzer.compute_interaction_statistics()

        # Compute divergence
        divergence = self._compute_interaction_divergence(
            measured_interactions,
            self.target_stats.interaction_stats
        )

        # Track best result. nan divergence (no signal anywhere) is not
        # comparable; we still record it as the best if we have nothing.
        improved = False
        if best_tissue is None:
            best_divergence = divergence
            best_tissue = tissue
            best_stats = measured_interactions
        elif not np.isnan(divergence) and (np.isnan(best_divergence) or divergence < best_divergence):
            best_divergence = divergence
            best_tissue = tissue
            best_stats = measured_interactions
            improved = True

        # Check if we've met tolerance (nan never satisfies <= tolerance)
        if not np.isnan(divergence) and divergence <= tolerance:
            break

        # Adaptive stopping: bail out once the best divergence plateaus.
        iters_since_improvement = 0 if improved else iters_since_improvement + 1
        if patience is not None and iters_since_improvement >= patience:
            break

        # Adjust parameters for next iteration
        if iteration < max_iterations - 1:
            current_radii = self._adjust_cell_type_proportions(tissue, iteration)

    # Create statistics object
    if best_tissue is None:
        raise RuntimeError(f"Failed to generate replicate {replicate_id}")

    tissue_stats = best_tissue.get_cell_statistics()

    replicate_stats = ReplicateStatistics(
        replicate_id=replicate_id,
        num_cells=tissue_stats['total_cells'],
        cell_type_counts=tissue_stats['cell_types'],
        packing_fraction=tissue_stats['packing_fraction'],
        interaction_stats=best_stats,
        divergence_score=best_divergence
    )

    return best_tissue, replicate_stats

generate_replicates

generate_replicates(num_replicates: int, max_attempts: int = 1000, min_spacing: float = 0.5, allow_boundary: bool = True, max_iterations: int = 5, tolerance: float = 0.15, patience: Optional[int] = None, parallel: bool = False, max_workers: Optional[int] = None) -> List[Tuple[TissueSection, ReplicateStatistics]]

Generate multiple tissue replicates.

Parameters:

Name Type Description Default
num_replicates int

Number of replicates to generate

required
max_attempts int

Max attempts for cell packing per replicate

1000
min_spacing float

Minimum spacing between cells

0.5
allow_boundary bool

Allow cells extending beyond bounds

True
max_iterations int

Max parameter adjustment iterations (radius-tuning only)

5
tolerance float

Acceptable divergence threshold (radius-tuning only)

0.15
patience Optional[int]

Optional adaptive-stopping budget forwarded to each replicate.

None
parallel bool

When True, generate replicates concurrently with a ProcessPoolExecutor. Each replicate is independent and deterministically seeded from (self.seed, replicate_id), so results are identical to the serial path regardless of worker scheduling.

False
max_workers Optional[int]

Worker count for the process pool (defaults to the executor's default when None).

None

Returns:

Type Description
List[Tuple[TissueSection, ReplicateStatistics]]

List of (TissueSection, ReplicateStatistics) tuples, in replicate order.

Source code in tissue_simulator/replicate_generator.py
def generate_replicates(self,
                       num_replicates: int,
                       max_attempts: int = 1000,
                       min_spacing: float = 0.5,
                       allow_boundary: bool = True,
                       max_iterations: int = 5,
                       tolerance: float = 0.15,
                       patience: Optional[int] = None,
                       parallel: bool = False,
                       max_workers: Optional[int] = None) -> List[Tuple[TissueSection, ReplicateStatistics]]:
    """
    Generate multiple tissue replicates.

    Args:
        num_replicates: Number of replicates to generate
        max_attempts: Max attempts for cell packing per replicate
        min_spacing: Minimum spacing between cells
        allow_boundary: Allow cells extending beyond bounds
        max_iterations: Max parameter adjustment iterations (radius-tuning only)
        tolerance: Acceptable divergence threshold (radius-tuning only)
        patience: Optional adaptive-stopping budget forwarded to each replicate.
        parallel: When True, generate replicates concurrently with a
            ``ProcessPoolExecutor``. Each replicate is independent and
            deterministically seeded from ``(self.seed, replicate_id)``, so
            results are identical to the serial path regardless of worker
            scheduling.
        max_workers: Worker count for the process pool (defaults to the
            executor's default when None).

    Returns:
        List of (TissueSection, ReplicateStatistics) tuples, in replicate order.
    """
    kwargs = dict(
        max_attempts=max_attempts,
        min_spacing=min_spacing,
        allow_boundary=allow_boundary,
        max_iterations=max_iterations,
        tolerance=tolerance,
        patience=patience,
    )

    if parallel and num_replicates > 1:
        from concurrent.futures import ProcessPoolExecutor, as_completed

        results: Dict[int, Tuple[TissueSection, ReplicateStatistics]] = {}
        with ProcessPoolExecutor(max_workers=max_workers) as executor:
            futures = {
                executor.submit(self.generate_single_replicate, replicate_id=i, **kwargs): i
                for i in range(num_replicates)
            }
            for future in as_completed(futures):
                i = futures[future]
                results[i] = future.result()
                stats = results[i][1]
                print(f"Replicate {i + 1}/{num_replicates} done: "
                      f"Cells: {stats.num_cells}, Divergence: {stats.divergence_score:.4f}")
        return [results[i] for i in range(num_replicates)]

    replicates = []
    for i in range(num_replicates):
        print(f"Generating replicate {i+1}/{num_replicates}...")

        tissue, stats = self.generate_single_replicate(replicate_id=i, **kwargs)

        replicates.append((tissue, stats))
        print(f"  Cells: {stats.num_cells}, Divergence: {stats.divergence_score:.4f}")

    return replicates

consistency_report

consistency_report(replicates, metric: str = 'divergence_score') -> Dict

Quantify run-to-run consistency of generated replicates.

Reports, per method, the mean / standard deviation / coefficient of variation of a chosen ReplicateStatistics metric, plus pairwise Cohen's d and the per-group N needed to distinguish methods (via :func:power_analysis.compare_initialization_variance). This is how you measure whether one strategy is more consistent than another.

Note: a smaller coefficient of variation usually means more consistent, but CV is std/|mean| and inflates when the mean sits near zero (e.g. a method that matches the target almost perfectly). Read it alongside the absolute std and mean in the returned per_method.

Parameters:

Name Type Description Default
replicates

Either a list of (tissue, ReplicateStatistics) tuples (a single method), or a dict mapping a method label to such a list (to compare methods, e.g. {"radius_tuning": [...], "graph_coloring": [...]}).

required
metric str

ReplicateStatistics attribute to summarize (default "divergence_score").

'divergence_score'

Returns:

Type Description
Dict

The dict from :func:compare_initialization_variance:

Dict

per_method (n / mean / std / cv) and pairwise

Dict

(Cohen's d, required N per group). NaN metric values (pairs with no

Dict

signal) are dropped before summarizing.

Source code in tissue_simulator/replicate_generator.py
def consistency_report(self,
                       replicates,
                       metric: str = "divergence_score") -> Dict:
    """
    Quantify run-to-run consistency of generated replicates.

    Reports, per method, the mean / standard deviation / coefficient of
    variation of a chosen ReplicateStatistics metric, plus pairwise
    Cohen's d and the per-group N needed to distinguish methods (via
    :func:`power_analysis.compare_initialization_variance`). This is how
    you *measure* whether one strategy is more consistent than another.

    Note: a smaller coefficient of variation usually means more consistent,
    but CV is ``std/|mean|`` and inflates when the mean sits near zero
    (e.g. a method that matches the target almost perfectly). Read it
    alongside the absolute ``std`` and ``mean`` in the returned ``per_method``.

    Args:
        replicates: Either a list of ``(tissue, ReplicateStatistics)``
            tuples (a single method), or a dict mapping a method label to
            such a list (to compare methods, e.g.
            ``{"radius_tuning": [...], "graph_coloring": [...]}``).
        metric: ReplicateStatistics attribute to summarize
            (default ``"divergence_score"``).

    Returns:
        The dict from :func:`compare_initialization_variance`:
        ``per_method`` (n / mean / std / cv) and ``pairwise``
        (Cohen's d, required N per group). NaN metric values (pairs with no
        signal) are dropped before summarizing.
    """
    def _endpoints(reps):
        vals = [getattr(stats, metric) for _, stats in reps]
        return [v for v in vals if v is not None and not np.isnan(v)]

    if isinstance(replicates, dict):
        endpoints_by_method = {name: _endpoints(reps)
                               for name, reps in replicates.items()}
    else:
        endpoints_by_method = {"replicates": _endpoints(replicates)}

    return compare_initialization_variance(endpoints_by_method)

export_replicate_statistics

export_replicate_statistics(replicates: List[Tuple[TissueSection, ReplicateStatistics]], base_filename: str)

Export statistics for all replicates to CSV files.

Parameters:

Name Type Description Default
replicates List[Tuple[TissueSection, ReplicateStatistics]]

List of (tissue, stats) tuples

required
base_filename str

Base name for output files

required
Source code in tissue_simulator/replicate_generator.py
def export_replicate_statistics(self,
                               replicates: List[Tuple[TissueSection, ReplicateStatistics]],
                               base_filename: str):
    """
    Export statistics for all replicates to CSV files.

    Args:
        replicates: List of (tissue, stats) tuples
        base_filename: Base name for output files
    """
    # Summary statistics
    summary_data = []
    for tissue, stats in replicates:
        row = {
            'replicate_id': stats.replicate_id,
            'num_cells': stats.num_cells,
            'packing_fraction': stats.packing_fraction,
            'divergence_score': stats.divergence_score
        }
        # Add cell type counts
        for ct, count in stats.cell_type_counts.items():
            row[f'count_{ct}'] = count
            row[f'proportion_{ct}'] = count / stats.num_cells

        summary_data.append(row)

    df_summary = pd.DataFrame(summary_data)
    df_summary.to_csv(f"{base_filename}_summary.csv", index=False)

    # Interaction statistics
    interaction_data = []
    for tissue, stats in replicates:
        for interaction in stats.interaction_stats:
            row = {
                'replicate_id': stats.replicate_id,
                'type_a': interaction.type_a,
                'type_b': interaction.type_b,
                'num_interactions': interaction.num_interactions,
                'normalized_interactions': interaction.normalized_interactions,
                'avg_distance': interaction.avg_distance,
                'median_distance': interaction.median_distance
            }
            interaction_data.append(row)

    df_interactions = pd.DataFrame(interaction_data)
    df_interactions.to_csv(f"{base_filename}_interactions.csv", index=False)

    print(f"Exported statistics to {base_filename}_summary.csv and {base_filename}_interactions.csv")

export_replicate_tissues

export_replicate_tissues(replicates: List[Tuple[TissueSection, ReplicateStatistics]], output_dir: str)

Export each replicate tissue to a separate CSV file.

Parameters:

Name Type Description Default
replicates List[Tuple[TissueSection, ReplicateStatistics]]

List of (tissue, stats) tuples

required
output_dir str

Directory for output files

required
Source code in tissue_simulator/replicate_generator.py
def export_replicate_tissues(self,
                            replicates: List[Tuple[TissueSection, ReplicateStatistics]],
                            output_dir: str):
    """
    Export each replicate tissue to a separate CSV file.

    Args:
        replicates: List of (tissue, stats) tuples
        output_dir: Directory for output files
    """
    output_path = Path(output_dir)
    output_path.mkdir(parents=True, exist_ok=True)

    for tissue, stats in replicates:
        filename = output_path / f"replicate_{stats.replicate_id:03d}_tissue.csv"
        tissue.export_to_csv(str(filename))

    print(f"Exported {len(replicates)} tissue files to {output_dir}")

load_target_statistics_from_csv

load_target_statistics_from_csv(filepath: str) -> TargetStatistics

Load target statistics from a CSV file.

Expected CSV format: type_a, type_b, num_interactions, normalized_interactions, avg_distance, median_distance

Parameters:

Name Type Description Default
filepath str

Path to CSV file

required

Returns:

Type Description
TargetStatistics

TargetStatistics object

Source code in tissue_simulator/replicate_generator.py
def load_target_statistics_from_csv(filepath: str) -> TargetStatistics:
    """
    Load target statistics from a CSV file.

    Expected CSV format:
    type_a, type_b, num_interactions, normalized_interactions, avg_distance, median_distance

    Args:
        filepath: Path to CSV file

    Returns:
        TargetStatistics object
    """
    df = pd.read_csv(filepath)

    required_cols = ['type_a', 'type_b', 'normalized_interactions']
    if not all(col in df.columns for col in required_cols):
        raise ValueError(f"CSV must contain columns: {required_cols}")

    # Create InteractionStatistics objects
    interaction_stats = []
    for _, row in df.iterrows():
        stat = InteractionStatistics(
            type_a=str(row['type_a']),
            type_b=str(row['type_b']),
            num_interactions=int(row.get('num_interactions', 0)),
            normalized_interactions=float(row['normalized_interactions']),
            avg_distance=float(row.get('avg_distance', 0.0)),
            median_distance=float(row.get('median_distance', 0.0))
        )
        interaction_stats.append(stat)

    return TargetStatistics(interaction_stats=interaction_stats)

load_target_statistics_from_tissue

load_target_statistics_from_tissue(tissue: TissueSection, network_mode: str = 'contact', network_radius: Optional[float] = None) -> TargetStatistics

Extract target statistics from an existing tissue.

Parameters:

Name Type Description Default
tissue TissueSection

TissueSection to analyze

required
network_mode str

"contact" or "radius"

'contact'
network_radius Optional[float]

Distance threshold for "radius" mode

None

Returns:

Type Description
TargetStatistics

TargetStatistics object

Source code in tissue_simulator/replicate_generator.py
def load_target_statistics_from_tissue(tissue: TissueSection,
                                      network_mode: str = "contact",
                                      network_radius: Optional[float] = None) -> TargetStatistics:
    """
    Extract target statistics from an existing tissue.

    Args:
        tissue: TissueSection to analyze
        network_mode: "contact" or "radius"
        network_radius: Distance threshold for "radius" mode

    Returns:
        TargetStatistics object
    """
    # Analyze the tissue
    analyzer = SpatialNetworkAnalyzer()
    analyzer.build_network_from_tissue(
        tissue,
        mode=network_mode,
        radius=network_radius
    )

    # Get interaction statistics
    interaction_stats = analyzer.compute_interaction_statistics()

    # Get cell type proportions
    tissue_stats = tissue.get_cell_statistics()
    total_cells = tissue_stats['total_cells']
    cell_type_proportions = {
        ct: count / total_cells
        for ct, count in tissue_stats['cell_types'].items()
    }

    return TargetStatistics(
        interaction_stats=interaction_stats,
        cell_type_proportions=cell_type_proportions,
        target_cell_count=total_cells,
        target_density=tissue_stats['packing_fraction']
    )

load_target_statistics_from_coordinates

load_target_statistics_from_coordinates(filepath: str, network_mode: str = 'contact', network_radius: Optional[float] = None) -> TargetStatistics

Load FULL target statistics from a coordinate CSV file.

This is a convenience composition: it reads a tissue from a coordinate CSV (one row per cell, with positions/radii/types) via load_tissue_from_csv and then derives target statistics from that reconstructed tissue via load_target_statistics_from_tissue. It is exactly equivalent to::

load_target_statistics_from_tissue(
    load_tissue_from_csv(filepath),
    network_mode=network_mode,
    network_radius=network_radius,
)

Because the statistics are computed from a real tissue, the returned TargetStatistics is fully populated: it includes the measured interaction statistics AND the cell_type_proportions and target_density (packing fraction) inferred from the cell coordinates.

This is DISTINCT from load_target_statistics_from_csv, which reads a precomputed interaction table (type_a, type_b, normalized_interactions, ...) and therefore does NOT populate cell_type_proportions or target_density. Use this function when you have raw cell coordinates; use load_target_statistics_from_csv when you already have an interaction-statistics table.

Parameters:

Name Type Description Default
network_mode str

"contact" (edges between touching cells) or "radius" (edges between cells within network_radius), passed through to load_target_statistics_from_tissue.

'contact'
network_radius Optional[float]

Distance threshold used when network_mode is "radius"; ignored otherwise.

None

Returns:

Type Description
TargetStatistics

TargetStatistics object with interactions, cell type proportions,

TargetStatistics

target cell count, and target density populated.

Source code in tissue_simulator/replicate_generator.py
def load_target_statistics_from_coordinates(filepath: str,
                                            network_mode: str = "contact",
                                            network_radius: Optional[float] = None) -> TargetStatistics:
    """
    Load FULL target statistics from a coordinate CSV file.

    This is a convenience composition: it reads a tissue from a coordinate
    CSV (one row per cell, with positions/radii/types) via
    ``load_tissue_from_csv`` and then derives target statistics from that
    reconstructed tissue via ``load_target_statistics_from_tissue``. It is
    exactly equivalent to::

        load_target_statistics_from_tissue(
            load_tissue_from_csv(filepath),
            network_mode=network_mode,
            network_radius=network_radius,
        )

    Because the statistics are computed from a real tissue, the returned
    ``TargetStatistics`` is fully populated: it includes the measured
    interaction statistics AND the ``cell_type_proportions`` and
    ``target_density`` (packing fraction) inferred from the cell coordinates.

    This is DISTINCT from ``load_target_statistics_from_csv``, which reads a
    precomputed interaction table (``type_a``, ``type_b``,
    ``normalized_interactions``, ...) and therefore does NOT populate
    ``cell_type_proportions`` or ``target_density``. Use this function when
    you have raw cell coordinates; use ``load_target_statistics_from_csv``
    when you already have an interaction-statistics table.

    Args:
        network_mode: "contact" (edges between touching cells) or "radius"
            (edges between cells within ``network_radius``), passed through
            to ``load_target_statistics_from_tissue``.
        network_radius: Distance threshold used when ``network_mode`` is
            "radius"; ignored otherwise.

    Returns:
        TargetStatistics object with interactions, cell type proportions,
        target cell count, and target density populated.
    """
    return load_target_statistics_from_tissue(
        load_tissue_from_csv(filepath),
        network_mode=network_mode,
        network_radius=network_radius,
    )

fit_density_model_from_coordinates

fit_density_model_from_coordinates(filepath: str, **kwargs) -> DensityModel

Fit a :class:~tissue_simulator.density.DensityModel to a coordinate CSV.

The companion of :func:load_target_statistics_from_coordinates; equivalent to DensityModel.from_tissue(load_tissue_from_csv(filepath), **kwargs).

Source code in tissue_simulator/replicate_generator.py
def fit_density_model_from_coordinates(filepath: str, **kwargs) -> DensityModel:
    """Fit a :class:`~tissue_simulator.density.DensityModel` to a coordinate CSV.

    The companion of :func:`load_target_statistics_from_coordinates`;
    equivalent to ``DensityModel.from_tissue(load_tissue_from_csv(filepath), **kwargs)``.
    """
    return DensityModel.from_tissue(load_tissue_from_csv(filepath), **kwargs)