Skip to main content

metrics

The output dataclasses of the per-scan calculation steps.

One dataclass per algorithm (GA, fluid volume, CST, GCC), each able to flatten itself into a DataFrame record (to_record) and to advertise the columns a criterion may match on (field_specs). The untyped per-label dict fields are dropped by to_record and re-projected as flat per-label columns on request.

Classes

CSTMetrics

class CSTMetrics(    *,    cst_mean_um: float | None = None,    cst_median_um: float | None = None,    cst_std_um: float | None = None,    cst_n_samples: int | None = None,    cst_radius_mm: float | None = None,    fovea_coordinates: tuple[float, float, float] | None = None,    ilm_layer_present: bool = False,    rpe_layer_present: bool = False,    inner_layer_used: str | None = None,    outer_layer_used: str | None = None,    measurement_type: str | None = None,    ez_intact_max_span_um: float | None = None,    ez_attenuated_max_span_um: float | None = None,    ez_measured_width_um: float | None = None,    ez_reference_bscan_index: int | None = None,    ez_reference_type: str = 'unavailable',    ez_layers_measured: bool = False,):

Output of the CST/CRT calculation algorithm.

The ez_* fields measure ellipsoid-zone integrity on the single B-scan the fovea model reports as central, and are populated only by cst_calculation v2 with an attenuation threshold configured. They live here rather than in their own metrics type because the measurement needs exactly the inputs CST already takes — the layer predictions and the fovea centre — and reuses the boundary grid CST already builds.

Every field defaults to its empty state, so a row cached by v1 and reconstructed as CSTMetrics(**value) stays valid.

Attributes

  • ez_intact_max_span_um: Widest contiguous run of A-scans whose EZ-to-RPE separation exceeds the configured threshold. This is the gate.
  • ez_attenuated_max_span_um: Widest contiguous run measurable but not exceeding it — including where EZ is absent while RPE is present, which is total attenuation rather than no attenuation.
  • ez_measured_width_um: Width of the reference B-scan that could be measured at all. Zero makes both spans absence of evidence.
  • ez_reference_bscan_index: Which B-scan was measured.
  • ez_reference_type: Where that B-scan's identity came from.
  • ez_layers_measured: Whether the EZ layer pair was available, so an empty span distinguishes "measured, none found" from "not measured".

Variables

  • static cst_mean_um : float | None
  • static cst_median_um : float | None
  • static cst_n_samples : int | None
  • static cst_radius_mm : float | None
  • static cst_std_um : float | None
  • static ez_attenuated_max_span_um : float | None
  • static ez_intact_max_span_um : float | None
  • static ez_layers_measured : bool
  • static ez_measured_width_um : float | None
  • static ez_reference_bscan_index : int | None
  • static ez_reference_type : str
  • static fovea_coordinates : tuple[float, float, float] | None
  • static ilm_layer_present : bool
  • static inner_layer_used : str | None
  • static measurement_type : str | None
  • static outer_layer_used : str | None
  • static rpe_layer_present : bool

Static methods


expected_cols

def expected_cols()> list[str]:

Returns the expected columns for a dataframe.

field_specs

def field_specs()> list[FieldSpec]:

SCAN-grain FieldSpecs for CST scalar columns.

Methods


to_record

def to_record(self)> dict[str, typing.Any]:

Convert to a record format compatible with pd.DataFrame.from_records().

FluidVolumeMetrics

class FluidVolumeMetrics(    *,    total_fluid_volume: float,    smallest_lesion_volume: float,    largest_lesion_volume: float,    num_bscans_with_fluid: int,    num_fluid_lesions: int,    distance_from_image_centre: float,    max_cnv_probability: float,    max_fluid_volume_bscan_index: int | None,    segmentation_volumes: dict[str, float],):

Output of the Fluid Volume calculation algorithm.

