Domain protocols and registries

The v2 capability packages implement the operator surface: subject-level callables, one cohort-level habitat fitter, table-ML components, and registries that construct them by name.

Import style

  • Import every public protocol, component, and pipeline from its canonical capability package, for example from habit.pipeline import SubjectPipeline.

  • Registries are not top-level exports:

    from habit.supervoxel import SupervoxelizerRegistry
    
    from habit.habitat_model import HabitatModelFitterRegistry
    

Importing the v2 capability packages registers built-in components.

Single-subject operators

Each subject-level operator is a one-argument callable. No cohort, backend, or YAML is required to process one subject:

from habit.voxel_features import RawVoxelFeatures

from habit.supervoxel import SlicSupervoxelizer
voxel_fx = RawVoxelFeatures(modalities=["T1", "T2"])
svx = SlicSupervoxelizer(n_supervoxels=50)

field = voxel_fx(subject)   # Subject -> VoxelFeatureField
unit = svx(field)           # VoxelFeatureField -> Supervoxelization

Registry construction (same pattern for every domain):

from habit.supervoxel import SupervoxelizerRegistry

from habit.habitat_model import HabitatModelFitterRegistry
svx = SupervoxelizerRegistry.create("slic", n_supervoxels=50)
fitter = HabitatModelFitterRegistry.create("kmeans", n_habitats=4, n_init=10)

Five habitat protocols

Protocol

Call shape

Level

VoxelFeatureExtractor

(Subject) -> VoxelFeatureField

Subject

Supervoxelizer

(VoxelFeatureField) -> Supervoxelization

Subject

HabitatModelFitter

fit(Sequence[Supervoxelization]) -> HabitatModel

Cohort

HabitatAssigner

(Supervoxelization) -> HabitatMap

Subject

HabitatFeatureExtractor

(Subject, HabitatMap) -> FeatureTable

Subject

Only fitting the habitat model is cohort-level. After that, mapping uses model.assigner() (or a registry-built assigner).

Hand-assembled two-step chain

from habit.habitat_model import HabitatModelFitterRegistry

from habit.voxel_features import RawVoxelFeatures

from habit.supervoxel import SlicSupervoxelizer

voxel_fx = RawVoxelFeatures(modalities=["T1", "T2"])
svx = SlicSupervoxelizer(n_supervoxels=50)
fields = [voxel_fx(s) for s in cohort]
units = [svx(f) for f in fields]

fitter = HabitatModelFitterRegistry.create("kmeans", n_habitats=4)
fitter.set_random_state(42)
model = fitter.fit(units, cohort=cohort)
maps = [model.assigner()(u) for u in units]

SubjectPipeline

Compose the subject-level chain into one callable (HABIT’s typed Compose):

from habit.habitat_features import GraphHabitatFeatures, HabitatVolumeFeatures, IthHabitatFeatures, MsiHabitatFeatures, NonRadiomicsHabitatFeatures

from habit.pipeline import SubjectPipeline

pipe = SubjectPipeline(
    voxel_feature_extractor=voxel_fx,
    supervoxelizer=svx,
    habitat_assigner=model.assigner(),
)
# Positional form also works: SubjectPipeline(voxel_fx, svx, model.assigner())

habitat_map = pipe(subject)
table = pipe.extract_features(
    subject,
    [
        HabitatVolumeFeatures(),
        MsiHabitatFeatures(),
        IthHabitatFeatures(),
        NonRadiomicsHabitatFeatures(),
        GraphHabitatFeatures(),
        # Heavy PyRadiomics families (opt-in; require pyradiomics):
        # TraditionalRadiomicsHabitatFeatures(),
        # WholeHabitatRadiomicsFeatures(),
        # EachHabitatRadiomicsFeatures(),
    ],
)
maps = cohort.map(pipe)

