Skip to content

Summaries

Structured, frozen, JSON-friendly structural facts. No scientific calculation, quality score or inferred value appears here.

ExperimentSummary

Bases: _Frozen

A factual, structural description of an experiment.

The summary describes structure and metadata only. It makes no claim about scientific validity or suitability for sedimentation analysis.

to_dict

to_dict() -> dict[str, Any]

Serialise to plain JSON-friendly Python types.

Source code in src/openauc/models/summary.py
def to_dict(self) -> dict[str, Any]:
    """Serialise to plain JSON-friendly Python types."""
    return self.model_dump(mode="json")

to_text

to_text() -> str

Render the human-readable summary.

Describes structure and metadata only; it makes no claim about scientific validity or suitability for sedimentation analysis.

Source code in src/openauc/models/summary.py
def to_text(self) -> str:
    """Render the human-readable summary.

    Describes structure and metadata only; it makes no claim about
    scientific validity or suitability for sedimentation analysis.
    """
    name_suffix = f" - {self.name}" if self.name else ""
    acquired = self.acquired_at.isoformat() if self.acquired_at else "unknown"
    lines = [
        f"Experiment: {self.experiment_id}{name_suffix}",
        f"  Type: {self.experiment_type.value}",
        f"  Acquired: {acquired}",
        f"  Operator: {self.operator or 'unknown'}",
        f"  Scans: {self.n_scans}",
        f"  Samples: {self.n_samples}",
        "  Optical systems: "
        + ", ".join(system.value for system in self.optical_systems),
        f"  Radius axis: {self.radius_axis_mode.value}",
        f"  Radius unit: {self.radius_unit.value}",
        f"  Signal unit: {self.signal_unit.value}",
    ]

    if self.radius.is_observed:
        lines.append(f"  Radius range: {self.radius.render()}")
    else:
        lines.append("  Radius range: n/a (no observations)")

    if self.elapsed_time.is_observed:
        lines.append(f"  Elapsed time: {self.elapsed_time.render()}")
    else:
        lines.append("  Elapsed time: unknown")

    lines.append(f"  Points per scan: {self._render_points_per_scan()}")
    lines.append(f"  Total observations: {self.total_valid_observations}")
    lines.append(f"  Wavelengths: {self._render_wavelengths()}")
    lines.append("  Cells: " + _render_labels(self.cells, self.scans_without_cell))
    lines.append(
        "  Channels: " + _render_labels(self.channels, self.scans_without_channel)
    )
    lines.append(f"  Rotor speed: {self.rotor_speed.render()}")
    lines.append(f"  Temperature: {self.temperature.render()}")

    if self.provenance_available:
        parser = self.parser_name or "unspecified parser"
        lines.append(f"  Provenance: recorded ({parser})")
    else:
        lines.append("  Provenance: not recorded")

    if self.checksum_available:
        lines.append("  Source checksum: recorded")
    else:
        lines.append("  Source checksum: not recorded (deferred to the AUCX phase)")

    lines.append(
        f"  Validation: {self.validation.error} error(s), "
        f"{self.validation.warning} warning(s), {self.validation.info} info"
    )
    lines.append(
        "  Note: structural summary only; no assessment of scientific "
        "validity or suitability for analysis."
    )
    return "\n".join(lines)

ValueRange

Bases: _Frozen

The observed span of one quantity, with presence counts.

minimum and maximum are None when nothing was present. The unit is the declared unit, retained verbatim; nothing is converted.

render

render() -> str

'6 to 7.2 cm (observed)', or 'unknown' when nothing present.

Source code in src/openauc/models/summary.py
def render(self) -> str:
    """``'6 to 7.2 cm (observed)'``, or ``'unknown'`` when nothing present."""
    if self.minimum is None or self.maximum is None:
        return "unknown"
    return f"{self.minimum:g} to {self.maximum:g} {self.unit.value} (observed)"