Variables

  • static distance_from_image_centre : float
  • static largest_lesion_volume : float
  • static max_cnv_probability : float
  • static max_fluid_volume_bscan_index : int | None
  • static num_bscans_with_fluid : int
  • static num_fluid_lesions : int
  • static segmentation_volumes : dict[str, float]
  • static smallest_lesion_volume : float
  • static total_fluid_volume : float

Static methods


expected_cols

def expected_cols()> list[str]:

Returns the expected columns for a dataframe.

field_specs

def field_specs()> list[FieldSpec]:

SCAN-grain FieldSpecs for fluid-volume scalar columns.

Methods


to_record

def to_record(self)> dict[str, typing.Any]:

Convert to a record format compatible with pd.DataFrame.from_records().

FoveaReference

class FoveaReference(*, centre: tuple[int, int, int], reference_type: str):

A detected fovea landmark and how it was obtained.

The "no landmark" state is LesionMetrics.fovea_reference being None, not an instance with a missing centre. A centre therefore only ever exists alongside a reference_type, so the inconsistent "centre present but provenance unavailable" combination cannot be built: reference_type is never the "unavailable" sentinel, which is synthesized at emission time for the None case.

Variables

  • static centre : tuple[int, int, int]
  • static reference_type : str

GAMetrics

class GAMetrics(    *,    total_ga_area: float,    smallest_lesion_size: float,    largest_lesion_size: float,    num_bscans_with_ga: int,    num_ga_lesions: int,    distance_from_image_centre: float,    max_cnv_probability: float,    max_ga_bscan_index: int | None,    segmentation_areas: dict[str, float],    max_pathology_probabilities: dict[str, float],    n_scan_run_lengths: dict[str, int] = {},    raw_pathology_probabilities: dict[str, np.ndarray[typing.Any, np.dtype[np.float64]]] | None = None,):

Output of the GA calculation algorithm.

