Skip to content

pelinker.config

ClusterCompositionSnapshot dataclass

Mention-weighted mixture of KB property labels per HDBSCAN cluster after Linker.fit.

  • :attr:global_property_mass — total mention count per property in the fitted corpus (denominator for “fraction of that property’s mass” views).
  • :attr:cluster_within_fraction — within each cluster, each property’s share of that cluster’s mention mass (sums to 1.0 per cluster).
  • :attr:cluster_fraction_of_property_mass — for each cluster and property, mentions(cluster ∩ property) / global_property_mass[property] (how much of that property’s corpus sits in this cluster; sums to ≤ 1.0 across disjoint cluster rows for a fixed property, excluding double-counting issues from overlapping keys).
Source code in pelinker/config.py
@dataclass(frozen=True)
class ClusterCompositionSnapshot:
    """
    Mention-weighted mixture of KB ``property`` labels per HDBSCAN cluster after ``Linker.fit``.

    * :attr:`global_property_mass` — total mention count per property in the fitted corpus
      (denominator for “fraction of that property’s mass” views).
    * :attr:`cluster_within_fraction` — within each cluster, each property’s share of that
      cluster’s mention mass (sums to 1.0 per cluster).
    * :attr:`cluster_fraction_of_property_mass` — for each cluster and property,
      ``mentions(cluster ∩ property) / global_property_mass[property]`` (how much of that
      property’s corpus sits in this cluster; sums to ≤ 1.0 across disjoint cluster rows
      for a fixed property, excluding double-counting issues from overlapping keys).
    """

    global_property_mass: dict[str, int]
    cluster_within_fraction: dict[int, dict[str, float]]
    cluster_fraction_of_property_mass: dict[int, dict[str, float]]

ClusteringOptimizationConfig dataclass

Configuration for clustering optimization grid search.