MetadataPresence

Bases: _Frozen

Presence counts for one metadata field across scans or samples.

absent counts records where the field itself is structurally absent (None); the remaining counters record the explicit :class:~openauc.models.enums.ValueStatus of the values that are present as quantities. The two levels of absence are never collapsed.

unrecorded property

unrecorded: int

Records carrying no usable value, however that absence is expressed.

ValidationCounts

Bases: _Frozen

Finding counts from the full (all-tier) validation report.

summarise_experiment

summarise_experiment(
    experiment: AUCExperiment,
) -> ExperimentSummary

Build the structured summary for experiment.

Source code in src/openauc/models/summary.py
def summarise_experiment(experiment: AUCExperiment) -> ExperimentSummary:
    """Build the structured summary for ``experiment``."""
    observations = experiment.observations
    scans = experiment.scans
    report = experiment.validate()
    errors, warnings, infos = report.counts()

    radius_range = observations.radius_range()
    points = observations.points_per_scan()

    presence: list[MetadataPresence] = [
        _presence("scan", field, [accessor(scan) for scan in scans])
        for field, accessor in _SCAN_PRESENCE_QUANTITIES
    ]
    presence.append(_string_presence("scan", "cell", [scan.cell for scan in scans]))
    presence.append(
        _string_presence("scan", "channel", [scan.channel for scan in scans])
    )
    presence.extend(
        _presence("sample", field, [accessor(sample) for sample in experiment.samples])
        for field, accessor in _SAMPLE_PRESENCE_QUANTITIES
    )
    presence.append(
        _string_presence(
            "sample",
            "buffer_description",
            [sample.buffer_description for sample in experiment.samples],
        )
    )

    provenance = experiment.provenance
    return ExperimentSummary(
        experiment_id=experiment.metadata.experiment_id,
        name=experiment.metadata.name,
        experiment_type=experiment.metadata.experiment_type,
        acquired_at=experiment.metadata.acquired_at,
        operator=experiment.metadata.operator,
        n_scans=len(scans),
        n_samples=len(experiment.samples),
        radius_axis_mode=observations.mode,
        radius_unit=observations.radius_unit,
        signal_unit=observations.signal_unit,
        signal_unit_declared=observations.signal_unit is not Unit.UNKNOWN,
        points_per_scan=points,
        total_valid_observations=sum(points),
        optical_systems=experiment.optical_systems(),
        wavelengths_nm=_distinct_wavelengths(scans),
        scans_without_wavelength=sum(
            1
            for scan in scans
            if scan.wavelength is None
            or scan.wavelength.status is not ValueStatus.PRESENT
        ),
        cells=tuple(sorted({scan.cell for scan in scans if scan.cell is not None})),
        scans_without_cell=sum(1 for scan in scans if scan.cell is None),
        channels=tuple(
            sorted({scan.channel for scan in scans if scan.channel is not None})
        ),
        scans_without_channel=sum(1 for scan in scans if scan.channel is None),
        radius=ValueRange(
            minimum=radius_range[0] if radius_range else None,
            maximum=radius_range[1] if radius_range else None,
            unit=observations.radius_unit,
            n_present=sum(points),
            n_absent=0,
        ),
        elapsed_time=_quantity_range(
            [scan.elapsed_time for scan in scans], default_unit=Unit.SECOND
        ),
        rotor_speed=_quantity_range(
            [scan.rotor_speed for scan in scans], default_unit=Unit.RPM
        ),
        temperature=_quantity_range(
            [scan.temperature for scan in scans], default_unit=Unit.DEGREE_CELSIUS
        ),
        metadata_presence=tuple(presence),
        provenance_available=provenance is not None,
        parser_name=provenance.parser_name if provenance is not None else None,
        checksum_available=provenance is not None and provenance.sha256 is not None,
        validation=ValidationCounts(error=errors, warning=warnings, info=infos),
    )