Set supervoxelizer=None for direct voxel → habitat designs (no SLIC step). Train / fit recipes that already hold clustering units should call pipe.assign(units) or pipe.label_and_describe(subject, units, extractors) instead of pipe(subject) / extract_features, so Stage-1 voxel features are not recomputed.

HabitatComponents (spec → live objects)

build_habitat_components() is the single construction site that turns a HabitatSpec into live operators. Attribute names on HabitatComponents match the corresponding HabitatSpec fields and SubjectPipeline parameters (assembled preprocessor chains use the singular form, as on the pipeline):

HabitatComponents attribute

Spec / pipeline counterpart

voxel_feature_extractor

HabitatSpec.voxel_feature_extractor

supervoxelizer

HabitatSpec.supervoxelizer

supervoxel_feature_extractor

HabitatSpec.supervoxel_feature_extractor

voxel_feature_preprocessor

assembled from voxel_feature_preprocessors

supervoxel_feature_preprocessor

assembled from supervoxel_feature_preprocessors

cohort_feature_preprocessor

assembled from cohort_feature_preprocessors

habitat_model_fitter

HabitatSpec.habitat_model_fitter

habitat_features

HabitatSpec.habitat_features

from habit.pipeline.assembly import build_habitat_components

components = build_habitat_components(spec)
# Same name as on the Spec — the live VoxelFeatureExtractor instance:
field = components.voxel_feature_extractor(subject)
# Fit-time units (subject-level chains only; no assigner / cohort chain):
units = components.pipeline(assigner=None).units(subject)

Factory helpers build_voxel_extractor / build_supervoxel_extractor / build_habitat_extractor build a single tree node; they are not HabitatComponents attributes.

Composing features: trees, combiners, and statistics

Every extraction stage accepts one node abstraction — a recursive Spec tree built from two shapes:

  • Leaf — one extraction form over one or more modalities. The single-modality form takes modality="T1"; the multi-modality stacking form keeps modalities=[...] (raw only).

  • Combiner node — a Combiner implementation with child nodes under params["children"]. Combiners merge child blocks column-wise and know nothing about Subject or files, which keeps them trivially testable and reusable at any level.

from habit.voxel_features import build_voxel_extractor

from habit.supervoxel import build_supervoxel_extractor
from habit.spec import parse_feature_expression

voxel_fx = build_voxel_extractor(
    parse_feature_expression(
        'concat(raw("T1"), ratio(raw("T1"), raw("T2"), as_="t1_over_t2"))'
    )
)
field = voxel_fx(subject)  # still a one-argument atomic call

build_voxel_extractor / build_supervoxel_extractor / build_habitat_extractor route a plain leaf Spec to the registry and a tree Spec to a wrapper that implements the stage’s protocol — the pipeline consumes both transparently. SubjectPipeline binds the working and original voxel fields to supervoxel extractors that declare bind_fields (the statistics extractors mean / std / percentile aggregate one modality’s voxel signal per supervoxel; source="original" selects the pre-preprocessing signal).

Built-in combiners: concat, weighted_concat / average (per-child weights), ratio / difference (exactly two children), kinetic (DCE slope pairs), expression (dataframe arithmetic). Column naming: single-column nodes keep their source label (modality > as_ > name); as_ renames only single-output nodes. See Feature composition for a runnable tour.

Precision screen: perturbations and precise features

Precision screening compares voxel-texture maps; it does not extract them. Maps come from extract_voxel_texture() or VoxelRadiomicsFeatures (see Voxel feature extractors). perturb_image() builds the simulated retest. Neither atom needs a Cohort or YAML.

import numpy as np
from habit.precision import perturb_image
from habit.voxel_features import extract_voxel_texture

retest_rng = np.random.default_rng(7)
noisy = perturb_image(image, method="gaussian_noise", rng=retest_rng)
shifted = perturb_image(noisy, method="translation", shift_fraction=0.5, rng=retest_rng)
perturbed = perturb_image(shifted, method="rotation", angle_degrees=0.5, rng=retest_rng)
feat_r1 = extract_voxel_texture(image, mask, kernel_radius=1, bin_width=12)
feat_r3 = extract_voxel_texture(image, mask, kernel_radius=3, bin_width=12)