Source code in pelinker/config.py
@dataclass
class ClusteringOptimizationConfig:
    """Configuration for clustering optimization grid search."""

    min_class_size: int = 20
    # Exclusive end of ``np.arange(resolved_min_scale(), max_scale, clustering_grid_step)``.
    max_scale: int = 100
    min_scale: int | None = None
    """Lower bound (inclusive) for the ``min_cluster_size`` grid.

    When ``None``, defaults to ``max(1, min_class_size // 2)``.
    """
    clustering_grid_step: int = 5
    """Step between consecutive ``min_cluster_size`` values on the grid (``numpy.arange`` step)."""
    rns: RandomState = field(default_factory=lambda: RandomState(seed=13))
    base_seed: int = 13
    """Seed for stratified selection draws; per-bootstrap seed is ``base_seed + sample_index``."""
    clustering_sample_rows: int | None = None
    """Max mention rows per clustering bootstrap draw (stratified). None = use all loaded rows."""
    batch_size: int = 1000
    """Rows per batch when **reading mention-level embedding parquet** (not encoder batch size)."""
    drop_rare_entities: bool = False
    min_mentions_per_entity: int = 20
    max_mentions_per_entity: int | None = None
    max_mentions_negative: int | None = None
    mention_cap_seed: int = 13
    optimization_method: str = "mean"
    """How to build the objective f(min_cluster_size) before smoothing (mean / lower_bound / weighted)."""
    grid_objective: GridObjectiveSpec = "dbcv_ari_mean_minmax"
    """Which scalar to optimize on the grid (single metric or pooled DBCV+ARI; see ``clustering_grid``)."""
    grid_smooth_window: int = 3
    """Odd-length centered moving-average window for smoothing f(x). Even values are bumped up by one."""
    grid_plateau_fraction: float = 0.92
    """Plateau threshold on the **smoothed** curve: ``y_min + this * (y_max - y_min)`` (finite values only)."""
    grid_derivative_rel_tol: float = 0.12
    """|df/dx| below this times max|df/dx| counts as “derivative near zero” on the smoothed curve."""
    grid_cluster_count_reward: float = 0.0
    """Weight on ``log(n_clusters / n_ref)`` added to the grid objective (0 = disabled)."""
    grid_n_entities: int | None = None
    """Reference entity count for the cluster-count term; when ``None``, uses max mean cluster count on the grid."""
    ambient_screener: NegativeScreenerConfig = field(
        default_factory=NegativeScreenerConfig
    )
    """Negative-class screening before PCA→UMAP (see :class:`NegativeScreenerConfig`)."""
    projection_screener: ManifoldOovScreenerConfig = field(
        default_factory=ManifoldOovScreenerConfig
    )
    """Validation config for manifold OOV model selection (analysis reporting only)."""

    def resolved_min_scale(self) -> int:
        """Inclusive start of the ``min_cluster_size`` grid (HDBSCAN hyperparameter)."""
        if self.min_scale is not None:
            return self.min_scale
        return max(1, self.min_class_size // 2)

    def __post_init__(self) -> None:
        if self.min_class_size < 1:
            raise ValueError("min_class_size must be >= 1")
        if self.min_scale is not None and self.min_scale < 1:
            raise ValueError("min_scale must be >= 1 when provided")
        lo = self.resolved_min_scale()
        if self.max_scale < lo:
            raise ValueError(
                f"max_scale must be >= resolved min_scale ({lo}); got max_scale={self.max_scale}"
            )
        if self.clustering_grid_step < 1:
            raise ValueError("clustering_grid_step must be >= 1")
        _validate_mention_frame_load_fields(
            batch_size=self.batch_size,
            min_mentions_per_entity=self.min_mentions_per_entity,
            max_mentions_per_entity=self.max_mentions_per_entity,
            max_mentions_negative=self.max_mentions_negative,
        )
        _validate_clustering_sample_rows(self.clustering_sample_rows)
        if not self.optimization_method:
            raise ValueError("optimization_method must be a non-empty string")
        if self.grid_objective not in _GRID_OBJECTIVES:
            raise ValueError(
                f"grid_objective must be one of {sorted(_GRID_OBJECTIVES)}"
            )
        if self.grid_smooth_window < 1:
            raise ValueError("grid_smooth_window must be >= 1")
        if not 0 < self.grid_plateau_fraction <= 1:
            raise ValueError("grid_plateau_fraction must be in (0, 1]")
        if self.grid_derivative_rel_tol <= 0:
            raise ValueError("grid_derivative_rel_tol must be > 0")
        if self.grid_cluster_count_reward < 0:
            raise ValueError("grid_cluster_count_reward must be >= 0")
        if self.grid_n_entities is not None and self.grid_n_entities < 1:
            raise ValueError("grid_n_entities must be >= 1 when provided")

    def to_dict(self) -> dict[str, Any]:
        return asdict(self)

ambient_screener = field(default_factory=NegativeScreenerConfig) class-attribute instance-attribute

Negative-class screening before PCA→UMAP (see :class:NegativeScreenerConfig).

base_seed = 13 class-attribute instance-attribute

Seed for stratified selection draws; per-bootstrap seed is base_seed + sample_index.

batch_size = 1000 class-attribute instance-attribute

Rows per batch when reading mention-level embedding parquet (not encoder batch size).

clustering_grid_step = 5 class-attribute instance-attribute

Step between consecutive min_cluster_size values on the grid (numpy.arange step).

clustering_sample_rows = None class-attribute instance-attribute

Max mention rows per clustering bootstrap draw (stratified). None = use all loaded rows.

grid_cluster_count_reward = 0.0 class-attribute instance-attribute

Weight on log(n_clusters / n_ref) added to the grid objective (0 = disabled).

grid_derivative_rel_tol = 0.12 class-attribute instance-attribute

|df/dx| below this times max|df/dx| counts as “derivative near zero” on the smoothed curve.

grid_n_entities = None class-attribute instance-attribute

Reference entity count for the cluster-count term; when None, uses max mean cluster count on the grid.

grid_objective = 'dbcv_ari_mean_minmax' class-attribute instance-attribute

Which scalar to optimize on the grid (single metric or pooled DBCV+ARI; see clustering_grid).

grid_plateau_fraction = 0.92 class-attribute instance-attribute

Plateau threshold on the smoothed curve: y_min + this * (y_max - y_min) (finite values only).

grid_smooth_window = 3 class-attribute instance-attribute

Odd-length centered moving-average window for smoothing f(x). Even values are bumped up by one.

min_scale = None class-attribute instance-attribute

Lower bound (inclusive) for the min_cluster_size grid.

When None, defaults to max(1, min_class_size // 2).

optimization_method = 'mean' class-attribute instance-attribute

How to build the objective f(min_cluster_size) before smoothing (mean / lower_bound / weighted).

projection_screener = field(default_factory=ManifoldOovScreenerConfig) class-attribute instance-attribute

Validation config for manifold OOV model selection (analysis reporting only).

resolved_min_scale()

Inclusive start of the min_cluster_size grid (HDBSCAN hyperparameter).

Source code in pelinker/config.py
def resolved_min_scale(self) -> int:
    """Inclusive start of the ``min_cluster_size`` grid (HDBSCAN hyperparameter)."""
    if self.min_scale is not None:
        return self.min_scale
    return max(1, self.min_class_size // 2)

EmbeddingModelMetadata dataclass

Describes which embedding backbones/layers produced the model (saved with the Linker).

Source code in pelinker/config.py
@dataclass(frozen=True)
class EmbeddingModelMetadata:
    """Describes which embedding backbones/layers produced the model (saved with the Linker)."""

    sources: tuple[EmbeddingSourceSpec, ...]

    def __post_init__(self) -> None:
        if not self.sources:
            raise ValueError("sources must contain at least one EmbeddingSourceSpec")

    @classmethod
    def from_single(cls, model_type: str, layers_spec: str) -> EmbeddingModelMetadata:
        return cls(
            sources=(
                EmbeddingSourceSpec(model_type=model_type, layers_spec=layers_spec),
            )
        )

EmbeddingSourceSpec dataclass

One backbone + layer selection (e.g. for a single encoder or one branch of a fused model).

Source code in pelinker/config.py
@dataclass(frozen=True)
class EmbeddingSourceSpec:
    """One backbone + layer selection (e.g. for a single encoder or one branch of a fused model)."""

    model_type: str
    layers_spec: str

    def __post_init__(self) -> None:
        if not self.model_type:
            raise ValueError("model_type must be a non-empty string")
        if not self.layers_spec:
            raise ValueError("layers_spec must be a non-empty string")

EmbeddingTrainingConfig dataclass

Inputs and runtime settings used only while embedding the corpus (not part of model identity).

Source code in pelinker/config.py
@dataclass
class EmbeddingTrainingConfig:
    """Inputs and runtime settings used only while embedding the corpus (not part of model identity)."""

    input_text_table_path: Path
    kb_csv_path: Path
    use_gpu: bool = False
    input_buffer_rows: int = 1000
    """Rows read per ``pandas.read_csv(..., chunksize=...)`` pass over the text table (I/O buffer only)."""
    encoder_batch_size: int = 200
    """How many table rows are encoded per transformer forward pass; lower if GPU memory is tight."""
    nlp_model: str = "en_core_web_trf"
    max_input_buffers: int | None = None
    """If set, stop after this many text-table read passes (each up to ``input_buffer_rows`` rows)."""
    negatives_per_positive: float = 0.0
    """Number of random negative mentions to sample per positive mention."""
    negative_label: str = NEGATIVE_LABEL
    """Entity label to use for synthetic negative rows."""
    negative_seed: int | None = 13
    """Optional random seed for deterministic negative sampling."""

    def __post_init__(self) -> None:
        if self.input_buffer_rows < 1:
            raise ValueError("input_buffer_rows must be >= 1")
        if self.encoder_batch_size < 1:
            raise ValueError("encoder_batch_size must be >= 1")
        if self.max_input_buffers is not None and self.max_input_buffers < 1:
            raise ValueError("max_input_buffers must be >= 1 when provided")
        if self.negatives_per_positive < 0:
            raise ValueError("negatives_per_positive must be >= 0")
        if not self.negative_label:
            raise ValueError("negative_label must be a non-empty string")
        self.input_text_table_path = Path(
            os.path.expandvars(os.fspath(self.input_text_table_path))
        ).expanduser()
        self.kb_csv_path = Path(
            os.path.expandvars(os.fspath(self.kb_csv_path))
        ).expanduser()

encoder_batch_size = 200 class-attribute instance-attribute

How many table rows are encoded per transformer forward pass; lower if GPU memory is tight.

input_buffer_rows = 1000 class-attribute instance-attribute

Rows read per pandas.read_csv(..., chunksize=...) pass over the text table (I/O buffer only).

max_input_buffers = None class-attribute instance-attribute

If set, stop after this many text-table read passes (each up to input_buffer_rows rows).

negative_label = NEGATIVE_LABEL class-attribute instance-attribute

Entity label to use for synthetic negative rows.

negative_seed = 13 class-attribute instance-attribute

Optional random seed for deterministic negative sampling.

negatives_per_positive = 0.0 class-attribute instance-attribute

Number of random negative mentions to sample per positive mention.

KBConfig dataclass

Metadata for the knowledge base packaged with a fitted Linker.

Source code in pelinker/config.py
@dataclass(frozen=True)
class KBConfig:
    """Metadata for the knowledge base packaged with a fitted Linker."""

    name: str
    version: str
    created_at: date
    description: str = ""
    entity_count: int | None = None
    """Set after fit from vocabulary size when None at construction time."""

    def __post_init__(self) -> None:
        if not self.name.strip():
            raise ValueError("name must be a non-empty string")
        _validate_semver(self.version)
        if self.entity_count is not None and self.entity_count < 0:
            raise ValueError("entity_count must be >= 0 when provided")

entity_count = None class-attribute instance-attribute

Set after fit from vocabulary size when None at construction time.

LinkerFitConfig dataclass

Parquet read + mention filters + screener settings for :meth:~pelinker.model.Linker.fit.

Source code in pelinker/config.py
@dataclass
class LinkerFitConfig:
    """Parquet read + mention filters + screener settings for :meth:`~pelinker.model.Linker.fit`."""

    batch_size: int = 1000
    drop_rare_entities: bool = False
    min_mentions_per_entity: int = 20
    max_mentions_per_entity: int | None = None
    max_mentions_negative: int | None = None
    mention_cap_seed: int = 13
    ambient_screener: NegativeScreenerConfig = field(
        default_factory=NegativeScreenerConfig
    )
    projection_screener: ManifoldOovScreenerConfig = field(
        default_factory=ManifoldOovScreenerConfig
    )
    screener_max_rows: int | None = 100_000
    """Max rows for ambient + projection screener training when using the full frame (stratified). None = no cap."""
    screener_seed: int = 13
    """Random seed for the stratified screener training draw when using the full frame."""
    clustering_sample_rows: int | None = None
    """Max mention rows per clustering bootstrap draw (stratified). None = use all loaded rows."""
    base_seed: int = 13
    """Seed for stratified clustering draws; draw seed is ``base_seed + clustering_sample_index``."""
    clustering_sample_index: int = 0
    """Bootstrap index for the clustering subsample (same contract as model selection ``sample_idx``)."""
    diagnostics_sample_size: int = 20_000
    """Max rows of :class:`~pelinker.reporting.LinkerFitDiagnostics` stored on the fit report."""
    diagnostics_random_state: int = 0
    """Stratified subsample seed for training diagnostics."""

    def to_clustering_sample_config(self) -> ClusteringOptimizationConfig:
        """Build a :class:`ClusteringOptimizationConfig` for load + subsample helpers."""
        return ClusteringOptimizationConfig(
            base_seed=self.base_seed,
            clustering_sample_rows=self.clustering_sample_rows,
            batch_size=self.batch_size,
            drop_rare_entities=self.drop_rare_entities,
            min_mentions_per_entity=self.min_mentions_per_entity,
            max_mentions_per_entity=self.max_mentions_per_entity,
            max_mentions_negative=self.max_mentions_negative,
            mention_cap_seed=self.mention_cap_seed,
            ambient_screener=self.ambient_screener,
            projection_screener=self.projection_screener,
        )

    def __post_init__(self) -> None:
        _validate_mention_frame_load_fields(
            batch_size=self.batch_size,
            min_mentions_per_entity=self.min_mentions_per_entity,
            max_mentions_per_entity=self.max_mentions_per_entity,
            max_mentions_negative=self.max_mentions_negative,
        )
        if self.screener_max_rows is not None and self.screener_max_rows < 1:
            raise ValueError("screener_max_rows must be >= 1 when provided")
        _validate_clustering_sample_rows(self.clustering_sample_rows)
        if self.clustering_sample_index < 0:
            raise ValueError("clustering_sample_index must be >= 0")
        if self.diagnostics_sample_size < 1:
            raise ValueError("diagnostics_sample_size must be >= 1")

base_seed = 13 class-attribute instance-attribute

Seed for stratified clustering draws; draw seed is base_seed + clustering_sample_index.

clustering_sample_index = 0 class-attribute instance-attribute

Bootstrap index for the clustering subsample (same contract as model selection sample_idx).

clustering_sample_rows = None class-attribute instance-attribute

Max mention rows per clustering bootstrap draw (stratified). None = use all loaded rows.

diagnostics_random_state = 0 class-attribute instance-attribute

Stratified subsample seed for training diagnostics.

diagnostics_sample_size = 20000 class-attribute instance-attribute

Max rows of :class:~pelinker.reporting.LinkerFitDiagnostics stored on the fit report.

screener_max_rows = 100000 class-attribute instance-attribute

Max rows for ambient + projection screener training when using the full frame (stratified). None = no cap.

screener_seed = 13 class-attribute instance-attribute

Random seed for the stratified screener training draw when using the full frame.

to_clustering_sample_config()

Build a :class:ClusteringOptimizationConfig for load + subsample helpers.

Source code in pelinker/config.py
def to_clustering_sample_config(self) -> ClusteringOptimizationConfig:
    """Build a :class:`ClusteringOptimizationConfig` for load + subsample helpers."""
    return ClusteringOptimizationConfig(
        base_seed=self.base_seed,
        clustering_sample_rows=self.clustering_sample_rows,
        batch_size=self.batch_size,
        drop_rare_entities=self.drop_rare_entities,
        min_mentions_per_entity=self.min_mentions_per_entity,
        max_mentions_per_entity=self.max_mentions_per_entity,
        max_mentions_negative=self.max_mentions_negative,
        mention_cap_seed=self.mention_cap_seed,
        ambient_screener=self.ambient_screener,
        projection_screener=self.projection_screener,
    )

ManifoldOovScreenerConfig dataclass

3D (residual, Mahalanobis, spectral entropy) OOV score model; predict-time gate only.

Source code in pelinker/config.py
@dataclass(frozen=True)
class ManifoldOovScreenerConfig:
    """3D (residual, Mahalanobis, spectral entropy) OOV score model; predict-time gate only."""

    enabled: bool = True
    cv_n_splits: int = 5
    cv_test_size: float = 0.2
    cv_random_state: int = 42
    oov_rbf_C: float = 1.0
    oov_rbf_gamma: float | Literal["scale", "auto"] = "scale"

    def __post_init__(self) -> None:
        if self.cv_n_splits < 2:
            raise ValueError("cv_n_splits must be >= 2")
        if not 0.0 < self.cv_test_size < 1.0:
            raise ValueError("cv_test_size must be in (0, 1)")
        if self.oov_rbf_C <= 0.0:
            raise ValueError("oov_rbf_C must be > 0")
        if isinstance(self.oov_rbf_gamma, (int, float)):
            if float(self.oov_rbf_gamma) <= 0.0:
                raise ValueError(
                    "oov_rbf_gamma must be > 0 when a numeric value is used"
                )
        elif self.oov_rbf_gamma not in ("scale", "auto"):
            raise ValueError(
                'oov_rbf_gamma must be "scale", "auto", or a positive float'
            )

MentionFrameLoadConfig dataclass

Shared mention-level parquet load and pre-subsample filters.

Source code in pelinker/config.py
@dataclass
class MentionFrameLoadConfig:
    """Shared mention-level parquet load and pre-subsample filters."""

    batch_size: int = 1000
    drop_rare_entities: bool = False
    """When true, drop KB entities with fewer than :attr:`min_mentions_per_entity` rows."""
    min_mentions_per_entity: int = 20
    max_mentions_per_entity: int | None = None
    """Cap mention rows per KB entity (seeded); ``None`` = no cap."""
    max_mentions_negative: int | None = None
    """Cap for :attr:`~NegativeScreenerConfig.negative_label`; ``None`` = exempt."""
    mention_cap_seed: int = 13

    def __post_init__(self) -> None:
        _validate_mention_frame_load_fields(
            batch_size=self.batch_size,
            min_mentions_per_entity=self.min_mentions_per_entity,
            max_mentions_per_entity=self.max_mentions_per_entity,
            max_mentions_negative=self.max_mentions_negative,
        )

drop_rare_entities = False class-attribute instance-attribute

When true, drop KB entities with fewer than :attr:min_mentions_per_entity rows.

max_mentions_negative = None class-attribute instance-attribute

Cap for :attr:~NegativeScreenerConfig.negative_label; None = exempt.

max_mentions_per_entity = None class-attribute instance-attribute

Cap mention rows per KB entity (seeded); None = no cap.

NegativeScreenerConfig dataclass

Binary LDA/SVM screen for negative_label vs KB mentions before PCA→UMAP.

Source code in pelinker/config.py
@dataclass(frozen=True)
class NegativeScreenerConfig:
    """Binary LDA/SVM screen for ``negative_label`` vs KB mentions before PCA→UMAP."""

    kind: ScreenerKind = "lda"
    """Estimator persisted on :class:`~pelinker.model.Linker` (``Linker.screener``)."""
    negative_label: str = NEGATIVE_LABEL
    cv_n_splits: int = 5
    cv_test_size: float = 0.2
    cv_random_state: int = 42

    def __post_init__(self) -> None:
        if not self.negative_label.strip():
            raise ValueError("negative_label must be non-empty")
        if self.cv_n_splits < 2:
            raise ValueError("cv_n_splits must be >= 2")
        if not 0.0 < self.cv_test_size < 1.0:
            raise ValueError("cv_test_size must be in (0, 1)")

kind = 'lda' class-attribute instance-attribute

Estimator persisted on :class:~pelinker.model.Linker (Linker.screener).

TransformConfig dataclass

Configuration for the embedding transformation pipeline.

Source code in pelinker/config.py
@dataclass
class TransformConfig:
    """Configuration for the embedding transformation pipeline."""

    # PCA configuration
    pca_components: int = 50
    """Number of principal components to keep after PCA reduction."""

    # UMAP configuration
    umap_components: int = 4
    """Number of UMAP dimensions for clustering (typically 3-5)."""
    umap_metric: str = "cosine"
    """Distance metric for UMAP (default: 'cosine')."""

    # Cluster-space visualization (reduces umap_clustering coords for plotting)
    cluster_viz_components: int = 3
    """Number of dimensions for cluster-space visualization (default: 3)."""
    cluster_viz_method: Literal["pca", "umap"] = "pca"
    """Reducer applied to clustering UMAP coords: ``pca`` (linear) or ``umap``."""
    cluster_viz_umap_metric: str = "euclidean"
    """Distance metric for cluster-space UMAP viz (only when ``cluster_viz_method='umap'``)."""

    pca_seed: int = 13
    """Random seed for PCA and cluster-viz PCA."""
    umap_seed: int | None = None
    """UMAP random seed; ``None`` enables parallel UMAP (non-reproducible). Cluster-viz UMAP uses ``umap_seed + 1`` when set."""

    def __post_init__(self):
        if self.pca_components < 1:
            raise ValueError("pca_components must be >= 1")
        if self.umap_components < 2:
            raise ValueError("umap_components must be >= 2")
        if self.cluster_viz_components < 2:
            raise ValueError("cluster_viz_components must be >= 2")
        if self.cluster_viz_components > self.umap_components:
            raise ValueError(
                "cluster_viz_components must be <= umap_components "
                f"(got {self.cluster_viz_components} > {self.umap_components})"
            )
        if self.cluster_viz_method not in ("pca", "umap"):
            raise ValueError(
                "cluster_viz_method must be 'pca' or 'umap', "
                f"got {self.cluster_viz_method!r}"
            )

cluster_viz_components = 3 class-attribute instance-attribute

Number of dimensions for cluster-space visualization (default: 3).

cluster_viz_method = 'pca' class-attribute instance-attribute

Reducer applied to clustering UMAP coords: pca (linear) or umap.

cluster_viz_umap_metric = 'euclidean' class-attribute instance-attribute

Distance metric for cluster-space UMAP viz (only when cluster_viz_method='umap').

pca_components = 50 class-attribute instance-attribute

Number of principal components to keep after PCA reduction.

pca_seed = 13 class-attribute instance-attribute

Random seed for PCA and cluster-viz PCA.

umap_components = 4 class-attribute instance-attribute

Number of UMAP dimensions for clustering (typically 3-5).

umap_metric = 'cosine' class-attribute instance-attribute

Distance metric for UMAP (default: 'cosine').

umap_seed = None class-attribute instance-attribute

UMAP random seed; None enables parallel UMAP (non-reproducible). Cluster-viz UMAP uses umap_seed + 1 when set.