Attributes

  • total_ga_area: Total area of GA in the image in mm^2.
  • smallest_lesion_size: Size of the smallest lesion in the image in mm^2.
  • largest_lesion_size: Size of the largest lesion in the image in mm^2.
  • num_bscans_with_ga: Number of B-scans with GA in the image.
  • num_ga_lesions: Number of GA lesions in the image.
  • distance_from_image_centre: Distance from the image centre to the nearest lesion in mm.
  • max_cnv_probability: Maximum probability of CNV across all B-scans.
  • max_ga_bscan_index: Index of the B-scan with the largest GA lesion.
  • segmentation_areas: Area of each segmentation class in mm^2.
  • n_scan_run_lengths: Longest consecutive-B-scan run (at each biomarker's per-scan threshold) per N-scan biomarker label. Defaults to an empty dict so a cache blob written before this field existed still constructs (rather than raising on a missing keyword argument); to_record flattens an empty dict the same as any other absent label, so this carries no behavioural cost.

Variables

  • static distance_from_image_centre : float
  • static largest_lesion_size : float
  • static max_cnv_probability : float
  • static max_ga_bscan_index : int | None
  • static max_pathology_probabilities : dict[str, float]
  • static n_scan_run_lengths : dict[str, int]
  • static num_bscans_with_ga : int
  • static num_ga_lesions : int
  • static segmentation_areas : dict[str, float]
  • static smallest_lesion_size : float
  • static total_ga_area : float

Static methods


expected_cols

def expected_cols()> list[str]:

Returns the expected columns for a dataframe.

field_specs

def field_specs()> list[FieldSpec]:

The matchable SCAN-grain FieldSpecs this metrics type contributes.

Methods


to_record

def to_record(    self,    additional_pathology_prob_cols: Collection[str] | None = None,    additional_area_cols: Collection[str] | None = None,    additional_run_length_cols: Collection[str] | None = None,)> dict[str, typing.Any]:

Convert to a record format compatible with pd.DataFrame.from_records().

The scalar fields are emitted as-is; the untyped dict fields (max_pathology_probabilities, raw_pathology_probabilities, n_scan_run_lengths) are dropped and, when requested, re-projected into flat per-label columns.

Arguments

  • additional_pathology_prob_cols: Pathology labels to flatten out of max_pathology_probabilities into max_<label>_probability columns. A label absent from the map becomes np.nan (→ unknown downstream), never a fabricated 0.0. None emits no probability columns.
  • additional_area_cols: Segmentation labels to flatten out of segmentation_areas into <label>_area columns. A PRESENT key (incl. 0.0) is a measured area; an ABSENT key becomes np.nan (→ unknown), so a criterion on a label outside the model's measured set does not silently pass a threshold. None emits no area columns.
  • additional_run_length_cols: N-scan biomarker labels to flatten out of n_scan_run_lengths into n_scan_run_<label> columns. A label absent from the map becomes np.nan (→ unknown downstream), never a fabricated 0. None emits no run-length columns.

Returns A {column: value} record: the scalar fields plus any requested flattened probability/area/run-length columns.

GAMetricsWithFovea

class GAMetricsWithFovea(    *,    total_ga_area: float,    smallest_lesion_size: float,    largest_lesion_size: float,    num_bscans_with_ga: int,    num_ga_lesions: int,    distance_from_image_centre: float,    max_cnv_probability: float,    max_ga_bscan_index: int | None,    segmentation_areas: dict[str, float],    max_pathology_probabilities: dict[str, float],    n_scan_run_lengths: dict[str, int] = {},    raw_pathology_probabilities: dict[str, np.ndarray[typing.Any, np.dtype[np.float64]]] | None = None,    distance_from_fovea_centre: float | None,    fovea_centre: tuple[int, int, int] | None,    fovea_landmarks: list[tuple[int, int, int]] | None,    est_fovea_distance: float,    distance_metric_type: str,    subfoveal_indicator: str | None,):

Output of the GA calculation algorithm with fovea detection.

Ancestors

Variables

  • static distance_from_fovea_centre : float | None
  • static distance_metric_type : str
  • static est_fovea_distance : float
  • static fovea_centre : tuple[int, int, int] | None
  • static fovea_landmarks : list[tuple[int, int, int]] | None
  • static subfoveal_indicator : str | None

Static methods


expected_cols

def expected_cols()> list[str]:

Inherited from:

GAMetrics.expected_cols :

Returns the expected columns for a dataframe.

field_specs

def field_specs()> list[FieldSpec]:

SCAN-grain FieldSpecs for GA + fovea scalar columns.

Methods


to_record

def to_record(    self,    additional_pathology_prob_cols: Collection[str] | None = None,    additional_area_cols: Collection[str] | None = None,    additional_run_length_cols: Collection[str] | None = None,)> dict[str, typing.Any]:

Inherited from:

GAMetrics.to_record :

Convert to a record format compatible with pd.DataFrame.from_records().

The scalar fields are emitted as-is; the untyped dict fields (max_pathology_probabilities, raw_pathology_probabilities, n_scan_run_lengths) are dropped and, when requested, re-projected into flat per-label columns.

Arguments

  • additional_pathology_prob_cols: Pathology labels to flatten out of max_pathology_probabilities into max_<label>_probability columns. A label absent from the map becomes np.nan (→ unknown downstream), never a fabricated 0.0. None emits no probability columns.
  • additional_area_cols: Segmentation labels to flatten out of segmentation_areas into <label>_area columns. A PRESENT key (incl. 0.0) is a measured area; an ABSENT key becomes np.nan (→ unknown), so a criterion on a label outside the model's measured set does not silently pass a threshold. None emits no area columns.
  • additional_run_length_cols: N-scan biomarker labels to flatten out of n_scan_run_lengths into n_scan_run_<label> columns. A label absent from the map becomes np.nan (→ unknown downstream), never a fabricated 0. None emits no run-length columns.

Returns A {column: value} record: the scalar fields plus any requested flattened probability/area/run-length columns.

GCCMetrics

class GCCMetrics(    *,    gcc_mean_superior: float | None = None,    gcc_mean_inferior: float | None = None,    gcc_asymmetry_index: float | None = None,    gcc_radius_mm: float | None = None,    min_thickness: float | None = None,    glaucoma_stage: str | None = None,    macula_coordinates: tuple[float, float, float] | None = None,    rnfl_layer_present: bool = False,    ipl_layer_present: bool = False,    inner_layer_used: str | None = None,    outer_layer_used: str | None = None,    measurement_type: str | None = None,):

Output of the GCC calculation algorithm.

Variables

  • static gcc_asymmetry_index : float | None
  • static gcc_mean_inferior : float | None
  • static gcc_mean_superior : float | None
  • static gcc_radius_mm : float | None
  • static glaucoma_stage : str | None
  • static inner_layer_used : str | None
  • static ipl_layer_present : bool
  • static macula_coordinates : tuple[float, float, float] | None
  • static measurement_type : str | None
  • static min_thickness : float | None
  • static outer_layer_used : str | None
  • static rnfl_layer_present : bool

Static methods


expected_cols

def expected_cols()> list[str]:

Returns the expected columns for a dataframe.

field_specs

def field_specs()> list[FieldSpec]:

SCAN-grain FieldSpecs for GCC scalar columns.

Methods


to_record

def to_record(self)> dict[str, typing.Any]:

Convert to a record format compatible with pd.DataFrame.from_records().

LesionGroupMetrics

class LesionGroupMetrics(    *,    total_area: float,    num_lesions: int,    largest_lesion_area: float,    smallest_lesion_area: float,    num_bscans_present: int,    distance_from_image_centre: float,    max_area_bscan_index: int | None,    total_volume_nl: float | None = None,    mean_thickness_um: float | None = None,    largest_lesion_max_base_width_um: float | None = None,    nearest_distance_from_fovea: float | None = None,    aggregate_angular_span: float | None = None,    lesions: tuple[LesionRecord, ...] = (),    truncated_lesions: int = 0,    roi: LesionGroupMetrics | None = None,    lesions_measured: bool = False,):

En-face area and lesion geometry for one named mask group.

The GAMetrics scalar shape, measured for a config-defined group of segmentation labels rather than the one hardcoded GA mask.

Attributes

  • total_area: Projected area of the group mask, in mm².
  • num_lesions: Connected components on the en-face plane.
  • largest_lesion_area: Largest single component in mm²; nan when the group is absent from the scan on the compute path, None on an instance rebuilt from a cache blob (the json_safe write coerces nan to None).
  • smallest_lesion_area: Smallest single component in mm²; nan when the group is absent from the scan on the compute path, None on an instance rebuilt from a cache blob (the json_safe write coerces nan to None).
  • num_bscans_present: B-scans on which the group appears at all.
  • distance_from_image_centre: Distance in mm from the image centre to the nearest component; nan when the group is absent from the scan on the compute path, None on an instance rebuilt from a cache blob (the json_safe write coerces nan to None).
  • max_area_bscan_index: B-scan carrying the most of the group, or None when the group is absent.
  • total_volume_nl: Sum of every lesion's volume in nL, or None when axial spacing was absent.
  • mean_thickness_um: total_volume_nl / total_area in µm, or None when volume is None.
  • largest_lesion_max_base_width_um: The widest single-B-scan base width across every lesion in the group, in µm, or None when the group has no lesions.
  • nearest_distance_from_fovea: mm from the fovea landmark to the closest lesion cell in the group, or None when no landmark was available. Never a fallback to image centre.
  • aggregate_angular_span: Degrees of the fovea's circumference occupied by at least one lesion in the group, or None when no landmark was available. Carries the same near-fovea over-read as LesionRecord.angular_span.
  • lesions: Every measured lesion in the group, ordered largest cell count first. Empty when the group has no lesions.
  • truncated_lesions: Count of lesions beyond the configured cap (LesionCalculationConfig.max_lesions_per_group) that were dropped from lesions; 0 when nothing was truncated.
  • roi: The same aggregates restricted to the configured region of interest, or None when no ROI was configured. Its own roi is always None.
  • lesions_measured: Whether per-lesion measurement ran for this group (MaskGroupSpec.measure_lesions). Without it, every fovea-derived field being None is ambiguous: the group might not have been measured, or it might have been measured and found no landmark, or measured and found no lesions. A criterion on <group>_aggregate_angular_span evaluates unknown for the whole cohort in the first two cases, so which one it was has to be recoverable from the report.

Variables

  • static aggregate_angular_span : float | None
  • static distance_from_image_centre : float
  • static largest_lesion_area : float
  • static largest_lesion_max_base_width_um : float | None
  • static lesions_measured : bool
  • static max_area_bscan_index : int | None
  • static mean_thickness_um : float | None
  • static nearest_distance_from_fovea : float | None
  • static num_bscans_present : int
  • static num_lesions : int
  • static smallest_lesion_area : float
  • static total_area : float
  • static total_volume_nl : float | None
  • static truncated_lesions : int

LesionMetrics

class LesionMetrics(    *,    groups: dict[str, LesionGroupMetrics],    roi_radius_mm: float | None = None,    fovea_reference: FoveaReference | None = None,):

Output of the lesion_calculation step, one per scan.

Attributes

  • groups: Group name to its measurements. Group names come from the step's config, so nothing static enumerates them — hence the empty expected_cols/field_specs below.
  • roi_radius_mm: Radius in mm of the configured region of interest around the fovea landmark, or None when no ROI was configured.
  • fovea_reference: Where the fovea landmark came from, or None when no landmark was available. The landmark voxel and its provenance live together in a FoveaReference so a centre cannot exist without a reference type.

Variables

  • static roi_radius_mm : float | None

Static methods


expected_cols

def expected_cols()> list[str]:

No statically-known columns: group names come from the config.

convert_metrics_to_df fabricates an empty column for every name this reports that the record lacks, and logs a missing-column warning while doing so. Since the group set is per-run, listing anything here would warn on every run.

Returns An empty list.

field_specs

def field_specs()> list[FieldSpec]:

No static catalog specs, for the same reason as expected_cols.

Area-group columns are SCAN-grain, which is exactly what a column_criteria with no explicit grain defaults to, so an absent catalog entry costs nothing but the unit annotation. criteria_matching still warns for a group column that neither the catalog nor the frame carries, which is the signal that matters (the step is unwired).

Returns An empty list.

Methods


to_record

def to_record(self)> dict[str, typing.Any]:

Flatten every group's scalar metrics into <group>_<metric> columns.

Per-lesion lists are deliberately omitted: a DataFrame cell holding a list of records is not something a ColumnFilter can compare. They stay in the cache blob, where a future MethodFilter could reach them, but NO such filter exists today — every shipped criterion is answered by a group scalar below, and largest_lesion_max_base_width_um in particular exists so "at least one lesion wider than T" needs no per-lesion access at all. Treat the persisted records as diagnostic detail, not as a matchable surface. The groups container itself is not surfaced as a column either, mirroring the GA converter dropping segmentation_areas.

The nested ROI record is re-projected into its own scalar columns, mirroring how GA re-projects n_scan_run_lengths into n_scan_run_<label>: <group>_roi_<metric> answers a region-of- interest question as a plain column comparison, without the ROI threshold entering the step's config or the cache key. Absent ROI yields None for every roi_<metric> column, never 0.0 — a fabricated zero would read as "measured, and nothing qualifies".

reference_type is emitted per group rather than once per scan, even though the fovea landmark is a scan-level fact. A criterion targets a <group>_<metric> column, so when it evaluates unknown the reader needs the provenance beside that group's own columns; an unqualified reference_type also collides with any other step that reports one, and is dropped by the report's group-prefix column selection. A group that did not measure lesions reports NOT_MEASURED rather than the scan's landmark state, because for that group the landmark is not why the fovea-derived columns are empty.

Returns A {column: value} record: one entry per group per scalar metric, one roi_<metric> entry per group per scalar metric, plus one <group>_reference_type per group.

LesionRecord

class LesionRecord(    *,    lesion_id: int,    area: float,    volume_nl: float | None,    mean_thickness_um: float | None,    max_base_width_um: float,    max_base_width_bscan_index: int,    max_feret_um: float,    bscan_extent: int,    centroid_bscan: float,    centroid_col: float,    distance_from_fovea: float | None,    angular_span: float | None,    within_roi: bool | None,):

One measured lesion of one group.

Attributes

  • lesion_id: Component id within its group, 1-based.
  • area: En-face area in mm².
  • volume_nl: Volume in nL, or None when the axial spacing was absent.
  • mean_thickness_um: volume / area in µm — the physical sanity value. None when volume is None.
  • max_base_width_um: Widest run of set columns on any single B-scan, in µm. The well-sampled diameter, and the one a size criterion should gate on.
  • max_base_width_bscan_index: The B-scan carrying that widest run.
  • max_feret_um: Widest en-face extent in any direction, in µm. Reported for completeness; unreliable below about three B-scans of extent because the B-scan axis is sampled about twenty times coarser.
  • bscan_extent: Number of B-scans the lesion appears on.
  • centroid_bscan: Mean B-scan index of its cells.
  • centroid_col: Mean column index of its cells.
  • distance_from_fovea: mm from the fovea landmark to the lesion's nearest cell, or None when no landmark was available. Never a fallback to image centre.
  • angular_span: Degrees subtended about the fovea landmark, or None when no landmark was available. Over-reads near the fovea landmark itself, where a single cell can subtend on the order of 180 degrees; not a precise measurement at short range.
  • within_roi: Whether any part of the lesion lies inside the configured region of interest. None when no ROI was configured or no landmark was available.

Variables

  • static angular_span : float | None
  • static area : float
  • static bscan_extent : int
  • static centroid_bscan : float
  • static centroid_col : float
  • static distance_from_fovea : float | None
  • static lesion_id : int
  • static max_base_width_bscan_index : int
  • static max_base_width_um : float
  • static max_feret_um : float
  • static mean_thickness_um : float | None
  • static volume_nl : float | None
  • static within_roi : bool | None

ParsedBScanPredictions

class ParsedBScanPredictions(    column_masks: NDArray[_Floating],    class_probabilities: dict[str, NDArray[_Floating]],    class_probabilities_by_bscan: dict[str, NDArray[_Floating]],    class_areas: dict[str, list[float]],    num_bscans: int,    na_bscan_indices: tuple[int, ...] = (),):

Container for the various outputs of parsing bscan predictions.

Attributes

  • column_masks: Per-B-scan columnar GA masks.
  • class_probabilities: Probabilities appended once per reported detection — a mask instance, or a classes entry. Its length is therefore a detection count, not a B-scan count, and its indices carry no B-scan alignment. Suitable only for order-independent reductions such as max.
  • class_probabilities_by_bscan: Probabilities indexed by B-scan, one entry per input B-scan, 0.0 where the class was not reported on that B-scan. Required for any positional reduction, such as the longest consecutive run. Validated against num_bscans on construction.
  • class_areas: Per-class areas, per B-scan, for logging.
  • num_bscans: The number of input B-scans, including those with no model output. Defines the index space of class_probabilities_by_bscan.
  • na_bscan_indices: Indices, in the input B-scan index space, of the frames that carried no model output. column_masks skips these, so its first axis is COMPACTED: row j of column_masks is input B-scan j + count(i in na_bscan_indices where i <= j). Anything comparing an input-space B-scan coordinate against column_masks must first shift it by the count of dropped frames below it — see compact_bscan_index.

Raises

  • ValueError: If any class_probabilities_by_bscan array does not hold exactly num_bscans entries.

Variables

  • static class_areas : dict[str, list[float]]
  • static na_bscan_indices : tuple[int, ...]
  • static num_bscans : int