perturb_image wraps the registered image_perturbation methods that return a perturbed intensity volume (gaussian_noise, translation, rotation, rigid, bspline_deform). Mask-only contour methods (morphological, gradient_weighted, slice_extent) must be called on a Subject via ImagePerturbationRegistry — see Precise features. extract_voxel_texture is one voxel-radiomics pass; paper combinations are repeated calls (R1 vs R3, B12 vs B25, original vs perturbed). Then compose ICC panels:

from habit.precision import (
    aggregate_panels,
    identify_precise_features,
    precision_panel,
)

panel = precision_panel(
    {"original": field, "perturbed": perturbed_field},
    agreement="absolute",      # ICC(3A,1); "consistency" is ICC(3C,1)
)
cohort_panel = aggregate_panels([panel_s1, panel_s2])  # median across subjects
precise = identify_precise_features(
    {"repeatability": cohort_panel}, lcl_threshold=0.5
)
precise.save("precise_features.json")

The ninth protocol, ImagePerturbation, is the Subject-level form of the same methods (component(subject, rng=...)). The paper default chain is prior2024_retest_perturbation() (Appendix S2 / MIRP 1.2.0). Optional bspline_deform (MONAI Rand3DElastic; extra monai) is not in that chain.

PreciseFeatureSet.preprocessor() returns a FeatureWhitelist. The one-call recipe is habit.recipes.identify_precise_voxel_features(); the runnable tour is Precise features.

Randomness (Seedable)

Components that participate in study seeding implement set_random_state (Seedable), including supervoxelizers. Default seed is 0 unless HabitatSpec.random_seed / an explicit call overrides it. A backend may still be numerically deterministic (current skimage SLIC); the method remains so every stage shares one seeding surface.

from habit.habitat_model import HabitatModelFitterRegistry

from habit.supervoxel import SupervoxelizerRegistry
slic = SupervoxelizerRegistry.create("slic", n_supervoxels=50)
km = SupervoxelizerRegistry.create("kmeans", n_supervoxels=40)
slic.set_random_state(42)
km.set_random_state(42)
fitter = HabitatModelFitterRegistry.create("kmeans", n_habitats=4)
fitter.set_random_state(42)

Built-in registry domains

Discover names at runtime with Plugin introspection API (list_plugins(domain)).

Registry

Domain string

Examples

VoxelFeatureExtractorRegistry

voxel_feature_extractor

raw, local_entropy, voxel_radiomics, concat, expression, kinetic

SupervoxelizerRegistry

supervoxelizer

slic, kmeans, gmm

SupervoxelFeatureExtractorRegistry

supervoxel_feature_extractor

mean_voxel_features, supervoxel_radiomics, mean, std, percentile

HabitatModelFitterRegistry

habitat_model_fitter

kmeans, gmm

HabitatAssignerRegistry

habitat_assigner

nearest_centroid

CombinerRegistry

combiner

concat, weighted_concat, ratio, …

ImagePerturbationRegistry

image_perturbation

gaussian_noise, translation, rotation, rigid, bspline_deform, morphological, gradient_weighted, slice_extent

HabitatFeatureExtractorRegistry

habitat_feature_extractor

volume, msi, ith_score, graph, non_radiomics (light); traditional, whole_habitat, each_habitat (heavy)

TablePreprocessorRegistry

table_preprocessor

zscore, minmax, binning, …

FeatureSelectorRegistry

feature_selector

anova, lasso, mrmr, …

ClassifierRegistry

classifier

LogisticRegression, SVM, …

MetricRegistry

metric

accuracy, auc, …

TablePipeline composes table preprocessors / selectors / classifiers for tabular ML outside the imaging path.

See also